<commit_before>/*
 *  Waltz
 * Copyright (c) David Watkins. All rights reserved.
 * The use and distribution terms for this software are covered by the
 * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
 * which can be found in the file epl-v10.html at the root of this distribution.
 * By using this software in any fashion, you are agreeing to be bound by
 * the terms of this license.
 * You must not remove this notice, or any other, from this software.
 *
 */

import '../style/style.scss';

import _ from 'lodash';
import angular from 'angular';

import 'angular-ui-grid/ui-grid';
import 'angular-ui-router';
import 'angular-ui-bootstrap';
import 'ui-select';
import 'satellizer';
import 'angular-animate';
import 'angular-sanitize';
import 'ng-tags-input';
import 'babel-core/polyfill';  // provides Object.assign etc.
import 'angular-local-storage';
import 'ng-redux';
import thunk from 'redux-thunk';

import rootReducer from './reports/reducers';

const dependencies = [
    'ui.bootstrap',
    'ui.router',
    'ui.select',
    'ui.grid',
    'ngAnimate',
    'ngSanitize',
    'ngTagsInput',
    'satellizer',
    'LocalStorageModule',
    'ngRedux',
    require('angular-formly'),
    require('angular-formly-templates-bootstrap')
];

const waltzApp = angular.module('waltz-app', dependencies);

const registrationFns = [
    require('./common/directives'),
    require('./common/filters'),
    require('./common/services'),
    require('./access-log'),
    require('./applications'),
    require('./app-capabilities'),
    require('./app-groups'),
    require('./app-view'),
    require('./asset-cost'),
    require('./auth-sources'),
    require('./bookmarks'),
    require('./capabilities'),
    require('./complexity'),
    require('./change-log'),
    require('./data-flow'),
    require('./data-types'),
    require('./end-user-apps'),
    require('./examples'),
    require('./history'),
    require('./involvement'),
    require('./navbar'),
    require('./org-units'),
    require('./perspectives'),
    require('./person'),
    require('./playpen'),
    require('./ratings'),
    require('./server-info'),
    require('./sidebar'),
    require('./static-panel'),
    require('./svg-diagram'),
    require('./user'),
    require('./formly'),
    require('./widgets'),
    require('./reports')
];

_.each(registrationFns, (registrationFn, idx) => {
    if (!_.isFunction(registrationFn)) {
        console.error('cannot register: ', registrationFn, 'at idx', idx);
    }
    registrationFn(waltzApp);
});

waltzApp.config([
    '$stateProvider',
    ($stateProvider) => {
        $stateProvider
            .state('main', {
                url: '/',
                views: {
                    'header': { template: '<waltz-navbar></waltz-navbar>'},
                    'sidebar': { template: '<waltz-sidebar></waltz-sidebar>' },
                    'content': require('./welcome/welcome.js'),
                    'footer': { template: require('./footer/footer.html') }
                }
            })
            .state('main.home', {
                url: 'home',
                views: {
                    'content': { template: require('./welcome/welcome.html') }
                }
            });
    }
]);

const baseUrl =
    __ENV__ === 'prod'
    ? './'
    : __ENV__ === 'test'
        ? 'http://192.168.1.147:8443/'
        : 'http://localhost:8443/';

waltzApp.constant('BaseApiUrl', baseUrl + 'api');
waltzApp.constant('BaseUrl', baseUrl);
waltzApp.constant('BaseExtractUrl', baseUrl + 'data-extract');

waltzApp.config([
    'uiSelectConfig',
    (uiSelectConfig) => {
        uiSelectConfig.theme = 'bootstrap';
        uiSelectConfig.resetSearchInput = true;
        uiSelectConfig.appendToBody = true;
    }
]);

waltzApp.config( [
    '$compileProvider',
    $compileProvider => {
        $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|sip|chrome-extension):/);
    }
]);

waltzApp.config([
    '$authProvider',
    'BaseUrl',
    function($authProvider, BaseUrl) {
        $authProvider.baseUrl = BaseUrl;
        $authProvider.withCredentials = false;

        $authProvider.google({
            clientId: 'Google account'
        });

        $authProvider.github({
            clientId: 'GitHub Client ID'
        });

        $authProvider.linkedin({
            clientId: 'LinkedIn Client ID'
        });

    }
]);

waltzApp.config([
    '$httpProvider',
    function($httpProvider) {
        $httpProvider.defaults.cache = false;
        if (!$httpProvider.defaults.headers.get) {
            $httpProvider.defaults.headers.get = {};
        }
        // disable IE ajax request caching
        $httpProvider.defaults.headers.get['If-Modified-Since'] = '0';
    }
]);

waltzApp.run(['$rootScope', '$document', ($rootScope, $doc) => {
    $rootScope.$on('$stateChangeSuccess', () => {
        $doc[0].body.scrollTop = 0;
        $doc[0].documentElement.scrollTop = 0;
    });
}]);

waltzApp.config(['$ngReduxProvider', ($ngReduxProvider) => {
    $ngReduxProvider.createStoreWith(rootReducer, [thunk], []);
}]);<commit_msg>Adding 'mailto' links to the ng-sanitize whitelist.<commit_after>/*
 *  Waltz
 * Copyright (c) David Watkins. All rights reserved.
 * The use and distribution terms for this software are covered by the
 * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
 * which can be found in the file epl-v10.html at the root of this distribution.
 * By using this software in any fashion, you are agreeing to be bound by
 * the terms of this license.
 * You must not remove this notice, or any other, from this software.
 *
 */

import '../style/style.scss';

import _ from 'lodash';
import angular from 'angular';

import 'angular-ui-grid/ui-grid';
import 'angular-ui-router';
import 'angular-ui-bootstrap';
import 'ui-select';
import 'satellizer';
import 'angular-animate';
import 'angular-sanitize';
import 'ng-tags-input';
import 'babel-core/polyfill';  // provides Object.assign etc.
import 'angular-local-storage';
import 'ng-redux';
import thunk from 'redux-thunk';

import rootReducer from './reports/reducers';

const dependencies = [
    'ui.bootstrap',
    'ui.router',
    'ui.select',
    'ui.grid',
    'ngAnimate',
    'ngSanitize',
    'ngTagsInput',
    'satellizer',
    'LocalStorageModule',
    'ngRedux',
    require('angular-formly'),
    require('angular-formly-templates-bootstrap')
];

const waltzApp = angular.module('waltz-app', dependencies);

const registrationFns = [
    require('./common/directives'),
    require('./common/filters'),
    require('./common/services'),
    require('./access-log'),
    require('./applications'),
    require('./app-capabilities'),
    require('./app-groups'),
    require('./app-view'),
    require('./asset-cost'),
    require('./auth-sources'),
    require('./bookmarks'),
    require('./capabilities'),
    require('./complexity'),
    require('./change-log'),
    require('./data-flow'),
    require('./data-types'),
    require('./end-user-apps'),
    require('./examples'),
    require('./history'),
    require('./involvement'),
    require('./navbar'),
    require('./org-units'),
    require('./perspectives'),
    require('./person'),
    require('./playpen'),
    require('./ratings'),
    require('./server-info'),
    require('./sidebar'),
    require('./static-panel'),
    require('./svg-diagram'),
    require('./user'),
    require('./formly'),
    require('./widgets'),
    require('./reports')
];

_.each(registrationFns, (registrationFn, idx) => {
    if (!_.isFunction(registrationFn)) {
        console.error('cannot register: ', registrationFn, 'at idx', idx);
    }
    registrationFn(waltzApp);
});

waltzApp.config([
    '$stateProvider',
    ($stateProvider) => {
        $stateProvider
            .state('main', {
                url: '/',
                views: {
                    'header': { template: '<waltz-navbar></waltz-navbar>'},
                    'sidebar': { template: '<waltz-sidebar></waltz-sidebar>' },
                    'content': require('./welcome/welcome.js'),
                    'footer': { template: require('./footer/footer.html') }
                }
            })
            .state('main.home', {
                url: 'home',
                views: {
                    'content': { template: require('./welcome/welcome.html') }
                }
            });
    }
]);

const baseUrl =
    __ENV__ === 'prod'
    ? './'
    : __ENV__ === 'test'
        ? 'http://192.168.1.147:8443/'
        : 'http://localhost:8443/';

waltzApp.constant('BaseApiUrl', baseUrl + 'api');
waltzApp.constant('BaseUrl', baseUrl);
waltzApp.constant('BaseExtractUrl', baseUrl + 'data-extract');

waltzApp.config([
    'uiSelectConfig',
    (uiSelectConfig) => {
        uiSelectConfig.theme = 'bootstrap';
        uiSelectConfig.resetSearchInput = true;
        uiSelectConfig.appendToBody = true;
    }
]);

waltzApp.config( [
    '$compileProvider',
    $compileProvider => {
        $compileProvider.aHrefSanitizationWhitelist(/^\s*(mailto|https?|sip|chrome-extension):/);
    }
]);

waltzApp.config([
    '$authProvider',
    'BaseUrl',
    function($authProvider, BaseUrl) {
        $authProvider.baseUrl = BaseUrl;
        $authProvider.withCredentials = false;

        $authProvider.google({
            clientId: 'Google account'
        });

        $authProvider.github({
            clientId: 'GitHub Client ID'
        });

        $authProvider.linkedin({
            clientId: 'LinkedIn Client ID'
        });

    }
]);

waltzApp.config([
    '$httpProvider',
    function($httpProvider) {
        $httpProvider.defaults.cache = false;
        if (!$httpProvider.defaults.headers.get) {
            $httpProvider.defaults.headers.get = {};
        }
        // disable IE ajax request caching
        $httpProvider.defaults.headers.get['If-Modified-Since'] = '0';
    }
]);

waltzApp.run(['$rootScope', '$document', ($rootScope, $doc) => {
    $rootScope.$on('$stateChangeSuccess', () => {
        $doc[0].body.scrollTop = 0;
        $doc[0].documentElement.scrollTop = 0;
    });
}]);

waltzApp.config(['$ngReduxProvider', ($ngReduxProvider) => {
    $ngReduxProvider.createStoreWith(rootReducer, [thunk], []);
}]);

<|endoftext|>


<commit_before>'use strict';

//var metadataEditorApp =
  //angular.module('metadataEditor', ['ngRoute', 'ui.date']);

// for minification, explicitly declare dependencies $scope and $http
metadataEditorApp.controller('BaseController', ['$scope', '$http', '$log',
  function($scope, $http, $log) {

    // first see if we have any user information given to us (from Drupal)
    if (typeof(window.session_id) === 'undefined')
    {
      var session_id = 'local';
    }
    else
    {
      var session_id = window.session_id;
    }

    // next see if there is a hostname defined
    var hostname = '';
    if (typeof(window.hostname) === 'undefined')
    {
      hostname = 'localhost:4000';
    }
    else
    {
      hostname = window.hostname;
    }
    $scope.hostname = hostname;

    // initialize list of existing metadata records
    displayCurrentRecords();

    // set date options
    $scope.dateOptions = {
        changeYear: true,
        changeMonth: true,
        yearRange: '1700:+10'
    };

    // create time picker vars
    $scope.hours = _.range(24);
    $scope.minute_seconds = _.range(60);

    $scope.knownDataFormats = ["ASCII", "csv", "DLG", "docx", "DRG", "DWG", "eps",
      "ERDAS", "Esri file GDB", "Esri grid", "Esri personal GDB","Esri TIN", "FASTA", "FASTQ", "GenBank",
      "GeoJSON", "GeoTIFF", "GML", "HDF", "jpeg", "KML", "LAS", "mp3",
      "MrSID", "netCDF", "pdf", "php", "png", "py", "R", "SDXF", "Shapefile",
      "SPSS", "Stata", "Tab", "tiff", "txt", "VBS", "wav", "xls", "xlsx",
      "xml"];

    $scope.dataFormats = [];

    $scope.spatialDataOptions = ["vector", "grid", "table or text",
      "triangulated irregular network", "stereographic imaging",
      "video recording of a scene"];
    $scope.hierarchyLevels = ["dataset", "series"];

    /**
     * Fetch the record with recordId and update the form to display it.
     *
     * Intended for use in conjunction with the Edit buttons in the records list
     */
    $scope.editRecord = function(recordId)
    {
      $scope.newRecord = false;

      $http.post('//' + hostname + '/api/metadata/' + recordId,
                 {'session_id': session_id})
           .success(function(data) {
             var record = data.record;
             if (typeof record.start_date == "undefined") {
               record.start_date = {$date: new Date(2010, 1, 1)};
             }
             if (typeof record.end_date == "undefined") {
               record.end_date = {$date: new Date()};
             }
             if (typeof record.last_mod_date == "undefined") {
               record.last_mod_date = {$date: new Date()};
             }
             if (typeof record.first_pub_date == "undefined") {
               record.first_pub_date = {$date: new Date()};
             }
             updateForms(record);
           });
    };

    var EMPTY_CONTACT =
    {
      'name': '', 'email': '', 'org': '', 'address': '',
      'city': '', 'state': '', 'zipcode': '', 'country': '', 'phone': ''
    };

    $scope.createNewRecord = function()
    {
      $scope.newRecord = true;

      $http.get('//' + hostname + '/api/metadata/placeholder')
           .success(function(data) {
             var placeholderRec = data.record;
             //$scope.currentRecord = placeholderRec;
             var emptyRec = JSON.parse(JSON.stringify(placeholderRec));

             $log.log(emptyRec);

             // clear out placeholder values
             for (var field in emptyRec)
             {
               if (['citation', 'access'].indexOf(field) > -1)
               {
                 emptyRec[field] = [JSON.parse(JSON.stringify(EMPTY_CONTACT))];
               }
               else if (
                 ['place_keywords', 'thematic_keywords',
                  'data_format', 'online'].indexOf(field) > -1)
               {
                 emptyRec[field] = [""];
               }
               else if (['_cls', '_id', 'start_date', 'end_date', 'last_mod_date',
                         'first_pub_date'].indexOf(field) == -1)
               {
                 emptyRec[field] = "";
               }
             }
             emptyRec.start_date = {$date: new Date(2010, 1, 1)};
             emptyRec.end_date = {$date: new Date()};
             emptyRec.end_date.$date.setHours(0);
             emptyRec.end_date.$date.setMinutes(0);
             emptyRec.end_date.$date.setSeconds(0);

             $scope.currentRecord = emptyRec;

             updateForms($scope.currentRecord);
           });
    };

    // initialize form with placeholder data for creating a new record
    $scope.createNewRecord();

    /**
     * Change the metadata type being generated between ISO for datasets
     * and Dublin Core for other research products
     */
    $scope.isDublin = false;
    $scope.isISO = true;
    $scope.toggleMetadataType = function(type)
    {
      switch (type)
      {
        case 'dublin':
          $scope.isDublin = true;
          $scope.isISO = false;
          break;

        case 'iso':
          $scope.isDublin = false;
          $scope.isISO = true;
          break;

        default:
          $scope.isDublin = false;
          $scope.isISO = true;
      }

      $log.log('dublin: ' + $scope.isDublin);
      $log.log('iso: ' + $scope.isISO);
    };

    /**On click of Load MILES Defaults button,
     * load the defaults in MILES defaults json file
     */
    $scope.defaultMILES = function()
    {
      $scope.newRecord = true;

      $http.get('//' + hostname + '/api/metadata/defaultMILES')
           .success(function(data) {

             var milesRec = data.record;

             for (var key in milesRec)
             {
               if (milesRec.hasOwnProperty(key))
               {
                  $scope.currentRecord[key] = milesRec[key];
               }
             }

             updateForms($scope.currentRecord);
           });
    };

    /**
     * On submit of metadata form, submitRecord. This both updates the server
     * and makes sure the form is current. Not sure how it wouldn't be, todo?
     */
    $scope.submitDraftRecord = function()
    {
      // the start and end dates currently have hours and minutes zero;
      // grab the hours and minutes and append them. Confusingly JS
      // uses setHours to set minutes and seconds as well
      var current = prepareCurrentScopedRecord();
      if ($scope.newRecord)
      {
        $http.put('//' + hostname + '/api/metadata',
                  {'record': current, 'session_id': session_id})
             .success(function(data) {
                 updateForms(data.record);
                 $scope.newRecord = false;
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             })
             .error(function(data) { $log.log(data.record); });
      }
      else
      {
        $http.put('//' + hostname + '/api/metadata/' + current._id.$oid,
                  {'record': current, 'session_id': session_id})
             .success(function(data) {
                 updateForms(data.record);
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             });
      }
    };

    /**
     * Publish a record to the portal. Requires all fields to be valid
     */
    $scope.publishRecord = function()
    {
      var current = prepareCurrentScopedRecord();
      // if the record is totally new, we first want to save the draft of it
      if ($scope.newRecord)
      {
        $http.post('//' + hostname + '/api/metadata', current)
             .success(function(data) {
                 updateForms(data.record);
                 $scope.newRecord = false;
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             })
             .error(function(data) { $log.log(data.record); });
      }
      // if the record already existed as a draft, we update the draft
      else
      {
        $http.put('//' + hostname + '/api/metadata/' + current._id.$oid,
                  current)
             .success(function(data) {
                 updateForms(data.record);
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             });
      }

      var currentId = $scope.currentRecord._id.$oid;

      $http.post('//' + hostname + '/api/metadata/' +
                 currentId + '/publish',
                 current)
            .success(function(data) {
              updateForms(data.record);
              addedContacts =
              {
                'access': 0,
                'citation': 0
              };

              displayCurrentRecords();
            });
    };

    function prepareCurrentScopedRecord()
    {
      $scope.currentRecord.start_date.$date.setHours(
        $scope.currentRecord.start_date.hours,
        $scope.currentRecord.start_date.minutes,
        $scope.currentRecord.start_date.seconds
      );

      $scope.currentRecord.end_date.$date.setHours(
        $scope.currentRecord.end_date.hours,
        $scope.currentRecord.end_date.minutes,
        $scope.currentRecord.end_date.seconds
      );

      var current = JSON.parse(JSON.stringify($scope.currentRecord));

      current.place_keywords = $scope.currentRecord.place_keywords.split(', ');
      current.thematic_keywords = $scope.currentRecord.thematic_keywords.split(', ');

      current.data_format = $scope.dataFormats;

      if ($scope.auxDataFormats)
      {
        var auxList = $scope.auxDataFormats.split(',')
                            .map(function(el){ return el.trim(); });

        current.data_format = $scope.dataFormats.concat(auxList);
      }

      current.last_mod_date =
        {$date: $scope.currentRecord.last_mod_date.$date.getTime()};
      current.start_date =
        {$date: $scope.currentRecord.start_date.$date.getTime()};
      current.end_date =
        {$date: $scope.currentRecord.end_date.$date.getTime()};

      current.first_pub_date =
        {$date: $scope.currentRecord.first_pub_date.$date.getTime()};

      return current;
    }

    function displayCurrentRecords()
    {
      $http.post('//' + hostname + '/api/metadata',
                 {'session_id': session_id})
           .success(function(data){
             $scope.allRecords = data.results;
           });
    }

    /**
     * Assign a record to the scope's current record.
     *
     * Args:
     *  (Object) actual record
     */
    function updateForms(record)
    {
       /* place and thematic keywords come as a list from the server */
       record.place_keywords =
         record.place_keywords.join(', ');

       record.thematic_keywords =
         record.thematic_keywords.join(', ');

       /*
        * Need to do $scope with dates because our service returns them as
        * epoch seconds.
        */
       record.start_date.$date =
         new Date(record.start_date.$date);

       record.end_date.$date =
         new Date(record.end_date.$date);

       record.last_mod_date.$date =
         new Date(record.last_mod_date.$date);

       record.first_pub_date.$date =
         new Date(record.first_pub_date.$date);

       // these hours, minutes, seconds get put into broken out select boxes
       record.start_date.hours = record.start_date.$date.getHours();
       record.end_date.hours = record.end_date.$date.getHours();

       record.start_date.minutes = record.start_date.$date.getMinutes();
       record.end_date.minutes = record.end_date.$date.getMinutes();

       record.start_date.seconds = record.start_date.$date.getSeconds();
       record.end_date.seconds = record.end_date.$date.getSeconds();

       $scope.currentRecord = record;
       $scope.auxDataFormats =
         record.data_format.filter(function (f) {
           return $scope.knownDataFormats.indexOf(f) === -1;
         }).join(', ');

       $scope.dataFormats =
         record.data_format.filter(function (f) {
           return $scope.knownDataFormats.indexOf(f) > -1;
         });

       if (!$scope.currentRecord.online) {
         record.online = [""];
       }
    }
    /*
     * Use these maps in the view: key gets displayed, value is actually the
     * object value
     */
    $scope.statusChoicesIsoMap = {
      'completed': 'completed',
      'continually updated': 'onGoing',
      'in process': 'underDevelopment',
      'planned': 'planned',
      'needs to be generated or updated': 'required',
      'stored in an offline facility': 'historicalArchive',
      'no longer valid': 'obsolete'
    };

    $scope.topicCategoryChoices =
      ['biota', 'boundaries',
       'climatologyMeteorologyAtmosphere', 'economy', 'elevation',
       'environment', 'farming', 'geoscientificInformation', 'health',
       'imageryBaseMapsEarthCover', 'inlandWaters', 'location',
       'intelligenceMilitary', 'oceans', 'planningCadastre', 'society',
       'structure', 'transportation',
       'utilitiesCommunication'];

    // our more human-readable update frequency choices need trans to ISO 19115
    $scope.updateFrequencyChoicesMap = {
      'continual': 'continual',
      'daily': 'daily',
      'weekly': 'weekly',
      'fortnightly': 'fortnightly',
      'monthly': 'monthly',
      'quarterly': 'quarterly',
      'biannually': 'biannually',
      'annually': 'annually',
      'as needed': 'asNeeded',
      'irregular': 'irregular',
      'not planned': 'notPlanned',
      'unknown': 'unknown'
    };

    // order for contact fields
    $scope.orderedContactFields =
      ['name', 'email', 'org', 'address', 'city',
       'state', 'zipcode', 'country', 'phone'];

    $scope.cfieldsMap =
    {
        'name': 'Name',
        'email': 'Email',
        'org': 'Organization',
        'address': 'Address',
        'city': 'City',
        'state': 'State',
        'zipcode': 'Zip Code',
        'country': 'Country',
        'phone': 'Phone'
    };

    var addedContacts =
    {
      'access': 0,
      'citation': 0
    };

    $scope.addContactCitation = function()
    {
      $scope.currentRecord.citation
            .push(JSON.parse(JSON.stringify(EMPTY_CONTACT)));

      addedContacts.citation += 1;
    };

    $scope.addContactAccess = function()
    {
      $scope.currentRecord.access
            .push(JSON.parse(JSON.stringify(EMPTY_CONTACT)));

      addedContacts.access += 1;
    };

    $scope.cancelAddContactCitation = function()
    {
      if (addedContacts.citation > 0)
      {
        $scope.currentRecord.citation.pop();
        addedContacts.citation -= 1;
      }
    };

    $scope.cancelAddContactAccess = function()
    {
      if (addedContacts.access > 0)
      {
        $scope.currentRecord.access.pop();
        addedContacts.access -= 1;
      }
    };

    $scope.removeOnlineResource = function(resourceIndex)
    {
      if ($scope.currentRecord.online.length === 1)
      {
        $scope.currentRecord.online[0] = "";
      }
      else
      {
        $scope.currentRecord.online.splice(resourceIndex, 1);
      }
    };

    $scope.addOnlineResource = function()
    {
      $scope.currentRecord.online.push("");
    };

    $scope.options = {};
    $scope.getBbox = function()
    {
      $log.log('bbox input: ' + $scope.options.bboxInput);
      var baseUrl = '//' + hostname + '/api/geocode/';
      var fullUrl = baseUrl + $scope.options.bboxInput;

      $http.get(fullUrl)
           .success(function(data)
           {
             $scope.currentRecord.north_lat = data.north;
             $scope.currentRecord.south_lat = data.south;
             $scope.currentRecord.east_lon = data.east;
             $scope.currentRecord.west_lon = data.west;
           });
    };
  } // end of callback for controller initialization
])
.controller('ISOController', [function()
  {
    this.standard = 'iso';
  }
])
.controller('DCController', [function()
  {
    this.standard = 'dc';
  }
]);
<commit_msg>miles defaults overwriting only the fields it covers<commit_after>'use strict';

//var metadataEditorApp =
  //angular.module('metadataEditor', ['ngRoute', 'ui.date']);

// for minification, explicitly declare dependencies $scope and $http
metadataEditorApp.controller('BaseController', ['$scope', '$http', '$log',
  function($scope, $http, $log) {

    // first see if we have any user information given to us (from Drupal)
    if (typeof(window.session_id) === 'undefined')
    {
      var session_id = 'local';
    }
    else
    {
      var session_id = window.session_id;
    }

    // next see if there is a hostname defined
    var hostname = '';
    if (typeof(window.hostname) === 'undefined')
    {
      hostname = 'localhost:4000';
    }
    else
    {
      hostname = window.hostname;
    }
    $scope.hostname = hostname;

    // initialize list of existing metadata records
    displayCurrentRecords();

    // set date options
    $scope.dateOptions = {
        changeYear: true,
        changeMonth: true,
        yearRange: '1700:+10'
    };

    // create time picker vars
    $scope.hours = _.range(24);
    $scope.minute_seconds = _.range(60);

    $scope.knownDataFormats = ["ASCII", "csv", "DLG", "docx", "DRG", "DWG", "eps",
      "ERDAS", "Esri file GDB", "Esri grid", "Esri personal GDB","Esri TIN", "FASTA", "FASTQ", "GenBank",
      "GeoJSON", "GeoTIFF", "GML", "HDF", "jpeg", "KML", "LAS", "mp3",
      "MrSID", "netCDF", "pdf", "php", "png", "py", "R", "SDXF", "Shapefile",
      "SPSS", "Stata", "Tab", "tiff", "txt", "VBS", "wav", "xls", "xlsx",
      "xml"];

    $scope.dataFormats = [];

    $scope.spatialDataOptions = ["vector", "grid", "table or text",
      "triangulated irregular network", "stereographic imaging",
      "video recording of a scene"];
    $scope.hierarchyLevels = ["dataset", "series"];

    /**
     * Fetch the record with recordId and update the form to display it.
     *
     * Intended for use in conjunction with the Edit buttons in the records list
     */
    $scope.editRecord = function(recordId)
    {
      $scope.newRecord = false;

      $http.post('//' + hostname + '/api/metadata/' + recordId,
                 {'session_id': session_id})
           .success(function(data) {
             var record = data.record;
             if (typeof record.start_date == "undefined") {
               record.start_date = {$date: new Date(2010, 1, 1)};
             }
             if (typeof record.end_date == "undefined") {
               record.end_date = {$date: new Date()};
             }
             if (typeof record.last_mod_date == "undefined") {
               record.last_mod_date = {$date: new Date()};
             }
             if (typeof record.first_pub_date == "undefined") {
               record.first_pub_date = {$date: new Date()};
             }
             $log.log(record);
             updateForms(record);
           });
    };

    var EMPTY_CONTACT =
    {
      'name': '', 'email': '', 'org': '', 'address': '',
      'city': '', 'state': '', 'zipcode': '', 'country': '', 'phone': ''
    };

    $scope.createNewRecord = function()
    {
      $scope.newRecord = true;

      $http.get('//' + hostname + '/api/metadata/placeholder')
           .success(function(data) {
             var placeholderRec = data.record;
             //$scope.currentRecord = placeholderRec;
             var emptyRec = JSON.parse(JSON.stringify(placeholderRec));

             $log.log(emptyRec);

             // clear out placeholder values
             for (var field in emptyRec)
             {
               if (['citation', 'access'].indexOf(field) > -1)
               {
                 emptyRec[field] = [JSON.parse(JSON.stringify(EMPTY_CONTACT))];
               }
               else if (
                 ['place_keywords', 'thematic_keywords',
                  'data_format', 'online'].indexOf(field) > -1)
               {
                 emptyRec[field] = [""];
               }
               else if (['_cls', '_id', 'start_date', 'end_date', 'last_mod_date',
                         'first_pub_date'].indexOf(field) == -1)
               {
                 emptyRec[field] = "";
               }
             }
             emptyRec.start_date = {$date: new Date(2010, 1, 1)};
             emptyRec.end_date = {$date: new Date()};
             emptyRec.end_date.$date.setHours(0);
             emptyRec.end_date.$date.setMinutes(0);
             emptyRec.end_date.$date.setSeconds(0);

             $scope.currentRecord = emptyRec;

             updateForms($scope.currentRecord);
           });
    };

    // initialize form with placeholder data for creating a new record
    $scope.createNewRecord();

    /**
     * Change the metadata type being generated between ISO for datasets
     * and Dublin Core for other research products
     */
    $scope.isDublin = false;
    $scope.isISO = true;
    $scope.toggleMetadataType = function(type)
    {
      switch (type)
      {
        case 'dublin':
          $scope.isDublin = true;
          $scope.isISO = false;
          break;

        case 'iso':
          $scope.isDublin = false;
          $scope.isISO = true;
          break;

        default:
          $scope.isDublin = false;
          $scope.isISO = true;
      }

      $log.log('dublin: ' + $scope.isDublin);
      $log.log('iso: ' + $scope.isISO);
    };

    /**On click of Load MILES Defaults button,
     * load the defaults in MILES defaults json file
     */
    $scope.defaultMILES = function()
    {
      $scope.newRecord = true;

      $http.get('//' + hostname + '/api/metadata/defaultMILES')
           .success(function(data) {

             var milesRec = data.record;

             for (var key in milesRec)
             {
               if (milesRec.hasOwnProperty(key))
               {
                  // only want to overwrite country and state for MILES
                  if (key === "citation")
                  {
                    $scope.currentRecord[key][0].country = milesRec[key][0].country;
                    $scope.currentRecord[key][0].state = milesRec[key][0].state;
                  }
                  else
                  {
                    $scope.currentRecord[key] = milesRec[key];
                  }
               }
             }

             updateForms($scope.currentRecord);
           });
    };

    /**
     * On submit of metadata form, submitRecord. This both updates the server
     * and makes sure the form is current. Not sure how it wouldn't be, todo?
     */
    $scope.submitDraftRecord = function()
    {
      // the start and end dates currently have hours and minutes zero;
      // grab the hours and minutes and append them. Confusingly JS
      // uses setHours to set minutes and seconds as well
      var current = prepareCurrentScopedRecord();
      if ($scope.newRecord)
      {
        $http.put('//' + hostname + '/api/metadata',
                  {'record': current, 'session_id': session_id})
             .success(function(data) {
                 updateForms(data.record);
                 $scope.newRecord = false;
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             })
             .error(function(data) { $log.log(data.record); });
      }
      else
      {
        $http.put('//' + hostname + '/api/metadata/' + current._id.$oid,
                  {'record': current, 'session_id': session_id})
             .success(function(data) {
                 updateForms(data.record);
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             });
      }
    };

    /**
     * Publish a record to the portal. Requires all fields to be valid
     */
    $scope.publishRecord = function()
    {
      var current = prepareCurrentScopedRecord();
      // if the record is totally new, we first want to save the draft of it
      if ($scope.newRecord)
      {
        $http.post('//' + hostname + '/api/metadata', current)
             .success(function(data) {
                 updateForms(data.record);
                 $scope.newRecord = false;
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             })
             .error(function(data) { $log.log(data.record); });
      }
      // if the record already existed as a draft, we update the draft
      else
      {
        $http.put('//' + hostname + '/api/metadata/' + current._id.$oid,
                  current)
             .success(function(data) {
                 updateForms(data.record);
                 addedContacts =
                 {
                  'access': 0,
                  'citation': 0
                 };
                 displayCurrentRecords();
             });
      }

      var currentId = $scope.currentRecord._id.$oid;

      $http.post('//' + hostname + '/api/metadata/' +
                 currentId + '/publish',
                 current)
            .success(function(data) {
              updateForms(data.record);
              addedContacts =
              {
                'access': 0,
                'citation': 0
              };

              displayCurrentRecords();
            });
    };

    function prepareCurrentScopedRecord()
    {
      $scope.currentRecord.start_date.$date.setHours(
        $scope.currentRecord.start_date.hours,
        $scope.currentRecord.start_date.minutes,
        $scope.currentRecord.start_date.seconds
      );

      $scope.currentRecord.end_date.$date.setHours(
        $scope.currentRecord.end_date.hours,
        $scope.currentRecord.end_date.minutes,
        $scope.currentRecord.end_date.seconds
      );

      var current = JSON.parse(JSON.stringify($scope.currentRecord));

      current.place_keywords = $scope.currentRecord.place_keywords.split(', ');
      current.thematic_keywords = $scope.currentRecord.thematic_keywords.split(', ');

      current.data_format = $scope.dataFormats;

      if ($scope.auxDataFormats)
      {
        var auxList = $scope.auxDataFormats.split(',')
                            .map(function(el){ return el.trim(); });

        current.data_format = $scope.dataFormats.concat(auxList);
      }

      current.last_mod_date =
        {$date: $scope.currentRecord.last_mod_date.$date.getTime()};
      current.start_date =
        {$date: $scope.currentRecord.start_date.$date.getTime()};
      current.end_date =
        {$date: $scope.currentRecord.end_date.$date.getTime()};

      current.first_pub_date =
        {$date: $scope.currentRecord.first_pub_date.$date.getTime()};

      return current;
    }

    function displayCurrentRecords()
    {
      $http.post('//' + hostname + '/api/metadata',
                 {'session_id': session_id})
           .success(function(data){
             $scope.allRecords = data.results;
           });
    }

    /**
     * Assign a record to the scope's current record.
     *
     * Args:
     *  (Object) actual record
     */
    function updateForms(record)
    {
       /* place and thematic keywords come as a list from the server */
       record.place_keywords =
         record.place_keywords.join(', ');

       record.thematic_keywords =
         record.thematic_keywords.join(', ');

       /*
        * Need to do $scope with dates because our service returns them as
        * epoch seconds.
        */
       record.start_date.$date =
         new Date(record.start_date.$date);

       record.end_date.$date =
         new Date(record.end_date.$date);

       record.last_mod_date.$date =
         new Date(record.last_mod_date.$date);

       record.first_pub_date.$date =
         new Date(record.first_pub_date.$date);

       // these hours, minutes, seconds get put into broken out select boxes
       record.start_date.hours = record.start_date.$date.getHours();
       record.end_date.hours = record.end_date.$date.getHours();

       record.start_date.minutes = record.start_date.$date.getMinutes();
       record.end_date.minutes = record.end_date.$date.getMinutes();

       record.start_date.seconds = record.start_date.$date.getSeconds();
       record.end_date.seconds = record.end_date.$date.getSeconds();

       $scope.currentRecord = record;
       $scope.auxDataFormats =
         record.data_format.filter(function (f) {
           return $scope.knownDataFormats.indexOf(f) === -1;
         }).join(', ');

       $scope.dataFormats =
         record.data_format.filter(function (f) {
           return $scope.knownDataFormats.indexOf(f) > -1;
         });

       if (!$scope.currentRecord.online) {
         record.online = [""];
       }
    }
    /*
     * Use these maps in the view: key gets displayed, value is actually the
     * object value
     */
    $scope.statusChoicesIsoMap = {
      'completed': 'completed',
      'continually updated': 'onGoing',
      'in process': 'underDevelopment',
      'planned': 'planned',
      'needs to be generated or updated': 'required',
      'stored in an offline facility': 'historicalArchive',
      'no longer valid': 'obsolete'
    };

    $scope.topicCategoryChoices =
      ['biota', 'boundaries',
       'climatologyMeteorologyAtmosphere', 'economy', 'elevation',
       'environment', 'farming', 'geoscientificInformation', 'health',
       'imageryBaseMapsEarthCover', 'inlandWaters', 'location',
       'intelligenceMilitary', 'oceans', 'planningCadastre', 'society',
       'structure', 'transportation',
       'utilitiesCommunication'];

    // our more human-readable update frequency choices need trans to ISO 19115
    $scope.updateFrequencyChoicesMap = {
      'continual': 'continual',
      'daily': 'daily',
      'weekly': 'weekly',
      'fortnightly': 'fortnightly',
      'monthly': 'monthly',
      'quarterly': 'quarterly',
      'biannually': 'biannually',
      'annually': 'annually',
      'as needed': 'asNeeded',
      'irregular': 'irregular',
      'not planned': 'notPlanned',
      'unknown': 'unknown'
    };

    // order for contact fields
    $scope.orderedContactFields =
      ['name', 'email', 'org', 'address', 'city',
       'state', 'zipcode', 'country', 'phone'];

    $scope.cfieldsMap =
    {
        'name': 'Name',
        'email': 'Email',
        'org': 'Organization',
        'address': 'Address',
        'city': 'City',
        'state': 'State',
        'zipcode': 'Zip Code',
        'country': 'Country',
        'phone': 'Phone'
    };

    var addedContacts =
    {
      'access': 0,
      'citation': 0
    };

    $scope.addContactCitation = function()
    {
      $scope.currentRecord.citation
            .push(JSON.parse(JSON.stringify(EMPTY_CONTACT)));

      addedContacts.citation += 1;
    };

    $scope.addContactAccess = function()
    {
      $scope.currentRecord.access
            .push(JSON.parse(JSON.stringify(EMPTY_CONTACT)));

      addedContacts.access += 1;
    };

    $scope.cancelAddContactCitation = function()
    {
      if (addedContacts.citation > 0)
      {
        $scope.currentRecord.citation.pop();
        addedContacts.citation -= 1;
      }
    };

    $scope.cancelAddContactAccess = function()
    {
      if (addedContacts.access > 0)
      {
        $scope.currentRecord.access.pop();
        addedContacts.access -= 1;
      }
    };

    $scope.removeOnlineResource = function(resourceIndex)
    {
      if ($scope.currentRecord.online.length === 1)
      {
        $scope.currentRecord.online[0] = "";
      }
      else
      {
        $scope.currentRecord.online.splice(resourceIndex, 1);
      }
    };

    $scope.addOnlineResource = function()
    {
      $scope.currentRecord.online.push("");
    };

    $scope.options = {};
    $scope.getBbox = function()
    {
      $log.log('bbox input: ' + $scope.options.bboxInput);
      var baseUrl = '//' + hostname + '/api/geocode/';
      var fullUrl = baseUrl + $scope.options.bboxInput;

      $http.get(fullUrl)
           .success(function(data)
           {
             $scope.currentRecord.north_lat = data.north;
             $scope.currentRecord.south_lat = data.south;
             $scope.currentRecord.east_lon = data.east;
             $scope.currentRecord.west_lon = data.west;
           });
    };
  } // end of callback for controller initialization
])
.controller('ISOController', [function()
  {
    this.standard = 'iso';
  }
])
.controller('DCController', [function()
  {
    this.standard = 'dc';
  }
]);
<|endoftext|>


<commit_before>'use strict'

const {spawnSync} = require('child_process')
const {EventEmitter} = require('events').EventEmitter
const moment = require('moment-timezone')
const _ = require('lodash')
const bucker = require('bucker')
const ms = require('ms')
const parse = require('./parse')

// I think this is always the last two items
const systemTimezone = () => {
  let tz = null
  try {
    tz = spawnSync('/bin/ls', ['-l', '/etc/localtime'])
      .stdout
      .toString()
      .trim()
      .split('/')
      .slice(-2)
      .join('/')
  } catch (e) {
    tz = null
  }
  return tz
}

class ScoreEmitter extends EventEmitter {
  constructor (options) {
    super()

    this.options = _.defaults(options, {
      url: null,
      logger: bucker.createNullLogger(),
      interval: ms('5m'),
      timezone: systemTimezone() || 'America/New_York',
      dailyCutoff: 180,
      completed: { seriesCompleted: true },
      filter: _.stubTrue,
      parse: {
        finalPeriod: null,
        periodLength: null,
        __now: null
      },
      __now: null,
      __skippedIds: []
    })

    if (typeof this.options.interval === 'string') {
      this.options.interval = ms(this.options.interval)
    }

    if (this.options.__now && !this.options.parse.__now) {
      this.options.parse.__now = this.options.__now
    }

    this.logger = this.options.logger
    this.__skippedIds = this.options.__skippedIds

    this.__timeoutId = null
    this.__lastDate = null

    const url = this.options.url
    const interval = this.options.interval

    if (!interval || typeof interval !== 'number' || interval < ms('1m')) {
      throw new Error('Interval must be a number of milliseconds no less than 1 second')
    }

    if (!url) {
      throw new Error('Url is required')
    }
  }

  start () {
    if (!this.__timeoutId) {
      this.logger.info('[START]')
      this.fetch({initial: true})
    }
    return this
  }

  stop () {
    if (this.__timeoutId) {
      this.logger.info('[STOP]')
      clearTimeout(this.__timeoutId)
      delete this.__timeoutId
    }
    return this
  }

  getCurrentMoment () {
    // __now is just for testing
    return moment(this.options.__now || void 0).tz(this.options.timezone)
  }

  getCurrentDate () {
    return this.getCurrentMoment().subtract(this.options.dailyCutoff, 'm').format('YYYYMMDD')
  }

  getNextCutoff () {
    const cutoff = this.options.dailyCutoff
    const current = this.getCurrentMoment()

    // eslint-disable-next-line no-magic-numbers
    const nextCutoff = current.clone().hour(23).minute(59).second(60).millisecond(0).add(cutoff, 'm')
    const thisCutoff = current.clone().startOf('day').add(cutoff, 'm')

    // Add one minute to the cutoff always. This avoids a rounding bug where sometimes once the
    // timeout to this cutoff is triggered, it is actually less than a second before the cutoff
    // so it completely misses the next days games
    return (current.isBefore(thisCutoff) ? thisCutoff : nextCutoff).add(1, 'm')
  }

  fetch (options) {
    const date = this.getCurrentDate()
    const url = this.options.url.replace('{date}', date)

    if (this.__lastDate && date !== this.__lastDate) {
      // Clear completed game IDs if we are on a new day
      this.__skippedIds = []
    }

    this.__lastDate = date

    this.logger.info('[FETCH]', url)

    parse(url, this.options.parse, (err, events) => {
      if (err) {
        this.logger.error('[FETCH]', err)
        this.emit('error', err)
        this.nextFetch()
        return
      }
      this.parse(events, options)
    })
  }

  parse (allEvents, options) {
    const completedOnly = !!this.options.completed
    const filteredEvents = _.filter(allEvents, this.options.filter)

    this.logger.debug('[ALL]', JSON.stringify(allEvents))
    this.logger.debug('[FILTERED]', JSON.stringify(filteredEvents))

    // Emit for completed events only after the initial fetch
    const shouldEmit = completedOnly ? !(options && options.initial) : true

    const available = filteredEvents.filter((event) => this.__skippedIds.indexOf(event.id) === -1)

    const found = _.filter(available, completedOnly ? this.options.completed : _.stubTrue)
    const eventsLeft = available.filter((event) => completedOnly ? !_.find(found, {id: event.id}) : _.stubTrue)

    this.logger.info('[PARSE]', JSON.stringify({
      emit: shouldEmit,
      total: allEvents.length,
      ignored: this.__skippedIds.length,
      pending: eventsLeft.length,
      found: found.length
    }))

    found.forEach((event) => {
      if (completedOnly) this.__skippedIds.push(event.id)

      if (shouldEmit) {
        this.logger.info('[EVENT]', event.id)
        this.emit('event', event)
      }
    })

    if (!completedOnly && shouldEmit) {
      this.emit('events', found)
    }

    // If we are not looking for only completed games then just go by the interval
    const nextUpdate = !completedOnly && this.options.interval

    // Otherwise figure it out based on next starting or remaining
    const nextUntil = _.minBy(eventsLeft, 'status.timeUntil')
    const nextRemaining = _.minBy(eventsLeft, 'status.timeRemaining')

    const allUnstarted = eventsLeft.every((event) => event.status.state === 'pre')
    const anyInProgress = eventsLeft.find((event) => event.status.state === 'in')

    if (eventsLeft.length === 0) {
      // There are no games today or no more new games so wait until tomorrow
      this.logger.info('[NEXT]', 'Waiting until tomorrow')
      this.nextFetch(this.getNextCutoff().diff(this.getCurrentMoment()))
    } else if (allUnstarted && nextUntil) {
      // There are games today but none have started, try and get the time of the first game
      this.logger.info('[NEXT]', 'Waiting for first event')
      this.nextFetch(nextUntil.status.timeUntil)
    } else if (anyInProgress && nextUpdate) {
      // There is at least one in progress game, wait for the interval and check again
      this.logger.info('[NEXT]', 'Waiting for interval to check on next event')
      this.nextFetch(nextUpdate)
    } else if (anyInProgress && nextRemaining) {
      // There is at least one in progress game, wait for the time in the next closest game
      this.logger.info('[NEXT]', 'Waiting for next event to finish')
      this.nextFetch(nextRemaining.status.timeRemaining)
    } else {
      // Something else happened?
      this.logger.info('[NEXT]', 'Unknown state, waiting 1hr')
      this.nextFetch(ms('1hr'))
    }
  }

  nextFetch (requestedTimeout) {
    // Always wait at least the interval
    const timeout = Math.max(requestedTimeout || 0, this.options.interval)

    this.logger.info('[NEXT]', JSON.stringify({
      display: ms(timeout),
      requested: requestedTimeout,
      actual: timeout
    }))

    this.emit('next', timeout)
    this.__timeoutId = setTimeout(() => this.fetch(), timeout)
  }
}

module.exports = ScoreEmitter
<commit_msg>debug<commit_after>'use strict'

const {spawnSync} = require('child_process')
const {EventEmitter} = require('events').EventEmitter
const moment = require('moment-timezone')
const _ = require('lodash')
const bucker = require('bucker')
const ms = require('ms')
const parse = require('./parse')

// I think this is always the last two items
const systemTimezone = () => {
  let tz = null
  try {
    tz = spawnSync('/bin/ls', ['-l', '/etc/localtime'])
      .stdout
      .toString()
      .trim()
      .split('/')
      .slice(-2)
      .join('/')
  } catch (e) {
    tz = null
  }
  return tz
}

class ScoreEmitter extends EventEmitter {
  constructor (options) {
    super()

    this.options = _.defaults(options, {
      url: null,
      logger: bucker.createNullLogger(),
      interval: ms('5m'),
      timezone: systemTimezone() || 'America/New_York',
      dailyCutoff: 180,
      completed: { seriesCompleted: true },
      filter: _.stubTrue,
      parse: {
        finalPeriod: null,
        periodLength: null,
        __now: null
      },
      __now: null,
      __skippedIds: []
    })

    if (typeof this.options.interval === 'string') {
      this.options.interval = ms(this.options.interval)
    }

    if (this.options.__now && !this.options.parse.__now) {
      this.options.parse.__now = this.options.__now
    }

    this.logger = this.options.logger
    this.__skippedIds = this.options.__skippedIds

    this.__timeoutId = null
    this.__lastDate = null

    const url = this.options.url
    const interval = this.options.interval

    if (!interval || typeof interval !== 'number' || interval < ms('1m')) {
      throw new Error('Interval must be a number of milliseconds no less than 1 second')
    }

    if (!url) {
      throw new Error('Url is required')
    }
  }

  start () {
    if (!this.__timeoutId) {
      this.logger.info('[START]')
      this.fetch({initial: true})
    }
    return this
  }

  stop () {
    if (this.__timeoutId) {
      this.logger.info('[STOP]')
      clearTimeout(this.__timeoutId)
      delete this.__timeoutId
    }
    return this
  }

  getCurrentMoment () {
    // __now is just for testing
    return moment(this.options.__now || void 0).tz(this.options.timezone)
  }

  getCurrentDate () {
    return this.getCurrentMoment().subtract(this.options.dailyCutoff, 'm').format('YYYYMMDD')
  }

  getNextCutoff () {
    const cutoff = this.options.dailyCutoff
    const current = this.getCurrentMoment()

    // eslint-disable-next-line no-magic-numbers
    const nextCutoff = current.clone().hour(23).minute(59).second(60).millisecond(0).add(cutoff, 'm')
    const thisCutoff = current.clone().startOf('day').add(cutoff, 'm')

    // Add one minute to the cutoff always. This avoids a rounding bug where sometimes once the
    // timeout to this cutoff is triggered, it is actually less than a second before the cutoff
    // so it completely misses the next days games
    return (current.isBefore(thisCutoff) ? thisCutoff : nextCutoff).add(1, 'm')
  }

  fetch (options) {
    const date = this.getCurrentDate()
    const url = this.options.url.replace('{date}', date)

    if (this.__lastDate && date !== this.__lastDate) {
      // Clear completed game IDs if we are on a new day
      this.__skippedIds = []
    }

    this.__lastDate = date

    this.logger.info('[FETCH]', url)

    parse(url, this.options.parse, (err, events) => {
      if (err) {
        this.logger.error('[FETCH]', err)
        this.emit('error', err)
        this.nextFetch()
        return
      }
      this.parse(events, options)
    })
  }

  parse (allEvents, options) {
    const completedOnly = !!this.options.completed
    const filteredEvents = _.filter(allEvents, this.options.filter)

    this.logger.debug('[ALL]', JSON.stringify(allEvents))
    this.logger.debug('[FILTERED]', JSON.stringify(filteredEvents))

    // Emit for completed events only after the initial fetch
    const shouldEmit = completedOnly ? !(options && options.initial) : true

    const available = filteredEvents.filter((event) => this.__skippedIds.indexOf(event.id) === -1)

    const found = _.filter(available, completedOnly ? this.options.completed : _.stubTrue)
    const eventsLeft = available.filter((event) => completedOnly ? !_.find(found, {id: event.id}) : _.stubTrue)

    this.logger.info('[PARSE]', JSON.stringify({
      emit: shouldEmit,
      total: allEvents.length,
      ignored: this.__skippedIds.length,
      pending: eventsLeft.length,
      found: found.length
    }))

    found.forEach((event) => {
      if (completedOnly) this.__skippedIds.push(event.id)

      if (shouldEmit) {
        this.logger.debug('[EVENT]', event.id)
        this.emit('event', event)
      }
    })

    if (!completedOnly && shouldEmit) {
      this.logger.debug('[EVENTS]', JSON.stringify(found))
      this.emit('events', found)
    }

    // If we are not looking for only completed games then just go by the interval
    const nextUpdate = !completedOnly && this.options.interval

    // Otherwise figure it out based on next starting or remaining
    const nextUntil = _.minBy(eventsLeft, 'status.timeUntil')
    const nextRemaining = _.minBy(eventsLeft, 'status.timeRemaining')

    const allUnstarted = eventsLeft.every((event) => event.status.state === 'pre')
    const anyInProgress = eventsLeft.find((event) => event.status.state === 'in')

    if (eventsLeft.length === 0) {
      // There are no games today or no more new games so wait until tomorrow
      this.logger.info('[NEXT]', 'Waiting until tomorrow')
      this.nextFetch(this.getNextCutoff().diff(this.getCurrentMoment()))
    } else if (allUnstarted && nextUntil) {
      // There are games today but none have started, try and get the time of the first game
      this.logger.info('[NEXT]', 'Waiting for first event')
      this.nextFetch(nextUntil.status.timeUntil)
    } else if (anyInProgress && nextUpdate) {
      // There is at least one in progress game, wait for the interval and check again
      this.logger.info('[NEXT]', 'Waiting for interval to check on next event')
      this.nextFetch(nextUpdate)
    } else if (anyInProgress && nextRemaining) {
      // There is at least one in progress game, wait for the time in the next closest game
      this.logger.info('[NEXT]', 'Waiting for next event to finish')
      this.nextFetch(nextRemaining.status.timeRemaining)
    } else {
      // Something else happened?
      this.logger.info('[NEXT]', 'Unknown state, waiting 1hr')
      this.nextFetch(ms('1hr'))
    }
  }

  nextFetch (requestedTimeout) {
    // Always wait at least the interval
    const timeout = Math.max(requestedTimeout || 0, this.options.interval)

    this.logger.info('[NEXT]', JSON.stringify({
      display: ms(timeout),
      requested: requestedTimeout,
      actual: timeout
    }))

    this.emit('next', timeout)
    this.__timeoutId = setTimeout(() => this.fetch(), timeout)
  }
}

module.exports = ScoreEmitter
<|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 Offline message screen implementation.
 */

cr.define('login', function() {
  // Screens that should have offline message overlay.
  const MANAGED_SCREENS = ['gaia-signin', 'signin'];

  /**
   * Creates a new offline message screen div.
   * @constructor
   * @extends {HTMLDivElement}
   */
  var OfflineMessageScreen = cr.ui.define('div');

  /**
   * Registers with Oobe.
   */
  OfflineMessageScreen.register = function() {
    var screen = $('offline-message');
    OfflineMessageScreen.decorate(screen);

    // Note that OfflineMessageScreen is not registered with Oobe because
    // it is shown on top of sign-in screen instead of as an independent screen.
  };

  OfflineMessageScreen.prototype = {
    __proto__: HTMLDivElement.prototype,

    /** @inheritDoc */
    decorate: function() {
      window.addEventListener('online',
                              this.handleNetworkStateChange_.bind(this));
      window.addEventListener('offline',
                              this.handleNetworkStateChange_.bind(this));
      cr.ui.DropDown.decorate($('offline-networks-list'));
    },

    onBeforeShow: function() {
      cr.ui.DropDown.setActive('offline-networks-list', true);
    },

    onBeforeHide: function() {
      cr.ui.DropDown.setActive('offline-networks-list', false);
    },

    /**
     * Shows or hides offline message based on network on/offline state.
     */
    update: function() {
      var currentScreen = Oobe.getInstance().currentScreen;
      var offlineMessage = this;
      var isOffline = !window.navigator.onLine;
      var shouldOverlay = MANAGED_SCREENS.indexOf(currentScreen.id) != -1;

      if (isOffline && shouldOverlay) {
        offlineMessage.onBeforeShow();

        $('offline-message-text').hidden = false;
        $('captive-portal-message-text').hidden = true;

        offlineMessage.classList.remove('hidden');
        offlineMessage.classList.remove('faded');

        if (!currentScreen.classList.contains('faded')) {
          currentScreen.classList.add('faded');
          currentScreen.addEventListener('webkitTransitionEnd',
            function f(e) {
              currentScreen.removeEventListener('webkitTransitionEnd', f);
              currentScreen.classList.add('hidden');
            });
        }
      } else {
        if (!offlineMessage.classList.contains('faded')) {
          offlineMessage.onBeforeHide();

          offlineMessage.classList.add('faded');
          offlineMessage.addEventListener('webkitTransitionEnd',
            function f(e) {
              offlineMessage.removeEventListener('webkitTransitionEnd', f);
              offlineMessage.classList.add('hidden');
            });

          currentScreen.classList.remove('hidden');
          currentScreen.classList.remove('faded');

          // Triggers refresh for Gaia sign-in screen.
          if (currentScreen.id == 'gaia-signin')
            chrome.send('showAddUser');
        }
      }
    },

    /**
     * Handler of online/offline event.
     */
    handleNetworkStateChange_: function() {
      this.update();
    }
  };

  /**
   * Handler for iframe's error notification coming from the outside.
   * For more info see C++ class 'SnifferObserver' which calls this method.
   * @param {number} error Error code.
   */
  OfflineMessageScreen.onFrameError = function(error) {
    var currentScreen = Oobe.getInstance().currentScreen;
    var offlineMessage = $('offline-message');
    var isOffline = !window.navigator.onLine;
    var shouldOverlay = MANAGED_SCREENS.indexOf(currentScreen.id) != -1;

    if (!shouldOverlay)
      return;

    if (!isOffline) {
      $('offline-message-text').hidden = true;
      $('captive-portal-message-text').hidden = false;
    }

    if (!currentScreen.classList.contains('faded')) {
      offlineMessage.onBeforeShow();

      offlineMessage.classList.remove('hidden');
      offlineMessage.classList.remove('faded');

      currentScreen.classList.add('faded');
      currentScreen.addEventListener('webkitTransitionEnd',
        function f(e) {
          currentScreen.removeEventListener('webkitTransitionEnd', f);
          currentScreen.classList.add('hidden');
        });
    }
  };

  return {
    OfflineMessageScreen: OfflineMessageScreen
  };
});
<commit_msg>[ChromeOS] Fix automation login flakyness.<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 Offline message screen implementation.
 */

cr.define('login', function() {
  // Screens that should have offline message overlay.
  const MANAGED_SCREENS = ['gaia-signin', 'signin'];

  /**
   * Creates a new offline message screen div.
   * @constructor
   * @extends {HTMLDivElement}
   */
  var OfflineMessageScreen = cr.ui.define('div');

  /**
   * Registers with Oobe.
   */
  OfflineMessageScreen.register = function() {
    var screen = $('offline-message');
    OfflineMessageScreen.decorate(screen);

    // Note that OfflineMessageScreen is not registered with Oobe because
    // it is shown on top of sign-in screen instead of as an independent screen.
  };

  OfflineMessageScreen.prototype = {
    __proto__: HTMLDivElement.prototype,

    /** @inheritDoc */
    decorate: function() {
      window.addEventListener('online',
                              this.handleNetworkStateChange_.bind(this));
      window.addEventListener('offline',
                              this.handleNetworkStateChange_.bind(this));
      cr.ui.DropDown.decorate($('offline-networks-list'));
    },

    onBeforeShow: function() {
      cr.ui.DropDown.setActive('offline-networks-list', true);
    },

    onBeforeHide: function() {
      cr.ui.DropDown.setActive('offline-networks-list', false);
    },

    /**
     * Shows or hides offline message based on network on/offline state.
     */
    update: function() {
      var currentScreen = Oobe.getInstance().currentScreen;
      var offlineMessage = this;
      var isOffline = !window.navigator.onLine;
      var shouldOverlay = MANAGED_SCREENS.indexOf(currentScreen.id) != -1;

      if (isOffline && shouldOverlay) {
        offlineMessage.onBeforeShow();

        $('offline-message-text').hidden = false;
        $('captive-portal-message-text').hidden = true;

        offlineMessage.classList.remove('hidden');
        offlineMessage.classList.remove('faded');

        if (!currentScreen.classList.contains('faded')) {
          currentScreen.classList.add('faded');
          currentScreen.addEventListener('webkitTransitionEnd',
            function f(e) {
              currentScreen.removeEventListener('webkitTransitionEnd', f);
              currentScreen.classList.add('hidden');
            });
        }
      } else {
        if (!offlineMessage.classList.contains('faded')) {
          offlineMessage.onBeforeHide();

          offlineMessage.classList.add('faded');
          offlineMessage.addEventListener('webkitTransitionEnd',
            function f(e) {
              offlineMessage.removeEventListener('webkitTransitionEnd', f);
              offlineMessage.classList.add('hidden');
            });

          currentScreen.classList.remove('hidden');
          currentScreen.classList.remove('faded');
        }
      }
    },

    /**
     * Handler of online/offline event.
     */
    handleNetworkStateChange_: function() {
      this.update();
    }
  };

  /**
   * Handler for iframe's error notification coming from the outside.
   * For more info see C++ class 'SnifferObserver' which calls this method.
   * @param {number} error Error code.
   */
  OfflineMessageScreen.onFrameError = function(error) {
    var currentScreen = Oobe.getInstance().currentScreen;
    var offlineMessage = $('offline-message');
    var isOffline = !window.navigator.onLine;
    var shouldOverlay = MANAGED_SCREENS.indexOf(currentScreen.id) != -1;

    if (!shouldOverlay)
      return;

    if (!isOffline) {
      $('offline-message-text').hidden = true;
      $('captive-portal-message-text').hidden = false;
    }

    if (!currentScreen.classList.contains('faded')) {
      offlineMessage.onBeforeShow();

      offlineMessage.classList.remove('hidden');
      offlineMessage.classList.remove('faded');

      currentScreen.classList.add('faded');
      currentScreen.addEventListener('webkitTransitionEnd',
        function f(e) {
          currentScreen.removeEventListener('webkitTransitionEnd', f);
          currentScreen.classList.add('hidden');
        });
    }
  };

  return {
    OfflineMessageScreen: OfflineMessageScreen
  };
});
<|endoftext|>


<commit_before>'use strict';
const Readline = require('@serialport/parser-readline'); // modify
const Delimiter = require('@serialport/parser-delimiter');
const SerialPort = require('@serialport/stream');
SerialPort.Binding = require('@entrylabs/bindings');

/**
 * 스캔이 끝난 후, 선택된 포트로 시리얼포트를 오픈하는 클래스
 * 스캐너에서 open, initialize 가 일어나고,
 * 라우터에서 setRouter, connect 를 거쳐 통신한다.
 */
class Connector {
    static get DEFAULT_CONNECT_LOST_MILLS() {
        return 1000;
    }

    static get DEFAULT_SLAVE_DURATION() {
        return 1000;
    }

    constructor(hwModule, hardwareOptions) {
        this.options = hardwareOptions;
        this.hwModule = hwModule;

        /**
         * @type {MainRouter}
         */
        this.router = undefined;

        /**
         * @type {SerialPort}
         */
        this.serialPort = undefined;

        this.connected = false;
        this.received = false;
    }

    /**
     * MainRouter 를 세팅한다.
     * @param {MainRouter} router
     */
    setRouter(router) {
        this.router = router;
    }

    /**
     * 시리얼포트 생성자 옵션을 만든다.
     * @param serialPortOptions
     * @returns {Object} SerialPort 내 defaultSettings 참조
     * @private
     */
    _makeSerialPortOptions(serialPortOptions) {
        const _options = {
            autoOpen: true,
            baudRate: 9600,
            parity: 'none',
            dataBits: 8,
            stopBits: 1,
            bufferSize: 65536,
        };

        if (serialPortOptions.flowControl === 'hardware') {
            _options.rtscts = true;
        } else if (serialPortOptions.flowControl === 'software') {
            _options.xon = true;
            _options.xoff = true;
        }

        Object.assign(_options, serialPortOptions);
        return _options;
    }

    /**
     * 시리얼포트를 오픈한다.
     * @param {string} port COMPortName
     * @returns {Promise<SerialPort>}
     * // TODO 외부에 오픈하는 시리얼포트 로직을 전부 커넥터로 이동
     */
    open(port) {
        return new Promise((resolve, reject) => {
            const hardwareOptions = this.options;
            this.lostTimer = hardwareOptions.lostTimer || Connector.DEFAULT_CONNECT_LOST_MILLS;

            const serialPort = new SerialPort(port, this._makeSerialPortOptions(hardwareOptions));
            this.serialPort = serialPort;

            const { delimiter, byteDelimiter } = hardwareOptions;
            if (delimiter) {
                serialPort.parser = serialPort.pipe(new Readline({ delimiter }));
            } else if (byteDelimiter) {
                serialPort.parser = serialPort.pipe(new Delimiter({
                    delimiter: byteDelimiter,
                    includeDelimiter: true,
                }));
            }

            serialPort.on('error', reject);
            serialPort.on('open', (error) => {
                serialPort.removeAllListeners('open');
                if (error) {
                    reject(error);
                } else {
                    resolve(this.serialPort);
                }
            });
        });
    };

    /**
     * checkInitialData, requestInitialData 가 둘다 존재하는 경우 handShake 를 진행한다.
     * 둘 중 하나라도 없는 경우는 로직을 종료한다.
     * 만약 firmwareCheck 옵션이 활성화 된 경우면 executeFlash 를 세팅하고 종료한다.
     * 이 플래그는 라우터에서 flasher 를 바로 사용해야하는지 판단한다.
     *
     * @returns {Promise<void>} 준비완료 or 펌웨어체크 준비
     */
    initialize() {
        return new Promise((resolve, reject) => {
            const {
                control,
                duration = Connector.DEFAULT_SLAVE_DURATION,
                firmwarecheck,
            } = this.options;
            const hwModule = this.hwModule;
            const serialPortReadStream =
                        this.serialPort.parser ? this.serialPort.parser : this.serialPort;

            const runAsMaster = () => {
                serialPortReadStream.on('data', (data) => {
                    const result = hwModule.checkInitialData(data, this.options);

                    if (result === undefined) {
                        this.send(hwModule.requestInitialData());
                    } else {
                        this.serialPort.removeAllListeners('data');
                        serialPortReadStream.removeAllListeners('data');
                        clearTimeout(this.flashFirmware);
                        if (result === true) {
                            if (hwModule.setSerialPort) {
                                hwModule.setSerialPort(this.serialPort);
                            }
                            resolve();
                        } else {
                            reject(new Error('Invalid hardware'));
                        }
                    }
                });
            };

            const runAsSlave = () => {
                // control type is slave
                serialPortReadStream.on('data', (data) => {
                    const result = hwModule.checkInitialData(data, this.options);
                    if (result !== undefined) {
                        this.serialPort.removeAllListeners('data');
                        serialPortReadStream.removeAllListeners('data');
                        clearTimeout(this.flashFirmware);
                        clearTimeout(this.slaveTimer);
                        if (result === true) {
                            if (hwModule.setSerialPort) {
                                hwModule.setSerialPort(this.serialPort);
                            }
                            if (hwModule.resetProperty) {
                                this.send(hwModule.resetProperty());
                            }
                            resolve();
                        } else {
                            reject(new Error('Invalid hardware'));
                        }
                    }
                });
                this.slaveTimer = setInterval(() => {
                    this.send(hwModule.requestInitialData(this.serialPort));
                }, duration);
            };

            if (firmwarecheck) {
                this.flashFirmware = setTimeout(() => {
                    if (this.serialPort) {
                        this.serialPort.parser ?
                            this.serialPort.parser.removeAllListeners('data') :
                            this.serialPort.removeAllListeners('data');
                        this.executeFlash = true;
                    }
                    resolve();
                }, 3000);
            }

            if (hwModule.checkInitialData && hwModule.requestInitialData) {
                if (control === 'master') {
                    runAsMaster();
                } else {
                    runAsSlave();
                }
            } else {
                resolve();
            }
        });
    }

    /**
     * router 와 hwModule 양쪽에 state 변경점을 보낸다.
     * @param {string} state
     * @private
     */
    _sendState(state) {
        this.hwModule.eventController && this.hwModule.eventController(state);
        this.router.sendState(state);
    }

    connect() {
        if (!this.router) {
            throw new Error('router must be set');
        }

        if (!this.serialPort) {
            throw new Error('serialPort must be open');
        }

        const router = this.router;
        const serialPort = this.serialPort;
        const hwModule = this.hwModule;
        const {
            control,
            duration = Connector.DEFAULT_SLAVE_DURATION,
            advertise,
            softwareReset,
        } = this.options;

        this.connected = false;
        this.received = true;

        if (hwModule.connect) {
            hwModule.connect();
        }
        this._sendState('connect');

        if (softwareReset) {
            serialPort.set({ dtr: false });
            setTimeout(() => {
                serialPort.set({ dtr: true });
            },1000);
        }

        if (hwModule.afterConnect) {
            hwModule.afterConnect(this, (state) => {
                this.router.sendState(state);
            });
        }

        const serialPortReadStream =
            serialPort.parser ? serialPort.pipe(serialPort.parser) : serialPort;

        serialPortReadStream.on('data', (data) => {
            if (!hwModule.validateLocalData || hwModule.validateLocalData(data)) {
                if (!this.connected) {
                    this.connected = true;
                    this._sendState('connected');
                }

                this.received = true;
                if (hwModule.handleLocalData) {
                    hwModule.handleLocalData(data);
                }

                // 서버로 데이터를 요청한다.
                router.setHandlerData();
                router.sendEncodedDataToServer();

                // 마스터모드인 경우, 데이터를 받자마자 디바이스로 데이터를 보낸다.
                if (control === 'master' && hwModule.requestLocalData) {
                    const data = hwModule.requestLocalData();
                    data && this.send(data);
                }
            }
        });

        serialPort.on('disconnect', () => {
            this.close();
            this._sendState('disconnected');
        });

        // 디바이스 연결 잃어버린 상태에 대한 관리를 모듈에 맡기거나, 직접 관리한다.
        if (hwModule.lostController) {
            hwModule.lostController(this, router.sendState.bind(router));
        } else {
            /*
             * this.lostTimer 타임 안에 데이터를 수신해야한다. 그렇지 않으면 연결해제처리한다.
             */
            this.connectionLostTimer = setInterval(() => {
                if (this.connected) {
                    if (this.received === false) {
                        this.connected = false;
                        this._sendState('lost');
                    }
                    this.received = false;
                }
            }, this.lostTimer);
        }

        if (duration && control !== 'master') {
            this.requestLocalDataInterval = setInterval(() => {
                if (hwModule.requestLocalData) {
                    const data = hwModule.requestLocalData();
                    data && this.send(data);
                }
                if (hwModule.getProperty) {
                    const data = hwModule.getProperty();
                    if (data) {
                        this.send(data);
                    }
                }
            }, duration);
        }

        if (advertise) {
            this.advertiseInterval = setInterval(() => {
                router.sendEncodedDataToServer();
            }, advertise);
        }
    }

    clear() {
        this.connected = false;
        if (this.connectionLostTimer) {
            clearInterval(this.connectionLostTimer);
            this.connectionLostTimer = undefined;
        }
        if (this.requestLocalDataInterval) {
            clearInterval(this.requestLocalDataInterval);
            this.requestLocalDataInterval = undefined;
        }
        if (this.advertiseInterval) {
            clearInterval(this.advertiseInterval);
            this.advertiseInterval = undefined;
        }
        if (this.serialPort) {
            this.serialPort.removeAllListeners();
            if (this.serialPort.parser) {
                this.serialPort.parser.removeAllListeners();
            }
        }
        if (this.flashFirmware) {
            clearTimeout(this.flashFirmware);
            this.flashFirmware = undefined;
        }
    };

    close() {
        this.clear();
        if (this.serialPort && this.serialPort.isOpen) {
            this.serialPort.close((e) => {
                this.serialPort = undefined;
            }, null);
        }
    };

    /**
     * 시리얼포트로 연결된 디바이스에 데이터를 보낸다.
     * @param data
     * @param callback
     */
    send(data, callback) {
        if (this.serialPort && this.serialPort.isOpen && data && !this.isSending) {
            this.isSending = true;

            if (this.options.stream === 'string') {
                data = Buffer.from(data, 'utf8');
            }

            this.serialPort.write(data, () => {
                if (this.serialPort) {
                    this.serialPort.drain(() => {
                        this.isSending = false;
                        callback && callback();
                    });
                }
            });
        }
    };
}

module.exports = Connector;
<commit_msg>[1.8.0!] 아두이노 기반 하드웨어들의 잘못된 패킷 수신 문제로 인해 byteDelimiter 정책수정<commit_after>'use strict';
const Readline = require('@serialport/parser-readline'); // modify
const Delimiter = require('@serialport/parser-delimiter');
const SerialPort = require('@serialport/stream');
SerialPort.Binding = require('@entrylabs/bindings');

/**
 * 스캔이 끝난 후, 선택된 포트로 시리얼포트를 오픈하는 클래스
 * 스캐너에서 open, initialize 가 일어나고,
 * 라우터에서 setRouter, connect 를 거쳐 통신한다.
 */
class Connector {
    static get DEFAULT_CONNECT_LOST_MILLS() {
        return 1000;
    }

    static get DEFAULT_SLAVE_DURATION() {
        return 1000;
    }

    constructor(hwModule, hardwareOptions) {
        this.options = hardwareOptions;
        this.hwModule = hwModule;

        /**
         * @type {MainRouter}
         */
        this.router = undefined;

        /**
         * @type {SerialPort}
         */
        this.serialPort = undefined;

        this.connected = false;
        this.received = false;
    }

    /**
     * MainRouter 를 세팅한다.
     * @param {MainRouter} router
     */
    setRouter(router) {
        this.router = router;
    }

    /**
     * 시리얼포트 생성자 옵션을 만든다.
     * @param serialPortOptions
     * @returns {Object} SerialPort 내 defaultSettings 참조
     * @private
     */
    _makeSerialPortOptions(serialPortOptions) {
        const _options = {
            autoOpen: true,
            baudRate: 9600,
            parity: 'none',
            dataBits: 8,
            stopBits: 1,
            bufferSize: 65536,
        };

        if (serialPortOptions.flowControl === 'hardware') {
            _options.rtscts = true;
        } else if (serialPortOptions.flowControl === 'software') {
            _options.xon = true;
            _options.xoff = true;
        }

        Object.assign(_options, serialPortOptions);
        return _options;
    }

    /**
     * 시리얼포트를 오픈한다.
     * @param {string} port COMPortName
     * @returns {Promise<SerialPort>}
     * // TODO 외부에 오픈하는 시리얼포트 로직을 전부 커넥터로 이동
     */
    open(port) {
        return new Promise((resolve, reject) => {
            const hardwareOptions = this.options;
            this.lostTimer = hardwareOptions.lostTimer || Connector.DEFAULT_CONNECT_LOST_MILLS;

            const serialPort = new SerialPort(port, this._makeSerialPortOptions(hardwareOptions));
            this.serialPort = serialPort;

            const { delimiter, byteDelimiter = [13, 10] } = hardwareOptions;
            if (delimiter) {
                serialPort.parser = serialPort.pipe(new Readline({ delimiter }));
            } else if (byteDelimiter) {
                serialPort.parser = serialPort.pipe(new Delimiter({
                    delimiter: byteDelimiter,
                    includeDelimiter: true,
                }));
            }

            serialPort.on('error', reject);
            serialPort.on('open', (error) => {
                serialPort.removeAllListeners('open');
                if (error) {
                    reject(error);
                } else {
                    resolve(this.serialPort);
                }
            });
        });
    };

    /**
     * checkInitialData, requestInitialData 가 둘다 존재하는 경우 handShake 를 진행한다.
     * 둘 중 하나라도 없는 경우는 로직을 종료한다.
     * 만약 firmwareCheck 옵션이 활성화 된 경우면 executeFlash 를 세팅하고 종료한다.
     * 이 플래그는 라우터에서 flasher 를 바로 사용해야하는지 판단한다.
     *
     * @returns {Promise<void>} 준비완료 or 펌웨어체크 준비
     */
    initialize() {
        return new Promise((resolve, reject) => {
            const {
                control,
                duration = Connector.DEFAULT_SLAVE_DURATION,
                firmwarecheck,
            } = this.options;
            const hwModule = this.hwModule;
            const serialPortReadStream =
                        this.serialPort.parser ? this.serialPort.parser : this.serialPort;

            const runAsMaster = () => {
                serialPortReadStream.on('data', (data) => {
                    const result = hwModule.checkInitialData(data, this.options);

                    if (result === undefined) {
                        this.send(hwModule.requestInitialData());
                    } else {
                        this.serialPort.removeAllListeners('data');
                        serialPortReadStream.removeAllListeners('data');
                        clearTimeout(this.flashFirmware);
                        if (result === true) {
                            if (hwModule.setSerialPort) {
                                hwModule.setSerialPort(this.serialPort);
                            }
                            resolve();
                        } else {
                            reject(new Error('Invalid hardware'));
                        }
                    }
                });
            };

            const runAsSlave = () => {
                // control type is slave
                serialPortReadStream.on('data', (data) => {
                    const result = hwModule.checkInitialData(data, this.options);
                    if (result !== undefined) {
                        this.serialPort.removeAllListeners('data');
                        serialPortReadStream.removeAllListeners('data');
                        clearTimeout(this.flashFirmware);
                        clearTimeout(this.slaveTimer);
                        if (result === true) {
                            if (hwModule.setSerialPort) {
                                hwModule.setSerialPort(this.serialPort);
                            }
                            if (hwModule.resetProperty) {
                                this.send(hwModule.resetProperty());
                            }
                            resolve();
                        } else {
                            reject(new Error('Invalid hardware'));
                        }
                    }
                });
                this.slaveTimer = setInterval(() => {
                    this.send(hwModule.requestInitialData(this.serialPort));
                }, duration);
            };

            if (firmwarecheck) {
                this.flashFirmware = setTimeout(() => {
                    if (this.serialPort) {
                        this.serialPort.parser ?
                            this.serialPort.parser.removeAllListeners('data') :
                            this.serialPort.removeAllListeners('data');
                        this.executeFlash = true;
                    }
                    resolve();
                }, 3000);
            }

            if (hwModule.checkInitialData && hwModule.requestInitialData) {
                if (control === 'master') {
                    runAsMaster();
                } else {
                    runAsSlave();
                }
            } else {
                resolve();
            }
        });
    }

    /**
     * router 와 hwModule 양쪽에 state 변경점을 보낸다.
     * @param {string} state
     * @private
     */
    _sendState(state) {
        this.hwModule.eventController && this.hwModule.eventController(state);
        this.router.sendState(state);
    }

    connect() {
        if (!this.router) {
            throw new Error('router must be set');
        }

        if (!this.serialPort) {
            throw new Error('serialPort must be open');
        }

        const router = this.router;
        const serialPort = this.serialPort;
        const hwModule = this.hwModule;
        const {
            control,
            duration = Connector.DEFAULT_SLAVE_DURATION,
            advertise,
            softwareReset,
        } = this.options;

        this.connected = false;
        this.received = true;

        if (hwModule.connect) {
            hwModule.connect();
        }
        this._sendState('connect');

        if (softwareReset) {
            serialPort.set({ dtr: false });
            setTimeout(() => {
                serialPort.set({ dtr: true });
            },1000);
        }

        if (hwModule.afterConnect) {
            hwModule.afterConnect(this, (state) => {
                this.router.sendState(state);
            });
        }

        const serialPortReadStream =
            serialPort.parser ? serialPort.pipe(serialPort.parser) : serialPort;

        serialPortReadStream.on('data', (data) => {
            if (!hwModule.validateLocalData || hwModule.validateLocalData(data)) {
                if (!this.connected) {
                    this.connected = true;
                    this._sendState('connected');
                }

                this.received = true;
                if (hwModule.handleLocalData) {
                    hwModule.handleLocalData(data);
                }

                // 서버로 데이터를 요청한다.
                router.setHandlerData();
                router.sendEncodedDataToServer();

                // 마스터모드인 경우, 데이터를 받자마자 디바이스로 데이터를 보낸다.
                if (control === 'master' && hwModule.requestLocalData) {
                    const data = hwModule.requestLocalData();
                    data && this.send(data);
                }
            }
        });

        serialPort.on('disconnect', () => {
            this.close();
            this._sendState('disconnected');
        });

        // 디바이스 연결 잃어버린 상태에 대한 관리를 모듈에 맡기거나, 직접 관리한다.
        if (hwModule.lostController) {
            hwModule.lostController(this, router.sendState.bind(router));
        } else {
            /*
             * this.lostTimer 타임 안에 데이터를 수신해야한다. 그렇지 않으면 연결해제처리한다.
             */
            this.connectionLostTimer = setInterval(() => {
                if (this.connected) {
                    if (this.received === false) {
                        this.connected = false;
                        this._sendState('lost');
                    }
                    this.received = false;
                }
            }, this.lostTimer);
        }

        if (duration && control !== 'master') {
            this.requestLocalDataInterval = setInterval(() => {
                if (hwModule.requestLocalData) {
                    const data = hwModule.requestLocalData();
                    data && this.send(data);
                }
                if (hwModule.getProperty) {
                    const data = hwModule.getProperty();
                    if (data) {
                        this.send(data);
                    }
                }
            }, duration);
        }

        if (advertise) {
            this.advertiseInterval = setInterval(() => {
                router.sendEncodedDataToServer();
            }, advertise);
        }
    }

    clear() {
        this.connected = false;
        if (this.connectionLostTimer) {
            clearInterval(this.connectionLostTimer);
            this.connectionLostTimer = undefined;
        }
        if (this.requestLocalDataInterval) {
            clearInterval(this.requestLocalDataInterval);
            this.requestLocalDataInterval = undefined;
        }
        if (this.advertiseInterval) {
            clearInterval(this.advertiseInterval);
            this.advertiseInterval = undefined;
        }
        if (this.serialPort) {
            this.serialPort.removeAllListeners();
            if (this.serialPort.parser) {
                this.serialPort.parser.removeAllListeners();
            }
        }
        if (this.flashFirmware) {
            clearTimeout(this.flashFirmware);
            this.flashFirmware = undefined;
        }
    };

    close() {
        this.clear();
        if (this.serialPort && this.serialPort.isOpen) {
            this.serialPort.close((e) => {
                this.serialPort = undefined;
            }, null);
        }
    };

    /**
     * 시리얼포트로 연결된 디바이스에 데이터를 보낸다.
     * @param data
     * @param callback
     */
    send(data, callback) {
        if (this.serialPort && this.serialPort.isOpen && data && !this.isSending) {
            this.isSending = true;

            if (this.options.stream === 'string') {
                data = Buffer.from(data, 'utf8');
            }

            this.serialPort.write(data, () => {
                if (this.serialPort) {
                    this.serialPort.drain(() => {
                        this.isSending = false;
                        callback && callback();
                    });
                }
            });
        }
    };
}

module.exports = Connector;
<|endoftext|>


<commit_before>import Vue from 'vue'
import Modal from './views/components/Modal/Modal.vue'
import LoginModal from './views/containers/LoginModal.vue'
import SignUp from './views/containers/SignUp.vue'

Vue.component('Modal', Modal)
Vue.component('LoginModal', LoginModal)
Vue.component('SignUp', SignUp)
<commit_msg>Fix lazy loading<commit_after>import Vue from 'vue'

Vue.component('Modal', () => import('../../containers/Modal.vue'))
Vue.component('LoginModal', () => import('../../containers/LoginModal.vue'))
Vue.component('SignUp', () => import('../../containers/SignUp.vue'))
<|endoftext|>