<commit_before>// @flow

import {
    DOMINANT_SPEAKER_CHANGED,
    PARTICIPANT_JOINED,
    PARTICIPANT_LEFT,
    PIN_PARTICIPANT
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import {
    getTrackByJitsiTrack,
    TRACK_ADDED,
    TRACK_REMOVED,
    TRACK_UPDATED
} from '../base/tracks';

import { selectParticipant, selectParticipantInLargeVideo } from './actions';

/**
 * Middleware that catches actions related to participants and tracks and
 * dispatches an action to select a participant depicted by LargeVideo.
 *
 * @param {Store} store - Redux store.
 * @returns {Function}
 */
MiddlewareRegistry.register(store => next => action => {
    const result = next(action);

    switch (action.type) {
    case DOMINANT_SPEAKER_CHANGED:
    case PARTICIPANT_JOINED:
    case PARTICIPANT_LEFT:
    case PIN_PARTICIPANT:
    case TRACK_ADDED:
    case TRACK_REMOVED:
        store.dispatch(selectParticipantInLargeVideo());
        break;

    case TRACK_UPDATED:
        // In order to minimize re-calculations, we need to select participant
        // only if the videoType of the current participant rendered in
        // LargeVideo has changed.
        if ('videoType' in action.track) {
            const state = store.getState();
            const track
                = getTrackByJitsiTrack(
                    state['features/base/tracks'],
                    action.track.jitsiTrack);
            const participantId = state['features/large-video'].participantId;

            (track.participantId === participantId)
                && store.dispatch(selectParticipant());
        }
        break;
    }

    return result;
});
<commit_msg>fix(large-video): do not reselect video on self dominant speaker<commit_after>// @flow

import {
    DOMINANT_SPEAKER_CHANGED,
    PARTICIPANT_JOINED,
    PARTICIPANT_LEFT,
    PIN_PARTICIPANT,
    getLocalParticipant
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import {
    getTrackByJitsiTrack,
    TRACK_ADDED,
    TRACK_REMOVED,
    TRACK_UPDATED
} from '../base/tracks';

import { selectParticipant, selectParticipantInLargeVideo } from './actions';

/**
 * Middleware that catches actions related to participants and tracks and
 * dispatches an action to select a participant depicted by LargeVideo.
 *
 * @param {Store} store - Redux store.
 * @returns {Function}
 */
MiddlewareRegistry.register(store => next => action => {
    const result = next(action);

    switch (action.type) {
    case DOMINANT_SPEAKER_CHANGED: {
        const localParticipant = getLocalParticipant(store.getState());

        if (localParticipant && localParticipant.id !== action.participant.id) {
            store.dispatch(selectParticipantInLargeVideo());
        }

        break;
    }

    case PARTICIPANT_JOINED:
    case PARTICIPANT_LEFT:
    case PIN_PARTICIPANT:
    case TRACK_ADDED:
    case TRACK_REMOVED:
        store.dispatch(selectParticipantInLargeVideo());
        break;

    case TRACK_UPDATED:
        // In order to minimize re-calculations, we need to select participant
        // only if the videoType of the current participant rendered in
        // LargeVideo has changed.
        if ('videoType' in action.track) {
            const state = store.getState();
            const track
                = getTrackByJitsiTrack(
                    state['features/base/tracks'],
                    action.track.jitsiTrack);
            const participantId = state['features/large-video'].participantId;

            (track.participantId === participantId)
                && store.dispatch(selectParticipant());
        }
        break;
    }

    return result;
});
<|endoftext|>


<commit_before>"use strict";

var ConfigurationData = {
  milestones: {},
  products: {},
  testsuiteFlag: -1,

  // useNext represents products where I'm reasonably
  // confident that the correct milestone is --- + 1
  useNext: ['Core',
            'Fennec',
            'Firefox for Android',
            'Firefox',
            'MailNews Core',
            'Mozilla Localizations',
            'Mozilla Services',
            'Other Applications',
            'SeaMonkey',
            'Testing',
            'Thunderbird',
            'Toolkit'],

  init: function CD_init(loadCallback, errorCallback) {
    var self = this;
    var callback  = function CD_initCallback(errmsg, data) {
      if (errmsg)
        errorCallback(errmsg);
      else
        self.parseData(data, loadCallback);
    };

    var bugzilla = bz.createClient({timeout: 30000});
    bugzilla.getConfiguration(callback);
  },

  parseData: function CD_parseData(data, loadCallback) {
    // Parse Milestones
    var productMilestones = {}
    if ('product' in data) {
      for (var product in data['product']) {
        var values = data.product[product].target_milestone;
        productMilestones[product] = {}
        productMilestones[product].values = values.map(UI.htmlEncode);
        var dashIndex = values.indexOf('---');
        if (dashIndex != -1) {
          if (dashIndex + 1 < values.length && this.useNext.indexOf(product) != -1)
            productMilestones[product].defaultIndex = dashIndex + 1;
          else
            productMilestones[product].defaultIndex = dashIndex;
        } else
          productMilestones[product].defaultIndex = 0;
      }
    }
    this.milestones = productMilestones;

    // Find the flag number for in-testsuite
    if ('flag_type' in data) {
      for (var flagNumber in data['flag_type']) {
        if (data['flag_type'][flagNumber].name == 'in-testsuite') {
          this.testsuiteFlag = parseInt(flagNumber);
          break;
        }
      }
    }

    // Find which products/components can have intestsuite set
    if ('product' in data && this.testsuiteFlag != -1) {
      for (var product in data.product) {
        this.products[product] = {};
        for (var component in data.product[product].component) {
          var hasTestsuite = data.product[product].component[component].flag_type.indexOf(this.testsuiteFlag) != -1;
          this.products[product][component] = hasTestsuite;
        }
      }
    }

    loadCallback();
  }
};
<commit_msg>Bug 896065 - Add support for setting the target milestone automatically in more components. r=graememcc<commit_after>"use strict";

var ConfigurationData = {
  milestones: {},
  products: {},
  testsuiteFlag: -1,

  // useNext represents products where I'm reasonably
  // confident that the correct milestone is --- + 1
  useNext: ['Add-on SDK',
            'Core',
            'Fennec',
            'Firefox',
            'Firefox for Android',
            'Firefox for Metro',
            'Firefox Health Report',
            'MailNews Core',
            'Mozilla Localizations',
            'Mozilla Services',
            'Other Applications',
            'SeaMonkey',
            'Testing',
            'Thunderbird',
            'Toolkit'],

  init: function CD_init(loadCallback, errorCallback) {
    var self = this;
    var callback  = function CD_initCallback(errmsg, data) {
      if (errmsg)
        errorCallback(errmsg);
      else
        self.parseData(data, loadCallback);
    };

    var bugzilla = bz.createClient({timeout: 30000});
    bugzilla.getConfiguration(callback);
  },

  parseData: function CD_parseData(data, loadCallback) {
    // Parse Milestones
    var productMilestones = {}
    if ('product' in data) {
      for (var product in data['product']) {
        var values = data.product[product].target_milestone;
        productMilestones[product] = {}
        productMilestones[product].values = values.map(UI.htmlEncode);
        var dashIndex = values.indexOf('---');
        if (dashIndex != -1) {
          if (dashIndex + 1 < values.length && this.useNext.indexOf(product) != -1)
            productMilestones[product].defaultIndex = dashIndex + 1;
          else
            productMilestones[product].defaultIndex = dashIndex;
        } else
          productMilestones[product].defaultIndex = 0;
      }
    }
    this.milestones = productMilestones;

    // Find the flag number for in-testsuite
    if ('flag_type' in data) {
      for (var flagNumber in data['flag_type']) {
        if (data['flag_type'][flagNumber].name == 'in-testsuite') {
          this.testsuiteFlag = parseInt(flagNumber);
          break;
        }
      }
    }

    // Find which products/components can have intestsuite set
    if ('product' in data && this.testsuiteFlag != -1) {
      for (var product in data.product) {
        this.products[product] = {};
        for (var component in data.product[product].component) {
          var hasTestsuite = data.product[product].component[component].flag_type.indexOf(this.testsuiteFlag) != -1;
          this.products[product][component] = hasTestsuite;
        }
      }
    }

    loadCallback();
  }
};
<|endoftext|>


<commit_before>'use strict';

var $ = require('jquery'),
    $window = window;

require('datatables.net')($window, $);
require('datatables.net-buttons')($window, $);
require('datatables.net-responsive')($window, $);
require('datatables.net-select')($window, $);
require('../libs/jquery.countdown/dist/jquery.countdown.min');

function PulsarUIComponent(html, history) {

    this.history = history;
    this.$html = html;

}

PulsarUIComponent.prototype.init = function () {

    var component = this;

    // Stop disabled links from being interactive
    this.$html.on('click', 'a[disabled]', function(e) {
        e.preventDefault();
    });

    // Watch for push-state requests via data-html attribute
    this.$html.on('click', '[data-href]', function(e) {
        var href = $(this).data('href');
        component.history.pushState({state:1}, href, href);
    });

    this.initTables();
    this.initDataTables();
    this.initCountdown();
}

PulsarUIComponent.prototype.initTables = function () {

    // Wrap non datatable tables in responsive container so they can scroll
    // when required
    var $tables = this.$html.find('.table:not(.datatable), .table--datagrid:not(.datatable), .table--horizontal');

    $tables.each(function(i, v) {
        var $table = $(v);

        if (!$table.parent().hasClass('table-container')) {
            $table.wrap('

');
        }
    });
}

PulsarUIComponent.prototype.initDataTables = function () {

    var component = this,
        datatables = this.$html.find('.datatable:not(.table--horizontal)'),
        scrollingDatatables = this.$html.find('.datatable.table--horizontal');

    datatables.each(function() {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  'td.table-selection'
        }

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T>t<"dataTables_bottom"lp>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"t><"dataTables_bottom"lp>';
            select = false;
        }

        $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            buttons: [
                'selectAll',
                'selectNone'
            ],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
                { "searchable": false, "targets": [0, 1] },
                { "orderable": false, "targets": [0, 1] }
            ],
            fixedColumns: true,
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again"
            },
            responsive: {
                details: {
                    type: 'column'
                }
            },
            select: select,
            stateSave: false
        });
    });

     scrollingDatatables.each(function() {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  'td.table-selection'
        };

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T>t<"dataTables_bottom"lp>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"t><"dataTables_bottom"lp>';
            select = false;
        }

        $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            "bScrollCollapse": true,
            buttons: [
                'selectAll',
                'selectNone'
            ],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
                { "searchable": false, "targets": [0, 1] },
                { "orderable": false, "targets": [0, 1] }
            ],
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again"
            },
            scrollX: true,
            select: select,
            stateSave: false
        });

        $this.closest('.dataTables_scrollBody').scroll(function() {
            component.styleTableOverflows();
        });
    });

    // Refresh datatables when tabs are switched, this fixes some layout issues
    this.$html.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
        component.refreshDatatables();
    });

    this.$html.find('.table--horizontal').each(function() {
        var $table = $(this).parent();

        $table.scroll(function() {
            component.styleTableOverflows($table);
        });

        $($window).on('load resize', function () {
            component.styleTableOverflows($table);
        });
    });

    // Refresh datatables when windows resized, makes sure scrolling tables
    // calculate their headers correctly
    // $($window).on('load resize', function () {
    //     component.refreshDatatables();
    //     component.styleTableOverflows();
    // });
}

// PulsarUIComponent.prototype.refreshDatatables = function () {

//     var $datatable = $($.fn.dataTable.tables(true));

//     // Datatables has trouble setting header size correctly, so we'll do it
//     // ourselves
//     if ($datatable.length) {
//         console.log("!");
//         $datatable
//             .closest('.dataTables_scroll')
//             .find('.dataTables_scrollHeadInner, .dataTables_scrollHeadInner .datatable')
//             .width($datatable[0].scrollWidth);
//     }
// }

PulsarUIComponent.prototype.styleTableOverflows = function ($container) {

    var $table = $container.find('.table'),
        tableFullWidth = $table[0].scrollWidth,
        tableVisibleWidth = $container.width();
        console.log(tableFullWidth);
        console.log(tableVisibleWidth);

    // Toggle right hand shadow, if overflowing to the right
    if (tableFullWidth === tableVisibleWidth) {
        console.log('a');
        $container
            .removeClass('table--overflow-right');
    }
    else {
        console.log('b');
        $container.addClass('table--overflow-right');
    }

    // Toggle left hand shadow, if overflowing to the left
    if (($table.offsetParent().offset().left - $table.offset().left) > 0) {
        console.log('c');
        $container.addClass('table--overflow-left');
    }
    else {
        console.log('d');
        $container.removeClass('table--overflow-left');
    }

    // Remove right hand shadow if table scrolled to right hand edge
    if (-Math.abs((tableFullWidth - tableVisibleWidth - $table.offsetParent().offset().left)) >= $table.offset().left) {
        console.log('e');
        $container.removeClass('table--overflow-right');
    }
}

PulsarUIComponent.prototype.initCountdown = function () {

    // Initial basic implementation of https://github.com/hilios/jQuery.countdown
    this.$html.find('.js-countdown').each(function() {

        var $this = $(this),
            format = '%ww %dd %Hh %Mm %S';

        if (typeof $this.data('format') !== 'undefined') {
            format = $this.data('format');
        }

        $this.countdown($this.data('final-date'), function(event) {
            $this.html(event.strftime(format));
        });
    });
}

module.exports = PulsarUIComponent;
<commit_msg>Remove commented code<commit_after>'use strict';

var $ = require('jquery'),
    $window = window;

require('datatables.net')($window, $);
require('datatables.net-buttons')($window, $);
require('datatables.net-responsive')($window, $);
require('datatables.net-select')($window, $);
require('../libs/jquery.countdown/dist/jquery.countdown.min');

function PulsarUIComponent(html, history) {

    this.history = history;
    this.$html = html;

}

PulsarUIComponent.prototype.init = function () {

    var component = this;

    // Stop disabled links from being interactive
    this.$html.on('click', 'a[disabled]', function(e) {
        e.preventDefault();
    });

    // Watch for push-state requests via data-html attribute
    this.$html.on('click', '[data-href]', function(e) {
        var href = $(this).data('href');
        component.history.pushState({state:1}, href, href);
    });

    this.initTables();
    this.initDataTables();
    this.initCountdown();
}

PulsarUIComponent.prototype.initTables = function () {

    // Wrap non datatable tables in responsive container so they can scroll
    // when required
    var $tables = this.$html.find('.table:not(.datatable), .table--datagrid:not(.datatable), .table--horizontal');

    $tables.each(function(i, v) {
        var $table = $(v);

        if (!$table.parent().hasClass('table-container')) {
            $table.wrap('

');
        }
    });
};

PulsarUIComponent.prototype.initDataTables = function () {

    var component = this,
        datatables = this.$html.find('.datatable:not(.table--horizontal)'),
        scrollingDatatables = this.$html.find('.datatable.table--horizontal');

    datatables.each(function() {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  'td.table-selection'
        }

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T>t<"dataTables_bottom"lp>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"t><"dataTables_bottom"lp>';
            select = false;
        }

        $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            buttons: [
                'selectAll',
                'selectNone'
            ],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
                { "searchable": false, "targets": [0, 1] },
                { "orderable": false, "targets": [0, 1] }
            ],
            fixedColumns: true,
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again"
            },
            responsive: {
                details: {
                    type: 'column'
                }
            },
            select: select,
            stateSave: false
        });
    });

     scrollingDatatables.each(function() {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  'td.table-selection'
        };

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T>t<"dataTables_bottom"lp>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"t><"dataTables_bottom"lp>';
            select = false;
        }

        $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            "bScrollCollapse": true,
            buttons: [
                'selectAll',
                'selectNone'
            ],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
                { "searchable": false, "targets": [0, 1] },
                { "orderable": false, "targets": [0, 1] }
            ],
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again"
            },
            scrollX: true,
            select: select,
            stateSave: false
        });

        $this.closest('.dataTables_scrollBody').scroll(function() {
            component.styleTableOverflows();
        });
    });

    // Refresh datatables when tabs are switched, this fixes some layout issues
    this.$html.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
        component.refreshDatatables();
    });

    this.$html.find('.table--horizontal').each(function() {
        var $table = $(this).parent();

        $table.scroll(function() {
            component.styleTableOverflows($table);
        });

        $($window).on('load resize', function () {
            component.styleTableOverflows($table);
        });
    });
};

PulsarUIComponent.prototype.styleTableOverflows = function ($container) {

    var $table = $container.find('.table'),
        tableFullWidth = $table[0].scrollWidth,
        tableVisibleWidth = $container.width();
        console.log(tableFullWidth);
        console.log(tableVisibleWidth);

    // Toggle right hand shadow, if overflowing to the right
    if (tableFullWidth === tableVisibleWidth) {
        console.log('a');
        $container
            .removeClass('table--overflow-right');
    }
    else {
        console.log('b');
        $container.addClass('table--overflow-right');
    }

    // Toggle left hand shadow, if overflowing to the left
    if (($table.offsetParent().offset().left - $table.offset().left) > 0) {
        console.log('c');
        $container.addClass('table--overflow-left');
    }
    else {
        console.log('d');
        $container.removeClass('table--overflow-left');
    }

    // Remove right hand shadow if table scrolled to right hand edge
    if (-Math.abs((tableFullWidth - tableVisibleWidth - $table.offsetParent().offset().left)) >= $table.offset().left) {
        console.log('e');
        $container.removeClass('table--overflow-right');
    }
};

PulsarUIComponent.prototype.initCountdown = function () {

    // Initial basic implementation of https://github.com/hilios/jQuery.countdown
    this.$html.find('.js-countdown').each(function() {

        var $this = $(this),
            format = '%ww %dd %Hh %Mm %S';

        if (typeof $this.data('format') !== 'undefined') {
            format = $this.data('format');
        }

        $this.countdown($this.data('final-date'), function(event) {
            $this.html(event.strftime(format));
        });
    });
};

module.exports = PulsarUIComponent;
<|endoftext|>


<commit_before>'use strict';

// https://codelabs.developers.google.com/codelabs/webrtc-web/#4
var lv = document.getElementById("local");

navigator.webkitGetUserMedia({
    video:true,audio:false
},function(stream){
    lv.src = window.URL.createObjectURL(stream);
    console.log("[navigator.webkitGetUserMedia] lv.src=localStream " + lv.src);

    rtcCall(stream);
}, function(error){
    console.error(error);
});

function copyObject(obj) {
    var cp = {};
    for (var key in obj) {
        cp[key] = obj[key];
    }
    return cp;
}

function rtcCall(localStream) {
    // local peer connection setup.
    var pcLocal = new window.webkitRTCPeerConnection(null);
    pcLocal.onicecandidate = function(e) {
        e.candidate && console.log("[pcLocal.onicecandidate] " + e.candidate.candidate);
        e.candidate && pcRemote.addIceCandidate(new window.RTCIceCandidate(e.candidate));
    };

    // remote peer connection setup.
    var pcRemote = new window.webkitRTCPeerConnection(null);
    pcRemote.onicecandidate = function(e) {
        e.candidate && console.log("[pcRemote.onicecandidate] " + e.candidate.candidate);
        e.candidate && pcLocal.addIceCandidate(new window.RTCIceCandidate(e.candidate));
    };

    pcRemote.onaddstream = function(e) {
        var rv = document.getElementById("remote");
        rv.src = window.URL.createObjectURL(e.stream);
        console.log("[pcRemote.onaddstream] rv.src=remoteStream " + rv.src);
    };

    // trigger connection between local and remote peer.
    pcLocal.addStream(localStream);
    console.log("[pcLocal.addStream] add localStream to peer connection");

    pcLocal.createOffer(function(offer){
        pcLocal.setLocalDescription(offer); // trigger pcLocal.onicecandidate().
        console.log("[pcLocal.createOffer] Request with offer " + offer.sdp.length + "B sdp as bellow:");
        console.log(offer);

        transmitOffer(offer);
    }, function(error){
        console.error(error);
    });

    // Local transmit offer to remote.
    var transmitOffer = function(offer) {
        offer = copyObject(offer); // To demonstrate offer transmit over network in JSON.
        setTimeout(onRemoteGotOffer, 200, offer);
    };
    var onRemoteGotOffer = function(offer) {
        pcRemote.setRemoteDescription(offer);
        console.log("[onRemoteGotOffer] Got offer " + offer.sdp.length + "B sdp as bellow:");
        console.log(offer);

        // Since the 'remote' side has no media stream we need
        // to pass in the right constraints in order for it to
        // accept the incoming offer of audio and video.
        pcRemote.createAnswer(function(answer){
            pcRemote.setLocalDescription(answer); // trigger pcRemote.onicecandiate().
            console.log("[pcRemote.createAnswer] Response offer=" + offer.sdp.length + "B answer " + answer.sdp.length + "B sdp as bellow:");
            console.log(answer);

            transmitAnswer(answer);
        }, function(error){
            console.error(error);
        });
    };

    // Remote transmit answer to local.
    var transmitAnswer = function(answer) {
        answer = copyObject(answer); // To demonstrate answer transmit over network in JSON.
        setTimeout(onLocalGotAnswer, 200, answer);
    };
    var onLocalGotAnswer = function(answer) {
        pcLocal.setRemoteDescription(answer);
        console.log("[pcRemote.createAnswer] Got answer " + answer.sdp.length + "B sdp as bellow:");
        console.log(answer);
    };
}
<commit_msg>Refine code to transmit candidate.<commit_after>'use strict';

// https://codelabs.developers.google.com/codelabs/webrtc-web/#4
var lv = document.getElementById("local");

navigator.webkitGetUserMedia({
    video:true,audio:false
},function(stream){
    lv.src = window.URL.createObjectURL(stream);
    console.log("[navigator.webkitGetUserMedia] lv.src=localStream " + lv.src);

    rtcCall(stream);
}, function(error){
    console.error(error);
});

function copyObject(obj) {
    var cp = {};
    for (var key in obj) {
        cp[key] = obj[key];
    }
    return cp;
}

function rtcCall(localStream) {
    // local peer connection setup.
    var pcLocal = new window.webkitRTCPeerConnection(null);
    pcLocal.onicecandidate = function(e) {
        if (!e.candidate) {
            return;
        }
        console.log("[pcLocal.onicecandidate] " + e.candidate.candidate);
        transmitCandidateTo(e.candidate, pcRemote);
    };

    // remote peer connection setup.
    var pcRemote = new window.webkitRTCPeerConnection(null);
    pcRemote.onicecandidate = function(e) {
        if (!e.candidate) {
            return;
        }
        console.log("[pcRemote.onicecandidate] " + e.candidate.candidate);
        transmitCandidateTo(e.candidate, pcLocal);
    };

    var transmitCandidateTo = function(candidate, target) {
        candidate = copyObject(candidate); // To demonstrate candidate transmit over network in JSON.
        target.addIceCandidate(new window.RTCIceCandidate(candidate));
    };

    pcRemote.onaddstream = function(e) {
        var rv = document.getElementById("remote");
        rv.src = window.URL.createObjectURL(e.stream);
        console.log("[pcRemote.onaddstream] rv.src=remoteStream " + rv.src);
    };

    // trigger connection between local and remote peer.
    pcLocal.addStream(localStream);
    console.log("[pcLocal.addStream] add localStream to peer connection");

    pcLocal.createOffer(function(offer){
        pcLocal.setLocalDescription(offer); // trigger pcLocal.onicecandidate().
        console.log("[pcLocal.createOffer] Request with offer " + offer.sdp.length + "B sdp as bellow:");
        console.log(offer);

        transmitOffer(offer);
    }, function(error){
        console.error(error);
    });

    // Local transmit offer to remote.
    var transmitOffer = function(offer) {
        offer = copyObject(offer); // To demonstrate offer transmit over network in JSON.
        setTimeout(onRemoteGotOffer, 200, offer);
    };
    var onRemoteGotOffer = function(offer) {
        pcRemote.setRemoteDescription(offer);
        console.log("[onRemoteGotOffer] Got offer " + offer.sdp.length + "B sdp as bellow:");
        console.log(offer);

        // Since the 'remote' side has no media stream we need
        // to pass in the right constraints in order for it to
        // accept the incoming offer of audio and video.
        pcRemote.createAnswer(function(answer){
            pcRemote.setLocalDescription(answer); // trigger pcRemote.onicecandiate().
            console.log("[pcRemote.createAnswer] Response offer=" + offer.sdp.length + "B answer " + answer.sdp.length + "B sdp as bellow:");
            console.log(answer);

            transmitAnswer(answer);
        }, function(error){
            console.error(error);
        });
    };

    // Remote transmit answer to local.
    var transmitAnswer = function(answer) {
        answer = copyObject(answer); // To demonstrate answer transmit over network in JSON.
        setTimeout(onLocalGotAnswer, 200, answer);
    };
    var onLocalGotAnswer = function(answer) {
        pcLocal.setRemoteDescription(answer);
        console.log("[pcRemote.createAnswer] Got answer " + answer.sdp.length + "B sdp as bellow:");
        console.log(answer);
    };
}
<|endoftext|>


<commit_before>// Copyright 2019-2021, University of Colorado Boulder

/**
 * An XYChartNode that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 *
 * @deprecated - please use bamboo
 */

import Utils from '../../dot/js/Utils.js';
import deprecationWarning from '../../phet-core/js/deprecationWarning.js';
import merge from '../../phet-core/js/merge.js';
import ArrowNode from '../../scenery-phet/js/ArrowNode.js';
import { DragListener } from '../../scenery/js/imports.js';
import { Circle } from '../../scenery/js/imports.js';
import { Node } from '../../scenery/js/imports.js';
import { Rectangle } from '../../scenery/js/imports.js';
import { Color } from '../../scenery/js/imports.js';
import Tandem from '../../tandem/js/Tandem.js';
import griddle from './griddle.js';
import XYChartNode from './XYChartNode.js';

// constants
const WIDTH_PROPORTION = 0.013; // empirically determined
const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
const CURSOR_STROKE_COLOR = Color.DARK_GRAY;
const ARROW_CUE_FILL_COLOR = new Color( 180, 180, 230 );
const ARROW_CUE_STROKE_COLOR = Color.DARK_GRAY;

class XYCursorChartNode extends XYChartNode {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {
    assert && deprecationWarning( 'Please use bamboo' );

    options = merge( {

      // options passed on to the cursor, see ChartCursor
      cursorOptions: null,

      chartPanelOptions: {

        // so that the cursor is draggable
        pickable: true
      },

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    super( options );

    // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
    this._cursorVisibleOverride = null;

    // @private {number} - value for the cursor, determines cursor positioning
    this.cursorValue = 0;

    // @public (read-only) - minimum and maximum recorded value, required by the cursor to limit dragging
    this.minRecordedXValue = 0;
    this.maxRecordedXValue = 0;

    // @private Map.<DynamicSeries,function> Keep track of the listener for each series, so the listener can be removed
    // when a series is removed.
    this.dynamicSeriesListenerMap = new Map();

    // @protected (read-only) {DynamicSeries[]}
    this.dynamicSeriesArray = [];

    // @private {ChartCursor} - draggable Node that shows the cursor value
    this.chartCursor = new ChartCursor( this, this.modelViewTransformProperty, options.cursorOptions );
    this.chartPanel.addChild( this.chartCursor );

    // initialize position and visibility of the cursor
    this.updateCursor();
  }

  /**
   * Adds a DynamicSeries to the XYCursorChartNode.
   * @override
   * @public
   *
   * @param {DynamicSeries} dynamicSeries
   */
  addDynamicSeries( dynamicSeries ) {
    super.addDynamicSeries( dynamicSeries );

    this.dynamicSeriesArray.push( dynamicSeries );

    // when a point is added, update the min and max recorded values
    const dynamicSeriesListener = () => {

      // update the
      this.updateMinMaxRecordedValues();

      // if all data has been removed from the chart, update cursor visibility
      this.updateCursor();
    };

    // save to map so that listener can be found again for disposal
    this.dynamicSeriesListenerMap.set( dynamicSeries, dynamicSeriesListener );
    dynamicSeries.addDynamicSeriesListener( dynamicSeriesListener );
  }

  /**
   * Removes a DynamicSeries from the chart and disposes of its listener.
   * @override
   * @public
   *
   * @param {DynamicSeries} dynamicSeries - to remove
   */
  removeDynamicSeries( dynamicSeries ) {
    super.removeDynamicSeries( dynamicSeries );

    const seriesIndex = this.dynamicSeriesArray.indexOf( dynamicSeries );
    this.dynamicSeriesArray.splice( seriesIndex, 1 );

    dynamicSeries.removeDynamicSeriesListener( this.dynamicSeriesListenerMap.get( dynamicSeries ) );
    this.dynamicSeriesListenerMap.delete( dynamicSeries );
  }

  /**
   * Sets the cursor value. The value of the cursor is constrained to be within chart bounds.
   * @public
   *
   * @param {number} value
   */
  setCursorValue( value ) {
    const modelViewTransform = this.modelViewTransformProperty.get();
    const minX = modelViewTransform.viewToModelX( 0 );
    const maxX = modelViewTransform.viewToModelX( this.chartWidth + this.chartCursor.width / 2 );
    this.cursorValue = Utils.clamp( value, minX, maxX );
    this.updateCursor();
  }

  /**
   * Gets the value currently under the cursor.
   * @public
   *
   * @returns {number}
   */
  getCursorValue() {
    return this.cursorValue;
  }

  /**
   * Resets the cursor.
   * @public
   */
  resetCursor() {
    this.chartCursor.reset();
  }

  /**
   * Overrides the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
   * described in updateCursorVisibility. Otherwise, visibility will equal the boolean value set here.
   * @param {boolean|null} visible
   * @public
   */
  setCursorVisibleOverride( visible ) {
    assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
    this._cursorVisibleOverride = visible;
    this.updateCursorVisibility();
  }

  /**
   * Updates the cursor visibility and position.
   * @private
   */
  updateCursor() {
    this.updateCursorVisibility();
    if ( this.chartCursor.isVisible() ) {
      this.updateCursorPosition();
    }
  }

  /**
   * Updates the cursor position.
   * @private
   */
  updateCursorPosition() {
    this.moveCursorToValue( this.cursorValue );
  }

  /**
   * Updates the cursor visibility. The cursor should be visible any time the cursor value is within
   * the recorded value range.
   * @private
   */
  updateCursorVisibility() {

    const wasVisible = this.chartCursor.visible;
    if ( typeof this._cursorVisibleOverride === 'boolean' ) {
      this.chartCursor.setVisible( this._cursorVisibleOverride );
    }
    else {

      const maxX = this.modelViewTransformProperty.get().viewToModelX( this.chartWidth + this.chartCursor.width / 2 );
      const minX = this.modelViewTransformProperty.get().viewToModelX( 0 );

      const isCurrentValueOnChart = ( this.cursorValue >= minX ) && ( this.cursorValue <= maxX );
      const hasData = this.hasData();
      const chartCursorVisible = isCurrentValueOnChart && hasData;

      this.chartCursor.setVisible( chartCursorVisible );
    }

    // if the cursor just became invisible, interrupt any active dragging
    if ( !this.chartCursor.visible && wasVisible ) {
      this.chartCursor.interruptDrag();
    }
  }

  /**
   * Returns true if any DynamicSeries associated with this chart has data.
   * @returns {boolean}
   * @public
   */
  hasData() {
    return _.some( this.dynamicSeriesArray, dynamicSeries => dynamicSeries.hasData() );
  }

  /**
   * Moves the cursor to the specified value.
   * @private
   *
   * @param {number} value
   */
  moveCursorToValue( value ) {
    const viewPosition = this.modelViewTransformProperty.get().modelToViewX( value );

    // keep the cursor within the grid bounds
    this.chartCursor.centerX = Utils.clamp( viewPosition, 0, this.chartWidth );
    this.chartCursor.centerY = this.gridNode.centerY;
  }

  /**
   * From the existing data, update the min and max recorded X values from all of the dynamicSeries of this chart,
   * so that the cursor can be limited to the recorded data.
   * @private
   */
  updateMinMaxRecordedValues() {
    let minValue = Number.POSITIVE_INFINITY;
    let maxValue = Number.NEGATIVE_INFINITY;
    this.dynamicSeriesArray.forEach( dynamicSeries => {
      if ( dynamicSeries.getLength() > 0 ) {
        const seriesMinValue = dynamicSeries.getDataPoint( 0 ).x;
        const seriesMaxValue = dynamicSeries.getDataPoint( dynamicSeries.getLength() - 1 ).x;
        if ( seriesMinValue < minValue ) {
          minValue = seriesMinValue;
        }
        if ( seriesMaxValue > maxValue ) {
          maxValue = seriesMaxValue;
        }
      }
    } );

    this.minRecordedXValue = minValue;
    this.maxRecordedXValue = maxValue;
  }
}

/**
 * ChartCursor is a rectangular cursor that indicates the current or selected value on the chart.
 */
class ChartCursor extends Rectangle {

  /**
   * @param {XYCursorChartNode} chart
   * @param {Property.<ModelViewTransform2>} modelViewTransformProperty
   * @param {Object} [options]
   */
  constructor( chart, modelViewTransformProperty, options ) {

    options = merge( {
      startDrag: () => {},
      endDrag: () => {},
      drag: () => {},

      // {boolean} - if true, a double headed arrow will be shown to indicate
      // that the cursor is draggable - becomes invisible after first
      // drag
      includeDragCue: false,

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    const width = chart.chartWidth * WIDTH_PROPORTION;
    const height = chart.chartHeight;

    // Set the shape. Origin is at the center top of the rectangle.
    super( 0, -height, width, height, 0, 0, {
      cursor: 'ew-resize',
      fill: CURSOR_FILL_COLOR,
      stroke: CURSOR_STROKE_COLOR,
      lineWidth: 0.4,
      lineDash: [ 4, 4 ]
    } );

    // @private
    this.chart = chart;

    // @public
    this.modelViewTransformProperty = modelViewTransformProperty;

    // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
    this.mouseArea = this.localBounds.dilatedX( 12 );
    this.touchArea = this.localBounds.dilatedX( 12 );

    // @private
    this.includeDragCue = options.includeDragCue;

    if ( this.includeDragCue ) {

      // @private - indicates to the user that the cursor is draggable, only created
      // and added if necessary
      this.dragCueArrowNode = new ArrowNode( -width * 2, 0, width * 2, 0, {
        doubleHead: true,
        headWidth: 12,
        headHeight: 10,
        fill: ARROW_CUE_FILL_COLOR,
        stroke: ARROW_CUE_STROKE_COLOR,
        center: this.center.plusXY( 0, height * 0.4 )
      } );

      this.addChild( this.dragCueArrowNode );
    }

    // Add the indentations that are intended to convey the idea of "gripability".
    const grippyNode = new Node();
    const indentSpacing = height * 0.05;
    for ( let i = 0; i < 3; i++ ) {
      const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
      indentNode.top = i * ( indentNode.height + indentSpacing );
      grippyNode.addChild( indentNode );
    }
    grippyNode.center = this.center;
    this.addChild( grippyNode );

    // @private - so that we can interrupt the DragListener if necessary
    this.dragListener = new DragListener( {
      start: ( event, listener ) => {
        assert && assert( this.chart.hasData(), 'chart should have data for the cursor to be draggable' );
        options.startDrag();
      },
      drag: ( event, listener ) => {
        const parentX = listener.parentPoint.x;
        let newValue = this.modelViewTransformProperty.get().viewToModelX( parentX );

        // limit cursor to the domain of recorded values
        newValue = Utils.clamp( newValue, this.chart.minRecordedXValue, this.chart.maxRecordedXValue );
        this.chart.setCursorValue( newValue );

        options.drag();
      },
      end: ( event, listener ) => {
        options.endDrag();

        // no need to show arrow after user successfully drags the cursor
        if ( this.includeDragCue ) {
          this.dragCueArrowNode.visible = false;
        }
      },
      tandem: options.tandem.createTandem( 'dragListener' )
    } );
    this.addInputListener( this.dragListener );
  }

  /**
   * Interrupts dragging of the cursor, useful when ChartCursor visibility changes.
   * @public
   */
  interruptDrag() {
    this.dragListener.interrupt();
  }

  /**
   * Resets the ChartCursor to its initial state. Note that this does not modify data
   * or the cursor position (cursorValue), only aspects of the view for the cursor itself.
   * @public
   */
  reset() {
    if ( this.includeDragCue ) {
      this.dragCueArrowNode.visible = true;
    }
  }
}

/**
 * GrippyIndentNode is a small round indentation on a surface.  This is a modern user interface paradigm that
 * is intended to convey the concept of "gripability", i.e. something that the user can grab.  This is meant to
 * look somewhat 3D, much like etched borders do.
 */
class GrippyIndentNode extends Circle {

  /**
   * @param {number} diameter
   * @param {Color} baseColor
   * @param {Object} [options]
   */
  constructor( diameter, baseColor, options ) {

    options = merge( {
      lineWidth: 0.5
    }, options );

    const baseDarkerColor = baseColor.darkerColor( 0.9 );
    const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
      baseDarkerColor.getBlue(), baseColor.getAlpha() );
    const baseLighterColor = baseColor.brighterColor( 0.9 );
    const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
      baseLighterColor.getBlue(), baseColor.getAlpha() );
    const radius = diameter / 2 - options.lineWidth;

    super( radius, {
      fill: translucentDarkerColor,
      stroke: translucentBrighterColor,
      lineWidth: options.lineWidth
    } );
  }
}

griddle.register( 'XYCursorChartNode', XYCursorChartNode );
export default XYCursorChartNode;<commit_msg>DynamicProperty => TypeScript + improving DragListener end consistency with an event. See https://github.com/phetsims/scenery-phet/issues/726<commit_after>// Copyright 2019-2021, University of Colorado Boulder

/**
 * An XYChartNode that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 *
 * @deprecated - please use bamboo
 */

import Utils from '../../dot/js/Utils.js';
import deprecationWarning from '../../phet-core/js/deprecationWarning.js';
import merge from '../../phet-core/js/merge.js';
import ArrowNode from '../../scenery-phet/js/ArrowNode.js';
import { DragListener } from '../../scenery/js/imports.js';
import { Circle } from '../../scenery/js/imports.js';
import { Node } from '../../scenery/js/imports.js';
import { Rectangle } from '../../scenery/js/imports.js';
import { Color } from '../../scenery/js/imports.js';
import Tandem from '../../tandem/js/Tandem.js';
import griddle from './griddle.js';
import XYChartNode from './XYChartNode.js';

// constants
const WIDTH_PROPORTION = 0.013; // empirically determined
const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
const CURSOR_STROKE_COLOR = Color.DARK_GRAY;
const ARROW_CUE_FILL_COLOR = new Color( 180, 180, 230 );
const ARROW_CUE_STROKE_COLOR = Color.DARK_GRAY;

class XYCursorChartNode extends XYChartNode {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {
    assert && deprecationWarning( 'Please use bamboo' );

    options = merge( {

      // options passed on to the cursor, see ChartCursor
      cursorOptions: null,

      chartPanelOptions: {

        // so that the cursor is draggable
        pickable: true
      },

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    super( options );

    // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
    this._cursorVisibleOverride = null;

    // @private {number} - value for the cursor, determines cursor positioning
    this.cursorValue = 0;

    // @public (read-only) - minimum and maximum recorded value, required by the cursor to limit dragging
    this.minRecordedXValue = 0;
    this.maxRecordedXValue = 0;

    // @private Map.<DynamicSeries,function> Keep track of the listener for each series, so the listener can be removed
    // when a series is removed.
    this.dynamicSeriesListenerMap = new Map();

    // @protected (read-only) {DynamicSeries[]}
    this.dynamicSeriesArray = [];

    // @private {ChartCursor} - draggable Node that shows the cursor value
    this.chartCursor = new ChartCursor( this, this.modelViewTransformProperty, options.cursorOptions );
    this.chartPanel.addChild( this.chartCursor );

    // initialize position and visibility of the cursor
    this.updateCursor();
  }

  /**
   * Adds a DynamicSeries to the XYCursorChartNode.
   * @override
   * @public
   *
   * @param {DynamicSeries} dynamicSeries
   */
  addDynamicSeries( dynamicSeries ) {
    super.addDynamicSeries( dynamicSeries );

    this.dynamicSeriesArray.push( dynamicSeries );

    // when a point is added, update the min and max recorded values
    const dynamicSeriesListener = () => {

      // update the
      this.updateMinMaxRecordedValues();

      // if all data has been removed from the chart, update cursor visibility
      this.updateCursor();
    };

    // save to map so that listener can be found again for disposal
    this.dynamicSeriesListenerMap.set( dynamicSeries, dynamicSeriesListener );
    dynamicSeries.addDynamicSeriesListener( dynamicSeriesListener );
  }

  /**
   * Removes a DynamicSeries from the chart and disposes of its listener.
   * @override
   * @public
   *
   * @param {DynamicSeries} dynamicSeries - to remove
   */
  removeDynamicSeries( dynamicSeries ) {
    super.removeDynamicSeries( dynamicSeries );

    const seriesIndex = this.dynamicSeriesArray.indexOf( dynamicSeries );
    this.dynamicSeriesArray.splice( seriesIndex, 1 );

    dynamicSeries.removeDynamicSeriesListener( this.dynamicSeriesListenerMap.get( dynamicSeries ) );
    this.dynamicSeriesListenerMap.delete( dynamicSeries );
  }

  /**
   * Sets the cursor value. The value of the cursor is constrained to be within chart bounds.
   * @public
   *
   * @param {number} value
   */
  setCursorValue( value ) {
    const modelViewTransform = this.modelViewTransformProperty.get();
    const minX = modelViewTransform.viewToModelX( 0 );
    const maxX = modelViewTransform.viewToModelX( this.chartWidth + this.chartCursor.width / 2 );
    this.cursorValue = Utils.clamp( value, minX, maxX );
    this.updateCursor();
  }

  /**
   * Gets the value currently under the cursor.
   * @public
   *
   * @returns {number}
   */
  getCursorValue() {
    return this.cursorValue;
  }

  /**
   * Resets the cursor.
   * @public
   */
  resetCursor() {
    this.chartCursor.reset();
  }

  /**
   * Overrides the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
   * described in updateCursorVisibility. Otherwise, visibility will equal the boolean value set here.
   * @param {boolean|null} visible
   * @public
   */
  setCursorVisibleOverride( visible ) {
    assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
    this._cursorVisibleOverride = visible;
    this.updateCursorVisibility();
  }

  /**
   * Updates the cursor visibility and position.
   * @private
   */
  updateCursor() {
    this.updateCursorVisibility();
    if ( this.chartCursor.isVisible() ) {
      this.updateCursorPosition();
    }
  }

  /**
   * Updates the cursor position.
   * @private
   */
  updateCursorPosition() {
    this.moveCursorToValue( this.cursorValue );
  }

  /**
   * Updates the cursor visibility. The cursor should be visible any time the cursor value is within
   * the recorded value range.
   * @private
   */
  updateCursorVisibility() {

    const wasVisible = this.chartCursor.visible;
    if ( typeof this._cursorVisibleOverride === 'boolean' ) {
      this.chartCursor.setVisible( this._cursorVisibleOverride );
    }
    else {

      const maxX = this.modelViewTransformProperty.get().viewToModelX( this.chartWidth + this.chartCursor.width / 2 );
      const minX = this.modelViewTransformProperty.get().viewToModelX( 0 );

      const isCurrentValueOnChart = ( this.cursorValue >= minX ) && ( this.cursorValue <= maxX );
      const hasData = this.hasData();
      const chartCursorVisible = isCurrentValueOnChart && hasData;

      this.chartCursor.setVisible( chartCursorVisible );
    }

    // if the cursor just became invisible, interrupt any active dragging
    if ( !this.chartCursor.visible && wasVisible ) {
      this.chartCursor.interruptDrag();
    }
  }

  /**
   * Returns true if any DynamicSeries associated with this chart has data.
   * @returns {boolean}
   * @public
   */
  hasData() {
    return _.some( this.dynamicSeriesArray, dynamicSeries => dynamicSeries.hasData() );
  }

  /**
   * Moves the cursor to the specified value.
   * @private
   *
   * @param {number} value
   */
  moveCursorToValue( value ) {
    const viewPosition = this.modelViewTransformProperty.get().modelToViewX( value );

    // keep the cursor within the grid bounds
    this.chartCursor.centerX = Utils.clamp( viewPosition, 0, this.chartWidth );
    this.chartCursor.centerY = this.gridNode.centerY;
  }

  /**
   * From the existing data, update the min and max recorded X values from all of the dynamicSeries of this chart,
   * so that the cursor can be limited to the recorded data.
   * @private
   */
  updateMinMaxRecordedValues() {
    let minValue = Number.POSITIVE_INFINITY;
    let maxValue = Number.NEGATIVE_INFINITY;
    this.dynamicSeriesArray.forEach( dynamicSeries => {
      if ( dynamicSeries.getLength() > 0 ) {
        const seriesMinValue = dynamicSeries.getDataPoint( 0 ).x;
        const seriesMaxValue = dynamicSeries.getDataPoint( dynamicSeries.getLength() - 1 ).x;
        if ( seriesMinValue < minValue ) {
          minValue = seriesMinValue;
        }
        if ( seriesMaxValue > maxValue ) {
          maxValue = seriesMaxValue;
        }
      }
    } );

    this.minRecordedXValue = minValue;
    this.maxRecordedXValue = maxValue;
  }
}

/**
 * ChartCursor is a rectangular cursor that indicates the current or selected value on the chart.
 */
class ChartCursor extends Rectangle {

  /**
   * @param {XYCursorChartNode} chart
   * @param {Property.<ModelViewTransform2>} modelViewTransformProperty
   * @param {Object} [options]
   */
  constructor( chart, modelViewTransformProperty, options ) {

    options = merge( {
      startDrag: () => {},
      endDrag: () => {},
      drag: () => {},

      // {boolean} - if true, a double headed arrow will be shown to indicate
      // that the cursor is draggable - becomes invisible after first
      // drag
      includeDragCue: false,

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    const width = chart.chartWidth * WIDTH_PROPORTION;
    const height = chart.chartHeight;

    // Set the shape. Origin is at the center top of the rectangle.
    super( 0, -height, width, height, 0, 0, {
      cursor: 'ew-resize',
      fill: CURSOR_FILL_COLOR,
      stroke: CURSOR_STROKE_COLOR,
      lineWidth: 0.4,
      lineDash: [ 4, 4 ]
    } );

    // @private
    this.chart = chart;

    // @public
    this.modelViewTransformProperty = modelViewTransformProperty;

    // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
    this.mouseArea = this.localBounds.dilatedX( 12 );
    this.touchArea = this.localBounds.dilatedX( 12 );

    // @private
    this.includeDragCue = options.includeDragCue;

    if ( this.includeDragCue ) {

      // @private - indicates to the user that the cursor is draggable, only created
      // and added if necessary
      this.dragCueArrowNode = new ArrowNode( -width * 2, 0, width * 2, 0, {
        doubleHead: true,
        headWidth: 12,
        headHeight: 10,
        fill: ARROW_CUE_FILL_COLOR,
        stroke: ARROW_CUE_STROKE_COLOR,
        center: this.center.plusXY( 0, height * 0.4 )
      } );

      this.addChild( this.dragCueArrowNode );
    }

    // Add the indentations that are intended to convey the idea of "gripability".
    const grippyNode = new Node();
    const indentSpacing = height * 0.05;
    for ( let i = 0; i < 3; i++ ) {
      const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
      indentNode.top = i * ( indentNode.height + indentSpacing );
      grippyNode.addChild( indentNode );
    }
    grippyNode.center = this.center;
    this.addChild( grippyNode );

    // @private - so that we can interrupt the DragListener if necessary
    this.dragListener = new DragListener( {
      start: ( event, listener ) => {
        assert && assert( this.chart.hasData(), 'chart should have data for the cursor to be draggable' );
        options.startDrag();
      },
      drag: ( event, listener ) => {
        const parentX = listener.parentPoint.x;
        let newValue = this.modelViewTransformProperty.get().viewToModelX( parentX );

        // limit cursor to the domain of recorded values
        newValue = Utils.clamp( newValue, this.chart.minRecordedXValue, this.chart.maxRecordedXValue );
        this.chart.setCursorValue( newValue );

        options.drag();
      },
      end: () => {
        options.endDrag();

        // no need to show arrow after user successfully drags the cursor
        if ( this.includeDragCue ) {
          this.dragCueArrowNode.visible = false;
        }
      },
      tandem: options.tandem.createTandem( 'dragListener' )
    } );
    this.addInputListener( this.dragListener );
  }

  /**
   * Interrupts dragging of the cursor, useful when ChartCursor visibility changes.
   * @public
   */
  interruptDrag() {
    this.dragListener.interrupt();
  }

  /**
   * Resets the ChartCursor to its initial state. Note that this does not modify data
   * or the cursor position (cursorValue), only aspects of the view for the cursor itself.
   * @public
   */
  reset() {
    if ( this.includeDragCue ) {
      this.dragCueArrowNode.visible = true;
    }
  }
}

/**
 * GrippyIndentNode is a small round indentation on a surface.  This is a modern user interface paradigm that
 * is intended to convey the concept of "gripability", i.e. something that the user can grab.  This is meant to
 * look somewhat 3D, much like etched borders do.
 */
class GrippyIndentNode extends Circle {

  /**
   * @param {number} diameter
   * @param {Color} baseColor
   * @param {Object} [options]
   */
  constructor( diameter, baseColor, options ) {

    options = merge( {
      lineWidth: 0.5
    }, options );

    const baseDarkerColor = baseColor.darkerColor( 0.9 );
    const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
      baseDarkerColor.getBlue(), baseColor.getAlpha() );
    const baseLighterColor = baseColor.brighterColor( 0.9 );
    const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
      baseLighterColor.getBlue(), baseColor.getAlpha() );
    const radius = diameter / 2 - options.lineWidth;

    super( radius, {
      fill: translucentDarkerColor,
      stroke: translucentBrighterColor,
      lineWidth: options.lineWidth
    } );
  }
}

griddle.register( 'XYCursorChartNode', XYCursorChartNode );
export default XYCursorChartNode;<|endoftext|>


<commit_before>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * Class handling creation and teardown of a remoting client session.
 *
 * This abstracts a <embed> element and controls the plugin which does the
 * actual remoting work.  There should be no UI code inside this class.  It
 * should be purely thought of as a controller of sorts.
 */

'use strict';

/** @suppress {duplicate} */
var remoting = remoting || {};

/**
 * @param {string} hostJid The jid of the host to connect to.
 * @param {string} hostPublicKey The base64 encoded version of the host's
 *     public key.
 * @param {string} accessCode The access code for the IT2Me connection.
 * @param {string} email The username for the talk network.
 * @param {function(remoting.ClientSession.State,
                    remoting.ClientSession.State):void} onStateChange
 *     The callback to invoke when the session changes state.
 * @constructor
 */
remoting.ClientSession = function(hostJid, hostPublicKey, accessCode, email,
                                  onStateChange) {
  this.state = remoting.ClientSession.State.CREATED;

  this.hostJid = hostJid;
  this.hostPublicKey = hostPublicKey;
  this.accessCode = accessCode;
  this.email = email;
  this.clientJid = '';
  this.sessionId = '';
  /** @type {remoting.ViewerPlugin} */ this.plugin = null;
  this.logToServer = new remoting.LogToServer();
  this.onStateChange = onStateChange;
};

// Note that the positive values in both of these enums are copied directly
// from chromoting_scriptable_object.h and must be kept in sync. The negative
// values represent states transitions that occur within the web-app that have
// no corresponding plugin state transition.
/** @enum {number} */
remoting.ClientSession.State = {
  CREATED: -3,
  BAD_PLUGIN_VERSION: -2,
  UNKNOWN_PLUGIN_ERROR: -1,
  UNKNOWN: 0,
  CONNECTING: 1,
  INITIALIZING: 2,
  CONNECTED: 3,
  CLOSED: 4,
  CONNECTION_FAILED: 5
};

/** @enum {number} */
remoting.ClientSession.ConnectionError = {
  NONE: 0,
  HOST_IS_OFFLINE: 1,
  SESSION_REJECTED: 2,
  INCOMPATIBLE_PROTOCOL: 3,
  NETWORK_FAILURE: 4
};

/**
 * The current state of the session.
 * @type {remoting.ClientSession.State}
 */
remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN;

/**
 * The last connection error. Set when state is set to CONNECTION_FAILED.
 * @type {remoting.ClientSession.ConnectionError}
 */
remoting.ClientSession.prototype.error =
    remoting.ClientSession.ConnectionError.NONE;

/**
 * Chromoting session API version (for this javascript).
 * This is compared with the plugin API version to verify that they are
 * compatible.
 *
 * @const
 * @private
 */
remoting.ClientSession.prototype.API_VERSION_ = 2;

/**
 * The oldest API version that we support.
 * This will differ from the |API_VERSION_| if we maintain backward
 * compatibility with older API versions.
 *
 * @const
 * @private
 */
remoting.ClientSession.prototype.API_MIN_VERSION_ = 1;

/**
 * Server used to bridge into the Jabber network for establishing Jingle
 * connections.
 *
 * @const
 * @private
 */
remoting.ClientSession.prototype.HTTP_XMPP_PROXY_ =
    'https://chromoting-httpxmpp-oauth2-dev.corp.google.com';

/**
 * The id of the client plugin
 *
 * @const
 */
remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';

/**
 * Callback to invoke when the state is changed.
 *
 * @param {remoting.ClientSession.State} oldState The previous state.
 * @param {remoting.ClientSession.State} newState The current state.
 */
remoting.ClientSession.prototype.onStateChange =
    function(oldState, newState) { };

/**
 * Adds <embed> element to |container| and readies the sesion object.
 *
 * @param {Element} container The element to add the plugin to.
 * @param {string} oauth2AccessToken A valid OAuth2 access token.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.createPluginAndConnect =
    function(container, oauth2AccessToken) {
  this.plugin = /** @type {remoting.ViewerPlugin} */
      document.createElement('embed');
  this.plugin.id = this.PLUGIN_ID;
  this.plugin.src = 'about://none';
  this.plugin.type = 'pepper-application/x-chromoting';
  this.plugin.width = 0;
  this.plugin.height = 0;
  container.appendChild(this.plugin);

  if (!this.isPluginVersionSupported_(this.plugin)) {
    // TODO(ajwong): Remove from parent.
    delete this.plugin;
    this.setState_(remoting.ClientSession.State.BAD_PLUGIN_VERSION);
    return;
  }

  /** @type {remoting.ClientSession} */ var that = this;
  /** @param {string} msg The IQ stanza to send. */
  this.plugin.sendIq = function(msg) { that.sendIq_(msg); };
  /** @param {string} msg The message to log. */
  this.plugin.debugInfo = function(msg) {
    remoting.debug.log('plugin: ' + msg);
  };

  // TODO(ajwong): Is it even worth having this class handle these events?
  // Or would it be better to just allow users to pass in their own handlers
  // and leave these blank by default?
  /**
   * @param {number} status The plugin status.
   * @param {number} error The plugin error status, if any.
   */
  this.plugin.connectionInfoUpdate = function(status, error) {
    that.connectionInfoUpdateCallback(status, error);
  };
  this.plugin.desktopSizeUpdate = function() { that.onDesktopSizeChanged_(); };

  // TODO(garykac): Clean exit if |connect| isn't a function.
  if (typeof this.plugin.connect === 'function') {
    this.connectPluginToWcs_(oauth2AccessToken);
  } else {
    remoting.debug.log('ERROR: remoting plugin not loaded');
    this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR);
  }
};

/**
 * Deletes the <embed> element from the container, without sending a
 * session_terminate request.  This is to be called when the session was
 * disconnected by the Host.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.removePlugin = function() {
  if (this.plugin) {
    var parentNode = this.plugin.parentNode;
    parentNode.removeChild(this.plugin);
    this.plugin = null;
  }
};

/**
 * Deletes the <embed> element from the container and disconnects.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.disconnect = function() {
  if (remoting.wcs) {
    remoting.wcs.setOnIq(function(stanza) {});
    this.sendIq_(
        '<cli:iq ' +
            'to="' + this.hostJid + '" ' +
            'type="set" ' +
            'id="session-terminate" ' +
            'xmlns:cli="jabber:client">' +
          '<jingle ' +
              'xmlns="urn:xmpp:jingle:1" ' +
              'action="session-terminate" ' +
              'initiator="' + this.clientJid + '" ' +
              'sid="' + this.sessionId + '">' +
            '<reason><success/></reason>' +
          '</jingle>' +
        '</cli:iq>');
  }
  this.removePlugin();
};

/**
 * Sends an IQ stanza via the http xmpp proxy.
 *
 * @private
 * @param {string} msg XML string of IQ stanza to send to server.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.sendIq_ = function(msg) {
  remoting.debug.logIq(true, msg);
  // Extract the session id, so we can close the session later.
  var parser = new DOMParser();
  var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
  var jingleNode = iqNode.firstChild;
  if (jingleNode) {
    var action = jingleNode.getAttribute('action');
    if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
      this.sessionId = jingleNode.getAttribute('sid');
    }
  }

  // Send the stanza.
  if (remoting.wcs) {
    remoting.wcs.sendIq(msg);
  } else {
    remoting.debug.log('Tried to send IQ before WCS was ready.');
    this.setState_(remoting.ClientSession.State.CONNECTION_FAILED);
  }
};

/**
 * @private
 * @param {remoting.ViewerPlugin} plugin The embed element for the plugin.
 * @return {boolean} True if the plugin and web-app versions are compatible.
 */
remoting.ClientSession.prototype.isPluginVersionSupported_ = function(plugin) {
  return this.API_VERSION_ >= plugin.apiMinVersion &&
      plugin.apiVersion >= this.API_MIN_VERSION_;
};

/**
 * Connects the plugin to WCS.
 *
 * @private
 * @param {string} oauth2AccessToken A valid OAuth2 access token.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.connectPluginToWcs_ =
    function(oauth2AccessToken) {
  this.clientJid = remoting.wcs.getJid();
  if (this.clientJid == '') {
    remoting.debug.log('Tried to connect without a full JID.');
  }
  remoting.debug.setJids(this.clientJid, this.hostJid);
  /** @type {remoting.ClientSession} */
  var that = this;
  /** @param {string} stanza The IQ stanza received. */
  var onIq = function(stanza) {
    remoting.debug.logIq(false, stanza);
    if (that.plugin.onIq) {
      that.plugin.onIq(stanza);
    } else {
      // plugin.onIq may not be set after the plugin has been shut
      // down. Particularly this happens when we receive response to
      // session-terminate stanza.
      remoting.debug.log(
          'plugin.onIq is not set so dropping incoming message.');
    }
  }
  remoting.wcs.setOnIq(onIq);
  that.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid,
                      this.accessCode);
};

/**
 * Callback that the plugin invokes to indicate that the connection
 * status has changed.
 *
 * @param {number} status The plugin's status.
 * @param {number} error The plugin's error state, if any.
 */
remoting.ClientSession.prototype.connectionInfoUpdateCallback =
    function(status, error) {
  // Old plugins didn't pass the status and error values, so get them directly.
  // Note that there is a race condition inherent in this approach.
  if (typeof(status) == 'undefined') {
    status = this.plugin.status;
  }
  if (typeof(error) == 'undefined') {
    error = this.plugin.error;
  }

  if (status == this.plugin.STATUS_CONNECTED) {
    this.onDesktopSizeChanged_();
  } else if (status == this.plugin.STATUS_FAILED) {
    this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error);
  }
  this.setState_(/** @type {remoting.ClientSession.State} */ (status));
};

/**
 * @private
 * @param {remoting.ClientSession.State} newState The new state for the session.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.setState_ = function(newState) {
  var oldState = this.state;
  this.state = newState;
  if (this.onStateChange) {
    this.onStateChange(oldState, newState);
  }
  this.logToServer.logClientSessionStateChange(this.state, this.error);
};

/**
 * This is a callback that gets called when the window is resized.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.onWindowSizeChanged = function() {
  this.updateDimensions();
};

/**
 * This is a callback that gets called when the plugin notifies us of a change
 * in the size of the remote desktop.
 *
 * @private
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
  remoting.debug.log('desktop size changed: ' +
                     this.plugin.desktopWidth + 'x' +
                     this.plugin.desktopHeight);
  this.updateDimensions();
};

/**
 * Refreshes the plugin's dimensions, taking into account the sizes of the
 * remote desktop and client window, and the current scale-to-fit setting.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.updateDimensions = function() {
  if (this.plugin.desktopWidth == 0 ||
      this.plugin.desktopHeight == 0)
    return;

  var windowWidth = window.innerWidth;
  var windowHeight = window.innerHeight;
  var scale = 1.0;

  if (remoting.scaleToFit) {
    var scaleFitHeight = 1.0 * windowHeight / this.plugin.desktopHeight;
    var scaleFitWidth = 1.0 * windowWidth / this.plugin.desktopWidth;
    scale = Math.min(1.0, scaleFitHeight, scaleFitWidth);
  }

  // Resize the plugin if necessary.
  this.plugin.width = this.plugin.desktopWidth * scale;
  this.plugin.height = this.plugin.desktopHeight * scale;

  // Position the container.
  // TODO(wez): We should take into account scrollbars when positioning.
  var parentNode = this.plugin.parentNode;
  if (this.plugin.width < windowWidth)
    parentNode.style.left = (windowWidth - this.plugin.width) / 2 + 'px';
  else
    parentNode.style.left = '0';
  if (this.plugin.height < windowHeight)
    parentNode.style.top = (windowHeight - this.plugin.height) / 2 + 'px';
  else
    parentNode.style.top = '0';

  remoting.debug.log('plugin dimensions: ' +
                     parentNode.style.left + ',' +
                     parentNode.style.top + '-' +
                     this.plugin.width + 'x' + this.plugin.height + '.');
  this.plugin.setScaleToFit(remoting.scaleToFit);
};

/**
 * Returns an associative array with a set of stats for this connection.
 *
 * @return {Object.<string, number>} The connection statistics.
 */
remoting.ClientSession.prototype.stats = function() {
  return {
    'video_bandwidth': this.plugin.videoBandwidth,
    'video_frame_rate': this.plugin.videoFrameRate,
    'capture_latency': this.plugin.videoCaptureLatency,
    'encode_latency': this.plugin.videoEncodeLatency,
    'decode_latency': this.plugin.videoDecodeLatency,
    'render_latency': this.plugin.videoRenderLatency,
    'roundtrip_latency': this.plugin.roundTripLatency
  };
};
<commit_msg>The chromoting client logs a session-close that was initiated by the client itself.<commit_after>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * Class handling creation and teardown of a remoting client session.
 *
 * This abstracts a <embed> element and controls the plugin which does the
 * actual remoting work.  There should be no UI code inside this class.  It
 * should be purely thought of as a controller of sorts.
 */

'use strict';

/** @suppress {duplicate} */
var remoting = remoting || {};

/**
 * @param {string} hostJid The jid of the host to connect to.
 * @param {string} hostPublicKey The base64 encoded version of the host's
 *     public key.
 * @param {string} accessCode The access code for the IT2Me connection.
 * @param {string} email The username for the talk network.
 * @param {function(remoting.ClientSession.State,
                    remoting.ClientSession.State):void} onStateChange
 *     The callback to invoke when the session changes state.
 * @constructor
 */
remoting.ClientSession = function(hostJid, hostPublicKey, accessCode, email,
                                  onStateChange) {
  this.state = remoting.ClientSession.State.CREATED;

  this.hostJid = hostJid;
  this.hostPublicKey = hostPublicKey;
  this.accessCode = accessCode;
  this.email = email;
  this.clientJid = '';
  this.sessionId = '';
  /** @type {remoting.ViewerPlugin} */ this.plugin = null;
  this.logToServer = new remoting.LogToServer();
  this.onStateChange = onStateChange;
};

// Note that the positive values in both of these enums are copied directly
// from chromoting_scriptable_object.h and must be kept in sync. The negative
// values represent states transitions that occur within the web-app that have
// no corresponding plugin state transition.
/** @enum {number} */
remoting.ClientSession.State = {
  CREATED: -3,
  BAD_PLUGIN_VERSION: -2,
  UNKNOWN_PLUGIN_ERROR: -1,
  UNKNOWN: 0,
  CONNECTING: 1,
  INITIALIZING: 2,
  CONNECTED: 3,
  CLOSED: 4,
  CONNECTION_FAILED: 5
};

/** @enum {number} */
remoting.ClientSession.ConnectionError = {
  NONE: 0,
  HOST_IS_OFFLINE: 1,
  SESSION_REJECTED: 2,
  INCOMPATIBLE_PROTOCOL: 3,
  NETWORK_FAILURE: 4
};

/**
 * The current state of the session.
 * @type {remoting.ClientSession.State}
 */
remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN;

/**
 * The last connection error. Set when state is set to CONNECTION_FAILED.
 * @type {remoting.ClientSession.ConnectionError}
 */
remoting.ClientSession.prototype.error =
    remoting.ClientSession.ConnectionError.NONE;

/**
 * Chromoting session API version (for this javascript).
 * This is compared with the plugin API version to verify that they are
 * compatible.
 *
 * @const
 * @private
 */
remoting.ClientSession.prototype.API_VERSION_ = 2;

/**
 * The oldest API version that we support.
 * This will differ from the |API_VERSION_| if we maintain backward
 * compatibility with older API versions.
 *
 * @const
 * @private
 */
remoting.ClientSession.prototype.API_MIN_VERSION_ = 1;

/**
 * Server used to bridge into the Jabber network for establishing Jingle
 * connections.
 *
 * @const
 * @private
 */
remoting.ClientSession.prototype.HTTP_XMPP_PROXY_ =
    'https://chromoting-httpxmpp-oauth2-dev.corp.google.com';

/**
 * The id of the client plugin
 *
 * @const
 */
remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';

/**
 * Callback to invoke when the state is changed.
 *
 * @param {remoting.ClientSession.State} oldState The previous state.
 * @param {remoting.ClientSession.State} newState The current state.
 */
remoting.ClientSession.prototype.onStateChange =
    function(oldState, newState) { };

/**
 * Adds <embed> element to |container| and readies the sesion object.
 *
 * @param {Element} container The element to add the plugin to.
 * @param {string} oauth2AccessToken A valid OAuth2 access token.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.createPluginAndConnect =
    function(container, oauth2AccessToken) {
  this.plugin = /** @type {remoting.ViewerPlugin} */
      document.createElement('embed');
  this.plugin.id = this.PLUGIN_ID;
  this.plugin.src = 'about://none';
  this.plugin.type = 'pepper-application/x-chromoting';
  this.plugin.width = 0;
  this.plugin.height = 0;
  container.appendChild(this.plugin);

  if (!this.isPluginVersionSupported_(this.plugin)) {
    // TODO(ajwong): Remove from parent.
    delete this.plugin;
    this.setState_(remoting.ClientSession.State.BAD_PLUGIN_VERSION);
    return;
  }

  /** @type {remoting.ClientSession} */ var that = this;
  /** @param {string} msg The IQ stanza to send. */
  this.plugin.sendIq = function(msg) { that.sendIq_(msg); };
  /** @param {string} msg The message to log. */
  this.plugin.debugInfo = function(msg) {
    remoting.debug.log('plugin: ' + msg);
  };

  // TODO(ajwong): Is it even worth having this class handle these events?
  // Or would it be better to just allow users to pass in their own handlers
  // and leave these blank by default?
  /**
   * @param {number} status The plugin status.
   * @param {number} error The plugin error status, if any.
   */
  this.plugin.connectionInfoUpdate = function(status, error) {
    that.connectionInfoUpdateCallback(status, error);
  };
  this.plugin.desktopSizeUpdate = function() { that.onDesktopSizeChanged_(); };

  // TODO(garykac): Clean exit if |connect| isn't a function.
  if (typeof this.plugin.connect === 'function') {
    this.connectPluginToWcs_(oauth2AccessToken);
  } else {
    remoting.debug.log('ERROR: remoting plugin not loaded');
    this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR);
  }
};

/**
 * Deletes the <embed> element from the container, without sending a
 * session_terminate request.  This is to be called when the session was
 * disconnected by the Host.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.removePlugin = function() {
  if (this.plugin) {
    var parentNode = this.plugin.parentNode;
    parentNode.removeChild(this.plugin);
    this.plugin = null;
  }
};

/**
 * Deletes the <embed> element from the container and disconnects.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.disconnect = function() {
  // The plugin won't send a state change notification, so we explicitly log
  // the fact that the connection has closed.
  this.logToServer.logClientSessionStateChange(
      remoting.ClientSession.State.CLOSED,
      remoting.ClientSession.ConnectionError.NONE);
  if (remoting.wcs) {
    remoting.wcs.setOnIq(function(stanza) {});
    this.sendIq_(
        '<cli:iq ' +
            'to="' + this.hostJid + '" ' +
            'type="set" ' +
            'id="session-terminate" ' +
            'xmlns:cli="jabber:client">' +
          '<jingle ' +
              'xmlns="urn:xmpp:jingle:1" ' +
              'action="session-terminate" ' +
              'initiator="' + this.clientJid + '" ' +
              'sid="' + this.sessionId + '">' +
            '<reason><success/></reason>' +
          '</jingle>' +
        '</cli:iq>');
  }
  this.removePlugin();
};

/**
 * Sends an IQ stanza via the http xmpp proxy.
 *
 * @private
 * @param {string} msg XML string of IQ stanza to send to server.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.sendIq_ = function(msg) {
  remoting.debug.logIq(true, msg);
  // Extract the session id, so we can close the session later.
  var parser = new DOMParser();
  var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
  var jingleNode = iqNode.firstChild;
  if (jingleNode) {
    var action = jingleNode.getAttribute('action');
    if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
      this.sessionId = jingleNode.getAttribute('sid');
    }
  }

  // Send the stanza.
  if (remoting.wcs) {
    remoting.wcs.sendIq(msg);
  } else {
    remoting.debug.log('Tried to send IQ before WCS was ready.');
    this.setState_(remoting.ClientSession.State.CONNECTION_FAILED);
  }
};

/**
 * @private
 * @param {remoting.ViewerPlugin} plugin The embed element for the plugin.
 * @return {boolean} True if the plugin and web-app versions are compatible.
 */
remoting.ClientSession.prototype.isPluginVersionSupported_ = function(plugin) {
  return this.API_VERSION_ >= plugin.apiMinVersion &&
      plugin.apiVersion >= this.API_MIN_VERSION_;
};

/**
 * Connects the plugin to WCS.
 *
 * @private
 * @param {string} oauth2AccessToken A valid OAuth2 access token.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.connectPluginToWcs_ =
    function(oauth2AccessToken) {
  this.clientJid = remoting.wcs.getJid();
  if (this.clientJid == '') {
    remoting.debug.log('Tried to connect without a full JID.');
  }
  remoting.debug.setJids(this.clientJid, this.hostJid);
  /** @type {remoting.ClientSession} */
  var that = this;
  /** @param {string} stanza The IQ stanza received. */
  var onIq = function(stanza) {
    remoting.debug.logIq(false, stanza);
    if (that.plugin.onIq) {
      that.plugin.onIq(stanza);
    } else {
      // plugin.onIq may not be set after the plugin has been shut
      // down. Particularly this happens when we receive response to
      // session-terminate stanza.
      remoting.debug.log(
          'plugin.onIq is not set so dropping incoming message.');
    }
  }
  remoting.wcs.setOnIq(onIq);
  that.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid,
                      this.accessCode);
};

/**
 * Callback that the plugin invokes to indicate that the connection
 * status has changed.
 *
 * @param {number} status The plugin's status.
 * @param {number} error The plugin's error state, if any.
 */
remoting.ClientSession.prototype.connectionInfoUpdateCallback =
    function(status, error) {
  // Old plugins didn't pass the status and error values, so get them directly.
  // Note that there is a race condition inherent in this approach.
  if (typeof(status) == 'undefined') {
    status = this.plugin.status;
  }
  if (typeof(error) == 'undefined') {
    error = this.plugin.error;
  }

  if (status == this.plugin.STATUS_CONNECTED) {
    this.onDesktopSizeChanged_();
  } else if (status == this.plugin.STATUS_FAILED) {
    this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error);
  }
  this.setState_(/** @type {remoting.ClientSession.State} */ (status));
};

/**
 * @private
 * @param {remoting.ClientSession.State} newState The new state for the session.
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.setState_ = function(newState) {
  var oldState = this.state;
  this.state = newState;
  if (this.onStateChange) {
    this.onStateChange(oldState, newState);
  }
  this.logToServer.logClientSessionStateChange(this.state, this.error);
};

/**
 * This is a callback that gets called when the window is resized.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.onWindowSizeChanged = function() {
  this.updateDimensions();
};

/**
 * This is a callback that gets called when the plugin notifies us of a change
 * in the size of the remote desktop.
 *
 * @private
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
  remoting.debug.log('desktop size changed: ' +
                     this.plugin.desktopWidth + 'x' +
                     this.plugin.desktopHeight);
  this.updateDimensions();
};

/**
 * Refreshes the plugin's dimensions, taking into account the sizes of the
 * remote desktop and client window, and the current scale-to-fit setting.
 *
 * @return {void} Nothing.
 */
remoting.ClientSession.prototype.updateDimensions = function() {
  if (this.plugin.desktopWidth == 0 ||
      this.plugin.desktopHeight == 0)
    return;

  var windowWidth = window.innerWidth;
  var windowHeight = window.innerHeight;
  var scale = 1.0;

  if (remoting.scaleToFit) {
    var scaleFitHeight = 1.0 * windowHeight / this.plugin.desktopHeight;
    var scaleFitWidth = 1.0 * windowWidth / this.plugin.desktopWidth;
    scale = Math.min(1.0, scaleFitHeight, scaleFitWidth);
  }

  // Resize the plugin if necessary.
  this.plugin.width = this.plugin.desktopWidth * scale;
  this.plugin.height = this.plugin.desktopHeight * scale;

  // Position the container.
  // TODO(wez): We should take into account scrollbars when positioning.
  var parentNode = this.plugin.parentNode;
  if (this.plugin.width < windowWidth)
    parentNode.style.left = (windowWidth - this.plugin.width) / 2 + 'px';
  else
    parentNode.style.left = '0';
  if (this.plugin.height < windowHeight)
    parentNode.style.top = (windowHeight - this.plugin.height) / 2 + 'px';
  else
    parentNode.style.top = '0';

  remoting.debug.log('plugin dimensions: ' +
                     parentNode.style.left + ',' +
                     parentNode.style.top + '-' +
                     this.plugin.width + 'x' + this.plugin.height + '.');
  this.plugin.setScaleToFit(remoting.scaleToFit);
};

/**
 * Returns an associative array with a set of stats for this connection.
 *
 * @return {Object.<string, number>} The connection statistics.
 */
remoting.ClientSession.prototype.stats = function() {
  return {
    'video_bandwidth': this.plugin.videoBandwidth,
    'video_frame_rate': this.plugin.videoFrameRate,
    'capture_latency': this.plugin.videoCaptureLatency,
    'encode_latency': this.plugin.videoEncodeLatency,
    'decode_latency': this.plugin.videoDecodeLatency,
    'render_latency': this.plugin.videoRenderLatency,
    'roundtrip_latency': this.plugin.roundTripLatency
  };
};
<|endoftext|>


<commit_before>define(['sprintf'], function(sprintf) {
  "use strict"

  var CLOCK_MAX = Math.pow(2, 53);

  // statically allocated event table
  // events with lower numbers are executed first
  var events = {
    PAUSE: 0,
    TIMER_OVERFLOW: 1,
    VIDEO_LINE: 2,
    VIDEO_HBLANK: 3
  };

  var proto = {
    init: function() {
      var n = Object.keys(events).length;
      this.clocks = Array(n);
      this.callbacks = Array(n);
      this.contexts = Array(n);
      for(var i = 0; i < n; i++) {
        this.clocks[i] = CLOCK_MAX;
      }
      this.nextEventCache = -1;
      this.nextClockCache = CLOCK_MAX;
      this.nextValid = true;

      this.clock = 0;
    },
    nextEvent: function() {
      this.nextClock(); // fill cache
      return this.nextEventCache;
    },
    nextClock: function() {
      if(this.nextValid)
        return this.nextClockCache;
      var bestClock = CLOCK_MAX;
      var ev = -1;
      for(var i = 0; i < this.clocks.length; i++) {
        var c = this.clocks[i];
        if(c >= this.clock && c < bestClock) {
          ev = i;
          bestClock = c;
        }
      }
      this.nextEventCache = ev;
      this.nextClockCache = bestClock;
      this.nextValid = true;
      return bestClock;
    },
    // It is safe to call register() and update() within a callback.
    register: function(ev, clock, context, callback) {
      this.callbacks[ev] = callback;
      this.contexts[ev] = context;
      this.update(ev, clock);
    },
    update: function(ev, clock) {
      if(clock < 0 || clock < this.clock) {
        clock = CLOCK_MAX;
      }
      this.nextValid = false;
      this.clocks[ev] = clock;
    },
    unregister: function(ev) {
      this.nextValid = false;
      this.clocks[ev] = CLOCK_MAX;
    },
    runEvent: function() {
      var next = this.nextClock();
      var ev = this.nextEvent();
      if(ev < 0)
        return;
      this.unregister(ev);
      this.callbacks[ev].call(this.contexts[ev], next);
      this.clock = next;
    },
    pause: function() {
      this.update(events.PAUSE, this.clock);
    },

    dump: function() {
      var keys = Object.keys(events);
      var res = [];
      for(var i = 0; i < keys.length; i++) {
        var ev = events[keys[i]];
        if(this.clocks[ev] == CLOCK_MAX) {
          res.push(sprintf("%s: DISABLED", keys[i]));
        } else {
          res.push(sprintf("%s: %d", keys[i], this.clocks[ev]));
        }
      }
      return res.join("\n");
    }
  }

  return {
    create: function() {
      var evm = Object.create(proto);
      evm.init();
      return evm;
    },
    events: events
  }
});
<commit_msg>change priority of pause event to last<commit_after>define(['sprintf'], function(sprintf) {
  "use strict"

  var CLOCK_MAX = Math.pow(2, 53);

  // statically allocated event table
  // events with lower numbers are executed first
  var events = {
    TIMER_OVERFLOW: 0,
    VIDEO_LINE: 1,
    VIDEO_HBLANK: 2,
    PAUSE: 3
  };

  var proto = {
    init: function() {
      var n = Object.keys(events).length;
      this.clocks = Array(n);
      this.callbacks = Array(n);
      this.contexts = Array(n);
      for(var i = 0; i < n; i++) {
        this.clocks[i] = CLOCK_MAX;
      }
      this.nextEventCache = -1;
      this.nextClockCache = CLOCK_MAX;
      this.nextValid = true;

      this.clock = 0;
    },
    nextEvent: function() {
      this.nextClock(); // fill cache
      return this.nextEventCache;
    },
    nextClock: function() {
      if(this.nextValid)
        return this.nextClockCache;
      var bestClock = CLOCK_MAX;
      var ev = -1;
      for(var i = 0; i < this.clocks.length; i++) {
        var c = this.clocks[i];
        if(c >= this.clock && c < bestClock) {
          ev = i;
          bestClock = c;
        }
      }
      this.nextEventCache = ev;
      this.nextClockCache = bestClock;
      this.nextValid = true;
      return bestClock;
    },
    // It is safe to call register() and update() within a callback.
    register: function(ev, clock, context, callback) {
      this.callbacks[ev] = callback;
      this.contexts[ev] = context;
      this.update(ev, clock);
    },
    update: function(ev, clock) {
      if(clock < 0 || clock < this.clock) {
        clock = CLOCK_MAX;
      }
      this.nextValid = false;
      this.clocks[ev] = clock;
    },
    unregister: function(ev) {
      this.nextValid = false;
      this.clocks[ev] = CLOCK_MAX;
    },
    runEvent: function() {
      var next = this.nextClock();
      var ev = this.nextEvent();
      if(ev < 0)
        return;
      this.unregister(ev);
      this.callbacks[ev].call(this.contexts[ev], next);
      this.clock = next;
    },
    pause: function() {
      this.update(events.PAUSE, this.clock);
    },

    dump: function() {
      var keys = Object.keys(events);
      var res = [];
      for(var i = 0; i < keys.length; i++) {
        var ev = events[keys[i]];
        if(this.clocks[ev] == CLOCK_MAX) {
          res.push(sprintf("%s: DISABLED", keys[i]));
        } else {
          res.push(sprintf("%s: %d", keys[i], this.clocks[ev]));
        }
      }
      return res.join("\n");
    }
  }

  return {
    create: function() {
      var evm = Object.create(proto);
      evm.init();
      return evm;
    },
    events: events
  }
});
<|endoftext|>


<commit_before>/**
 * cbpAnimatedHeader.js v1.0.0
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 *
 * Copyright 2013, Codrops
 * http://www.codrops.com
 */
var cbpAnimatedHeader = (function() {

	var docElem = document.documentElement,
		header = document.querySelector( '.navbar-fixed-top' ),
		didScroll = false,
		changeHeaderOn = 300;

	function init() {
		window.addEventListener( 'scroll', function( event ) {
			if( !didScroll ) {
				didScroll = true;
				setTimeout( scrollPage, 250 );
			}
		}, false );
	}

	function scrollPage() {
		var sy = scrollY();
		if ( sy >= changeHeaderOn ) {
			classie.add( header, 'navbar-shrink' );
		}
		else {
			classie.remove( header, 'navbar-shrink' );
		}
		didScroll = true;
	}

	function scrollY() {
		return window.pageYOffset || docElem.scrollTop;
	}

	init();

})();
<commit_msg>Update cbpAnimatedHeader.js<commit_after>/**
 * cbpAnimatedHeader.js v1.0.0
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 *
 * Copyright 2013, Codrops
 * http://www.codrops.com
 */
var cbpAnimatedHeader = (function() {

	var docElem = document.documentElement,
		header = document.querySelector( '.navbar-fixed-top' ),
		didScroll = false,
		changeHeaderOn = 300;

	function init() {
		window.addEventListener( 'scroll', function( event ) {
			if( !didScroll ) {
				didScroll = true;
				setTimeout( scrollPage, 250 );
			}
		}, false );
	}

	function scrollPage() {
		var sy = scrollY();
		if ( sy >= changeHeaderOn ) {
			classie.add( header, 'navbar-shrink' );
		}
		else {
			classie.remove( header, 'navbar-shrink' );
		}
		didScroll = false;
	}

	function scrollY() {
		return window.pageYOffset || docElem.scrollTop;
	}

	init();

})();
<|endoftext|>


<commit_before>/* global clearTimeout, setTimeout */

export default class UndoStack {

	defaultOptions = {
		maxSize: 30,
		lock: false,
		undoGroupingInterval: 400,
		onUndoRedo: () => {},
		callback: () => {}
	};

	constructor(opts = {}) {
		opts = { ...this.defaultOptions, ...opts };
		this.stack = [];
		this.index = -1;
		this.timer = null;

		this.maxSize = opts.maxSize;
		this.undoGroupingInterval = opts.undoGroupingInterval;
		this.isLocked = opts.lock;
		this.onUndoRedo = opts.onUndoRedo;
		this.callback = opts.callback;
	}

	runCallback(type) {
		this.callback({
			type,
			index: this.index,
			length: this.stack.length,
			canUndo: this.canUndo(),
			canRedo: this.canRedo()
		});
		return this;
	}

	lock(lock = true) {
		this.isLocked = lock;
		return this;
	}

	unlock() {
		return this.lock(false);
	}

	setUndoRedo(func) {
		this.onUndoRedo = func;
		return this;
	}

	setCallback(func) {
		this.callback = func;
		return this;
	}

	init(data) {
		return this.add(data).unlock();
	}

	add(data) {
		if(this.timer) {
			clearTimeout(this.timer);
		}

		this.timer = setTimeout(() => {
			// if called after undoing, remove stack above this index
			this.stack.splice(this.index + 1, this.stack.length - this.index);

			// remove oldest item if we surpass maxSize
			if(this.stack.length === this.maxSize) {
				this.stack.shift();
			}

			this.stack.push(JSON.stringify(data));

			this.index = this.stack.length - 1;

			this.runCallback('add');
		}, this.undoGroupingInterval);

		return this;
	}

	undoOrRedo(type) {
		const
			isUndo = type === 'undo',
			index = this.index + (isUndo ? -1 : 1),
			data = this.stack[index];

		if(!data) {
			return this;
		}

		this.onUndoRedo(data);

		isUndo ? this.index-- : this.index++;

		this.runCallback(type);

		return this;
	}

	undo() {
		return this.undoOrRedo('undo');
	}

	redo() {
		return this.undoOrRedo('redo');
	}

	clear() {
		this.stack = [];
		this.index = -1;
		return this;
	}

	canUndo() {
		return !this.isLocked && this.index > 0;
	}

	canRedo() {
		return !this.isLocked && this.index < this.stack.length - 1;
	}
}
<commit_msg>Add comments to undo stack class, might help others understand what it's doing better.<commit_after>/* global clearTimeout, setTimeout */

export default class UndoStack {

	defaultOptions = {
		maxSize: 30,
		lock: false,
		undoGroupingInterval: 400,
		onUndoRedo: () => {},
		callback: () => {}
	};

	/**
	 * Create a stack of changes and set up options.
	 *
	 * @param {Object}   opts - The options to merge with the defaults.
	 * @param {number}   opts.maxSize
	 * @param {boolean}  opts.lock
	 * @param {number}   opts.undoGroupingInterval
	 * @param {function} opts.onUndoRedo
	 * @param {function} opts.callback
	 */
	constructor(opts = {}) {
		opts = { ...this.defaultOptions, ...opts };
		this.stack = [];
		this.index = -1;
		this.timer = null;

		this.maxSize = opts.maxSize;
		this.undoGroupingInterval = opts.undoGroupingInterval;
		this.isLocked = opts.lock;
		this.onUndoRedo = opts.onUndoRedo;
		this.callback = opts.callback;
	}

	/**
	 * Runs the callback supplied in the inital options.
	 *
	 * @param      {string}  type    The type of action performed (for
	 * better debugging/feedback), currently add|undo|redo.
	 *
	 * @return     {Object}  "this" for method chaining.
	 */
	runCallback(type) {
		this.callback({
			type,
			index: this.index,
			length: this.stack.length,
			canUndo: this.canUndo(),
			canRedo: this.canRedo()
		});
		return this;
	}

	/**
	 * Set lock flag for undo/redo.
	 *
	 * @param      {boolean}  lock    Whether to lock or unlock.
	 *
	 * @return     {Object}  "this" for method chaining.
	 */
	lock(lock = true) {
		this.isLocked = lock;
		return this;
	}

	/**
	 * Alias for unlocking undo/redo
	 */
	unlock() {
		return this.lock(false);
	}

	/**
	 * Set function to run on undo/redo.
	 *
	 * @return     {Object}  "this" for method chaining.
	 */
	setUndoRedo(func) {
		this.onUndoRedo = func;
		return this;
	}

	/**
	 * Set the callback to run on every action.
	 *
	 * @return     {Object}  "this" for method chaining.
	 */
	setCallback(func) {
		this.callback = func;
		return this;
	}

	/**
	 * Shortcut to add initial data and unlock undo/redo.
	 */
	init(data) {
		return this.add(data).unlock();
	}

	/**
	 * Add data to the stack, and group changes based on grouping interval.
	 * Then run the callback with the "add" action type.
	 *
	 * @return     {Object}  "this" for method chaining.
	 */
	add(data) {
		if(this.timer) {
			clearTimeout(this.timer);
		}

		this.timer = setTimeout(() => {
			// if called after undoing, remove stack above this index
			this.stack.splice(this.index + 1, this.stack.length - this.index);

			// remove oldest item if we surpass maxSize
			if(this.stack.length === this.maxSize) {
				this.stack.shift();
			}

			this.stack.push(JSON.stringify(data));

			this.index = this.stack.length - 1;

			// run our callback and pass "add" as action type
			this.runCallback('add');

		}, this.undoGroupingInterval);

		return this;
	}

	/**
	 * Travel backwards or forwards through stack.
	 * Calls our undo/redo function and the generic callback.
	 *
	 * @return     {Object}  "this" for method chaining.
	 */
	undoOrRedo(type) {
		const
			isUndo = type === 'undo',
			index = this.index + (isUndo ? -1 : 1),
			data = this.stack[index];

		if(!data) {
			return this;
		}

		this.onUndoRedo(data);

		isUndo ? this.index-- : this.index++;

		this.runCallback(type);

		return this;
	}

	/**
	 * Undo latest change.
	 */
	undo() {
		return this.undoOrRedo('undo');
	}

	/**
	 * Redo previous change.
	 */
	redo() {
		return this.undoOrRedo('redo');
	}

	/**
	 * Clear the whole undo stack and reset current index.
	 *
	 * @return     {Object}  "this" for method chaining.
	 */
	clear() {
		this.stack = [];
		this.index = -1;
		return this;
	}

	/**
	 * See if there is anything to undo and the stack isn't locked.
	 *
	 * @return     {boolean}  If we can undo changes.
	 */
	canUndo() {
		return !this.isLocked && this.index > 0;
	}

	/**
	 * See if there is anything to redo and the stack isn't locked.
	 *
	 * @return     {boolean}  If we can redo changes.
	 */
	canRedo() {
		return !this.isLocked && this.index < this.stack.length - 1;
	}
}
<|endoftext|>