<commit_before>define([
  'angular',
  'jquery',
  'kbn',
  'underscore',
  'config',
  'moment',
  'modernizr',
  'filesaver'
],
function (angular, $, kbn, _, config, moment, Modernizr) {
  'use strict';

  var module = angular.module('kibana.services');

  module.service('dashboard', function($routeParams, $http, $rootScope, $injector,
    ejsResource, timer, kbnIndex, alertSrv
  ) {
    // A hash of defaults to use when loading a dashboard

    var _dash = {
      title: "",
      style: "dark",
      editable: true,
      failover: false,
      panel_hints: true,
      rows: [],
      services: {},
      loader: {
        save_gist: false,
        save_elasticsearch: true,
        save_local: true,
        save_default: true,
        save_temp: true,
        save_temp_ttl_enable: true,
        save_temp_ttl: '30d',
        load_gist: true,
        load_elasticsearch: true,
        load_elasticsearch_size: 20,
        load_local: true,
        hide: false
      },
      index: {
        interval: 'none',
        pattern: '_all',
        default: 'INDEX_MISSING'
      },
    };

    // An elasticJS client to use
    var ejs = ejsResource(config.elasticsearch);
    var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;

    // Store a reference to this
    var self = this;
    var filterSrv,querySrv;

    this.current = _.clone(_dash);
    this.last = {};

    $rootScope.$on('$routeChangeSuccess',function(){
      // Clear the current dashboard to prevent reloading
      self.current = {};
      self.indices = [];
      route();
    });

    var route = function() {
      // Is there a dashboard type and id in the URL?
      if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
        var _type = $routeParams.kbnType;
        var _id = $routeParams.kbnId;

        switch(_type) {
        case ('elasticsearch'):
          self.elasticsearch_load('dashboard',_id);
          break;
        case ('temp'):
          self.elasticsearch_load('temp',_id);
          break;
        case ('file'):
          self.file_load(_id);
          break;
        case('script'):
          self.script_load(_id);
          break;
        default:
          self.file_load('default.json');
        }

      // No dashboard in the URL
      } else {
        // Check if browser supports localstorage, and if there's a dashboard
        if (Modernizr.localstorage &&
          !(_.isUndefined(window.localStorage['dashboard'])) &&
          window.localStorage['dashboard'] !== ''
        ) {
          var dashboard = JSON.parse(window.localStorage['dashboard']);
          self.dash_load(dashboard);
        // No? Ok, grab default.json, its all we have now
        } else {
          self.file_load('default.json');
        }
      }
    };

    // Since the dashboard is responsible for index computation, we can compute and assign the indices
    // here before telling the panels to refresh
    this.refresh = function() {
      if(self.current.index.interval !== 'none') {
        if(filterSrv.idsByType('time').length > 0) {
          var _range = filterSrv.timeRange('min');
          kbnIndex.indices(_range.from,_range.to,
            self.current.index.pattern,self.current.index.interval
          ).then(function (p) {
            if(p.length > 0) {
              self.indices = p;
            } else {
              // Option to not failover
              if(self.current.failover) {
                self.indices = [self.current.index.default];
              } else {
                // Do not issue refresh if no indices match. This should be removed when panels
                // properly understand when no indices are present
                alertSrv.set('No results','There were no results because no indices were found that match your'+
                  ' selected time span','info',5000);
                return false;
              }
            }
            $rootScope.$broadcast('refresh');
          });
        } else {
          if(self.current.failover) {
            self.indices = [self.current.index.default];
            $rootScope.$broadcast('refresh');
          } else {
            alertSrv.set("No time filter",
              'Timestamped indices are configured without a failover. Waiting for time filter.',
              'info',5000);
          }
        }
      } else {
        self.indices = [self.current.index.default];
        $rootScope.$broadcast('refresh');
      }
    };

    var dash_defaults = function(dashboard) {
      _.defaults(dashboard,_dash);
      _.defaults(dashboard.index,_dash.index);
      _.defaults(dashboard.loader,_dash.loader);
      return dashboard;
    };

    this.dash_load = function(dashboard) {
      // Cancel all timers
      timer.cancel_all();

      // Make sure the dashboard being loaded has everything required
      dashboard = dash_defaults(dashboard);

      // If not using time based indices, use the default index
      if(dashboard.index.interval === 'none') {
        self.indices = [dashboard.index.default];
      }

      self.current = _.clone(dashboard);

      // Ok, now that we've setup the current dashboard, we can inject our services
      querySrv = $injector.get('querySrv');
      filterSrv = $injector.get('filterSrv');

      // Make sure these re-init
      querySrv.init();
      filterSrv.init();

      // If there's an index interval set and no existing time filter, send a refresh to set one
      if(dashboard.index.interval !== 'none' && filterSrv.idsByType('time').length === 0) {
        self.refresh();
      }

      return true;
    };

    this.gist_id = function(string) {
      if(self.is_gist(string)) {
        return string.match(gist_pattern)[0].replace(/.*\//, '');
      }
    };

    this.is_gist = function(string) {
      if(!_.isUndefined(string) && string !== '' && !_.isNull(string.match(gist_pattern))) {
        return string.match(gist_pattern).length > 0 ? true : false;
      } else {
        return false;
      }
    };

    this.to_file = function() {
      var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
      // from filesaver.js
      window.saveAs(blob, self.current.title+"-"+new Date().getTime());
      return true;
    };

    this.set_default = function(dashboard) {
      if (Modernizr.localstorage) {
        window.localStorage['dashboard'] = angular.toJson(dashboard || self.current);
        return true;
      } else {
        return false;
      }
    };

    this.purge_default = function() {
      if (Modernizr.localstorage) {
        window.localStorage['dashboard'] = '';
        return true;
      } else {
        return false;
      }
    };

    // TOFIX: Pretty sure this breaks when you're on a saved dashboard already
    this.share_link = function(title,type,id) {
      return {
        location  : window.location.href.replace(window.location.hash,""),
        type      : type,
        id        : id,
        link      : window.location.href.replace(window.location.hash,"")+"#dashboard/"+type+"/"+id,
        title     : title
      };
    };

    var renderTemplate = function(json,params) {
      var _r;
      _.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
      var template = _.template(json);
      var rendered = template({ARGS:params});
      try {
        _r = angular.fromJson(rendered);
      } catch(e) {
        _r = false;
      }
      return _r;
    };

    this.file_load = function(file) {
      return $http({
        url: "app/dashboards/"+file+'?' + new Date().getTime(),
        method: "GET",
        transformResponse: function(response) {
          return renderTemplate(response,$routeParams);
        }
      }).then(function(result) {
        if(!result) {
          return false;
        }
        self.dash_load(dash_defaults(result.data));
        return true;
      },function() {
        alertSrv.set('Error',"Could not load dashboards/"+file+". Please make sure it exists" ,'error');
        return false;
      });
    };

    this.elasticsearch_load = function(type,id) {
      return $http({
        url: config.elasticsearch + "/" + config.kibana_index + "/"+type+"/"+id,
        method: "GET",
        transformResponse: function(response) {
          return renderTemplate(angular.fromJson(response)._source.dashboard, $routeParams);
        }
      }).error(function(data, status) {
        if(status === 0) {
          alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
            ". Please ensure that Elasticsearch is reachable from your system." ,'error');
        } else {
          alertSrv.set('Error',"Could not find "+id+". If you"+
            " are using a proxy, ensure it is configured correctly",'error');
        }
        return false;
      }).success(function(data) {
        self.dash_load(data);
      });
    };

    this.script_load = function(file) {
      return $http({
        url: "app/dashboards/"+file,
        method: "GET",
        transformResponse: function(response) {
          /*jshint -W054 */
          var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response);
          return _f($routeParams,kbn,_,moment);
        }
      }).then(function(result) {
        if(!result) {
          return false;
        }
        self.dash_load(dash_defaults(result.data));
        return true;
      },function() {
        alertSrv.set('Error',
          "Could not load scripts/"+file+". Please make sure it exists and returns a valid dashboard" ,
          'error');
        return false;
      });
    };

    this.elasticsearch_save = function(type,title,ttl) {
      // Clone object so we can modify it without influencing the existing obejct
      var save = _.clone(self.current);
      var id;

      // Change title on object clone
      if (type === 'dashboard') {
        id = save.title = _.isUndefined(title) ? self.current.title : title;
      }

      // Create request with id as title. Rethink this.
      var request = ejs.Document(config.kibana_index,type,id).source({
        user: 'guest',
        group: 'guest',
        title: save.title,
        dashboard: angular.toJson(save)
      });

      request = type === 'temp' && ttl ? request.ttl(ttl) : request;

      return request.doIndex(
        // Success
        function(result) {
          return result;
        },
        // Failure
        function() {
          return false;
        }
      );
    };

    this.elasticsearch_delete = function(id) {
      return ejs.Document(config.kibana_index,'dashboard',id).doDelete(
        // Success
        function(result) {
          return result;
        },
        // Failure
        function() {
          return false;
        }
      );
    };

    this.elasticsearch_list = function(query,count) {
      var request = ejs.Request().indices(config.kibana_index).types('dashboard');
      return request.query(
        ejs.QueryStringQuery(query || '*')
        ).size(count).doSearch(
          // Success
          function(result) {
            return result;
          },
          // Failure
          function() {
            return false;
          }
        );
    };

    this.save_gist = function(title,dashboard) {
      var save = _.clone(dashboard || self.current);
      save.title = title || self.current.title;
      return $http({
        url: "https://api.github.com/gists",
        method: "POST",
        data: {
          "description": save.title,
          "public": false,
          "files": {
            "kibana-dashboard.json": {
              "content": angular.toJson(save,true)
            }
          }
        }
      }).then(function(data) {
        return data.data.html_url;
      }, function() {
        return false;
      });
    };

    this.gist_list = function(id) {
      return $http.jsonp("https://api.github.com/gists/"+id+"?callback=JSON_CALLBACK"
      ).then(function(response) {
        var files = [];
        _.each(response.data.data.files,function(v) {
          try {
            var file = JSON.parse(v.content);
            files.push(file);
          } catch(e) {
            return false;
          }
        });
        return files;
      }, function() {
        return false;
      });
    };
  });

});<commit_msg>Added route change when saving to elasticsearch or browser default<commit_after>define([
  'angular',
  'jquery',
  'kbn',
  'underscore',
  'config',
  'moment',
  'modernizr',
  'filesaver'
],
function (angular, $, kbn, _, config, moment, Modernizr) {
  'use strict';

  var module = angular.module('kibana.services');

  module.service('dashboard', function($routeParams, $http, $rootScope, $injector, $location,
    ejsResource, timer, kbnIndex, alertSrv
  ) {
    // A hash of defaults to use when loading a dashboard

    var _dash = {
      title: "",
      style: "dark",
      editable: true,
      failover: false,
      panel_hints: true,
      rows: [],
      services: {},
      loader: {
        save_gist: false,
        save_elasticsearch: true,
        save_local: true,
        save_default: true,
        save_temp: true,
        save_temp_ttl_enable: true,
        save_temp_ttl: '30d',
        load_gist: true,
        load_elasticsearch: true,
        load_elasticsearch_size: 20,
        load_local: true,
        hide: false
      },
      index: {
        interval: 'none',
        pattern: '_all',
        default: 'INDEX_MISSING'
      },
    };

    // An elasticJS client to use
    var ejs = ejsResource(config.elasticsearch);
    var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;

    // Store a reference to this
    var self = this;
    var filterSrv,querySrv;

    this.current = _.clone(_dash);
    this.last = {};

    $rootScope.$on('$routeChangeSuccess',function(){
      // Clear the current dashboard to prevent reloading
      self.current = {};
      self.indices = [];
      route();
    });

    var route = function() {
      // Is there a dashboard type and id in the URL?
      if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
        var _type = $routeParams.kbnType;
        var _id = $routeParams.kbnId;

        switch(_type) {
        case ('elasticsearch'):
          self.elasticsearch_load('dashboard',_id);
          break;
        case ('temp'):
          self.elasticsearch_load('temp',_id);
          break;
        case ('file'):
          self.file_load(_id);
          break;
        case('script'):
          self.script_load(_id);
          break;
        default:
          self.file_load('default.json');
        }

      // No dashboard in the URL
      } else {
        // Check if browser supports localstorage, and if there's a dashboard
        if (Modernizr.localstorage &&
          !(_.isUndefined(window.localStorage['dashboard'])) &&
          window.localStorage['dashboard'] !== ''
        ) {
          var dashboard = JSON.parse(window.localStorage['dashboard']);
          self.dash_load(dashboard);
        // No? Ok, grab default.json, its all we have now
        } else {
          self.file_load('default.json');
        }
      }
    };

    // Since the dashboard is responsible for index computation, we can compute and assign the indices
    // here before telling the panels to refresh
    this.refresh = function() {
      if(self.current.index.interval !== 'none') {
        if(filterSrv.idsByType('time').length > 0) {
          var _range = filterSrv.timeRange('min');
          kbnIndex.indices(_range.from,_range.to,
            self.current.index.pattern,self.current.index.interval
          ).then(function (p) {
            if(p.length > 0) {
              self.indices = p;
            } else {
              // Option to not failover
              if(self.current.failover) {
                self.indices = [self.current.index.default];
              } else {
                // Do not issue refresh if no indices match. This should be removed when panels
                // properly understand when no indices are present
                alertSrv.set('No results','There were no results because no indices were found that match your'+
                  ' selected time span','info',5000);
                return false;
              }
            }
            $rootScope.$broadcast('refresh');
          });
        } else {
          if(self.current.failover) {
            self.indices = [self.current.index.default];
            $rootScope.$broadcast('refresh');
          } else {
            alertSrv.set("No time filter",
              'Timestamped indices are configured without a failover. Waiting for time filter.',
              'info',5000);
          }
        }
      } else {
        self.indices = [self.current.index.default];
        $rootScope.$broadcast('refresh');
      }
    };

    var dash_defaults = function(dashboard) {
      _.defaults(dashboard,_dash);
      _.defaults(dashboard.index,_dash.index);
      _.defaults(dashboard.loader,_dash.loader);
      return dashboard;
    };

    this.dash_load = function(dashboard) {
      // Cancel all timers
      timer.cancel_all();

      // Make sure the dashboard being loaded has everything required
      dashboard = dash_defaults(dashboard);

      // If not using time based indices, use the default index
      if(dashboard.index.interval === 'none') {
        self.indices = [dashboard.index.default];
      }

      self.current = _.clone(dashboard);

      // Ok, now that we've setup the current dashboard, we can inject our services
      querySrv = $injector.get('querySrv');
      filterSrv = $injector.get('filterSrv');

      // Make sure these re-init
      querySrv.init();
      filterSrv.init();

      // If there's an index interval set and no existing time filter, send a refresh to set one
      if(dashboard.index.interval !== 'none' && filterSrv.idsByType('time').length === 0) {
        self.refresh();
      }

      return true;
    };

    this.gist_id = function(string) {
      if(self.is_gist(string)) {
        return string.match(gist_pattern)[0].replace(/.*\//, '');
      }
    };

    this.is_gist = function(string) {
      if(!_.isUndefined(string) && string !== '' && !_.isNull(string.match(gist_pattern))) {
        return string.match(gist_pattern).length > 0 ? true : false;
      } else {
        return false;
      }
    };

    this.to_file = function() {
      var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
      // from filesaver.js
      window.saveAs(blob, self.current.title+"-"+new Date().getTime());
      return true;
    };

    this.set_default = function(dashboard) {
      if (Modernizr.localstorage) {
        window.localStorage['dashboard'] = angular.toJson(dashboard || self.current);
        $location.path('/dashboard');
        return true;
      } else {
        return false;
      }
    };

    this.purge_default = function() {
      if (Modernizr.localstorage) {
        window.localStorage['dashboard'] = '';
        return true;
      } else {
        return false;
      }
    };

    // TOFIX: Pretty sure this breaks when you're on a saved dashboard already
    this.share_link = function(title,type,id) {
      return {
        location  : window.location.href.replace(window.location.hash,""),
        type      : type,
        id        : id,
        link      : window.location.href.replace(window.location.hash,"")+"#dashboard/"+type+"/"+id,
        title     : title
      };
    };

    var renderTemplate = function(json,params) {
      var _r;
      _.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
      var template = _.template(json);
      var rendered = template({ARGS:params});
      try {
        _r = angular.fromJson(rendered);
      } catch(e) {
        _r = false;
      }
      return _r;
    };

    this.file_load = function(file) {
      return $http({
        url: "app/dashboards/"+file+'?' + new Date().getTime(),
        method: "GET",
        transformResponse: function(response) {
          return renderTemplate(response,$routeParams);
        }
      }).then(function(result) {
        if(!result) {
          return false;
        }
        self.dash_load(dash_defaults(result.data));
        return true;
      },function() {
        alertSrv.set('Error',"Could not load dashboards/"+file+". Please make sure it exists" ,'error');
        return false;
      });
    };

    this.elasticsearch_load = function(type,id) {
      return $http({
        url: config.elasticsearch + "/" + config.kibana_index + "/"+type+"/"+id,
        method: "GET",
        transformResponse: function(response) {
          return renderTemplate(angular.fromJson(response)._source.dashboard, $routeParams);
        }
      }).error(function(data, status) {
        if(status === 0) {
          alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
            ". Please ensure that Elasticsearch is reachable from your system." ,'error');
        } else {
          alertSrv.set('Error',"Could not find "+id+". If you"+
            " are using a proxy, ensure it is configured correctly",'error');
        }
        return false;
      }).success(function(data) {
        self.dash_load(data);
      });
    };

    this.script_load = function(file) {
      return $http({
        url: "app/dashboards/"+file,
        method: "GET",
        transformResponse: function(response) {
          /*jshint -W054 */
          var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response);
          return _f($routeParams,kbn,_,moment);
        }
      }).then(function(result) {
        if(!result) {
          return false;
        }
        self.dash_load(dash_defaults(result.data));
        return true;
      },function() {
        alertSrv.set('Error',
          "Could not load scripts/"+file+". Please make sure it exists and returns a valid dashboard" ,
          'error');
        return false;
      });
    };

    this.elasticsearch_save = function(type,title,ttl) {
      // Clone object so we can modify it without influencing the existing obejct
      var save = _.clone(self.current);
      var id;

      // Change title on object clone
      if (type === 'dashboard') {
        id = save.title = _.isUndefined(title) ? self.current.title : title;
      }

      // Create request with id as title. Rethink this.
      var request = ejs.Document(config.kibana_index,type,id).source({
        user: 'guest',
        group: 'guest',
        title: save.title,
        dashboard: angular.toJson(save)
      });

      request = type === 'temp' && ttl ? request.ttl(ttl) : request;

      return request.doIndex(
        // Success
        function(result) {
          if(type === 'dashboard') {
            $location.path('/dashboard/elasticsearch/'+title);
          }
          return result;
        },
        // Failure
        function() {
          return false;
        }
      );
    };

    this.elasticsearch_delete = function(id) {
      return ejs.Document(config.kibana_index,'dashboard',id).doDelete(
        // Success
        function(result) {
          return result;
        },
        // Failure
        function() {
          return false;
        }
      );
    };

    this.elasticsearch_list = function(query,count) {
      var request = ejs.Request().indices(config.kibana_index).types('dashboard');
      return request.query(
        ejs.QueryStringQuery(query || '*')
        ).size(count).doSearch(
          // Success
          function(result) {
            return result;
          },
          // Failure
          function() {
            return false;
          }
        );
    };

    this.save_gist = function(title,dashboard) {
      var save = _.clone(dashboard || self.current);
      save.title = title || self.current.title;
      return $http({
        url: "https://api.github.com/gists",
        method: "POST",
        data: {
          "description": save.title,
          "public": false,
          "files": {
            "kibana-dashboard.json": {
              "content": angular.toJson(save,true)
            }
          }
        }
      }).then(function(data) {
        return data.data.html_url;
      }, function() {
        return false;
      });
    };

    this.gist_list = function(id) {
      return $http.jsonp("https://api.github.com/gists/"+id+"?callback=JSON_CALLBACK"
      ).then(function(response) {
        var files = [];
        _.each(response.data.data.files,function(v) {
          try {
            var file = JSON.parse(v.content);
            files.push(file);
          } catch(e) {
            return false;
          }
        });
        return files;
      }, function() {
        return false;
      });
    };
  });

});<|endoftext|>


<commit_before>(function() {
    var PATH = require('path'),
        FS = require('fs'),
        GUI = require('nw.gui'),
        recursive = require('recursive-readdir'),
        os = require('os');

    app.factory('nwService', ['$q', nwService]);

    /**
     * NodeWebkit related Service (Path, Fs, GUI, recursive, os)
     *
     * @method nwService
     * @module ssgl
     * @submodule nwService
     * @param  {Object}  $q Async
     */
    function nwService($q) {
        var service = {};
        service.execpath = PATH.dirname(process.execPath);
        service.pathsep = _getSeperator();

        /**
         * Gives back the directory seperator for active OS
         *
         * @method _getSeperator
         * @for nwService
         * @return {String} Os specific directory seperator
         * @private
         */
        function _getSeperator() {
            if ( os.platform() === 'win32') {
                return '\\';
            } else {
                return '/';
            }
        }

        //TODO better methodname
        /**
         * Gives back Fullpath of BASEDIR
         *
         * @method _checkRel
         * @for nwService
         * @param  {String} path given path
         * @param  {Boolean} relative When true give absolute path from BASEDIR back
         * @return {String} gives back absolute path when path is relative
         * @private
         */
        function _checkRel(path, relative) {
            if (typeof relative === 'undefined') {
                relative = false;
            }

            if (relative === true) {
                path = service.execpath + service.pathsep + path;
            }

            return path;
        }

        /**
         * Gives back the Platform
         *
         * @method getPlatform
         * @for nwService
         * @return {String} Plattform (win32, linux etc)
         */
        service.getPlatform = function() {
            return os.platform();
        };

        /**
         * Gives back only Filename back of an file, can also trim extensions
         * @method getName
         * @for nwService
         * @param  {String} path given Path
         * @param  {cut} cut  default 5 (.json)
         * @return {String} filename
         */
        service.getName = function(path, cut) {
            if (typeof cut === 'undefined') {
                cut = -5;
            }

            if (os.platform() === 'win32') {
                return path.replace(/^.*[\\\/]/, '').slice(0, cut);
            } else {
                return path.replace(/^.*[/]/, '').slice(0, cut);
            }
        };

        /**
         * Builds Path for active OS
         *
         * @method buildPath
         * @for nwService
         * @param  {Array}  array Array with paths to join
         * @param  {Boolean}  execpath When true - merge all given Paths into execpath
         * @return {String} Merged Path
         */
        service.buildPath = function(array, execpath) {
            if (typeof execpath === 'undefined') {
                execpath = false;
            }

            if (execpath === true) {
                path = service.execpath;
            } else {
                path = '';
            }

            path = path + service.pathsep + array.join(service.pathsep);

            return path;
        };

        /**
         * Goes through a Directory recursively via callback method
         *
         * @method recursiveDir
         * @for nwService
         * @param  {String}     path
         * @param  {Function}   callback Callback what todo when walk through the directory
         * @async
         * @return {Promise} Promise
         */
        service.recursiveDir = function(path, callback) {
            var def = $q.defer();
            recursive(path, function(err, files) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(callback(files));
                }
            });

            return def.promise;
        };

        /**
         * gives back path as array
         * @method splitPath
         * @for nwService
         * @param  {String}  path
         * @return {Array} Array with Pathsegments
         */
        service.splitPath = function(path) {
            return path.split(service.pathsep);
        };

        /**
         * Gives back JSON Package Manifest
         *
         * @method getManifest
         * @for nwService
         * @return {Object} Package Manifest as Object
         */
        service.getManifest = function() {
            return GUI.App.manifest;
        };

        //TODO Bug ?
        /**
         * Gives back Absolute BASEPATH
         * @method getAbsolute
         * @for nwService
         * @param  {String} to append on execpath
         * @return {String} path with BASEDIR
         */
        service.getAbsolute = function(string) {
            return service.execpath + string;
        };

        /**
         * Gives back Directoryname
         *
         * @method getDirname
         * @for nwService
         * @param  {String} filepath
         * @return {String} Directorypath of Filepath
         */
        service.getDirname = function(file) {
            return PATH.dirname(file);
        };

        /**
         * Async Rename
         *
         * @method rename
         * @for nwService
         * @param  {String} oldpath
         * @param  {String} newpath
         * @async
         * @return {Promise}
         */
        service.rename = function(oldpath, newpath) {
            var def = $q.defer();
            FS.rename(oldpath, newpath, function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(service.getName(newpath));
                }
            });
            return def.promise;
        };

        /**
         * Async Delete
         * @method remove
         * @for nwService
         * @param  {String} path
         * @async
         * @return {Promise}
         */
        service.remove = function(path) {
            var def = $q.defer();

            FS.unlink(path, function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(service.getName(path));
                }
            });
            return def.promise;
        };
        /**
         * Read a "flat" Directory async
         *
         * @method getDir
         * @for nwService
         * @param  {String} path
         * @param  {Boolean} When true use BASEDIR
         * @async
         * @return {Promise}
         */
        service.getDir = function(path, relative) {
            var def = $q.defer();
            path = _checkRel(path, relative);

            FS.readdir(path, function(err, fileArr) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(fileArr);
                }
            });

            return def.promise;
        };

        /**
         * Get NWJS Shell Object
         *
         * @method getShell
         * @for nwService
         * @return {Object}
         */
        service.getShell = function() {
            return GUI.Shell;
        };

        /**
         * Make Directory
         * @method mkDir
         * @for nwService
         * @param  {String} path
         * @param  {Boolean} relative When true use BASEDIR
         */
        service.mkDir = function(path, relative) {
            FS.exists(path, function(exists) {
                if (!exists) {
                    FS.mkdirSync(path);
                }
            });
        };

        // todo: try catch
        /**
         * Read JSON Sync
         * @method readSyncJSON
         * @for nwService
         * @param  {String}     path
         * @param  {[type]}     relative When true use BASEDIR
         * @return {Object} Object parsed from JSON
         */
        service.readSyncJSON = function(path, relative) {
            path = _checkRel(path, relative);
            return JSON.parse(FS.readFileSync(path, "utf8"));
        };

        /**
         * Read JSON Async
         * @method readJSON
         * @for nwService
         * @async
         * @param  {String} path
         * @param  {String} enc  encoding, default utf8
         * @return {Promise}
         */
        service.readJSON = function(path, enc) {
            var def = $q.defer();
            if (typeof enc === 'undefined') {
                enc = 'utf8';
            }

            FS.readFile(path, enc, function(err, data) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(data);
                }
            });

            return def.promise;
        };

        /**
         * Write Text into file async
         * @method writeTxt
         * @for nwService
         * @async
         * @param  {String} content
         * @param  {String} path
         * @param  {Boolean} relative When true use BASEDIR
         * @return {Promise}
         */
        service.writeTxt = function(content, path, relative) {
            var def = $q.defer();
            path = _checkRel(path, relative);

            FS.writeFile(path, content, function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve();
                }
            });

            return def.promise;
        };

        /**
         * Writes Object to JSON File async
         *
         * @method writeJSON
         * @async
         * @for nwService
         * @param  {Object} givenObject
         * @param  {String} path
         * @param  {Boolean} relative When true use BASEDIR
         * @return {Promise}
         */
        service.writeJSON = function(givenObject, path, relative) {
            var def = $q.defer();
            path = _checkRel(path, relative);

            FS.writeFile(path, JSON.stringify(givenObject, null, 4), function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(service.getName(path));
                }
            });

            return def.promise;
        };

        return service;
    }

})();<commit_msg>doc nwService<commit_after>(function() {
    var PATH = require('path'),
        FS = require('fs'),
        GUI = require('nw.gui'),
        recursive = require('recursive-readdir'),
        os = require('os');

    app.factory('nwService', ['$q', nwService]);

    /**
     * NodeWebkit related Service (Path, Fs, GUI, recursive, os)
     *
     * @method nwService
     * @module ssgl
     * @submodule nwService
     * @param  {Object}  $q Async
     */
    function nwService($q) {
        var service = {};
        /**
         * BASEDIR
         * @property execpath
         * @type {String}
         */
        service.execpath = PATH.dirname(process.execPath);

        /**
         * Operating System Directory seperator (/ or \\)
         * @property {pathsep}
         * @type {String}
         */
        service.pathsep = _getSeperator();

        /**
         * Gives back the directory seperator for active OS
         *
         * @method _getSeperator
         * @for nwService
         * @return {String} Os specific directory seperator
         * @private
         */
        function _getSeperator() {
            if ( os.platform() === 'win32') {
                return '\\';
            } else {
                return '/';
            }
        }

        //TODO better methodname
        /**
         * Gives back Fullpath of BASEDIR
         *
         * @method _checkRel
         * @for nwService
         * @param  {String} path given path
         * @param  {Boolean} relative When true give absolute path from BASEDIR back
         * @return {String} gives back absolute path when path is relative
         * @private
         */
        function _checkRel(path, relative) {
            if (typeof relative === 'undefined') {
                relative = false;
            }

            if (relative === true) {
                path = service.execpath + service.pathsep + path;
            }

            return path;
        }

        /**
         * Gives back the Platform
         *
         * @method getPlatform
         * @for nwService
         * @return {String} Plattform (win32, linux etc)
         */
        service.getPlatform = function() {
            return os.platform();
        };

        /**
         * Gives back only Filename back of an file, can also trim extensions
         * @method getName
         * @for nwService
         * @param  {String} path given Path
         * @param  {cut} cut  default 5 (.json)
         * @return {String} filename
         */
        service.getName = function(path, cut) {
            if (typeof cut === 'undefined') {
                cut = -5;
            }

            if (os.platform() === 'win32') {
                return path.replace(/^.*[\\\/]/, '').slice(0, cut);
            } else {
                return path.replace(/^.*[/]/, '').slice(0, cut);
            }
        };

        /**
         * Builds Path for active OS
         *
         * @method buildPath
         * @for nwService
         * @param  {Array}  array Array with paths to join
         * @param  {Boolean}  execpath When true - merge all given Paths into execpath
         * @return {String} Merged Path
         */
        service.buildPath = function(array, execpath) {
            if (typeof execpath === 'undefined') {
                execpath = false;
            }

            if (execpath === true) {
                path = service.execpath;
            } else {
                path = '';
            }

            path = path + service.pathsep + array.join(service.pathsep);

            return path;
        };

        /**
         * Goes through a Directory recursively via callback method
         *
         * @method recursiveDir
         * @for nwService
         * @param  {String}     path
         * @param  {Function}   callback Callback what todo when walk through the directory
         * @async
         * @return {Promise} Promise
         */
        service.recursiveDir = function(path, callback) {
            var def = $q.defer();
            recursive(path, function(err, files) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(callback(files));
                }
            });

            return def.promise;
        };

        /**
         * gives back path as array
         * @method splitPath
         * @for nwService
         * @param  {String}  path
         * @return {Array} Array with Pathsegments
         */
        service.splitPath = function(path) {
            return path.split(service.pathsep);
        };

        /**
         * Gives back JSON Package Manifest
         *
         * @method getManifest
         * @for nwService
         * @return {Object} Package Manifest as Object
         */
        service.getManifest = function() {
            return GUI.App.manifest;
        };

        //TODO Bug ?
        /**
         * Gives back Absolute BASEPATH
         * @method getAbsolute
         * @for nwService
         * @param  {String} to append on execpath
         * @return {String} path with BASEDIR
         */
        service.getAbsolute = function(string) {
            return service.execpath + string;
        };

        /**
         * Gives back Directoryname
         *
         * @method getDirname
         * @for nwService
         * @param  {String} filepath
         * @return {String} Directorypath of Filepath
         */
        service.getDirname = function(file) {
            return PATH.dirname(file);
        };

        /**
         * Async Rename
         *
         * @method rename
         * @for nwService
         * @param  {String} oldpath
         * @param  {String} newpath
         * @async
         * @return {Promise}
         */
        service.rename = function(oldpath, newpath) {
            var def = $q.defer();
            FS.rename(oldpath, newpath, function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(service.getName(newpath));
                }
            });
            return def.promise;
        };

        /**
         * Async Delete
         * @method remove
         * @for nwService
         * @param  {String} path
         * @async
         * @return {Promise}
         */
        service.remove = function(path) {
            var def = $q.defer();

            FS.unlink(path, function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(service.getName(path));
                }
            });
            return def.promise;
        };
        /**
         * Read a "flat" Directory async
         *
         * @method getDir
         * @for nwService
         * @param  {String} path
         * @param  {Boolean} When true use BASEDIR
         * @async
         * @return {Promise}
         */
        service.getDir = function(path, relative) {
            var def = $q.defer();
            path = _checkRel(path, relative);

            FS.readdir(path, function(err, fileArr) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(fileArr);
                }
            });

            return def.promise;
        };

        /**
         * Get NWJS Shell Object
         *
         * @method getShell
         * @for nwService
         * @return {Object}
         */
        service.getShell = function() {
            return GUI.Shell;
        };

        /**
         * Make Directory
         * @method mkDir
         * @for nwService
         * @param  {String} path
         * @param  {Boolean} relative When true use BASEDIR
         */
        service.mkDir = function(path, relative) {
            FS.exists(path, function(exists) {
                if (!exists) {
                    FS.mkdirSync(path);
                }
            });
        };

        // todo: try catch
        /**
         * Read JSON Sync
         * @method readSyncJSON
         * @for nwService
         * @param  {String}     path
         * @param  {[type]}     relative When true use BASEDIR
         * @return {Object} Object parsed from JSON
         */
        service.readSyncJSON = function(path, relative) {
            path = _checkRel(path, relative);
            return JSON.parse(FS.readFileSync(path, "utf8"));
        };

        /**
         * Read JSON Async
         * @method readJSON
         * @for nwService
         * @async
         * @param  {String} path
         * @param  {String} enc  encoding, default utf8
         * @return {Promise}
         */
        service.readJSON = function(path, enc) {
            var def = $q.defer();
            if (typeof enc === 'undefined') {
                enc = 'utf8';
            }

            FS.readFile(path, enc, function(err, data) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(data);
                }
            });

            return def.promise;
        };

        /**
         * Write Text into file async
         * @method writeTxt
         * @for nwService
         * @async
         * @param  {String} content
         * @param  {String} path
         * @param  {Boolean} relative When true use BASEDIR
         * @return {Promise}
         */
        service.writeTxt = function(content, path, relative) {
            var def = $q.defer();
            path = _checkRel(path, relative);

            FS.writeFile(path, content, function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve();
                }
            });

            return def.promise;
        };

        /**
         * Writes Object to JSON File async
         *
         * @method writeJSON
         * @async
         * @for nwService
         * @param  {Object} givenObject
         * @param  {String} path
         * @param  {Boolean} relative When true use BASEDIR
         * @return {Promise}
         */
        service.writeJSON = function(givenObject, path, relative) {
            var def = $q.defer();
            path = _checkRel(path, relative);

            FS.writeFile(path, JSON.stringify(givenObject, null, 4), function(err) {
                if (err) {
                    def.reject(err);
                } else {
                    def.resolve(service.getName(path));
                }
            });

            return def.promise;
        };

        return service;
    }

})();<|endoftext|>


<commit_before>const list = `
polonox
poloneix
biitrex
biittrex
bitter
bitterx
bittex
bittrax
bittre
bittrec
bittres
bittrex.com
bittrexe
bittrexx
bittrez
bittrix
bittrx
bitttrex
bitrex
bitrexx
bitrix
bitrrex
bttrex
btrex
bttrex
ittrex
bittrex-deposit
poloiex
poloinex
polomiex
polon
poloneex
poloneix
polonex
poloni
poloniax
polonie
poloniec
poloniee
polonieex
poloniek
polonieks
polonies
poloniet
poloniets
poloniex.com
poloniexcold
poloniexe
poloniexs
poloniext
poloniexx
poloniey
poloniez
poloniiex
poloniix
poloniks
poloniox
polonium
polonix
polonniex
polooniex
pooniex
poooniex
plolniex
ploniex
plooniex
poloex
oloniex
pooloniex
poliniex
polniex
poleniex
polionex
pollniex
polloniex
polnoiex
polonyex
polonied
polonixe
blockrade
blocktardes
blocktrade
blocktradees
blocktraded
blocktrader
bocktrades
changelly.com
changely
shapeshif
shapeshift
randomwhale
randowale
coinpayments
minnowboost
minnowboster
minowbooster
blocktades
bloocktrades
bloctrades
blocktradess
blocktrade
`.trim().split('\n');

export default list;
<commit_msg>yet another one<commit_after>const list = `
polonox
poloneix
biitrex
biittrex
bitter
bitterx
bittex
bittrax
bittre
bittrec
bittres
bittrex.com
bittrexe
bittrexx
bittrez
bittrix
bittrx
bitttrex
bitrex
bitrexx
bitrix
bitrrex
bttrex
btrex
bttrex
ittrex
bittrex-deposit
poloiex
poloinex
polomiex
polon
poloneex
poloneix
polonex
poloni
poloniax
polonie
poloniec
poloniee
polonieex
poloniek
polonieks
polonies
poloniet
poloniets
poloniex.com
poloniexcold
poloniexe
poloniexs
poloniext
poloniexx
poloniey
poloniez
poloniiex
poloniix
poloniks
poloniox
polonium
polonix
polonniex
polooniex
pooniex
poooniex
plolniex
ploniex
plooniex
poloex
oloniex
pooloniex
poliniex
polniex
poleniex
polionex
pollniex
polloniex
polnoiex
polonyex
polonied
polonixe
blockrade
blocktardes
blocktrade
blocktradees
blocktraded
blocktrader
blocktrads
bocktrades
changelly.com
changely
shapeshif
shapeshift
randomwhale
randowale
coinpayments
minnowboost
minnowboster
minowbooster
blocktades
bloocktrades
bloctrades
blocktradess
blocktrade
`.trim().split('\n');

export default list;
<|endoftext|>


<commit_before>import * as path from "path";
import * as fableUtils from "fable-utils";
import fable from 'rollup-plugin-fable';
import livereload from 'rollup-plugin-livereload';
import serve from 'rollup-plugin-serve';
import cjs from 'rollup-plugin-commonjs';
import nodeResolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import postcss from 'rollup-plugin-postcss';
import sass from 'node-sass';
import babel from "rollup-plugin-babel";

var babelOptions = fableUtils.resolveBabelOptions({
    presets: [["es2015", { "modules": false }]],
    plugins: ["external-helpers"]
});

const preprocessor = (content, id) => new Promise((resolve, reject) => {
    const result = sass.renderSync({ file: id });
    resolve({ code: result.css.toString() });
});

const isWatching = process.argv.some(x => x === "-w");

let rollupPlugins = [
    fable({
        babel: babelOptions
    }),
    babel(Object.assign({
        exclude: ["../node_modules/**", "**/*.sass", "**/*.css"]
    }, babelOptions)),
    cjs({
        include: '../node_modules/**',
        namedExports: {
            'react': ['createElement', 'Component'],
            'react-dom': ['render']
        }
    }),
    nodeResolve({
        jsnext: true,
        main: true,
        browser: true,
        customResolveOptions: {
            moduleDirectory: path.join(__dirname, '../node_modules')
        }
    }),
    replace({ 'process.env.NODE_ENV':
        JSON.stringify(isWatching ? 'development' : 'production')}),
    postcss({
        plugins: [
        ],
        extract: './public/dist/css/bundle.css', // default value
        preprocessor,
        extensions: ['.css', '.sass']  // default value
    })
];

if (isWatching) {
    console.log("Bundling for development...");
    rollupPlugins.splice(0, 0,
        livereload(),
        serve({
            contentBase: 'public',
            port: 8080
        }));
}
else {
    console.log("Bundling for production...");
}

export default {
    entry: './docs.fsproj',
    dest: './public/dist/js/bundle.js',
    plugins: rollupPlugins,
    format: 'iife',
    moduleName: 'bulmaDocSite'
};
<commit_msg>Remove rollup config<commit_after><|endoftext|>


<commit_before>import path from 'path';
import { LRUCache } from '../utilities/LRUCache.js';
import { PriorityQueue } from '../utilities/PriorityQueue.js';
import { determineFrustumSet, toggleTiles, skipTraversal, markUsedSetLeaves, traverseSet } from './traverseFunctions.js';
import { UNLOADED, LOADING, PARSING, LOADED, FAILED } from './constants.js';

// TODO: find out why tiles are left dangling in the hierarchy
// TODO: Address the issue of too many promises, garbage collection
// TODO: remove more redundant computation
// TODO: See if using classes improves performance
// TODO: See if declaring function inline improves performance
// TODO: Make sure active state works as expected

export class TilesRendererBase {

	get root() {

		const tileSet = this.tileSets[ this.rootSet ];
		if ( ! tileSet || tileSet instanceof Promise ) {

			return null;

		} else {

			return tileSet.root;

		}

	}

	constructor( url, cache = new LRUCache(), downloadQueue = new PriorityQueue( 6 ), parseQueue = new PriorityQueue( 1 ) ) {

		// state
		this.tileSets = {};
		this.rootSet = url;
		this.lruCache = cache;

		this.downloadQueue = downloadQueue;
		this.parseQueue = parseQueue;
		this.stats = {
			parsing: 0,
			downloading: 0,
			inFrustum: 0,
			used: 0,
			active: 0,
			visible: 0,
		};
		this.frameCount = 0;

		// options
		this.errorTarget = 6.0;
		this.errorThreshold = 6.0;
		this.loadSiblings = true;
		this.maxDepth = Infinity;

		this.loadTileSet( url );

	}

	traverse( cb ) {

		const tileSets = this.tileSets;
		const rootTileSet = tileSets[ this.rootSet ];
		if ( ! rootTileSet || ! rootTileSet.root ) return;

		traverseSet( rootTileSet.root, cb );

	}

	// Public API
	update() {

		const stats = this.stats;
		const lruCache = this.lruCache;
		const tileSets = this.tileSets;
		const rootTileSet = tileSets[ this.rootSet ];
		if ( ! rootTileSet || ! rootTileSet.root ) return;

		const root = rootTileSet.root;

		stats.inFrustum = 0,
		stats.used = 0,
		stats.active = 0,
		stats.visible = 0,
		this.frameCount ++;

		determineFrustumSet( root, this );
		markUsedSetLeaves( root, this );
		skipTraversal( root, this );
		toggleTiles( root, this );

		// TODO: We may want to add this function in the requestTileContents function
		lruCache.scheduleUnload( null );

	}

	// Overrideable
	parseTile( buffer, tile ) {

		return null;

	}

	disposeTile( tile ) {

	}

	preprocessNode( tile, parentTile, tileSetDir ) {

		if ( tile.content ) {

			if ( ! ( 'uri' in tile.content ) && 'url' in tile.content ) {

				tile.content.uri = tile.content.url;
				delete tile.content.url;

			}

			if ( tile.content.uri ) {

				tile.content.uri = path.join( tileSetDir, tile.content.uri );

			}

			// TODO: fix for some cases where tilesets provide the bounding volume
			// but volumes are not present. See
			if (
				tile.content.boundingVolume &&
				! (
					'box' in tile.content.boundingVolume ||
					'sphere' in tile.content.boundingVolume ||
					'region' in tile.content.boundingVolume
				)
			) {

				delete tile.content.boundingVolume;

			}

		}

		tile.parent = parentTile;
		tile.children = tile.children || [];
		tile.__contentEmpty = ! tile.content || ! tile.content.uri;

		tile.__error = 0.0;
		tile.__inFrustum = false;
		tile.__isLeaf = false;

		tile.__wasUsed = false;
		tile.__used = false;

		tile.__wasVisible = false;
		tile.__visible = false;

		tile.__wasActive = false;
		tile.__active = false;

		tile.__childrenActive = false;
		tile.__loadingState = UNLOADED;
		tile.__loadIndex = 0;

		tile.__loadAbort = null;

		if ( parentTile === null ) {

			tile.__depth = 0;

		} else {

			tile.__depth = parentTile.__depth + 1;

		}

	}

	setTileActive( tile, state ) {

	}

	setTileVisible( tile, state ) {

	}

	calculateError( tile ) {

		return 0;

	}

	tileInView( tile ) {

		return true;

	}

	// Private Functions
	loadTileSet( url ) {

		const tileSets = this.tileSets;
		if ( ! ( url in tileSets ) ) {

			const pr =
                fetch( url, { credentials: 'same-origin' } )
                	.then( res => {

                		if ( res.ok ) {

                			return res.json();

                		} else {

                			throw new Error( `Status ${ res.status } (${ res.statusText })` );

                		}

                	} )
                	.then( json => {

                		// TODO: Add version query?
                		const version = json.asset.version;
                		console.assert( version === '1.0' || version === '0.0' );

                		const basePath = path.dirname( url );

                		traverseSet( json.root, ( node, parent ) => this.preprocessNode( node, parent, basePath ) );

                		tileSets[ url ] = json;

                		// TODO: schedule an update to avoid doing this too many times
                		this.update();

                	} );

			pr.catch( e => {

				console.error( `TilesLoader: Failed to load tile set json "${ url }"` );
				console.error( e );
				delete tileSets[ url ];

			} );

			tileSets[ url ] = pr;

		}

		return Promise.resolve( tileSets[ url ] );

	}

	requestTileContents( tile ) {

		// If the tile is already being loaded then don't
		// start it again.
		if ( tile.__loadingState !== UNLOADED ) {

			return;

		}

		// TODO: reuse the functions created here?
		const lruCache = this.lruCache;
		const downloadQueue = this.downloadQueue;
		const parseQueue = this.parseQueue;
		lruCache.add( tile, t => {

			if ( t.__loadingState === LOADING ) {

				t.__loadAbort.abort();
				t.__loadAbort = null;

			} else {

				this.disposeTile( t );

			}

			if ( t.__loadingState === LOADING ) {

				stats.downloading --;

			} else if ( t.__loadingState === PARSING ) {

				stats.parsing --;

			}

			t.__loadingState = UNLOADED;
			t.__loadIndex ++;

			// TODO: Removing from the queues here is slow
			// parseQueue.remove( t );
			// downloadQueue.remove( t );

		} );

		tile.__loadIndex ++;
		const loadIndex = tile.__loadIndex;
		const priority = 1 / ( tile.__depth + 1 );
		const stats = this.stats;

		const controller = new AbortController();
		const signal = controller.signal;

		stats.downloading ++;
		tile.__loadAbort = controller;
		tile.__loadingState = LOADING;
		downloadQueue.add( tile, priority, tile => {

			if ( tile.__loadIndex !== loadIndex ) {

				return Promise.resolve();

			}

			return fetch( tile.content.uri, { credentials: 'same-origin', signal } );

		} )
			.then( res => {

				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				if ( res.ok ) {

					return res.arrayBuffer();

				} else {

					throw new Error( `Failed to load model with error code ${res.status}` );

				}

			} )
			.then( buffer => {

				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				stats.downloading --;
				stats.parsing ++;
				tile.__loadAbort = null;
				tile.__loadingState = PARSING;

				return parseQueue.add( buffer, priority, buffer => {

					if ( tile.__loadIndex !== loadIndex ) {

						return Promise.resolve();

					}

					return this.parseTile( buffer, tile );

				} );

			} )
			.then( () => {

				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				stats.parsing --;
				tile.__loadingState = LOADED;
				this.setTileActive( tile, tile.__active );
				this.setTileVisible( tile, tile.__visible );

			} )
			.catch( e => {

				// if it has been unloaded then the tile has been disposed
				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				if ( e.name !== 'AbortError' ) {

					console.error( 'TilesRenderer : Failed to load tile.' );
					console.error( e );
					tile.__loadingState = FAILED;

				} else {

					lruCache.remove( tile );

				}

			} );

	}

}
<commit_msg>add ability to use fetch options<commit_after>import path from 'path';
import { LRUCache } from '../utilities/LRUCache.js';
import { PriorityQueue } from '../utilities/PriorityQueue.js';
import { determineFrustumSet, toggleTiles, skipTraversal, markUsedSetLeaves, traverseSet } from './traverseFunctions.js';
import { UNLOADED, LOADING, PARSING, LOADED, FAILED } from './constants.js';

// TODO: find out why tiles are left dangling in the hierarchy
// TODO: Address the issue of too many promises, garbage collection
// TODO: remove more redundant computation
// TODO: See if using classes improves performance
// TODO: See if declaring function inline improves performance
// TODO: Make sure active state works as expected

export class TilesRendererBase {

	get root() {

		const tileSet = this.tileSets[ this.rootSet ];
		if ( ! tileSet || tileSet instanceof Promise ) {

			return null;

		} else {

			return tileSet.root;

		}

	}

	constructor( url, cache = new LRUCache(), downloadQueue = new PriorityQueue( 6 ), parseQueue = new PriorityQueue( 1 ) ) {

		// state
		this.tileSets = {};
		this.rootSet = url;
		this.lruCache = cache;
		this.fetchOptions = { credentials: 'same-origin' };

		this.downloadQueue = downloadQueue;
		this.parseQueue = parseQueue;
		this.stats = {
			parsing: 0,
			downloading: 0,
			inFrustum: 0,
			used: 0,
			active: 0,
			visible: 0,
		};
		this.frameCount = 0;

		// options
		this.errorTarget = 6.0;
		this.errorThreshold = 6.0;
		this.loadSiblings = true;
		this.maxDepth = Infinity;

		this.loadTileSet( url );

	}

	traverse( cb ) {

		const tileSets = this.tileSets;
		const rootTileSet = tileSets[ this.rootSet ];
		if ( ! rootTileSet || ! rootTileSet.root ) return;

		traverseSet( rootTileSet.root, cb );

	}

	// Public API
	update() {

		const stats = this.stats;
		const lruCache = this.lruCache;
		const tileSets = this.tileSets;
		const rootTileSet = tileSets[ this.rootSet ];
		if ( ! rootTileSet || ! rootTileSet.root ) return;

		const root = rootTileSet.root;

		stats.inFrustum = 0,
		stats.used = 0,
		stats.active = 0,
		stats.visible = 0,
		this.frameCount ++;

		determineFrustumSet( root, this );
		markUsedSetLeaves( root, this );
		skipTraversal( root, this );
		toggleTiles( root, this );

		// TODO: We may want to add this function in the requestTileContents function
		lruCache.scheduleUnload( null );

	}

	// Overrideable
	parseTile( buffer, tile ) {

		return null;

	}

	disposeTile( tile ) {

	}

	preprocessNode( tile, parentTile, tileSetDir ) {

		if ( tile.content ) {

			if ( ! ( 'uri' in tile.content ) && 'url' in tile.content ) {

				tile.content.uri = tile.content.url;
				delete tile.content.url;

			}

			if ( tile.content.uri ) {

				tile.content.uri = path.join( tileSetDir, tile.content.uri );

			}

			// TODO: fix for some cases where tilesets provide the bounding volume
			// but volumes are not present. See
			if (
				tile.content.boundingVolume &&
				! (
					'box' in tile.content.boundingVolume ||
					'sphere' in tile.content.boundingVolume ||
					'region' in tile.content.boundingVolume
				)
			) {

				delete tile.content.boundingVolume;

			}

		}

		tile.parent = parentTile;
		tile.children = tile.children || [];
		tile.__contentEmpty = ! tile.content || ! tile.content.uri;

		tile.__error = 0.0;
		tile.__inFrustum = false;
		tile.__isLeaf = false;

		tile.__wasUsed = false;
		tile.__used = false;

		tile.__wasVisible = false;
		tile.__visible = false;

		tile.__wasActive = false;
		tile.__active = false;

		tile.__childrenActive = false;
		tile.__loadingState = UNLOADED;
		tile.__loadIndex = 0;

		tile.__loadAbort = null;

		if ( parentTile === null ) {

			tile.__depth = 0;

		} else {

			tile.__depth = parentTile.__depth + 1;

		}

	}

	setTileActive( tile, state ) {

	}

	setTileVisible( tile, state ) {

	}

	calculateError( tile ) {

		return 0;

	}

	tileInView( tile ) {

		return true;

	}

	// Private Functions
	loadTileSet( url ) {

		const tileSets = this.tileSets;
		if ( ! ( url in tileSets ) ) {

			const pr =
                fetch( url, this.fetchOptions )
                	.then( res => {

                		if ( res.ok ) {

                			return res.json();

                		} else {

                			throw new Error( `Status ${ res.status } (${ res.statusText })` );

                		}

                	} )
                	.then( json => {

                		// TODO: Add version query?
                		const version = json.asset.version;
                		console.assert( version === '1.0' || version === '0.0' );

                		const basePath = path.dirname( url );

                		traverseSet( json.root, ( node, parent ) => this.preprocessNode( node, parent, basePath ) );

                		tileSets[ url ] = json;

                		// TODO: schedule an update to avoid doing this too many times
                		this.update();

                	} );

			pr.catch( e => {

				console.error( `TilesLoader: Failed to load tile set json "${ url }"` );
				console.error( e );
				delete tileSets[ url ];

			} );

			tileSets[ url ] = pr;

		}

		return Promise.resolve( tileSets[ url ] );

	}

	requestTileContents( tile ) {

		// If the tile is already being loaded then don't
		// start it again.
		if ( tile.__loadingState !== UNLOADED ) {

			return;

		}

		// TODO: reuse the functions created here?
		const lruCache = this.lruCache;
		const downloadQueue = this.downloadQueue;
		const parseQueue = this.parseQueue;
		lruCache.add( tile, t => {

			if ( t.__loadingState === LOADING ) {

				t.__loadAbort.abort();
				t.__loadAbort = null;

			} else {

				this.disposeTile( t );

			}

			if ( t.__loadingState === LOADING ) {

				stats.downloading --;

			} else if ( t.__loadingState === PARSING ) {

				stats.parsing --;

			}

			t.__loadingState = UNLOADED;
			t.__loadIndex ++;

			// TODO: Removing from the queues here is slow
			// parseQueue.remove( t );
			// downloadQueue.remove( t );

		} );

		tile.__loadIndex ++;
		const loadIndex = tile.__loadIndex;
		const priority = 1 / ( tile.__depth + 1 );
		const stats = this.stats;

		const controller = new AbortController();
		const signal = controller.signal;

		stats.downloading ++;
		tile.__loadAbort = controller;
		tile.__loadingState = LOADING;
		downloadQueue.add( tile, priority, tile => {

			if ( tile.__loadIndex !== loadIndex ) {

				return Promise.resolve();

			}

			return fetch( tile.content.uri, Object.assign( { signal }, this.fetchOptions ) );

		} )
			.then( res => {

				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				if ( res.ok ) {

					return res.arrayBuffer();

				} else {

					throw new Error( `Failed to load model with error code ${res.status}` );

				}

			} )
			.then( buffer => {

				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				stats.downloading --;
				stats.parsing ++;
				tile.__loadAbort = null;
				tile.__loadingState = PARSING;

				return parseQueue.add( buffer, priority, buffer => {

					if ( tile.__loadIndex !== loadIndex ) {

						return Promise.resolve();

					}

					return this.parseTile( buffer, tile );

				} );

			} )
			.then( () => {

				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				stats.parsing --;
				tile.__loadingState = LOADED;
				this.setTileActive( tile, tile.__active );
				this.setTileVisible( tile, tile.__visible );

			} )
			.catch( e => {

				// if it has been unloaded then the tile has been disposed
				if ( tile.__loadIndex !== loadIndex ) {

					return;

				}

				if ( e.name !== 'AbortError' ) {

					console.error( 'TilesRenderer : Failed to load tile.' );
					console.error( e );
					tile.__loadingState = FAILED;

				} else {

					lruCache.remove( tile );

				}

			} );

	}

}
<|endoftext|>


<commit_before> /* global angular */
 /* global document */
 /* global console */
 /* global window */
 /* global window */

var app = angular.module('ryvr', []);

app.config(function($locationProvider, $httpProvider) {
    $locationProvider.html5Mode(true);
    $httpProvider.interceptors.push(function($q) {
        return {
            'request'(config) {
                angular.element(document.getElementById('controller')).scope().controller.loading = true;
                // app.controller.loading = true;
                // config.headers.get['Accept'] =
                config.headers['Accept'] = 'application/hal+json;q=1,application/json;q=0.8,*/*;q=0.1';
                return config;
            },

            'requestError'(rejection) {
                angular.element(document.getElementById('controller')).scope().controller.loading = false;
                return $q.reject(rejection);
            },

            'response'(response) {
                angular.element(document.getElementById('controller')).scope().controller.loading = false;
                if (response.headers('Content-Type') !== 'application/hal+json') {
                    window.location.href = response.config.url;
                }
                return response;
            },

            'responseError'(rejection) {
                angular.element(document.getElementById('controller')).scope().controller.loading = false;
                return $q.reject(rejection);
            }
        };
    });
});

function getLocation(href) {
    var location = document.createElement('a');
    location.href = href;
    // IE doesn't populate all link properties when setting .href with a
    // relative URL,
    // however .href will return an absolute URL which then can be used on
    // itself
    // to populate these additional fields.
    if (location.host === '') {
        location.href = location.href;
    }
    return location;
}

app.controller('ResourceController', function($scope, $http, $location, $window) {
    var controller = this;
    controller.loaded = false;
    controller.loading = false;
    controller.actionValues = {};
    controller.error = {};
    controller.lastForm = null;
    controller.href = $window.location.href;
    controller.debug = true;

    controller.processNavClick = function(event) {
        return false;
    };

    controller.successCallback = function(response) {

        delete controller.actionValues[controller.lastForm.action.name];

        if (response.status === 201) {
            var location = getLocation(response.headers('Location'));
            controller.load(location);

        } else if (response.status === 200) {
            controller.resource = response.data;
        } else if (response.status === 204) {
            var href = $window.location.href;
            href = href.substring(0, href.lastIndexOf('/'));
            controller.load(href);

        } else {
            alert('TODO: handle ' + response.status + ' responses');
        }
    };

    controller.errorCallback = function(response) {
        var error = response;
        var errors = [];
        if (response.data && response.data.errors) {
            error = response.data.error;
            errors = response.data.errors;
        }
        if (controller.lastForm != null) {

            controller.error[controller.lastForm.action.name] = {
                error,
                errors
            };
            // todo iterate over errors and
            // update form accordingly
            Object.keys(errors).forEach(function(key, index) {
                controller.lastForm.action.fields[key].error = response.data.errors[key];
            });
            controller.lastForm = null;
        } else {
            alert('TODO: handle error', error);
        }
    };

    controller.doLoad = function(href) {
        controller.href = href;
        $http.get(href, {
            cache : false
        }).then(function successCallback(response) {
            controller.resource = response.data;
            controller.lastForm = null;
        }, controller.errorCallback);

    };

    controller.doLoadRoot = function(href) {
        $http.get(href, {
            cache : false
        }).then(function successCallback(response) {
            controller.root = response.data;
        }, controller.errorCallback);

    };

    controller.load = function(href) {
        var currLoc = getLocation($location.absUrl());

        if (href.protocol === currLoc.protocol && href.host === currLoc.host) {
            var relativeHref = href.pathname + href.search + href.hash;
            $location.url(relativeHref);
        } else {

            $window.location.href = href;
        }
    };

    $scope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
    });

    $scope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
        if (newUrl !== null && oldUrl !== newUrl) {
            controller.doLoad(newUrl);
        }
    });

    controller.initRoot = function() {
        controller.root = JSON.parse(document.getElementById('init-root').textContent);
    };

    controller.initResource = function() {
        controller.resource = JSON.parse(document.getElementById('init-resource').textContent);
    };

    controller.initRoot();
    controller.initResource();
    controller.loaded = true;

    controller.processForm = function(form) {
        controller.loading = true;
        var action = form.action;
        var href = $(location).attr('href');

        var method = action.method || 'GET';

        var values = controller.actionValues[action.name];
        var requestData = null;
        var requestParams = null;
        if (values !== null) {
            if (method === 'GET') {
                requestParams = values;
            } else {
                requestData = $.param(values);
            }
        }
        var request = {
            method,
            url : href,
            data : requestData, // pass in data as strings
            params : requestParams,
            headers : {
                'Content-Type' : action.type || 'application/x-www-form-urlencoded'
            }
        };
        controller.lastForm = form;

        if (method === 'GET') {
            if (values !== null) {
                href = href + '?' + $.param(values);
            }
            controller.load(getLocation(href));
        } else {
            $http(request).then(controller.successCallback, controller.errorCallback);
        }
        return false;
    };

    // credit to http://stackoverflow.com/a/12592693/269221
    $scope.isActive = function(path) {
        if ($location.path().substr(0, path.length) === path) {
            if (path === '/' && $location.path() === '/') {
                return true;
            } else if (path === '/') {
                return false;
            }
            return true;
        } else {
            return false;
        }
    };

    $scope.resourceProperties = function() {
        var rval = {};
        if (controller.hasOwnProperty('resource')) {
            var keys = Object.keys(controller.resource);
            keys.forEach(function(key) {
                if (key !== 'title' && key[0] !== '_') {
                    rval[key] = controller.resource[key];
                }
            });
        }
        return rval;
    };

    $scope.resourceHasProperties = function() {
        return Object.keys($scope.resourceProperties()).length !== 0;
    };

    controller.itemHeadings = function() {
        var keys = [];
        controller.resource._embedded.item.forEach(function(embeddedItem) {
            keys = keys.concat(Object.keys(embeddedItem));
        });
        var keySet = new Set(keys);
        keySet.delete("$$hashKey");
        keys = Array.from(keySet);
        return keys;
    };

    controller.linkedItem = function(item) {
        var filtered = controller.resource._embedded.item.filter(function(embeddedItem) {
           return embeddedItem._links.self.href === item.href;
        });
        return filtered.length > 0 ? filtered[0] : item;
    };

    controller.itemNavLinks = function(_links) {
        var itemRels = [ 'first', 'prev', 'next', 'last', 'current' ];
        var rval = {};
        Object.keys(_links).filter(function(key) {
            return itemRels.includes(key);
        }).forEach(function(key) {
            rval[key] = _links[key];
        });
        return rval;
    };

    controller.typeOf = function(value) {
        return typeof value;
    };

    // credit to http://stackoverflow.com/a/7220510/269221
    controller.pretty = function(json) {
        return JSON.stringify(json, null, 2);
    };
});<commit_msg>AngualarJS Cahcing is Borken<commit_after> /* global angular */
 /* global document */
 /* global console */
 /* global window */
 /* global window */

var app = angular.module('ryvr', []);

app.config(function($locationProvider, $httpProvider) {
    $locationProvider.html5Mode(true);
    $httpProvider.interceptors.push(function($q) {
        return {
            'request'(config) {
                angular.element(document.getElementById('controller')).scope().controller.loading = true;
                // app.controller.loading = true;
                // config.headers.get['Accept'] =
                config.headers['Accept'] = 'application/hal+json;q=1,application/json;q=0.8,*/*;q=0.1';
                return config;
            },

            'requestError'(rejection) {
                angular.element(document.getElementById('controller')).scope().controller.loading = false;
                return $q.reject(rejection);
            },

            'response'(response) {
                angular.element(document.getElementById('controller')).scope().controller.loading = false;
                if (response.headers('Content-Type') !== 'application/hal+json') {
                    window.location.href = response.config.url;
                }
                return response;
            },

            'responseError'(rejection) {
                angular.element(document.getElementById('controller')).scope().controller.loading = false;
                return $q.reject(rejection);
            }
        };
    });
});

function getLocation(href) {
    var location = document.createElement('a');
    location.href = href;
    // IE doesn't populate all link properties when setting .href with a
    // relative URL,
    // however .href will return an absolute URL which then can be used on
    // itself
    // to populate these additional fields.
    if (location.host === '') {
        location.href = location.href;
    }
    return location;
}

app.controller('ResourceController', function($scope, $http, $location, $window) {
    var controller = this;
    controller.loaded = false;
    controller.loading = false;
    controller.actionValues = {};
    controller.error = {};
    controller.lastForm = null;
    controller.href = $window.location.href;
    controller.debug = false;

    controller.processNavClick = function(event) {
        return false;
    };

    controller.successCallback = function(response) {

        delete controller.actionValues[controller.lastForm.action.name];

        if (response.status === 201) {
            var location = getLocation(response.headers('Location'));
            controller.load(location);

        } else if (response.status === 200) {
            controller.resource = response.data;
        } else if (response.status === 204) {
            var href = $window.location.href;
            href = href.substring(0, href.lastIndexOf('/'));
            controller.load(href);

        } else {
            alert('TODO: handle ' + response.status + ' responses');
        }
    };

    controller.errorCallback = function(response) {
        var error = response;
        var errors = [];
        if (response.data && response.data.errors) {
            error = response.data.error;
            errors = response.data.errors;
        }
        if (controller.lastForm != null) {

            controller.error[controller.lastForm.action.name] = {
                error,
                errors
            };
            // todo iterate over errors and
            // update form accordingly
            Object.keys(errors).forEach(function(key, index) {
                controller.lastForm.action.fields[key].error = response.data.errors[key];
            });
            controller.lastForm = null;
        } else {
            alert('TODO: handle error', error);
        }
    };

    controller.doLoad = function(href) {
        controller.href = href;
        $http.get(href, {
            cache : false
        }).then(function successCallback(response) {
            controller.resource = response.data;
            controller.lastForm = null;
        }, controller.errorCallback);

    };

    controller.doLoadRoot = function(href) {
        $http.get(href, {
            cache : false
        }).then(function successCallback(response) {
            controller.root = response.data;
        }, controller.errorCallback);

    };

    controller.load = function(href) {
        var currLoc = getLocation($location.absUrl());

        if (href.protocol === currLoc.protocol && href.host === currLoc.host) {
            var relativeHref = href.pathname + href.search + href.hash;
            $location.url(relativeHref);
        } else {

            $window.location.href = href;
        }
    };

    $scope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
    });

    $scope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
        if (newUrl !== null && oldUrl !== newUrl) {
            controller.doLoad(newUrl);
        }
    });

    controller.initRoot = function() {
        controller.root = JSON.parse(document.getElementById('init-root').textContent);
    };

    controller.initResource = function() {
        controller.resource = JSON.parse(document.getElementById('init-resource').textContent);
    };

    controller.initRoot();
    controller.initResource();
    controller.loaded = true;

    controller.processForm = function(form) {
        controller.loading = true;
        var action = form.action;
        var href = $(location).attr('href');

        var method = action.method || 'GET';

        var values = controller.actionValues[action.name];
        var requestData = null;
        var requestParams = null;
        if (values !== null) {
            if (method === 'GET') {
                requestParams = values;
            } else {
                requestData = $.param(values);
            }
        }
        var request = {
            method,
            url : href,
            data : requestData, // pass in data as strings
            params : requestParams,
            headers : {
                'Content-Type' : action.type || 'application/x-www-form-urlencoded'
            }
        };
        controller.lastForm = form;

        if (method === 'GET') {
            if (values !== null) {
                href = href + '?' + $.param(values);
            }
            controller.load(getLocation(href));
        } else {
            $http(request, {
                cache : false
            }).then(controller.successCallback, controller.errorCallback);
        }
        return false;
    };

    // credit to http://stackoverflow.com/a/12592693/269221
    $scope.isActive = function(path) {
        if ($location.path().substr(0, path.length) === path) {
            if (path === '/' && $location.path() === '/') {
                return true;
            } else if (path === '/') {
                return false;
            }
            return true;
        } else {
            return false;
        }
    };

    $scope.resourceProperties = function() {
        var rval = {};
        if (controller.hasOwnProperty('resource')) {
            var keys = Object.keys(controller.resource);
            keys.forEach(function(key) {
                if (key !== 'title' && key[0] !== '_') {
                    rval[key] = controller.resource[key];
                }
            });
        }
        return rval;
    };

    $scope.resourceHasProperties = function() {
        return Object.keys($scope.resourceProperties()).length !== 0;
    };

    controller.itemHeadings = function() {
        var keys = [];
        controller.resource._embedded.item.forEach(function(embeddedItem) {
            keys = keys.concat(Object.keys(embeddedItem));
        });
        var keySet = new Set(keys);
        keySet.delete("$$hashKey");
        keys = Array.from(keySet);
        return keys;
    };

    controller.linkedItem = function(item) {
        var filtered = controller.resource._embedded.item.filter(function(embeddedItem) {
           return embeddedItem._links.self.href === item.href;
        });
        return filtered.length > 0 ? filtered[0] : item;
    };

    controller.itemNavLinks = function(_links) {
        var itemRels = [ 'first', 'prev', 'next', 'last', 'current' ];
        var rval = {};
        Object.keys(_links).filter(function(key) {
            return itemRels.includes(key);
        }).forEach(function(key) {
            rval[key] = _links[key];
        });
        return rval;
    };

    controller.typeOf = function(value) {
        return typeof value;
    };

    // credit to http://stackoverflow.com/a/7220510/269221
    controller.pretty = function(json) {
        return JSON.stringify(json, null, 2);
    };
});<|endoftext|>


<commit_before>import { getDirection } from '../../intl/locale';

const reHtml = /[&<>]/g;
const htmlEntities = {
  '&': '&',
  '<': '<',
  '>': '>',
};

const observerConfig = {
  attributes: true,
  characterData: false,
  childList: true,
  subtree: true,
  attributeFilter: ['data-l10n-id', 'data-l10n-args']
};

export class LocalizationObserver extends Map {
  constructor() {
    super();
    this.rootsByLocalization = new WeakMap();
    this.localizationsByRoot = new WeakMap();
    this.observer = new MutationObserver(
      mutations => this.translateMutations(mutations)
    );
  }

  observeRoot(root, l10n = this.get('main')) {
    this.localizationsByRoot.set(root, l10n);
    if (!this.rootsByLocalization.has(l10n)) {
      this.rootsByLocalization.set(l10n, new Set());
    }
    this.rootsByLocalization.get(l10n).add(root);
    this.observer.observe(root, observerConfig);
  }

  disconnectRoot(root) {
    this.pause();
    this.localizationsByRoot.delete(root);
    for (let [name, l10n] of this) {
      const roots = this.rootsByLocalization.get(l10n);
      if (roots && roots.has(root)) {
        roots.delete(root);
        if (roots.size === 0) {
          this.delete(name);
          this.rootsByLocalization.delete(l10n);
        }
      }
    }
    this.resume();
  }

  pause() {
    this.observer.disconnect();
  }

  resume() {
    for (let l10n of this.values()) {
      if (this.rootsByLocalization.has(l10n)) {
        for (let root of this.rootsByLocalization.get(l10n)) {
          this.observer.observe(root, observerConfig)
        }
      }
    }
  }

  requestLanguages(requestedLangs) {
    const localizations = Array.from(this.values());
    return Promise.all(
      localizations.map(l10n => l10n.requestLanguages(requestedLangs))
    ).then(
      () => this.translateAllRoots()
    )
  }

  handleEvent() {
    return this.requestLanguages();
  }

  getTranslatables(element) {
    const nodes = Array.from(element.querySelectorAll('[data-l10n-id]'));

    if (typeof element.hasAttribute === 'function' &&
        element.hasAttribute('data-l10n-id')) {
      nodes.push(element);
    }

    return nodes;
  }

  translateMutations(mutations) {
    const targets = new Set();

    for (let mutation of mutations) {
      switch (mutation.type) {
        case 'attributes':
          targets.add(mutation.target);
          break;
        case 'childList':
          for (let addedNode of mutation.addedNodes) {
            if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
              if (addedNode.childElementCount) {
                this.getTranslatables(addedNode).forEach(
                  targets.add.bind(targets)
                );
              } else {
                if (addedNode.hasAttribute('data-l10n-id')) {
                  targets.add(addedNode);
                }
              }
            }
          }
          break;
      }
    }

    if (targets.size === 0) {
      return;
    }

    this.translateElements(Array.from(targets));
  }

  getLocalizationForElement(elem) {
    return this.get(elem.getAttribute('data-l10n-bundle') || 'main');
  }

  // XXX the following needs to be optimized, perhaps getTranslatables should
  // sort elems by localization they refer to so that it is easy to group them,
  // handle each group individually and finally concatenate the resulting
  // translations into a flat array whose elements correspond one-to-one to
  // elems?
  getElementsTranslation(elems) {
    const keys = elems.map(elem => {
      const args = elem.getAttribute('data-l10n-args');
      return [
        this.getLocalizationForElement(elem),
        elem.getAttribute('data-l10n-id'),
        args ?
          JSON.parse(args.replace(reHtml, match => htmlEntities[match])) :
          null
      ];
    });

    return Promise.all(
      keys.map(
        ([l10n, id, args]) => l10n.formatEntities([[id, args]]).then(
          translations => [l10n, translations]
        )
      )
    );
  }

  translateAllRoots() {
    const localizations = Array.from(this.values());
    return Promise.all(
      localizations.map(
        l10n => this.translateRoots(l10n)
      )
    );
  }

  translateRoots(l10n) {
    if (!this.rootsByLocalization.has(l10n)) {
      return Promise.resolve();
    }

    const roots = Array.from(this.rootsByLocalization.get(l10n));
    return Promise.all(
      roots.map(root => l10n.interactive.then(
        bundles => this.translateRoot(root)
      ))
    );
  }

  translateRoot(root) {
    const l10n = this.localizationsByRoot.get(root);
    return l10n.interactive.then(bundles => {
      const langs = bundles.map(bundle => bundle.lang);

      function setLangs() {
        const wasLocalizedBefore = root.hasAttribute('langs');

        root.setAttribute('langs', langs.join(' '));
        root.setAttribute('lang', langs[0]);
        root.setAttribute('dir', getDirection(langs[0]));

        if (wasLocalizedBefore) {
          root.dispatchEvent(new CustomEvent('DOMRetranslated', {
            bubbles: false,
            cancelable: false,
          }));
        }
      }

      return this.translateFragment(root).then(setLangs);
    });
  }

  translateFragment(frag) {
    return this.translateElements(this.getTranslatables(frag));
  }

  translateElements(elements) {
    return this.getElementsTranslation(elements).then(
      translations => this.applyTranslations(elements, translations));
  }

  applyTranslations(elems, translations) {
    this.pause();
    for (let i = 0; i < elems.length; i++) {
      const [l10n, [translation]] = translations[i];
      l10n.overlayElement(elems[i], translation);
    }
    this.resume();
  }

  setAttributes(element, id, args) {
    element.setAttribute('data-l10n-id', id);
    if (args) {
      element.setAttribute('data-l10n-args', JSON.stringify(args));
    }
    return element;
  }

  getAttributes(element) {
    return {
      id: element.getAttribute('data-l10n-id'),
      args: JSON.parse(element.getAttribute('data-l10n-args'))
    };
  }

}

<commit_msg>Add data-l10n-bundle to observed attributes<commit_after>import { getDirection } from '../../intl/locale';

const reHtml = /[&<>]/g;
const htmlEntities = {
  '&': '&',
  '<': '<',
  '>': '>',
};

const observerConfig = {
  attributes: true,
  characterData: false,
  childList: true,
  subtree: true,
  attributeFilter: ['data-l10n-id', 'data-l10n-args', 'data-l10n-bundle']
};

export class LocalizationObserver extends Map {
  constructor() {
    super();
    this.rootsByLocalization = new WeakMap();
    this.localizationsByRoot = new WeakMap();
    this.observer = new MutationObserver(
      mutations => this.translateMutations(mutations)
    );
  }

  observeRoot(root, l10n = this.get('main')) {
    this.localizationsByRoot.set(root, l10n);
    if (!this.rootsByLocalization.has(l10n)) {
      this.rootsByLocalization.set(l10n, new Set());
    }
    this.rootsByLocalization.get(l10n).add(root);
    this.observer.observe(root, observerConfig);
  }

  disconnectRoot(root) {
    this.pause();
    this.localizationsByRoot.delete(root);
    for (let [name, l10n] of this) {
      const roots = this.rootsByLocalization.get(l10n);
      if (roots && roots.has(root)) {
        roots.delete(root);
        if (roots.size === 0) {
          this.delete(name);
          this.rootsByLocalization.delete(l10n);
        }
      }
    }
    this.resume();
  }

  pause() {
    this.observer.disconnect();
  }

  resume() {
    for (let l10n of this.values()) {
      if (this.rootsByLocalization.has(l10n)) {
        for (let root of this.rootsByLocalization.get(l10n)) {
          this.observer.observe(root, observerConfig)
        }
      }
    }
  }

  requestLanguages(requestedLangs) {
    const localizations = Array.from(this.values());
    return Promise.all(
      localizations.map(l10n => l10n.requestLanguages(requestedLangs))
    ).then(
      () => this.translateAllRoots()
    )
  }

  handleEvent() {
    return this.requestLanguages();
  }

  getTranslatables(element) {
    const nodes = Array.from(element.querySelectorAll('[data-l10n-id]'));

    if (typeof element.hasAttribute === 'function' &&
        element.hasAttribute('data-l10n-id')) {
      nodes.push(element);
    }

    return nodes;
  }

  translateMutations(mutations) {
    const targets = new Set();

    for (let mutation of mutations) {
      switch (mutation.type) {
        case 'attributes':
          targets.add(mutation.target);
          break;
        case 'childList':
          for (let addedNode of mutation.addedNodes) {
            if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
              if (addedNode.childElementCount) {
                this.getTranslatables(addedNode).forEach(
                  targets.add.bind(targets)
                );
              } else {
                if (addedNode.hasAttribute('data-l10n-id')) {
                  targets.add(addedNode);
                }
              }
            }
          }
          break;
      }
    }

    if (targets.size === 0) {
      return;
    }

    this.translateElements(Array.from(targets));
  }

  getLocalizationForElement(elem) {
    return this.get(elem.getAttribute('data-l10n-bundle') || 'main');
  }

  // XXX the following needs to be optimized, perhaps getTranslatables should
  // sort elems by localization they refer to so that it is easy to group them,
  // handle each group individually and finally concatenate the resulting
  // translations into a flat array whose elements correspond one-to-one to
  // elems?
  getElementsTranslation(elems) {
    const keys = elems.map(elem => {
      const args = elem.getAttribute('data-l10n-args');
      return [
        this.getLocalizationForElement(elem),
        elem.getAttribute('data-l10n-id'),
        args ?
          JSON.parse(args.replace(reHtml, match => htmlEntities[match])) :
          null
      ];
    });

    return Promise.all(
      keys.map(
        ([l10n, id, args]) => l10n.formatEntities([[id, args]]).then(
          translations => [l10n, translations]
        )
      )
    );
  }

  translateAllRoots() {
    const localizations = Array.from(this.values());
    return Promise.all(
      localizations.map(
        l10n => this.translateRoots(l10n)
      )
    );
  }

  translateRoots(l10n) {
    if (!this.rootsByLocalization.has(l10n)) {
      return Promise.resolve();
    }

    const roots = Array.from(this.rootsByLocalization.get(l10n));
    return Promise.all(
      roots.map(root => l10n.interactive.then(
        bundles => this.translateRoot(root)
      ))
    );
  }

  translateRoot(root) {
    const l10n = this.localizationsByRoot.get(root);
    return l10n.interactive.then(bundles => {
      const langs = bundles.map(bundle => bundle.lang);

      function setLangs() {
        const wasLocalizedBefore = root.hasAttribute('langs');

        root.setAttribute('langs', langs.join(' '));
        root.setAttribute('lang', langs[0]);
        root.setAttribute('dir', getDirection(langs[0]));

        if (wasLocalizedBefore) {
          root.dispatchEvent(new CustomEvent('DOMRetranslated', {
            bubbles: false,
            cancelable: false,
          }));
        }
      }

      return this.translateFragment(root).then(setLangs);
    });
  }

  translateFragment(frag) {
    return this.translateElements(this.getTranslatables(frag));
  }

  translateElements(elements) {
    return this.getElementsTranslation(elements).then(
      translations => this.applyTranslations(elements, translations));
  }

  applyTranslations(elems, translations) {
    this.pause();
    for (let i = 0; i < elems.length; i++) {
      const [l10n, [translation]] = translations[i];
      l10n.overlayElement(elems[i], translation);
    }
    this.resume();
  }

  setAttributes(element, id, args) {
    element.setAttribute('data-l10n-id', id);
    if (args) {
      element.setAttribute('data-l10n-args', JSON.stringify(args));
    }
    return element;
  }

  getAttributes(element) {
    return {
      id: element.getAttribute('data-l10n-id'),
      args: JSON.parse(element.getAttribute('data-l10n-args'))
    };
  }

}

<|endoftext|>