<commit_before>var Material = function (name)
{
	this._Name = name;
	this._Program = null;
	this._Textures = [];
};

Material.extend(
{

});

Material.prototype.extend(
{

	getName: function()
	{
		return this._Name;
	},

	getProgram: function()
	{
		return this._Program;
	},

	getTextures: function()
	{
		return this._Textures;
	}

});<commit_msg>started implementing entity-component model<commit_after>var Material = function (vertexSource, fragmentSource, textureName)
{
	var vertexSource = vertexSource;//
	fragmentSource = fragmentSource; //Shader.GetSourceFromHTMLElement("fragment-shader");
	this.program = Shader.CreateProgram(vertexSource, fragmentSource);
	this.texture = new Texture(textureName);
};

Material.extend(
{

});

Material.prototype.extend(
{

});<|endoftext|>


<commit_before>(function(global) {

  var RS = RemoteStorage;

  var BASE_URL = 'https://www.googleapis.com';
  var AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';
  var AUTH_SCOPE = 'https://www.googleapis.com/auth/drive';

  var GD_DIR_MIME_TYPE = 'application/vnd.google-apps.folder';
  var RS_DIR_MIME_TYPE = 'application/json; charset=UTF-8';

  function buildQueryString(params) {
    return Object.keys(params).map(function(key) {
      return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
    }).join('&');
  }

  function fileNameFromMeta(meta) {
    return encodeURIComponent(meta.title) + (meta.mimeType == GD_DIR_MIME_TYPE ? '/' : '');
  }

  RS.GoogleDrive = function(remoteStorage, clientId, apiKey) {

    RS.eventHandling(this, 'connected');

    this.rs = remoteStorage;
    this.clientId = clientId, this.apiKey = apiKey;

    this._fileIdCache = {};

    setTimeout(function() {
      this.configure(undefined, undefined, undefined, localStorage['remotestorage:googledrive:token']);
    }.bind(this), 0);
  };

  RS.GoogleDrive.prototype = {

    configure: function(_x, _y, _z, token) { // parameter list compatible with WireClient
      console.log('configure wiht token', token);
      localStorage['remotestorage:googledrive:token'] = token;
      if(token) {
        this.token = token;
        this.connected = true;
        this._emit('connected');
      } else {
        this.connected = false;
        delete this.token;
        delete localStorage.googledrive;
      }
    },

    connect: function() {
      localStorage.googledrive = true;
      RS.Authorize(AUTH_URL, AUTH_SCOPE, String(document.location), this.clientId);
    },

    get: function(path, options) {
      if(path.substr(-1) == '/') {
        return this._getDir(path, options);
      } else {
        return this._getFile(path, options);
      }
    },

    _getFile: function(path, options) {
      return promising(function(p) {
        console.log('fake file', path);
        p.fulfill(200, '', 'text/plain');
      });
    },

    _getDir: function(path, options) {
      var promise = promising();
      this._getFileId(path, function(idError, id) {
        if(idError) {
          promise.reject(idError);
        } else if(! id) {
          promise.fulfill(404);
        } else {
          this._request('GET', BASE_URL + '/drive/v2/files/' + id + '/children', {}, function(childrenError, response) {
            if(childrenError) {
              promise.reject(childrenError);
            } else {
              if(response.status == 200) {
                var data = JSON.parse(response.responseText);
                var n = data.items.length, i = 0;
                if(n == 0) {
                  // FIXME: add revision of directory!
                  promise.fulfill(200, {}, RS_DIR_MIME_TYPE, undefined);
                  return;
                }
                var result = {};
                var idCache = {};
                var gotMeta = function(err, meta) {
                  if(err) {
                    // FIXME: actually propagate the error.
                    console.error("getting meta stuff failed: ", err);
                  } else {
                    var fileName = fileNameFromMeta(meta);
                    // NOTE: the ETags are double quoted. This is not a bug, but just the
                    // way etags from google drive look like.
                    // Example listing:
                    //  {
                    //    "CMakeCache.txt": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTk5NjE1NA\"",
                    //    "CMakeFiles": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTk5NjUxNQ\"",
                    //    "Makefile": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA2MDIwNDA0OQ\"",
                    //    "bgrive": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTkzODE4Nw\"",
                    //    "cmake_install.cmake": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTkzNzU2NA\"",
                    //    "grive": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTk2Njg2Ng\"",
                    //    "libgrive": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA2MDAxNDk1NQ\""
                    //  }
                    result[fileName] = meta.etag;

                    // propagate id cache
                    this._fileIdCache[path + fileName] = meta.id;
                  }
                  i++;
                  if(i == n) {
                    promise.fulfill(200, result, RS_DIR_MIME_TYPE, undefined);
                  }
                }.bind(this);
                data.items.forEach(function(item) {
                  this._getMeta(item.id, gotMeta);
                }.bind(this));
              } else {
                promise.reject('request failed or something: ' + response.status);
              }
            }
          });
        }
      });
      return promise;
    },

    _getFileId: function(path, callback) {
      if(path == '/') {
        // "root" is a special alias for the fileId of the root directory
        callback.call(this, null, 'root');
      } else if(path in this._fileIdCache) {
        // id is cached.
        callback.call(this, null, this._fileIdCache[path]);
      } else {
        // id is not cached (or file doesn't exist).
        // load parent directory listing to propagate / update id cache.
        this._getDir(path.replace(/[^\/]+\/?$/, '')).then(function() {
          callback.call(this, null, this._fileIdCache[path]);
        }.bind(this), callback.bind(this));
      }
    },

    _getMeta: function(id, callback) {
      this._request('GET', BASE_URL + '/drive/v2/files/' + id, {}, function(err, response) {
        if(err) {
          callback(err);
        } else {
          if(response.status == 200) {
            callback(null, JSON.parse(response.responseText));
          } else {
            callback("request (getting metadata for " + id + ") failed with status: " + response.status);
          }
        }
      });
    },

    _request: function(method, url, options, callback) {
      if(! this.token) {
        callback.call(this, "Not authorized!");
      }
      var xhr = new XMLHttpRequest();
      xhr.open(method, url, true);
      xhr.setRequestHeader('Authorization', 'Bearer ' + this.token);
      if(options.headers) {
        for(var key in options.headers) {
          xhr.setRequestHeader(key, options.headers[key]);
        }
      }
      xhr.onload = function() {
        callback.call(this, null, xhr);
      }.bind(this);
      xhr.onerror = function(error) {
        callback.call(this, error);
      }.bind(this);
      xhr.send(options.body);
    }
  };

  RS.GoogleDrive._rs_init = function(remoteStorage) {
    var config = remoteStorage.apiKeys.googledrive;
    if(config) {
      remoteStorage.googledrive = new RS.GoogleDrive(remoteStorage, config.client_id, config.api_key);
      if(localStorage.googledrive) {
        remoteStorage.remote = remoteStorage.googledrive;
      }
    }
  };

})(this);
<commit_msg>googledrive: read-only fully working.<commit_after>(function(global) {

  var RS = RemoteStorage;

  var BASE_URL = 'https://www.googleapis.com';
  var AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';
  var AUTH_SCOPE = 'https://www.googleapis.com/auth/drive';

  var GD_DIR_MIME_TYPE = 'application/vnd.google-apps.folder';
  var RS_DIR_MIME_TYPE = 'application/json; charset=UTF-8';

  function buildQueryString(params) {
    return Object.keys(params).map(function(key) {
      return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
    }).join('&');
  }

  function fileNameFromMeta(meta) {
    return encodeURIComponent(meta.title) + (meta.mimeType == GD_DIR_MIME_TYPE ? '/' : '');
  }

  RS.GoogleDrive = function(remoteStorage, clientId, apiKey) {

    RS.eventHandling(this, 'connected');

    this.rs = remoteStorage;
    this.clientId = clientId, this.apiKey = apiKey;

    this._fileIdCache = {};

    setTimeout(function() {
      this.configure(undefined, undefined, undefined, localStorage['remotestorage:googledrive:token']);
    }.bind(this), 0);
  };

  RS.GoogleDrive.prototype = {

    configure: function(_x, _y, _z, token) { // parameter list compatible with WireClient
      localStorage['remotestorage:googledrive:token'] = token;
      if(token) {
        this.token = token;
        this.connected = true;
        this._emit('connected');
      } else {
        this.connected = false;
        delete this.token;
        delete localStorage.googledrive;
      }
    },

    connect: function() {
      localStorage.googledrive = true;
      RS.Authorize(AUTH_URL, AUTH_SCOPE, String(document.location), this.clientId);
    },

    get: function(path, options) {
      if(path.substr(-1) == '/') {
        return this._getDir(path, options);
      } else {
        return this._getFile(path, options);
      }
    },

    _getFile: function(path, options) {
      var promise = promising();
      this._getFileId(path, function(idError, id) {
        if(idError) {
          promise.reject(idError);
        } else {
          this._getMeta(id, function(metaError, meta) {
            if(metaError) {
              promise.reject(metaError);
            } else if(meta.downloadUrl) {
              var options = {};
              if(meta.mimeType.match(/charset=binary/)) {
                options.responseType = 'blob';
              }
              this._request('GET', meta.downloadUrl, options, function(downloadError, response) {
                if(downloadError) {
                  promise.reject(downloadError);
                } else {
                  var body = response.response;
                  if(meta.mimeType.match(/^application\/json/)) {
                    try {
                      body = JSON.parse(body);
                    } catch(e) {}
                  }
                  promise.fulfill(200, body, meta.mimeType, meta.etag);
                }
              });
            } else {
              // empty file
              promise.fulfill(200, '', meta.mimeType, meta.etag);
            }
          });
        }
      });
      return promise;
    },

    _getDir: function(path, options) {
      var promise = promising();
      this._getFileId(path, function(idError, id) {
        if(idError) {
          promise.reject(idError);
        } else if(! id) {
          promise.fulfill(404);
        } else {
          this._request('GET', BASE_URL + '/drive/v2/files/' + id + '/children', {}, function(childrenError, response) {
            if(childrenError) {
              promise.reject(childrenError);
            } else {
              if(response.status == 200) {
                var data = JSON.parse(response.responseText);
                var n = data.items.length, i = 0;
                if(n == 0) {
                  // FIXME: add revision of directory!
                  promise.fulfill(200, {}, RS_DIR_MIME_TYPE, undefined);
                  return;
                }
                var result = {};
                var idCache = {};
                var gotMeta = function(err, meta) {
                  if(err) {
                    // FIXME: actually propagate the error.
                    console.error("getting meta stuff failed: ", err);
                  } else {
                    var fileName = fileNameFromMeta(meta);
                    // NOTE: the ETags are double quoted. This is not a bug, but just the
                    // way etags from google drive look like.
                    // Example listing:
                    //  {
                    //    "CMakeCache.txt": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTk5NjE1NA\"",
                    //    "CMakeFiles": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTk5NjUxNQ\"",
                    //    "Makefile": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA2MDIwNDA0OQ\"",
                    //    "bgrive": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTkzODE4Nw\"",
                    //    "cmake_install.cmake": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTkzNzU2NA\"",
                    //    "grive": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA1OTk2Njg2Ng\"",
                    //    "libgrive": "\"HK9znrxLd1pIgz63yXyznaLN5rM/MTM3NzA2MDAxNDk1NQ\""
                    //  }
                    result[fileName] = meta.etag;

                    // propagate id cache
                    this._fileIdCache[path + fileName] = meta.id;
                  }
                  i++;
                  if(i == n) {
                    promise.fulfill(200, result, RS_DIR_MIME_TYPE, undefined);
                  }
                }.bind(this);
                data.items.forEach(function(item) {
                  this._getMeta(item.id, gotMeta);
                }.bind(this));
              } else {
                promise.reject('request failed or something: ' + response.status);
              }
            }
          });
        }
      });
      return promise;
    },

    _getFileId: function(path, callback) {
      callback = callback.bind(this);
      if(path == '/') {
        // "root" is a special alias for the fileId of the root directory
        callback(null, 'root');
      } else if(path in this._fileIdCache) {
        // id is cached.
        callback(null, this._fileIdCache[path]);
      } else {
        // id is not cached (or file doesn't exist).
        // load parent directory listing to propagate / update id cache.
        this._getDir(path.replace(/[^\/]+\/?$/, '')).then(function() {
          callback(null, this._fileIdCache[path]);
        }.bind(this), callback);
      }
    },

    _getMeta: function(id, callback) {
      callback = callback.bind(this);
      this._request('GET', BASE_URL + '/drive/v2/files/' + id, {}, function(err, response) {
        if(err) {
          callback(err);
        } else {
          if(response.status == 200) {
            callback(null, JSON.parse(response.responseText));
          } else {
            callback("request (getting metadata for " + id + ") failed with status: " + response.status);
          }
        }
      });
    },

    _request: function(method, url, options, callback) {
      callback = callback.bind(this);
      if(! this.token) {
        callback("Not authorized!");
      }
      var xhr = new XMLHttpRequest();
      xhr.open(method, url, true);
      if(options.responseType) {
        xhr.responseType = options.responseType;
      }
      xhr.setRequestHeader('Authorization', 'Bearer ' + this.token);
      if(options.headers) {
        for(var key in options.headers) {
          xhr.setRequestHeader(key, options.headers[key]);
        }
      }
      xhr.onload = function() {
        // google tokens expire from time to time...
        if(xhr.status == 401) {
          this.connect();
          return;
        }
        callback(null, xhr);
      }.bind(this);
      xhr.onerror = function(error) {
        callback(error);
      }.bind(this);
      xhr.send(options.body);
    }
  };

  RS.GoogleDrive._rs_init = function(remoteStorage) {
    var config = remoteStorage.apiKeys.googledrive;
    if(config) {
      remoteStorage.googledrive = new RS.GoogleDrive(remoteStorage, config.client_id, config.api_key);
      if(localStorage.googledrive) {
        remoteStorage.remote = remoteStorage.googledrive;
      }
    }
  };

})(this);
<|endoftext|>


<commit_before>import {inject, customElement, bindable} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {BindingEngine} from 'aurelia-binding';
import d3 from 'd3';
import GraphModel from './graph-model';
import {GraphLibD3} from './graphlib-d3';
import {GraphFinder} from './graph-finder';
import {GraphModifier} from './graph-modifier';
import {ActionManager} from './action-manager';
import {GraphPresentationService} from './graph-presentation-service';
import {GraphPresentationModel} from '../models/graph-presentation-model';

@customElement('graph')
@inject(Element, EventAggregator, GraphLibD3, GraphModel, GraphFinder, GraphModifier, ActionManager, BindingEngine,
  GraphPresentationService, GraphPresentationModel)
export class Graph {
  @bindable data;
  @bindable graphInteractionModel;

  constructor(element, pubSub, graphLibD3, graphModel, graphFinder, graphModifier, actionManager, bindingEngine,
      graphPresentationService, graphPresentationModel) {
    this.element = element;
    this.pubSub = pubSub;
    this.graphLibD3 = graphLibD3;
    this.graphModel = graphModel;
    this.graphFinder = graphFinder;
    this.graphModifier = graphModifier;
    this.actionManager = actionManager;
    this.bindingEngine = bindingEngine;
    this.graphPresentationService = graphPresentationService;
    this.graphPresentationModel = graphPresentationModel;

    this.windowSizeAdapter();

    this.displayNodes = [];
    this.displayEdges = [];

    this.pubSub.subscribe('search.node', nodeId => {
      this.addNodeAction([nodeId], true);
    });
  }

  /**
   * Update graph dimensions. Introduce the concept of using a service to update
   * the model (but lots of other places in app where viewModes update models directly)
   */
  windowSizeAdapter() {
    // TODO Also need to respond to window resize so that svg can resize.
    this.graphPresentationService.updateDimensions(
      window.innerWidth - 100,
      window.innerHeight - 112
    );
  }

  /**
   * A new global graph (i.e. project analysis) is available.
   * Create an empty display graph and initialize it with some
   * sample data from the global graph.
   */
  dataChanged(newVal) {
    let unusedTypes;

    if (newVal) {
      unusedTypes = this.graphFinder.findUnusedTypes(this.graphModel.globalGraphModel);
      this.graphModel.initRadii();
      this.displayGraph = this.graphModel.emptyGraph();
      this.addNodeAction([unusedTypes[0]], true);
    }
  }

  /**
   * Perform the add action, and register undo and redo handlers.
   */
  addNodeAction(nodeIds, withNeighbours) {
    this.addAndDisplay(nodeIds, withNeighbours);
    this.actionManager.addAction(this,
      this.removeAndDisplay, [nodeIds, withNeighbours],
      this.addAndDisplay, [nodeIds, withNeighbours]
    );
  }

  addAndDisplay(nodeIds, withNeighbours) {
    this.addToDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  removeAndDisplay(nodeIds, withNeighbours) {
    this.removeFromDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  /**
   * Modify the graph display model to include the given node id's.
   * If withNeihbours is true, then also include each node's neighbours.
   */
  addToDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.addNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.addNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Modify the graph display model to remove the given node id's.
   * If withNeihbours is true, then also remove each node's neighbours.
   */
  removeFromDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.removeNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.removeNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Use D3 force layout to calculate node positions.
   * A high negative charge value avoids node overlap.
   * a visually pleasing, uncluttered effect.
   */
  updateForceLayout() {
    this.d3Data = this.graphLibD3.mapToD3(this.displayGraph);
    let currentNode;
    let numNodes = this.d3Data.nodes.length;

    let force = d3.layout.force()
       .nodes(this.d3Data.nodes)
       .links(this.d3Data.links)
       .size([this.graphPresentationModel.width, this.graphPresentationModel.height])
       .gravity(0.4)
       .linkDistance(200)
       .charge(-4000)
       .on('tick', this.tick.bind(this));

    this.computeLayout(force, numNodes);
    this.updateDisplayData(this.d3Data);
  }

  /**
   * On each tick of D3's force layout calculation, adjust node positions by applying
   * collision detection and bounding box constraints. Both of these algorithms
   * need to know the node's radius. A heuristic is to estimate radius = 45.
   * A complete solution would know each node * actual radius, but this is very
   * difficult from timing perspective because its not known until after its
   * in the DOM, and calculated by NodeCustomElement in micro task queue.
   */
  tick() {
    const heuristicRadius = 45;
    this.applyCollisionDetection(heuristicRadius);
    this.applyBoundingBox(heuristicRadius);
  }

  /**
   * Collision detection algorithm from
   * https://bl.ocks.org/mbostock/3231298
   */
  applyCollisionDetection(heuristicRadius) {
    let q = d3.geom.quadtree(this.d3Data.nodes),
      i = 0,
      n = this.d3Data.nodes.length;

    while (++i < n) q.visit(this.collide(this.d3Data.nodes[i], heuristicRadius));
  }

  collide(node, r) {
    let nx1 = node.x - r,
      nx2 = node.x + r,
      ny1 = node.y - r,
      ny2 = node.y + r;
    return function(quad, x1, y1, x2, y2) {
      if (quad.point && (quad.point !== node)) {
        let x = node.x - quad.point.x,
          y = node.y - quad.point.y,
          l = Math.sqrt(x * x + y * y),
          r = node.radius + quad.point.radius;
        if (l < r) {
          l = (l - r) / l * 0.5;
          node.x -= x *= l;
          node.y -= y *= l;
          quad.point.x += x;
          quad.point.y += y;
        }
      }
      return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
    };
  }

  /**
  * Make nodes fit within bounding box represented by this.graphPresentationModel.
  * http://mbostock.github.io/d3/talk/20110921/bounding.html
  */
  applyBoundingBox(r) {
    this.d3Data.nodes.forEach( node => {
      let curX = node.x;
      node.x = Math.max(r, Math.min(this.graphPresentationModel.width - r, curX));
      let curY = node.y;
      node.y = Math.max(r, Math.min(this.graphPresentationModel.height - r, node.y));
    });
  }

  /**
   * Technique to minimize D3 chaotic bouncing in force layout.
   * http://stackoverflow.com/questions/13463053/calm-down-initial-tick-of-a-force-layout
   */
  computeLayout(force, numIterations) {
    let safety = 0;

    force.start();

    while(force.alpha() > 0.05) {
      force.tick();
      if(safety++ > 500) {
        break;
      }
    }

    force.stop();

    if(safety >= 500) {
      console.warn('Unable to stabilize force layout.');
    }
  }

  /**
   * Update list of display nodes and edges from d3 data.
   */
  updateDisplayData(d3Data) {
    // add nodes that are in d3Data, but not in display
    d3Data.nodes.forEach( node => {
      if (!this.graphLibD3.containsNode(this.displayNodes, node)) {
        this.displayNodes.push(node);
      }
    });

    // add links that are in d3Data, but not in display
    d3Data.links.forEach( link => {
      if (!this.graphLibD3.containsEdge(this.displayEdges, link)) {
        this.displayEdges.push(link);
      }
    });

    // remove nodes that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayNodes = this.displayNodes.length;
    for (let i=numDisplayNodes-1; i >= 0; i--) {
      if (!this.graphLibD3.containsNode(d3Data.nodes, this.displayNodes[i])) {
        this.displayNodes.splice(i, 1);
      }
    }

    // remove edges that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayEdges = this.displayEdges.length;
    for (let j=numDisplayEdges-1; j >=0; j--) {
      if (!this.graphLibD3.containsEdge(d3Data.links, this.displayEdges[j])) {
        this.displayEdges.splice(j, 1);
      }
    }

  }

  /**
   * Based on the currently selected nodes, use 'interaction' and 'type'
   * to find nodes from globalGraph that should be added, but only if
   * they're not already in the display graph.
   */
  findNodesToAdd(interaction, type) {
    let selectedNodeIds,
      relationship,
      nodesByEdgeRelationship;

    selectedNodeIds = this.graphFinder.findSelectedNodeIds(this.displayGraph);
    relationship = type === 'of it' ? 'target' : 'source';
    nodesByEdgeRelationship = this.graphFinder.findNodesByEdgeRelationship(
      this.graphModel.globalGraphModel, selectedNodeIds, interaction, relationship
    );

    return this.graphFinder.filterAlreadyInGraph(nodesByEdgeRelationship, this.displayGraph);
  }

  /**
   * Determine nodes that should be added, and add them as an undo-able action.
   */
  addNodesToDisplay(interaction, type) {
    let nodesToAdd = this.findNodesToAdd(interaction, type);

    if (nodesToAdd.length > 0) {
      this.addNodeAction(nodesToAdd);
    } else {
      // TODO notify https://github.com/CANVE/canve-viz/issues/32
      console.warn('No interaction results');
    }
  }

  /**
   * Invoked by Aurelia when 'graphInteractionModel' binding value has changed.
   * In practice this object it set once in the parent view. After that,
   * register property observers to fire when specific properties of the interaction model change,
   * indicating that the graph should respond to the change by adding more nodes.
   */
  graphInteractionModelChanged(newValue) {
    if (newValue) {
      this.graphInteractionModel = newValue;

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'callsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('uses', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'extensionsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('extends', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'ownershipSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('declares member', newValue);
      });

      // TODO dispose in appropriate lifecycle method http://stackoverflow.com/questions/30283569/array-subscription-in-aurelia
    }
  }

}
<commit_msg>remove old comment line<commit_after>import {inject, customElement, bindable} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {BindingEngine} from 'aurelia-binding';
import d3 from 'd3';
import GraphModel from './graph-model';
import {GraphLibD3} from './graphlib-d3';
import {GraphFinder} from './graph-finder';
import {GraphModifier} from './graph-modifier';
import {ActionManager} from './action-manager';
import {GraphPresentationService} from './graph-presentation-service';
import {GraphPresentationModel} from '../models/graph-presentation-model';

@customElement('graph')
@inject(Element, EventAggregator, GraphLibD3, GraphModel, GraphFinder, GraphModifier, ActionManager, BindingEngine,
  GraphPresentationService, GraphPresentationModel)
export class Graph {
  @bindable data;
  @bindable graphInteractionModel;

  constructor(element, pubSub, graphLibD3, graphModel, graphFinder, graphModifier, actionManager, bindingEngine,
      graphPresentationService, graphPresentationModel) {
    this.element = element;
    this.pubSub = pubSub;
    this.graphLibD3 = graphLibD3;
    this.graphModel = graphModel;
    this.graphFinder = graphFinder;
    this.graphModifier = graphModifier;
    this.actionManager = actionManager;
    this.bindingEngine = bindingEngine;
    this.graphPresentationService = graphPresentationService;
    this.graphPresentationModel = graphPresentationModel;

    this.windowSizeAdapter();

    this.displayNodes = [];
    this.displayEdges = [];

    this.pubSub.subscribe('search.node', nodeId => {
      this.addNodeAction([nodeId], true);
    });
  }

  /**
   * Update graph dimensions. Introduce the concept of using a service to update
   * the model (but lots of other places in app where viewModes update models directly)
   */
  windowSizeAdapter() {
    // TODO Also need to respond to window resize so that svg can resize.
    this.graphPresentationService.updateDimensions(
      window.innerWidth - 100,
      window.innerHeight - 112
    );
  }

  /**
   * A new global graph (i.e. project analysis) is available.
   * Create an empty display graph and initialize it with some
   * sample data from the global graph.
   */
  dataChanged(newVal) {
    let unusedTypes;

    if (newVal) {
      unusedTypes = this.graphFinder.findUnusedTypes(this.graphModel.globalGraphModel);
      this.graphModel.initRadii();
      this.displayGraph = this.graphModel.emptyGraph();
      this.addNodeAction([unusedTypes[0]], true);
    }
  }

  /**
   * Perform the add action, and register undo and redo handlers.
   */
  addNodeAction(nodeIds, withNeighbours) {
    this.addAndDisplay(nodeIds, withNeighbours);
    this.actionManager.addAction(this,
      this.removeAndDisplay, [nodeIds, withNeighbours],
      this.addAndDisplay, [nodeIds, withNeighbours]
    );
  }

  addAndDisplay(nodeIds, withNeighbours) {
    this.addToDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  removeAndDisplay(nodeIds, withNeighbours) {
    this.removeFromDisplayGraphModel(nodeIds, withNeighbours);
    this.updateForceLayout();
  }

  /**
   * Modify the graph display model to include the given node id's.
   * If withNeihbours is true, then also include each node's neighbours.
   */
  addToDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.addNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.addNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Modify the graph display model to remove the given node id's.
   * If withNeihbours is true, then also remove each node's neighbours.
   */
  removeFromDisplayGraphModel(nodeIds, withNeighbours) {
    nodeIds.forEach( nodeId => {
      if (withNeighbours) {
        this.graphModifier.removeNodeEnv(this.displayGraph, nodeId, 1, this.svgText);
      } else {
        this.graphModifier.removeNodeOnly(this.displayGraph, nodeId, this.svgText);
      }
    });
  }

  /**
   * Use D3 force layout to calculate node positions.
   * A high negative charge value avoids node overlap.
   */
  updateForceLayout() {
    this.d3Data = this.graphLibD3.mapToD3(this.displayGraph);
    let currentNode;
    let numNodes = this.d3Data.nodes.length;

    let force = d3.layout.force()
       .nodes(this.d3Data.nodes)
       .links(this.d3Data.links)
       .size([this.graphPresentationModel.width, this.graphPresentationModel.height])
       .gravity(0.4)
       .linkDistance(200)
       .charge(-4000)
       .on('tick', this.tick.bind(this));

    this.computeLayout(force, numNodes);
    this.updateDisplayData(this.d3Data);
  }

  /**
   * On each tick of D3's force layout calculation, adjust node positions by applying
   * collision detection and bounding box constraints. Both of these algorithms
   * need to know the node's radius. A heuristic is to estimate radius = 45.
   * A complete solution would know each node * actual radius, but this is very
   * difficult from timing perspective because its not known until after its
   * in the DOM, and calculated by NodeCustomElement in micro task queue.
   */
  tick() {
    const heuristicRadius = 45;
    this.applyCollisionDetection(heuristicRadius);
    this.applyBoundingBox(heuristicRadius);
  }

  /**
   * Collision detection algorithm from
   * https://bl.ocks.org/mbostock/3231298
   */
  applyCollisionDetection(heuristicRadius) {
    let q = d3.geom.quadtree(this.d3Data.nodes),
      i = 0,
      n = this.d3Data.nodes.length;

    while (++i < n) q.visit(this.collide(this.d3Data.nodes[i], heuristicRadius));
  }

  collide(node, r) {
    let nx1 = node.x - r,
      nx2 = node.x + r,
      ny1 = node.y - r,
      ny2 = node.y + r;
    return function(quad, x1, y1, x2, y2) {
      if (quad.point && (quad.point !== node)) {
        let x = node.x - quad.point.x,
          y = node.y - quad.point.y,
          l = Math.sqrt(x * x + y * y),
          r = node.radius + quad.point.radius;
        if (l < r) {
          l = (l - r) / l * 0.5;
          node.x -= x *= l;
          node.y -= y *= l;
          quad.point.x += x;
          quad.point.y += y;
        }
      }
      return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
    };
  }

  /**
  * Make nodes fit within bounding box represented by this.graphPresentationModel.
  * http://mbostock.github.io/d3/talk/20110921/bounding.html
  */
  applyBoundingBox(r) {
    this.d3Data.nodes.forEach( node => {
      let curX = node.x;
      node.x = Math.max(r, Math.min(this.graphPresentationModel.width - r, curX));
      let curY = node.y;
      node.y = Math.max(r, Math.min(this.graphPresentationModel.height - r, node.y));
    });
  }

  /**
   * Technique to minimize D3 chaotic bouncing in force layout.
   * http://stackoverflow.com/questions/13463053/calm-down-initial-tick-of-a-force-layout
   */
  computeLayout(force, numIterations) {
    let safety = 0;

    force.start();

    while(force.alpha() > 0.05) {
      force.tick();
      if(safety++ > 500) {
        break;
      }
    }

    force.stop();

    if(safety >= 500) {
      console.warn('Unable to stabilize force layout.');
    }
  }

  /**
   * Update list of display nodes and edges from d3 data.
   */
  updateDisplayData(d3Data) {
    // add nodes that are in d3Data, but not in display
    d3Data.nodes.forEach( node => {
      if (!this.graphLibD3.containsNode(this.displayNodes, node)) {
        this.displayNodes.push(node);
      }
    });

    // add links that are in d3Data, but not in display
    d3Data.links.forEach( link => {
      if (!this.graphLibD3.containsEdge(this.displayEdges, link)) {
        this.displayEdges.push(link);
      }
    });

    // remove nodes that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayNodes = this.displayNodes.length;
    for (let i=numDisplayNodes-1; i >= 0; i--) {
      if (!this.graphLibD3.containsNode(d3Data.nodes, this.displayNodes[i])) {
        this.displayNodes.splice(i, 1);
      }
    }

    // remove edges that are in display, but not in d3Data (iterate in reverse because splice re-indexes the array)
    let numDisplayEdges = this.displayEdges.length;
    for (let j=numDisplayEdges-1; j >=0; j--) {
      if (!this.graphLibD3.containsEdge(d3Data.links, this.displayEdges[j])) {
        this.displayEdges.splice(j, 1);
      }
    }

  }

  /**
   * Based on the currently selected nodes, use 'interaction' and 'type'
   * to find nodes from globalGraph that should be added, but only if
   * they're not already in the display graph.
   */
  findNodesToAdd(interaction, type) {
    let selectedNodeIds,
      relationship,
      nodesByEdgeRelationship;

    selectedNodeIds = this.graphFinder.findSelectedNodeIds(this.displayGraph);
    relationship = type === 'of it' ? 'target' : 'source';
    nodesByEdgeRelationship = this.graphFinder.findNodesByEdgeRelationship(
      this.graphModel.globalGraphModel, selectedNodeIds, interaction, relationship
    );

    return this.graphFinder.filterAlreadyInGraph(nodesByEdgeRelationship, this.displayGraph);
  }

  /**
   * Determine nodes that should be added, and add them as an undo-able action.
   */
  addNodesToDisplay(interaction, type) {
    let nodesToAdd = this.findNodesToAdd(interaction, type);

    if (nodesToAdd.length > 0) {
      this.addNodeAction(nodesToAdd);
    } else {
      // TODO notify https://github.com/CANVE/canve-viz/issues/32
      console.warn('No interaction results');
    }
  }

  /**
   * Invoked by Aurelia when 'graphInteractionModel' binding value has changed.
   * In practice this object it set once in the parent view. After that,
   * register property observers to fire when specific properties of the interaction model change,
   * indicating that the graph should respond to the change by adding more nodes.
   */
  graphInteractionModelChanged(newValue) {
    if (newValue) {
      this.graphInteractionModel = newValue;

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'callsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('uses', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'extensionsSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('extends', newValue);
      });

      this.bindingEngine.propertyObserver(this.graphInteractionModel, 'ownershipSelectedVal').subscribe((newValue, oldValue) => {
        this.addNodesToDisplay('declares member', newValue);
      });

      // TODO dispose in appropriate lifecycle method http://stackoverflow.com/questions/30283569/array-subscription-in-aurelia
    }
  }

}
<|endoftext|>


<commit_before>import EventClass from "event-class-es6";

import valueTypes from "./utils/types";
import Connection from "./connection";
import PointPolicy from "./policy";

import uuid from "./utils/uuid";

const _errno = Symbol("errno");
const _blocks = Symbol("blocks");
const _blockIds = Symbol("blockIds");
const _valueTypes = Symbol("valueTypes");
const _connections = Symbol("connections");

export default class Graph extends EventClass {

    constructor() {
        super();

        this[_errno] = null;
        this[_blocks] = [];
        this[_blockIds] = {};
        this[_valueTypes] = valueTypes;
        this[_connections] = [];
    }

    /**
     * Returns this graph fancy name
     * @returns {string}
     */
    get fancyName() { return "graph (" + this[_blocks].length + " blocks)"; }
    /**
     * Returns this graph blocks
     * @returns {Array<Block>}
     */
    get blocks() { return this[_blocks]; }
    /**
     * Returns this graph connections
     * @returns {Array<Connection>}
     */
    get connections() { return this[_connections]; }

    /**
     * Adds the specified block to this graph
     * @param {Block} block - the block to add
     */
    addBlock(block) {
        if (block.graph !== null) {
            throw new Error(block.fancyName + " cannot redefine graph");
        }
        if (block.id !== null && typeof this[_blockIds][block.id] !== "undefined") {
            throw new Error(this.fancyName + " cannot redefine id " + block.id);
        }
        if (block.id === null) {
            block.id = this.nextId();
        }
        block.graph = this;
        for (const templateId in block.templates) {
            // for(const [template, templateId] of Object.entries(block.templates))
            if (block.templates.hasOwnProperty(templateId)) {
                block.changeTemplate(templateId, block.templates[templateId].valueType, true);
            }
        }
        this.blocks.push(block);
        this[_blockIds][block.id] = block;
        this.emit("block-add", block);
        block.added();
    }
    /**
     * Removes the specified block from this graph
     * @param {Block} block - the block to remove
     */
    removeBlock(block) {
        if (block.graph !== this || !this.blocks.includes(block)) {
            throw new Error(this.fancyName + " has no block " + block.fancyName);
        }
        block.removePoints();
        block.graph = null;
        this.blocks.splice(this.blocks.indexOf(block), 1);
        this[_blockIds][block.id] = undefined;
        this.emit("block-remove", block);
        block.removed();
    }

    //noinspection JSMethodCanBeStatic
    /**
     * Returns the next unique block id
     * @returns {string}
     */
    nextId() {
        return uuid();
    }
    /**
     * Returns the block corresponding to the specified block id
     * @param {string} id - specifies the block id
     * @returns {Block|null}
     */
    blockById(id) {
        return this[_blockIds][id] || null;
    }
    /**
     * Returns the blocks corresponding to the specified block name
     * @param {string} name - specifies the block name
     * @returns {Array<Block>}
     */
    blocksByName(name) {
        return this.blocks.filter(block => block.name === name);
    }
    /**
     * Returns the blocks corresponding to the specified block type
     * @param {string} type - specifies the block type
     * @returns {Array<Block>}
     */
    blocksByType(type) {
        return this.blocks.filter(block => block.type === type || block instanceof type);
    }

    /**
     * Converts the specified value to the corresponding value type
     * @param {Graph.valueTypeTypedef} valueType - specifies the value type
     * @param {*|null} value - specifies the value
     * @returns {*|undefined} - returns undefined on failure
     */
    convertValue(valueType, value) {
        if (value === null) {
            return null;
        }
        const valueTypeInfo = this.valueTypeByName(valueType);
        if (valueTypeInfo === null) {
            throw new Error(this.fancyName + " has no valueType " + valueType);
        }
        if (typeof valueTypeInfo.typeConvert === "undefined") {
            throw new Error(this.fancyName + " has no valueType " + valueType + " converter");
        }
        return valueTypeInfo.typeConvert(value);
    }
    /**
     * Returns whether the connection is possible from the specified output point to the specified input point
     * @param {Point} outputPoint - specifies the output point
     * @param {Point} inputPoint - specifies the input point
     * @returns {boolean}
     */
    connectionPossible(outputPoint, inputPoint) {
        const inputValueType = this.valueTypeByName(inputPoint.valueType);

        if (inputValueType === null) {
            throw new Error(this.fancyName + " cannot find compatible type to convert connection from " +
                outputPoint.valueType + " to " + inputPoint.valueType);
        }

        if (!inputPoint.input) {
            this.errno(Error(inputPoint.fancyName + " is not an input"));
            return false;
        }
        if (!outputPoint.output) {
            this.errno(Error(outputPoint.fancyName + " is not an output"));
            return false;
        }

        if (inputPoint.value !== null) {
            this.errno(new Error(inputPoint.fancyName + " have a non-null value and cannot be connected"));
            return false;
        }
        if (outputPoint.value !== null) {
            this.errno(new Error(outputPoint.fancyName + " have a non-null value and cannot be connected"));
            return false;
        }

        if (inputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !inputPoint.emptyConnection()) {
            this.errno(new Error(inputPoint.fancyName + " cannot have multiple connections"));
            return false;
        }
        if (outputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !outputPoint.emptyConnection()) {
            this.errno(new Error(outputPoint.fancyName + " cannot have multiple connections"));
            return false;
        }

        if (!inputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !inputPoint.hasPolicy(PointPolicy.MULTIPLE_CONNECTIONS)) {
            this.errno(new Error(inputPoint.fancyName + " cannot have connections"));
            return false;
        }
        if (!outputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !outputPoint.hasPolicy(PointPolicy.MULTIPLE_CONNECTIONS)) {
            this.errno(new Error(outputPoint.fancyName + " cannot have connections"));
            return false;
        }

        if (inputPoint.valueType !== outputPoint.valueType) {
            if (!inputPoint.hasPolicy(PointPolicy.CONVERSION)) {
                this.errno(new Error(inputPoint.fancyName + " cannot be converted"));
                return false;
            }
            if (!outputPoint.hasPolicy(PointPolicy.CONVERSION)) {
                this.errno(new Error(outputPoint.fancyName + " cannot be converted"));
                return false;
            }

            if (!inputValueType.typeCompatibles.includes(outputPoint.valueType)) {
                this.errno(new Error(inputPoint.valueType + " is not compatible with " +
                    outputPoint.valueType));
                return false;
            }
        }

        let previousErrno = this[_errno];
        if (!inputPoint.acceptConnect(outputPoint)) {
            if (this[_errno] !== null && this[_errno] !== previousErrno) {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName + ": " + this[_errno].message));
            } else {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName));
            }
            return false;
        }
        previousErrno = this[_errno];
        if (!outputPoint.acceptConnect(inputPoint)) {
            if (this[_errno] !== null && this[_errno] !== previousErrno) {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName + ": " + this[_errno].message));
            } else {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName));
            }
            return false;
        }

        return true;
    }

    /**
     * Connects the specified points
     * @param {Point} inputPoint - specifies the input point
     * @param {Point} outputPoint - specifies the output point
     * @returns {Connection}
     */
    connect(inputPoint, outputPoint) {
        if (inputPoint.block === null) {
            throw new Error(inputPoint.fancyName + " cannot connect to another point when not bound to a block");
        }
        if (outputPoint.block === null) {
            throw new Error(outputPoint.fancyName + " cannot connect to another point when not bound to a block");
        }
        if (inputPoint === outputPoint) {
            throw new Error(this.fancyName + " cannot connect " + inputPoint.fancyName + " to itself");
        }
        if (!inputPoint.input) {
            throw new Error(outputPoint.fancyName + " is not an input");
        }
        if (!outputPoint.output) {
            throw new Error(outputPoint.fancyName + " is not an output");
        }
        if (!this.connectionPossible(outputPoint, inputPoint)) {
            if (outputPoint.template !== null || inputPoint.template !== null) {
                try {
                    outputPoint.block.changeTemplate(outputPoint.template, inputPoint.valueType);
                } catch (ex) {
                    if (inputPoint.template !== null) {
                        inputPoint.block.changeTemplate(inputPoint.template, outputPoint.valueType);
                    }
                }
            }
            if (!this.connectionPossible(outputPoint, inputPoint)) {
                throw new Error(this.fancyName + " cannot connect " +
                    outputPoint.fancyName + " to " + inputPoint.fancyName + ": " + this[_errno].message);
            }
        }
        const connectionFound = this.connectionForPoints(inputPoint, outputPoint);
        if (connectionFound !== null) {
            throw new Error(connectionFound.fancyName + " already exists");
        }
        const connection = new Connection(inputPoint, outputPoint);
        if (!inputPoint.block.acceptConnect(inputPoint, outputPoint)) {
            throw new Error(this[_errno]);
        }
        if (!outputPoint.block.acceptConnect(outputPoint, inputPoint)) {
            throw new Error(this[_errno]);
        }
        this._addConnection(connection);
        inputPoint.block.pointConnected(inputPoint, outputPoint);
        outputPoint.block.pointConnected(outputPoint, inputPoint);
        inputPoint.connected(outputPoint);
        outputPoint.connected(inputPoint);
        inputPoint.emit("connect", connection);
        outputPoint.emit("connect", connection);
        inputPoint.block.emit("point-connect", inputPoint, connection);
        outputPoint.block.emit("point-connect", outputPoint, connection);
        this.emit("point-connect", inputPoint, connection);
        this.emit("point-connect", outputPoint, connection);
        return connection;
    }
    /**
     * Disconnects the specified points
     * @param {Point} inputPoint - specifies the input point
     * @param {Point} outputPoint - specifies the output point
     * @returns {Connection}
     */
    disconnect(inputPoint, outputPoint) {
        if (inputPoint.block === null) {
            throw new Error(inputPoint.fancyName + " cannot disconnect from another point when not bound to a block");
        }
        if (outputPoint.block === null) {
            throw new Error(outputPoint.fancyName + " cannot disconnect from another point when not bound to a block");
        }
        const connectionFound = this.connectionForPoints(inputPoint, outputPoint);
        if (connectionFound === null) {
            throw new Error(this.fancyName + " cannot find a connection between " +
                inputPoint.fancyName + " and " + outputPoint.fancyName);
        }
        this._removeConnection(connectionFound);
        inputPoint.block.pointDisconnected(inputPoint, outputPoint);
        outputPoint.block.pointDisconnected(outputPoint, inputPoint);
        inputPoint.disconnected(outputPoint);
        outputPoint.disconnected(inputPoint);
        inputPoint.emit("disconnect", connectionFound);
        outputPoint.emit("disconnect", connectionFound);
        inputPoint.block.emit("point-disconnect", inputPoint, connectionFound);
        outputPoint.block.emit("point-disconnect", outputPoint, connectionFound);
        this.emit("point-disconnect", inputPoint, connectionFound);
        this.emit("point-disconnect", outputPoint, connectionFound);
        return connectionFound;
    }
    /**
     * Returns the connection between the specified output point and input point or null
     * @param {Point} inputPoint - specifies the input point
     * @param {Point} outputPoint - specifies the output point
     * @returns {Connection|null}
     */
    connectionForPoints(inputPoint, outputPoint) {
        return this.connections.find(connection => {
                return connection.inputPoint === inputPoint && connection.outputPoint === outputPoint;
            }) || null;
    }
    /**
     * Adds the specified connection
     * @param {Connection} connection - specifies the connection
     * @private
     */
    _addConnection (connection) {
        const inputPoint = connection.inputPoint;
        const outputPoint = connection.outputPoint;
        if (inputPoint.connections.includes(connection)) {
            throw new Error(inputPoint.fancyName + " cannot redefine " + connection.fancyName);
        }
        if (outputPoint.connections.includes(connection)) {
            throw new Error(outputPoint.fancyName + " cannot redefine " + connection.fancyName);
        }
        inputPoint.connections.push(connection);
        outputPoint.connections.push(connection);
        this.connections.push(connection);
    }
    /**
     * Removes the specified connection
     * @param {Connection} connection - specifies the connection
     * @private
     */
    _removeConnection (connection) {
        const inputPoint = connection.inputPoint;
        const outputPoint = connection.outputPoint;
        if (!inputPoint.connections.includes(connection)) {
            throw new Error(inputPoint.fancyName + " has no connection " + connection.fancyName);
        }
        if (!outputPoint.connections.includes(connection)) {
            throw new Error(outputPoint.fancyName + " has no connection " + connection.fancyName);
        }
        connection.inputPoint.connections.splice(connection.inputPoint.connections.indexOf(connection), 1);
        connection.outputPoint.connections.splice(connection.outputPoint.connections.indexOf(connection), 1);
        this.connections.splice(this.connections.indexOf(connection), 1);
    }

    /**
     * Adds the specified value type to this graph
     * @param {Graph.valueTypeInfoTypedef} valueTypeInfo - specifies the value type
     */
    addValueType(valueTypeInfo) {
        if (this.valueTypeByName(valueTypeInfo.typeName) !== null) {
            throw new Error(this.fancyName + " cannot redefine value type" + valueTypeInfo.typeName);
        }
        this[_valueTypes][valueTypeInfo.typeName] = valueTypeInfo;
    }
    /**
     * Returns the value type corresponding to the specified value type name
     * @param {Graph.valueTypeTypedef} typeName - specifies the value type name
     * @returns {Graph.valueTypeInfoTypedef|null}
     */
    valueTypeByName(typeName) {
        return this[_valueTypes][typeName] || null;
    }

    /**
     * Sets the last error
     * @param {Error} errno - specifies the last error
     */
    errno(errno) {
        this[_errno] = errno;
    }

}

/**
 * @typedef {Object} Graph.templateTypedef
 * @property {string} valueType
 * @property {Array<string>} templates
 */

/**
 * @typedef {string} Graph.valueTypeTypedef
 */

/**
 * @typedef {Object} Graph.valueTypeInfoTypedef
 * @property {string} typeName
 * @property {Graph.convertTypeTypedef} typeConvert
 * @property {Array<string>} [typeCompatibles=[]]
 */

/**
 * @typedef {function} Graph.convertTypeTypedef
 * @param {*|null} value
 * @returns {*|undefined}
 */
<commit_msg>Fixed bug in blocksByType where instanceof would fail with a string in the right-hand side<commit_after>import EventClass from "event-class-es6";

import valueTypes from "./utils/types";
import Connection from "./connection";
import PointPolicy from "./policy";

import uuid from "./utils/uuid";

const _errno = Symbol("errno");
const _blocks = Symbol("blocks");
const _blockIds = Symbol("blockIds");
const _valueTypes = Symbol("valueTypes");
const _connections = Symbol("connections");

export default class Graph extends EventClass {

    constructor() {
        super();

        this[_errno] = null;
        this[_blocks] = [];
        this[_blockIds] = {};
        this[_valueTypes] = valueTypes;
        this[_connections] = [];
    }

    /**
     * Returns this graph fancy name
     * @returns {string}
     */
    get fancyName() { return "graph (" + this[_blocks].length + " blocks)"; }
    /**
     * Returns this graph blocks
     * @returns {Array<Block>}
     */
    get blocks() { return this[_blocks]; }
    /**
     * Returns this graph connections
     * @returns {Array<Connection>}
     */
    get connections() { return this[_connections]; }

    /**
     * Adds the specified block to this graph
     * @param {Block} block - the block to add
     */
    addBlock(block) {
        if (block.graph !== null) {
            throw new Error(block.fancyName + " cannot redefine graph");
        }
        if (block.id !== null && typeof this[_blockIds][block.id] !== "undefined") {
            throw new Error(this.fancyName + " cannot redefine id " + block.id);
        }
        if (block.id === null) {
            block.id = this.nextId();
        }
        block.graph = this;
        for (const templateId in block.templates) {
            // for(const [template, templateId] of Object.entries(block.templates))
            if (block.templates.hasOwnProperty(templateId)) {
                block.changeTemplate(templateId, block.templates[templateId].valueType, true);
            }
        }
        this.blocks.push(block);
        this[_blockIds][block.id] = block;
        this.emit("block-add", block);
        block.added();
    }
    /**
     * Removes the specified block from this graph
     * @param {Block} block - the block to remove
     */
    removeBlock(block) {
        if (block.graph !== this || !this.blocks.includes(block)) {
            throw new Error(this.fancyName + " has no block " + block.fancyName);
        }
        block.removePoints();
        block.graph = null;
        this.blocks.splice(this.blocks.indexOf(block), 1);
        this[_blockIds][block.id] = undefined;
        this.emit("block-remove", block);
        block.removed();
    }

    //noinspection JSMethodCanBeStatic
    /**
     * Returns the next unique block id
     * @returns {string}
     */
    nextId() {
        return uuid();
    }
    /**
     * Returns the block corresponding to the specified block id
     * @param {string} id - specifies the block id
     * @returns {Block|null}
     */
    blockById(id) {
        return this[_blockIds][id] || null;
    }
    /**
     * Returns the blocks corresponding to the specified block name
     * @param {string} name - specifies the block name
     * @returns {Array<Block>}
     */
    blocksByName(name) {
        return this.blocks.filter(block => block.name === name);
    }
    /**
     * Returns the blocks corresponding to the specified block type
     * @param {string} type - specifies the block type
     * @returns {Array<Block>}
     */
    blocksByType(type) {
        return this.blocks.filter(block => block.type === type || (typeof type === "function" && block instanceof type));
    }

    /**
     * Converts the specified value to the corresponding value type
     * @param {Graph.valueTypeTypedef} valueType - specifies the value type
     * @param {*|null} value - specifies the value
     * @returns {*|undefined} - returns undefined on failure
     */
    convertValue(valueType, value) {
        if (value === null) {
            return null;
        }
        const valueTypeInfo = this.valueTypeByName(valueType);
        if (valueTypeInfo === null) {
            throw new Error(this.fancyName + " has no valueType " + valueType);
        }
        if (typeof valueTypeInfo.typeConvert === "undefined") {
            throw new Error(this.fancyName + " has no valueType " + valueType + " converter");
        }
        return valueTypeInfo.typeConvert(value);
    }
    /**
     * Returns whether the connection is possible from the specified output point to the specified input point
     * @param {Point} outputPoint - specifies the output point
     * @param {Point} inputPoint - specifies the input point
     * @returns {boolean}
     */
    connectionPossible(outputPoint, inputPoint) {
        const inputValueType = this.valueTypeByName(inputPoint.valueType);

        if (inputValueType === null) {
            throw new Error(this.fancyName + " cannot find compatible type to convert connection from " +
                outputPoint.valueType + " to " + inputPoint.valueType);
        }

        if (!inputPoint.input) {
            this.errno(Error(inputPoint.fancyName + " is not an input"));
            return false;
        }
        if (!outputPoint.output) {
            this.errno(Error(outputPoint.fancyName + " is not an output"));
            return false;
        }

        if (inputPoint.value !== null) {
            this.errno(new Error(inputPoint.fancyName + " have a non-null value and cannot be connected"));
            return false;
        }
        if (outputPoint.value !== null) {
            this.errno(new Error(outputPoint.fancyName + " have a non-null value and cannot be connected"));
            return false;
        }

        if (inputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !inputPoint.emptyConnection()) {
            this.errno(new Error(inputPoint.fancyName + " cannot have multiple connections"));
            return false;
        }
        if (outputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !outputPoint.emptyConnection()) {
            this.errno(new Error(outputPoint.fancyName + " cannot have multiple connections"));
            return false;
        }

        if (!inputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !inputPoint.hasPolicy(PointPolicy.MULTIPLE_CONNECTIONS)) {
            this.errno(new Error(inputPoint.fancyName + " cannot have connections"));
            return false;
        }
        if (!outputPoint.hasPolicy(PointPolicy.SINGLE_CONNECTION) && !outputPoint.hasPolicy(PointPolicy.MULTIPLE_CONNECTIONS)) {
            this.errno(new Error(outputPoint.fancyName + " cannot have connections"));
            return false;
        }

        if (inputPoint.valueType !== outputPoint.valueType) {
            if (!inputPoint.hasPolicy(PointPolicy.CONVERSION)) {
                this.errno(new Error(inputPoint.fancyName + " cannot be converted"));
                return false;
            }
            if (!outputPoint.hasPolicy(PointPolicy.CONVERSION)) {
                this.errno(new Error(outputPoint.fancyName + " cannot be converted"));
                return false;
            }

            if (!inputValueType.typeCompatibles.includes(outputPoint.valueType)) {
                this.errno(new Error(inputPoint.valueType + " is not compatible with " +
                    outputPoint.valueType));
                return false;
            }
        }

        let previousErrno = this[_errno];
        if (!inputPoint.acceptConnect(outputPoint)) {
            if (this[_errno] !== null && this[_errno] !== previousErrno) {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName + ": " + this[_errno].message));
            } else {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName));
            }
            return false;
        }
        previousErrno = this[_errno];
        if (!outputPoint.acceptConnect(inputPoint)) {
            if (this[_errno] !== null && this[_errno] !== previousErrno) {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName + ": " + this[_errno].message));
            } else {
                this.errno(new Error(outputPoint.fancyName +
                    " cannot accept to connect to " + inputPoint.fancyName));
            }
            return false;
        }

        return true;
    }

    /**
     * Connects the specified points
     * @param {Point} inputPoint - specifies the input point
     * @param {Point} outputPoint - specifies the output point
     * @returns {Connection}
     */
    connect(inputPoint, outputPoint) {
        if (inputPoint.block === null) {
            throw new Error(inputPoint.fancyName + " cannot connect to another point when not bound to a block");
        }
        if (outputPoint.block === null) {
            throw new Error(outputPoint.fancyName + " cannot connect to another point when not bound to a block");
        }
        if (inputPoint === outputPoint) {
            throw new Error(this.fancyName + " cannot connect " + inputPoint.fancyName + " to itself");
        }
        if (!inputPoint.input) {
            throw new Error(outputPoint.fancyName + " is not an input");
        }
        if (!outputPoint.output) {
            throw new Error(outputPoint.fancyName + " is not an output");
        }
        if (!this.connectionPossible(outputPoint, inputPoint)) {
            if (outputPoint.template !== null || inputPoint.template !== null) {
                try {
                    outputPoint.block.changeTemplate(outputPoint.template, inputPoint.valueType);
                } catch (ex) {
                    if (inputPoint.template !== null) {
                        inputPoint.block.changeTemplate(inputPoint.template, outputPoint.valueType);
                    }
                }
            }
            if (!this.connectionPossible(outputPoint, inputPoint)) {
                throw new Error(this.fancyName + " cannot connect " +
                    outputPoint.fancyName + " to " + inputPoint.fancyName + ": " + this[_errno].message);
            }
        }
        const connectionFound = this.connectionForPoints(inputPoint, outputPoint);
        if (connectionFound !== null) {
            throw new Error(connectionFound.fancyName + " already exists");
        }
        const connection = new Connection(inputPoint, outputPoint);
        if (!inputPoint.block.acceptConnect(inputPoint, outputPoint)) {
            throw new Error(this[_errno]);
        }
        if (!outputPoint.block.acceptConnect(outputPoint, inputPoint)) {
            throw new Error(this[_errno]);
        }
        this._addConnection(connection);
        inputPoint.block.pointConnected(inputPoint, outputPoint);
        outputPoint.block.pointConnected(outputPoint, inputPoint);
        inputPoint.connected(outputPoint);
        outputPoint.connected(inputPoint);
        inputPoint.emit("connect", connection);
        outputPoint.emit("connect", connection);
        inputPoint.block.emit("point-connect", inputPoint, connection);
        outputPoint.block.emit("point-connect", outputPoint, connection);
        this.emit("point-connect", inputPoint, connection);
        this.emit("point-connect", outputPoint, connection);
        return connection;
    }
    /**
     * Disconnects the specified points
     * @param {Point} inputPoint - specifies the input point
     * @param {Point} outputPoint - specifies the output point
     * @returns {Connection}
     */
    disconnect(inputPoint, outputPoint) {
        if (inputPoint.block === null) {
            throw new Error(inputPoint.fancyName + " cannot disconnect from another point when not bound to a block");
        }
        if (outputPoint.block === null) {
            throw new Error(outputPoint.fancyName + " cannot disconnect from another point when not bound to a block");
        }
        const connectionFound = this.connectionForPoints(inputPoint, outputPoint);
        if (connectionFound === null) {
            throw new Error(this.fancyName + " cannot find a connection between " +
                inputPoint.fancyName + " and " + outputPoint.fancyName);
        }
        this._removeConnection(connectionFound);
        inputPoint.block.pointDisconnected(inputPoint, outputPoint);
        outputPoint.block.pointDisconnected(outputPoint, inputPoint);
        inputPoint.disconnected(outputPoint);
        outputPoint.disconnected(inputPoint);
        inputPoint.emit("disconnect", connectionFound);
        outputPoint.emit("disconnect", connectionFound);
        inputPoint.block.emit("point-disconnect", inputPoint, connectionFound);
        outputPoint.block.emit("point-disconnect", outputPoint, connectionFound);
        this.emit("point-disconnect", inputPoint, connectionFound);
        this.emit("point-disconnect", outputPoint, connectionFound);
        return connectionFound;
    }
    /**
     * Returns the connection between the specified output point and input point or null
     * @param {Point} inputPoint - specifies the input point
     * @param {Point} outputPoint - specifies the output point
     * @returns {Connection|null}
     */
    connectionForPoints(inputPoint, outputPoint) {
        return this.connections.find(connection => {
                return connection.inputPoint === inputPoint && connection.outputPoint === outputPoint;
            }) || null;
    }
    /**
     * Adds the specified connection
     * @param {Connection} connection - specifies the connection
     * @private
     */
    _addConnection (connection) {
        const inputPoint = connection.inputPoint;
        const outputPoint = connection.outputPoint;
        if (inputPoint.connections.includes(connection)) {
            throw new Error(inputPoint.fancyName + " cannot redefine " + connection.fancyName);
        }
        if (outputPoint.connections.includes(connection)) {
            throw new Error(outputPoint.fancyName + " cannot redefine " + connection.fancyName);
        }
        inputPoint.connections.push(connection);
        outputPoint.connections.push(connection);
        this.connections.push(connection);
    }
    /**
     * Removes the specified connection
     * @param {Connection} connection - specifies the connection
     * @private
     */
    _removeConnection (connection) {
        const inputPoint = connection.inputPoint;
        const outputPoint = connection.outputPoint;
        if (!inputPoint.connections.includes(connection)) {
            throw new Error(inputPoint.fancyName + " has no connection " + connection.fancyName);
        }
        if (!outputPoint.connections.includes(connection)) {
            throw new Error(outputPoint.fancyName + " has no connection " + connection.fancyName);
        }
        connection.inputPoint.connections.splice(connection.inputPoint.connections.indexOf(connection), 1);
        connection.outputPoint.connections.splice(connection.outputPoint.connections.indexOf(connection), 1);
        this.connections.splice(this.connections.indexOf(connection), 1);
    }

    /**
     * Adds the specified value type to this graph
     * @param {Graph.valueTypeInfoTypedef} valueTypeInfo - specifies the value type
     */
    addValueType(valueTypeInfo) {
        if (this.valueTypeByName(valueTypeInfo.typeName) !== null) {
            throw new Error(this.fancyName + " cannot redefine value type" + valueTypeInfo.typeName);
        }
        this[_valueTypes][valueTypeInfo.typeName] = valueTypeInfo;
    }
    /**
     * Returns the value type corresponding to the specified value type name
     * @param {Graph.valueTypeTypedef} typeName - specifies the value type name
     * @returns {Graph.valueTypeInfoTypedef|null}
     */
    valueTypeByName(typeName) {
        return this[_valueTypes][typeName] || null;
    }

    /**
     * Sets the last error
     * @param {Error} errno - specifies the last error
     */
    errno(errno) {
        this[_errno] = errno;
    }

}

/**
 * @typedef {Object} Graph.templateTypedef
 * @property {string} valueType
 * @property {Array<string>} templates
 */

/**
 * @typedef {string} Graph.valueTypeTypedef
 */

/**
 * @typedef {Object} Graph.valueTypeInfoTypedef
 * @property {string} typeName
 * @property {Graph.convertTypeTypedef} typeConvert
 * @property {Array<string>} [typeCompatibles=[]]
 */

/**
 * @typedef {function} Graph.convertTypeTypedef
 * @param {*|null} value
 * @returns {*|undefined}
 */
<|endoftext|>


<commit_before>/**
 * UB-RIA-UI 1.0
 * Copyright 2013 Baidu Inc. All rights reserved.
 *
 * @ignore
 * @file 工具模块
 * @author curarchy
 */
define(
    function (require) {
        /**
         * 工具模块
         *
         * @class util
         * @singleton
         */
        var util = {};

        /**
         * 比较两个字符串
         * stringA 是否等于 stringB
         * 或 stringA 是否包含 stringB
         *
         * @param {string} stringA 第一个字符串
         * @param {string} stringB 第二个字符串
         * @param {Object} config 比较配置
         * @param {boolean} config.caseSensitive 大小写敏感
         * @param {boolean} config.isPartial 是否部分匹配
         * @return {boolean} 比较结果
         */
        util.compare = function (stringA, stringB, config) {
            config = config || {};
            if (!config.caseSensitive) {
                stringA = stringA.toLowerCase();
                stringB = stringB.toLowerCase();
            }

            if (config.isPartial) {
                return stringA.indexOf(stringB) !== -1;
            }
            return stringA === stringB;
        };

        return util;
    }
);
<commit_msg>添加比较时的特殊情况考虑<commit_after>/**
 * UB-RIA-UI 1.0
 * Copyright 2013 Baidu Inc. All rights reserved.
 *
 * @ignore
 * @file 工具模块
 * @author curarchy
 */
define(
    function (require) {
        /**
         * 工具模块
         *
         * @class util
         * @singleton
         */
        var util = {};

        /**
         * 比较两个字符串
         * stringA 是否等于 stringB
         * 或 stringA 是否包含 stringB
         *
         * @param {string} stringA 第一个字符串
         * @param {string} stringB 第二个字符串
         * @param {Object} config 比较配置
         * @param {boolean} config.caseSensitive 大小写敏感
         * @param {boolean} config.isPartial 是否部分匹配
         * @return {boolean} 比较结果
         */
        util.compare = function (stringA, stringB, config) {
            if (!u.isString(stringA) || !u.isString(stringB)) {
                return stringA === stringB;
            }

            config = config || {};
            if (!config.caseSensitive) {
                stringA = stringA.toLowerCase();
                stringB = stringB.toLowerCase();
            }

            if (config.isPartial) {
                return stringA.indexOf(stringB) !== -1;
            }
            return stringA === stringB;
        };

        return util;
    }
);
<|endoftext|>


<commit_before>var fs = require('fs');
var my = require('./my');
var parse = require('./parse');
var eeprom = require('./eeprom');
var util = require('util');
var winston = require('winston');

var debug = process.env.DEBUG ? true : false;
if(debug) {
    winston.remove(winston.transports.Console);
    winston.add(winston.transports.Console, {colorize: true});
}

var gpioFile = {};
var pwmPrefix = {};
var ainPrefix = "/sys/bus/iio/devices/iio:device0";
var SLOTS = "/sys/devices/platform/bone_capemgr/slots";
var AINdts = "BB-ADC";

exports.logfile = '/var/lib/cloud9/bonescript.log';

exports.readPWMFreqAndValue = function(pin, pwm) {
    var mode = {};
    try {
        var period = fs.readFileSync(pwmPrefix[pin.pwm.name]+'/period_ns');
        var duty = fs.readFileSync(pwmPrefix[pin.pwm.name]+'/duty_ns');
        mode.freq = 1.0e9 / period;
        mode.value = duty / period;
    } catch(ex) {
    }
    return(mode);
};

exports.readGPIODirection = function(n, gpio) {
    var mode = {};
    var directionFile = "/sys/class/gpio/gpio" + n + "/direction";
    if(my.file_existsSync(directionFile)) {
        mode.active = true;
        var direction = fs.readFileSync(directionFile, 'utf-8');
        direction = direction.replace(/^\s+|\s+$/g, '');
        mode.direction = direction;
    }
    return(mode);
};

exports.readPinMux = function(pin, mode, callback) {
    var pinctrlFile = '/sys/kernel/debug/pinctrl/44e10800.pinmux/pins';
    var muxRegOffset = parseInt(pin.muxRegOffset, 16);
    var readPinctrl = function(err, data) {
        if(err) {
            mode.err = 'readPinctrl error: ' + err;
            if(debug) winston.debug(mode.err);
            callback(mode);
        }
        mode = parse.modeFromPinctrl(data, muxRegOffset, 0x44e10800, mode);
        callback(mode);
    };
    var tryPinctrl = function(exists) {
        if(exists) {
            fs.readFile(pinctrlFile, 'utf8', readPinctrl);
        } else {
            if(debug) winston.debug('getPinMode(' + pin.key + '): no valid mux data');
            callback(mode);
        }
    };
    if(callback) {
        my.file_exists(pinctrlFile, tryPinctrl);
    } else {
        try {
            var data2 = fs.readFileSync(pinctrlFile, 'utf8');
            mode = parse.modeFromPinctrl(data2, muxRegOffset, 0x44e10800, mode);
        } catch(ex) {
            if(debug) winston.debug('getPinMode(' + pin.key + '): ' + ex);
        }
    }
    return(mode);
};

exports.setPinMode = function(pin, pinData, template, resp, callback) {
    if(debug) winston.debug('hw.setPinMode(' + [pin.key, pinData, template, JSON.stringify(resp)] + ');');
    var p = "ocp:" + pin.key + "_pinmux";
    if(pin.universalName) p = "ocp:" + pin.universalName + "_pinmux";
    var pinmux = my.find_sysfsFile(p, my.is_ocp(), p);
    if(!pinmux) { throw p + " was not found under " + my.is_ocp(); }
    if((pinData & 7) == 7) {
        gpioFile[pin.key] = '/sys/class/gpio/gpio' + pin.gpio + '/value';
        fs.writeFileSync(pinmux+"/state", 'gpio');
    } else if(template == 'bspwm') {
        fs.writeFileSync(pinmux+"/state", 'pwm');
        // Buld a path like: /sys/devices/platform/ocp/48304000.epwmss/48304200.ehrpwm/pwm/pwmchip5
        // pin.pwm.chip looks up the first address and pin.pwm.addr looks up the second
        // file_find figures which pwmchip to use
        // pin.pwm.index tells with half of the PWM to use (0 or 1)
        var pwmPath = my.file_find('/sys/devices/platform/ocp/'+pin.pwm.chip
                + '.epwmss/'+pin.pwm.addr+'.ehrpwm/pwm', 'pwmchip', 1);
        pwmPrefix[pin.pwm.name] = pwmPath + '/pwm' + pin.pwm.index;
        if(debug) winston.debug("pwmPrefix[pin.pwm.name] = " + pwmPrefix[pin.pwm.name]);
        if(debug) winston.debug("pin.pwm.sysfs = " + pin.pwm.sysfs);
        if(!my.file_existsSync(pwmPrefix[pin.pwm.name])) {
            fs.appendFileSync(pwmPath+'/export', pin.pwm.index);
        }
        fs.appendFileSync(pwmPrefix[pin.pwm.name]+'/enable', 1);
    } else {
        resp.err = 'Unknown pin mode template';
    }
    if(callback) callback(resp);
    return(resp);
};

exports.setLEDPinToGPIO = function(pin, resp) {
    var path = "/sys/class/leds/beaglebone:green:" + pin.led + "/trigger";

    if(my.file_existsSync(path)) {
        fs.writeFileSync(path, "gpio");
    } else {
        resp.err = "Unable to find LED " + pin.led;
        winston.error(resp.err);
        resp.value = false;
    }

    return(resp);
};

exports.exportGPIOControls = function(pin, direction, resp, callback) {
    if(debug) winston.debug('hw.exportGPIOControls(' + [pin.key, direction, resp] + ');');
    var n = pin.gpio;
    var exists = my.file_existsSync(gpioFile[pin.key]);

    if(!exists) {
        if(debug) winston.debug("exporting gpio: " + n);
        fs.writeFileSync("/sys/class/gpio/export", "" + n, null);
    }
    var directionFile = "/sys/class/gpio/gpio" + n + "/direction";
    if(debug) winston.debug('Writing GPIO direction(' + direction + ') to ' +
        directionFile + ');');
    fs.writeFileSync(directionFile, direction);
    return(resp);
};

exports.writeGPIOValue = function(pin, value, callback) {
    if(typeof gpioFile[pin.key] == 'undefined') {
        gpioFile[pin.key] = '/sys/class/gpio/gpio' + pin.gpio + '/value';
        if(pin.led) {
            gpioFile[pin.key] = "/sys/class/leds/beaglebone:";
            gpioFile[pin.key] += "green:" + pin.led + "/brightness";
        }
        if(!my.file_existsSync(gpioFile[pin.key])) {
            winston.error("Unable to find gpio: " + gpioFile[pin.key]);
        }
    }
    if(debug) winston.debug("gpioFile = " + gpioFile[pin.key]);
    if(callback) {
        fs.writeFile(gpioFile[pin.key], '' + value, null, callback);
    } else {
        try {
            fs.writeFileSync(gpioFile[pin.key], '' + value, null);
        } catch(ex) {
            winston.error("Unable to write to " + gpioFile[pin.key]);
        }
    }
};

exports.readGPIOValue = function(pin, resp, callback) {
    var gpioFile = '/sys/class/gpio/gpio' + pin.gpio + '/value';
    if(callback) {
        var readFile = function(err, data) {
            if(err) {
                resp.err = 'digitalRead error: ' + err;
                winston.error(resp.err);
            }
            resp.value = parseInt(data, 2);
            callback(resp);
        };
        fs.readFile(gpioFile, readFile);
        return(true);
    }
    resp.value = parseInt(fs.readFileSync(gpioFile), 2);
    return(resp);
};

exports.enableAIN = function(callback) {
    if(!my.file_existsSync(ainPrefix)) {
        if(debug) winston.debug('enableAIN: loading ' + AINdts);
        fs.writeFileSync(SLOTS, AINdts);    // Loads AINdts
    }
    if(!my.file_existsSync(ainPrefix)) {
        if(debug) winston.debug('enableAIN: load of ' + AINdts + ' failed');
    }
    if(callback) {
        callback({'path': ainPrefix});
    }
    return(ainPrefix);
};

exports.readAIN = function(pin, resp, callback) {
    var maxValue = 4095;
    var ainFile = ainPrefix + '/in_voltage' + pin.ain.toString() + '_raw';
    if(debug) winston.debug("readAIN: ainFile="+ainFile);
    if(callback) {
        var readFile = function(err, data) {
            if(err) {
                resp.err = 'analogRead error: ' + err;
                winston.error(resp.err);
            }
            resp.value = parseInt(data, 10) / maxValue;
            callback(resp);
        };
        fs.readFile(ainFile, readFile);
        return(resp);
    }
    resp.value = parseInt(fs.readFileSync(ainFile), 10);
    if(isNaN(resp.value)) {
        resp.err = 'analogRead(' + pin.key + ') returned ' + resp.value;
        winston.error(resp.err);
    }
    resp.value = resp.value / maxValue;
    if(isNaN(resp.value)) {
        resp.err = 'analogRead(' + pin.key + ') scaled to ' + resp.value;
        winston.error(resp.err);
    }
    return(resp);
};

exports.writeGPIOEdge = function(pin, mode) {
    fs.writeFileSync('/sys/class/gpio/gpio' + pin.gpio + '/edge', mode);

    var resp = {};
    resp.gpioFile = '/sys/class/gpio/gpio' + pin.gpio + '/value';
    resp.valuefd = fs.openSync(resp.gpioFile, 'r');
    resp.value = new Buffer(1);

    return(resp);
};

exports.writePWMFreqAndValue = function(pin, pwm, freq, value, resp, callback) {
    if(debug) winston.debug('hw.writePWMFreqAndValue(' + [pin.key,util.inspect(pwm),freq,value,resp] + ');');
    var path = pwmPrefix[pin.pwm.name];
    try {
        var period = Math.round( 1.0e9 / freq ); // period in ns
        if(debug) winston.debug('hw.writePWMFreqAndValue: pwm.freq='+pwm.freq
                        +', freq='+freq+', period='+period);
        if(pwm.freq != freq) {
            // if(debug) winston.debug('Stopping PWM');
            // fs.writeFileSync(path+'/enable', "0\n");
            // It appears that the first time you set the pwm you have to
            // set the period before you set the duty_cycle
            try {
                if(debug) winston.debug('Updating PWM period: ' + period);
                fs.writeFileSync(path+'/period', period + "\n");
            } catch(ex2) {
                period = fs.readFileSync(path+'/period');
                winston.info('Unable to update PWM period, period is set to '
                    + period
                    + "\tIs other half of PWM enabled?");
            }
            // if(debug) winston.debug('Starting PWM');
            // fs.writeFileSync(path+'/enable', "1\n");
        }
        var duty = Math.round( period * value );
        if(debug) winston.debug('Updating PWM duty: ' + duty);
        //if(duty == 0) winston.error('Updating PWM duty: ' + duty);
        fs.writeFileSync(path+'/duty_cycle', duty + "\n");
    } catch(ex) {
        resp.err = 'error updating PWM freq and value: ' + path + ', ' + ex;
        winston.error(resp.err);
    }
    return(resp);
};

exports.readEeproms = function(eeproms) {
    var EepromFiles = {
        '/sys/bus/i2c/devices/0-0050/at24-0/nvmem': { type: 'bone' },
        '/sys/bus/i2c/devices/2-0054/at24-1/nvmem': { type: 'cape' },
        '/sys/bus/i2c/devices/2-0055/at24-2/nvmem': { type: 'cape' },
        '/sys/bus/i2c/devices/2-0056/at24-3/nvmem': { type: 'cape' },
        '/sys/bus/i2c/devices/2-0057/at24-4/nvmem': { type: 'cape' }
    };
    eeproms = eeprom.readEeproms(EepromFiles);
    return(eeproms);
};

exports.readPlatform = function(platform) {
    var eeproms = eeprom.readEeproms({
        '/sys/bus/i2c/devices/0-0050/at24-0/nvmem': { type: 'bone' }
    });
    var x = eeproms['/sys/bus/i2c/devices/0-0050/at24-0/nvmem'];
    if(x.boardName == 'A335BONE') platform.name = 'BeagleBone';
    if(x.boardName == 'A335BNLT') platform.name = 'BeagleBone Black';
    platform.version = x.version;
    if(!platform.version.match(/^[\040-\176]*$/)) delete platform.version;
    platform.serialNumber = x.serialNumber;
    if(!platform.serialNumber.match(/^[\040-\176]*$/)) delete platform.serialNumber;
    try {
        platform.dogtag = fs.readFileSync('/etc/dogtag', 'ascii');
    } catch(ex) {
    }
    return(platform);
};
<commit_msg>Update hw_mainline.js<commit_after>var fs = require('fs');
var my = require('./my');
var parse = require('./parse');
var eeprom = require('./eeprom');
var util = require('util');
var winston = require('winston');

var debug = process.env.DEBUG ? true : false;
if(debug) {
    winston.remove(winston.transports.Console);
    winston.add(winston.transports.Console, {colorize: true});
}

var gpioFile = {};
var pwmPrefix = {};
var ainPrefix = "/sys/bus/iio/devices/iio:device0";
var SLOTS = "/sys/devices/platform/bone_capemgr/slots";
var AINdts = "BB-ADC";

exports.logfile = '/var/lib/cloud9/bonescript.log';

exports.readPWMFreqAndValue = function(pin, pwm) {
    var mode = {};
    try {
        var period = fs.readFileSync(pwmPrefix[pin.pwm.name]+'/period_ns');
        var duty = fs.readFileSync(pwmPrefix[pin.pwm.name]+'/duty_ns');
        mode.freq = 1.0e9 / period;
        mode.value = duty / period;
    } catch(ex) {
    }
    return(mode);
};

exports.readGPIODirection = function(n, gpio) {
    var mode = {};
    var directionFile = "/sys/class/gpio/gpio" + n + "/direction";
    if(my.file_existsSync(directionFile)) {
        mode.active = true;
        var direction = fs.readFileSync(directionFile, 'utf-8');
        direction = direction.replace(/^\s+|\s+$/g, '');
        mode.direction = direction;
    }
    return(mode);
};

exports.readPinMux = function(pin, mode, callback) {
    var pinctrlFile = '/sys/kernel/debug/pinctrl/44e10800.pinmux/pins';
    var muxRegOffset = parseInt(pin.muxRegOffset, 16);
    var readPinctrl = function(err, data) {
        if(err) {
            mode.err = 'readPinctrl error: ' + err;
            if(debug) winston.debug(mode.err);
            callback(mode);
        }
        mode = parse.modeFromPinctrl(data, muxRegOffset, 0x44e10800, mode);
        callback(mode);
    };
    var tryPinctrl = function(exists) {
        if(exists) {
            fs.readFile(pinctrlFile, 'utf8', readPinctrl);
        } else {
            if(debug) winston.debug('getPinMode(' + pin.key + '): no valid mux data');
            callback(mode);
        }
    };
    if(callback) {
        my.file_exists(pinctrlFile, tryPinctrl);
    } else {
        try {
            var data2 = fs.readFileSync(pinctrlFile, 'utf8');
            mode = parse.modeFromPinctrl(data2, muxRegOffset, 0x44e10800, mode);
        } catch(ex) {
            if(debug) winston.debug('getPinMode(' + pin.key + '): ' + ex);
        }
    }
    return(mode);
};

exports.setPinMode = function(pin, pinData, template, resp, callback) {
    if(debug) winston.debug('hw.setPinMode(' + [pin.key, pinData, template, JSON.stringify(resp)] + ');');
    var p = "ocp:" + pin.key + "_pinmux";
    if(pin.universalName) p = "ocp:" + pin.universalName + "_pinmux";
    var pinmux = my.find_sysfsFile(p, my.is_ocp(), p);
    if(!pinmux) { throw p + " was not found under " + my.is_ocp(); }
    if((pinData & 7) == 7) {
        gpioFile[pin.key] = '/sys/class/gpio/gpio' + pin.gpio + '/value';
        fs.writeFileSync(pinmux+"/state", 'gpio');
    } else if(template == 'bspwm') {
        fs.writeFileSync(pinmux+"/state", 'pwm');
        // Buld a path like: /sys/devices/platform/ocp/48304000.epwmss/48304200.ehrpwm/pwm/pwmchip5
        // pin.pwm.chip looks up the first address and pin.pwm.addr looks up the second
        // file_find figures which pwmchip to use
        // pin.pwm.index tells with half of the PWM to use (0 or 1)
        var pwmPath = my.file_find('/sys/devices/platform/ocp/'+pin.pwm.chip
                + '.epwmss/'+pin.pwm.addr+'.ehrpwm/pwm', 'pwmchip', 1);
        pwmPrefix[pin.pwm.name] = pwmPath + '/pwm' + pin.pwm.index;
        if(debug) winston.debug("pwmPrefix[pin.pwm.name] = " + pwmPrefix[pin.pwm.name]);
        if(debug) winston.debug("pin.pwm.sysfs = " + pin.pwm.sysfs);
        if(!my.file_existsSync(pwmPrefix[pin.pwm.name])) {
            fs.appendFileSync(pwmPath+'/export', pin.pwm.index);
        }
        fs.appendFileSync(pwmPrefix[pin.pwm.name]+'/enable', 1);
    } else {
        resp.err = 'Unknown pin mode template';
    }
    if(callback) callback(resp);
    return(resp);
};

exports.setLEDPinToGPIO = function(pin, resp) {
    var path = "/sys/class/leds/beaglebone:green:" + pin.led + "/trigger";

    if(my.file_existsSync(path)) {
        fs.writeFileSync(path, "gpio");
    } else {
        resp.err = "Unable to find LED " + pin.led;
        winston.error(resp.err);
        resp.value = false;
    }

    return(resp);
};

exports.exportGPIOControls = function(pin, direction, resp, callback) {
    if(debug) winston.debug('hw.exportGPIOControls(' + [pin.key, direction, resp] + ');');
    var n = pin.gpio;
    var exists = my.file_existsSync(gpioFile[pin.key]);

    if(!exists) {
        if(debug) winston.debug("exporting gpio: " + n);
        fs.writeFileSync("/sys/class/gpio/export", "" + n, null);
    }
    var directionFile = "/sys/class/gpio/gpio" + n + "/direction";
    if(debug) winston.debug('Writing GPIO direction(' + direction + ') to ' +
        directionFile + ');');
    fs.writeFileSync(directionFile, direction);
    return(resp);
};

exports.writeGPIOValue = function(pin, value, callback) {
    if(typeof gpioFile[pin.key] == 'undefined') {
        gpioFile[pin.key] = '/sys/class/gpio/gpio' + pin.gpio + '/value';
        if(pin.led) {
            gpioFile[pin.key] = "/sys/class/leds/beaglebone:";
            gpioFile[pin.key] += "green:" + pin.led + "/brightness";
        }
        if(!my.file_existsSync(gpioFile[pin.key])) {
            winston.error("Unable to find gpio: " + gpioFile[pin.key]);
        }
    }
    if(debug) winston.debug("gpioFile = " + gpioFile[pin.key]);
    if(callback) {
        fs.writeFile(gpioFile[pin.key], '' + value, null, callback);
    } else {
        try {
            fs.writeFileSync(gpioFile[pin.key], '' + value, null);
        } catch(ex) {
            winston.error("Unable to write to " + gpioFile[pin.key]);
        }
    }
};

exports.readGPIOValue = function(pin, resp, callback) {
    var gpioFile = '/sys/class/gpio/gpio' + pin.gpio + '/value';
    if(callback) {
        var readFile = function(err, data) {
            if(err) {
                resp.err = 'digitalRead error: ' + err;
                winston.error(resp.err);
            }
            resp.value = parseInt(data, 2);
            callback(resp);
        };
        fs.readFile(gpioFile, readFile);
        return(true);
    }
    resp.value = parseInt(fs.readFileSync(gpioFile), 2);
    return(resp);
};

exports.enableAIN = function(callback) {
    if(!my.file_existsSync(ainPrefix)) {
        if(debug) winston.debug('enableAIN: loading ' + AINdts);
        fs.writeFileSync(SLOTS, AINdts);    // Loads AINdts
    }
    if(!my.file_existsSync(ainPrefix)) {
        if(debug) winston.debug('enableAIN: load of ' + AINdts + ' failed');
    }
    if(callback) {
        callback({'path': ainPrefix});
    }
    return(ainPrefix);
};

exports.readAIN = function(pin, resp, callback) {
    var maxValue = 4095;
    var ainFile = ainPrefix + '/in_voltage' + pin.ain.toString() + '_raw';
    if(debug) winston.debug("readAIN: ainFile="+ainFile);
    if(callback) {
        var readFile = function(err, data) {
            if(err) {
                resp.err = 'analogRead error: ' + err;
                winston.error(resp.err);
            }
            resp.value = parseInt(data, 10) / maxValue;
            callback(resp);
        };
        fs.readFile(ainFile, readFile);
        return(resp);
    }
    resp.value = parseInt(fs.readFileSync(ainFile), 10);
    if(isNaN(resp.value)) {
        resp.err = 'analogRead(' + pin.key + ') returned ' + resp.value;
        winston.error(resp.err);
    }
    resp.value = resp.value / maxValue;
    if(isNaN(resp.value)) {
        resp.err = 'analogRead(' + pin.key + ') scaled to ' + resp.value;
        winston.error(resp.err);
    }
    return(resp);
};

exports.writeGPIOEdge = function(pin, mode) {
    fs.writeFileSync('/sys/class/gpio/gpio' + pin.gpio + '/edge', mode);

    var resp = {};
    resp.gpioFile = '/sys/class/gpio/gpio' + pin.gpio + '/value';
    resp.valuefd = fs.openSync(resp.gpioFile, 'r');
    resp.value = new Buffer(1);

    return(resp);
};

exports.writePWMFreqAndValue = function(pin, pwm, freq, value, resp, callback) {
    if(debug) winston.debug('hw.writePWMFreqAndValue(' + [pin.key,util.inspect(pwm),freq,value,resp] + ');');
    var path = pwmPrefix[pin.pwm.name];
    try {
        var period = Math.round( 1.0e9 / freq ); // period in ns
        if(debug) winston.debug('hw.writePWMFreqAndValue: pwm.freq='+pwm.freq
                        +', freq='+freq+', period='+period);
        if(pwm.freq != freq) {
            // if(debug) winston.debug('Stopping PWM');
            // fs.writeFileSync(path+'/enable', "0\n");
            // It appears that the first time you set the pwm you have to
            // set the period before you set the duty_cycle
            try {
                if(debug) winston.debug('Updating PWM period: ' + period);
                fs.writeFileSync(path+'/period', period + "\n");
            } catch(ex2) {
                period = fs.readFileSync(path+'/period');
                winston.info('Unable to update PWM period, period is set to '
                    + period
                    + "\tIs other half of PWM enabled?");
            }
            // if(debug) winston.debug('Starting PWM');
            // fs.writeFileSync(path+'/enable', "1\n");
        }
        var duty = Math.round( period * value );
        if(debug) winston.debug('Updating PWM duty: ' + duty);
        //if(duty == 0) winston.error('Updating PWM duty: ' + duty);
        fs.writeFileSync(path+'/duty_cycle', duty + "\n");
    } catch(ex) {
        resp.err = 'error updating PWM freq and value: ' + path + ', ' + ex;
        winston.error(resp.err);
    }
    return(resp);
};

exports.readEeproms = function(eeproms) {
    var EepromFiles = {
        '/sys/bus/i2c/devices/0-0050/at24-0/nvmem': { type: 'bone' },
        '/sys/bus/i2c/devices/2-0054/at24-1/nvmem': { type: 'cape' },
        '/sys/bus/i2c/devices/2-0055/at24-2/nvmem': { type: 'cape' },
        '/sys/bus/i2c/devices/2-0056/at24-3/nvmem': { type: 'cape' },
        '/sys/bus/i2c/devices/2-0057/at24-4/nvmem': { type: 'cape' }
    };
    eeproms = eeprom.readEeproms(EepromFiles);
    return(eeproms);
};

exports.readPlatform = function(platform) {
    var eeproms = eeprom.readEeproms({
        '/sys/bus/i2c/devices/0-0050/at24-0/nvmem': { type: 'bone' }
    });
    var x = eeproms['/sys/bus/i2c/devices/0-0050/at24-0/nvmem'];
    platform.name = fs.readFileSync('/proc/device-tree/name', 'ascii').trim();
    if(platform.name.indexOf('Green') > 0) {
        platform.name = platform.name.replace('TI AM335x', 'SeeedStudio')
    }
    if(platform.name.indexOf('Arduino' > 0) {
        platform.name = platform.name.replace('TI AM335x', '')
    }
    platform.name = platform.name.replace('TI AM335x BeagleBone', 'BeagleBoard.org BeagleBone');
    platform.name = platform.name.replace('TI AM5728 BeagleBoard-X15', 'BeagleBoard.org BeagleBoard-X15');
    platform.version = x.version;
    if(!platform.version.match(/^[\040-\176]*$/)) delete platform.version;
    platform.serialNumber = x.serialNumber;
    if(!platform.serialNumber.match(/^[\040-\176]*$/)) delete platform.serialNumber;
    try {
        platform.dogtag = fs.readFileSync('/etc/dogtag', 'ascii');
    } catch(ex) {
    }
    return(platform);
};
<|endoftext|>