Initial refactor for the topology renderer 47/16347/3
authorMaxime Millette-Coulombe <mmcoulombe@inocybe.com>
Wed, 11 Mar 2015 20:03:28 +0000 (16:03 -0400)
committerMaxime Millette-Coulombe <mmcoulombe@inocybe.com>
Fri, 20 Mar 2015 14:49:22 +0000 (10:49 -0400)
This commit contain a "home made" graph renderer. It's build with the
combination of NGraph as graph manager and Pixijs as WebGL (2D) renderer.
The goal is to replace Visjs to something more customizable and later on
add some 3D features. For now, the graph does the basic thing
(move nodes, pan and zoom).

Change-Id: Iee9ee2cb59ffb7a8ef812b0ee9def4ffd4164556
Signed-off-by: Maxime Millette-Coulombe <mmcoulombe@inocybe.com>
17 files changed:
dlux-web/Gruntfile.js
dlux-web/bower.json
dlux-web/build.config.js
dlux-web/package.json
dlux-web/pom.xml
features/src/main/resources/features.xml
modules/graph-resources/pom.xml [new file with mode: 0644]
modules/graph-resources/src/main/resources/graph/eventListener.js [new file with mode: 0644]
modules/graph-resources/src/main/resources/graph/index.js [new file with mode: 0644]
modules/graph-resources/src/main/resources/graph/renderer.js [new file with mode: 0644]
modules/loader-resources/src/main/resources/main.js
modules/loader-resources/src/main/resources/test-main.js
modules/pom.xml
modules/topology-resources/pom.xml
modules/topology-resources/src/main/resources/topology/topology.controller.js
modules/topology-resources/src/main/resources/topology/topology.tpl.html
pom.xml

index 48cbba5a223b3c8e4c2f0a676e9a781aec0b1f99..7d0ec23e916bcf5b285b032e37c9cd03ee6bc8b6 100644 (file)
@@ -4,11 +4,12 @@ var mountFolder = function (connect, dir) {
 };\r
 \r
 module.exports = function ( grunt ) {\r
-  \r
-  /** \r
+\r
+  /**\r
    * Load required Grunt tasks. These are installed based on the versions listed\r
    * in `package.json` when you do `npm install` in this directory.\r
    */\r
+  grunt.loadNpmTasks('grunt-browserify');\r
   grunt.loadNpmTasks('grunt-contrib-clean');\r
   grunt.loadNpmTasks('grunt-contrib-copy');\r
   grunt.loadNpmTasks('grunt-contrib-jshint');\r
@@ -73,7 +74,7 @@ module.exports = function ( grunt ) {
   }\r
 \r
   /**\r
-   * This is the configuration object Grunt uses to give each plugin its \r
+   * This is the configuration object Grunt uses to give each plugin its\r
    * instructions.\r
    */\r
   var taskConfig = {\r
@@ -84,12 +85,12 @@ module.exports = function ( grunt ) {
     pkg: grunt.file.readJSON("package.json"),\r
 \r
     /**\r
-     * The banner is the comment that is placed at the top of our compiled \r
+     * The banner is the comment that is placed at the top of our compiled\r
      * source files. It is first processed as a Grunt template, where the `<%=`\r
      * pairs are evaluated based on this very configuration object.\r
      */\r
     meta: {\r
-      banner: \r
+      banner:\r
         '/**\n' +\r
         ' * <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +\r
         ' * <%= pkg.homepage %>\n' +\r
@@ -115,13 +116,13 @@ module.exports = function ( grunt ) {
     bump: {\r
       options: {\r
         files: [\r
-          "package.json", \r
+          "package.json",\r
           "bower.json"\r
         ],\r
         commit: false,\r
         commitMessage: 'chore(release): v%VERSION%',\r
         commitFiles: [\r
-          "package.json", \r
+          "package.json",\r
           "client/bower.json"\r
         ],\r
         createTag: false,\r
@@ -130,13 +131,13 @@ module.exports = function ( grunt ) {
         push: false,\r
         pushTo: 'origin'\r
       }\r
-    },    \r
+    },\r
 \r
     /**\r
      * The directories to delete when `grunt clean` is executed.\r
      */\r
-    clean: [ \r
-      '<%= build_dir %>', \r
+    clean: [\r
+      '<%= build_dir %>',\r
       '<%= compile_dir %>'\r
     ],\r
 \r
@@ -148,24 +149,24 @@ module.exports = function ( grunt ) {
     copy: {\r
       build_app_assets: {\r
         files: [\r
-          { \r
+          {\r
             src: [ '**' ],\r
             dest: '<%= build_dir %>/assets/',\r
             cwd: 'src/assets',\r
             expand: true\r
           }\r
-       ]   \r
+       ]\r
       },\r
       build_vendor_assets: {\r
         files: [\r
-          { \r
+          {\r
             src: [ '<%= vendor_files.assets %>' ],\r
             dest: '<%= build_dir %>/assets/',\r
             cwd: '.',\r
             expand: true,\r
             flatten: true\r
           }\r
-       ]   \r
+       ]\r
       },\r
       build_appjs: {\r
         files: [\r
@@ -239,6 +240,17 @@ module.exports = function ( grunt ) {
         ]\r
       }\r
     },\r
+    browserify: {\r
+        dist: {\r
+            src: ['src/app/graph/index.js'],\r
+            dest: 'src/assets/js/graphRenderer.js',\r
+            options: {\r
+              browserifyOptions: {\r
+                standalone: 'DLUX'\r
+              }\r
+            }\r
+        }\r
+       },\r
 \r
     /**\r
      * `grunt concat` concatenates multiple source files into a single file.\r
@@ -263,13 +275,13 @@ module.exports = function ( grunt ) {
         options: {\r
           banner: '<%= meta.banner %>'\r
         },\r
-        src: [ \r
-          '<%= vendor_files.js %>', \r
-          'module.prefix', \r
-          '<%= build_dir %>/src/**/*.js', \r
-          '<%= html2js.common.dest %>', \r
-          '<%= html2js.app.dest %>', \r
-          'module.suffix' \r
+        src: [\r
+          '<%= vendor_files.js %>',\r
+          'module.prefix',\r
+          '<%= build_dir %>/src/**/*.js',\r
+          '<%= html2js.common.dest %>',\r
+          '<%= html2js.app.dest %>',\r
+          'module.suffix'\r
         ],\r
         dest: '<%= compile_dir %>/assets/<%= pkg.name %>-<%= pkg.version %>.js'\r
       }\r
@@ -308,7 +320,7 @@ module.exports = function ( grunt ) {
         }\r
       }\r
     },\r
-      \r
+\r
       /**\r
        * `less` less plugin handles the LESS compilation and minification automatically\r
        * this has been changed to the LESS plugin from recess plugin above because of\r
@@ -348,7 +360,7 @@ module.exports = function ( grunt ) {
      * nonetheless inside `src/`.\r
      */\r
     jshint: {\r
-      src: [ \r
+      src: [\r
         '<%= app_files.js %>'\r
       ],\r
       test: [\r
@@ -368,7 +380,7 @@ module.exports = function ( grunt ) {
       },\r
       globals: {}\r
     },\r
-   \r
+\r
 \r
     /**\r
      * HTML2JS is a Grunt plugin that takes all of your template files and\r
@@ -406,7 +418,7 @@ module.exports = function ( grunt ) {
     karma: {\r
       options: {\r
         configFile: '<%= build_dir %>/karma-unit.js'\r
-      },   \r
+      },\r
       unit: {\r
         runnerPort: 9102,\r
         background: true,\r
@@ -461,7 +473,7 @@ module.exports = function ( grunt ) {
     karmaconfig: {\r
       unit: {\r
         dir: '<%= build_dir %>',\r
-        src: [ \r
+        src: [\r
           '<%= vendor_files.js %>',\r
           '<%= html2js.app.dest %>',\r
           '<%= html2js.common.dest %>',\r
@@ -509,13 +521,13 @@ module.exports = function ( grunt ) {
     },\r
     /**\r
      * And for rapid development, we have a watch set up that checks to see if\r
-     * any of the files listed below change, and then to execute the listed \r
+     * any of the files listed below change, and then to execute the listed\r
      * tasks when they do. This just saves us from having to type "grunt" into\r
      * the command-line every time we want to see what we're working on; we can\r
      * instead just leave "grunt watch" running in a background terminal. Set it\r
      * and forget it, as Ron Popeil used to tell us.\r
      *\r
-     * But we don't need the same thing to happen for all the files. \r
+     * But we don't need the same thing to happen for all the files.\r
      */\r
     delta: {\r
       /**\r
@@ -545,7 +557,7 @@ module.exports = function ( grunt ) {
        * run our unit tests.\r
        */\r
       jssrc: {\r
-        files: [ \r
+        files: [\r
           '<%= app_files.js %>'\r
         ],\r
         tasks: [ 'jshint:src', 'karma:unit:run', 'copy:build_appjs' ]\r
@@ -556,7 +568,7 @@ module.exports = function ( grunt ) {
        * files, so this is probably not very useful.\r
        */\r
       assets: {\r
-        files: [ \r
+        files: [\r
           'src/assets/**/*'\r
         ],\r
         tasks: [ 'copy:build_app_assets' ]\r
@@ -574,8 +586,8 @@ module.exports = function ( grunt ) {
        * When our templates change, we only rewrite the template cache.\r
        */\r
       tpls: {\r
-        files: [ \r
-          '<%= app_files.atpl %>', \r
+        files: [\r
+          '<%= app_files.atpl %>',\r
           '<%= app_files.ctpl %>'\r
         ],\r
         tasks: [ 'html2js' ]\r
@@ -633,7 +645,7 @@ module.exports = function ( grunt ) {
    */\r
   grunt.registerTask( 'common', [\r
       'clean', 'html2js', 'jshint', 'less:development',\r
-      'concat:build_css', 'copy:build_app_assets', 'copy:build_vendor_assets',\r
+      'concat:build_css', 'browserify:dist', 'copy:build_app_assets', 'copy:build_vendor_assets',\r
       'copy:build_appjs', 'copy:copy_template', 'copy:build_vendorimages', 'copy:build_vendorjs', 'copy:build_vendorcss', 'karmaconfig', 'index:build'\r
   ]);\r
 \r
@@ -663,7 +675,7 @@ module.exports = function ( grunt ) {
     });\r
   }\r
 \r
-  /** \r
+  /**\r
    * The index.html template includes the stylesheet and javascript sources\r
    * based on dynamic names calculated in this Gruntfile. This task assembles\r
    * the list into variables for the template to use and then runs the\r
@@ -678,7 +690,7 @@ module.exports = function ( grunt ) {
       return file.replace( dirRE, '' );\r
     });\r
 \r
-    grunt.file.copy('src/index.html', this.data.dir + '/index.html', { \r
+    grunt.file.copy('src/index.html', this.data.dir + '/index.html', {\r
       process: function ( contents, path ) {\r
         return grunt.template.process( contents, {\r
           data: {\r
@@ -698,8 +710,8 @@ module.exports = function ( grunt ) {
    */\r
   grunt.registerMultiTask( 'karmaconfig', 'Process karma config templates', function () {\r
     var jsFiles = filterForJS( this.filesSrc );\r
-    \r
-    grunt.file.copy( 'karma/karma-unit.tpl.js', grunt.config( 'build_dir' ) + '/karma-unit.js', { \r
+\r
+    grunt.file.copy( 'karma/karma-unit.tpl.js', grunt.config( 'build_dir' ) + '/karma-unit.js', {\r
       process: function ( contents, path ) {\r
         return grunt.template.process( contents, {\r
           data: {\r
index 704a452f093bbd6612eba3a98ea046927b9c856f..6eed1f98d41babf3e6244ae42995c28b853f33bd 100644 (file)
@@ -1,44 +1,45 @@
-{\r
-  "name": "opendaylight-dlux",\r
-  "version": "0.1.0",\r
-  "dependencies": {\r
-    "angular": "1.2.16",\r
-    "json3": "~3.2.4",\r
-    "jquery": "~1.9.1",\r
-    "jquery-ui": "1.11.1",\r
-    "es5-shim": "~2.0.8",\r
-    "angular-resource": "1.2.16",\r
-    "angular-cookies": "1.2.16",\r
-    "angular-sanitize": "1.2.16",\r
-    "angular-ui-date": "latest",\r
-    "requirejs": "2.1.14",\r
-    "font-awesome": "~4.0.3",\r
-    "bootstrap": "~3.0.2",\r
-    "angular-ui-router": "~0.2.10",\r
-    "angular-ui-utils": "~0.0.3",\r
-    "d3": "~3.3.2",\r
-    "restangular": "1.2.1",\r
-    "angular-ui-select2": "https://raw.github.com/angular-ui/ui-select2/master/src/select2.js",\r
-    "underscore": "latest",\r
-    "underscore.string": "latest",\r
-    "select2": "master",\r
-    "select2-bootstrap-css": "https://github.com/fk/select2-bootstrap-css/archive/master.zip",\r
-    "footable": "2.0.1",\r
-    "angular-translate": "2.2.0",\r
-    "angular-translate-loader-static-files": "2.2.0",\r
-    "vis": "2.0.0",\r
-    "sigma": "https://github.com/jacomyal/sigma.js/releases/download/v1.0.3/release-v1.0.3.zip"\r
-  },\r
-  "devDependencies": {\r
-    "angular": "1.2.16",\r
-    "angular-mocks": "1.2.16",\r
-    "bootstrap": "~3.0.2",\r
-    "angular-bootstrap": "~0.7.0",\r
-    "ng-grid": "~2.0.7",\r
-    "angularAMD": "~0.1.1",\r
-    "requirejs-domready": "~2.0.1",\r
-    "ocLazyLoad": "0.3.0",\r
-    "angular-css-injector": "~1.0.3",\r
-    "grunt-shell": "~0.7.0"\r
-  }\r
-}\r
+{
+  "name": "opendaylight-dlux",
+  "version": "0.1.0",
+  "dependencies": {
+    "angular": "1.2.16",
+    "json3": "~3.2.4",
+    "jquery": "~1.9.1",
+    "jquery-ui": "1.11.1",
+    "es5-shim": "~2.0.8",
+    "angular-resource": "1.2.16",
+    "angular-cookies": "1.2.16",
+    "angular-sanitize": "1.2.16",
+    "angular-ui-date": "latest",
+    "requirejs": "2.1.14",
+    "font-awesome": "~4.0.3",
+    "bootstrap": "~3.0.2",
+    "angular-ui-router": "~0.2.10",
+    "angular-ui-utils": "~0.0.3",
+    "d3": "~3.3.2",
+    "restangular": "1.2.1",
+    "angular-ui-select2": "https://raw.github.com/angular-ui/ui-select2/master/src/select2.js",
+    "underscore": "latest",
+    "underscore.string": "latest",
+    "select2": "master",
+    "select2-bootstrap-css": "https://github.com/fk/select2-bootstrap-css/archive/master.zip",
+    "footable": "2.0.1",
+    "angular-translate": "2.2.0",
+    "angular-translate-loader-static-files": "2.2.0",
+    "vis": "2.0.0",
+    "sigma": "https://github.com/jacomyal/sigma.js/releases/download/v1.0.3/release-v1.0.3.zip",
+    "pixi": "~2.2.5"
+  },
+  "devDependencies": {
+    "angular": "1.2.16",
+    "angular-mocks": "1.2.16",
+    "bootstrap": "~3.0.2",
+    "angular-bootstrap": "~0.7.0",
+    "ng-grid": "~2.0.7",
+    "angularAMD": "~0.1.1",
+    "requirejs-domready": "~2.0.1",
+    "ocLazyLoad": "0.3.0",
+    "angular-css-injector": "~1.0.3",
+    "grunt-shell": "~0.7.0"
+  }
+}
index 60d9cd1a744326fcde5b097f35726c8da835be08..c9ab92384152bcf44d0454cdfc11b69b6cb9fa76 100644 (file)
@@ -30,6 +30,8 @@ module.exports = {
 \r
     html: [ 'src/index.html'],\r
     less: 'src/less/main.less',\r
+    graph_path: 'src/app/graph',\r
+    graph_entry_point: 'index.js',\r
     templates: ['src/**/*.tpl.html']\r
   },\r
 \r
@@ -94,7 +96,8 @@ module.exports = {
       'vendor/sigma/plugins/sigma.parsers.gexf.min.js',\r
       'vendor/sigma/plugins/sigma.layout.forceAtlas2.min.js',\r
       'vendor/sigma/plugins/sigma.plugins.dragNodes.min.js',\r
-      'vendor/sigma/plugins/sigma.renderers.customShapes.min.js'\r
+      'vendor/sigma/plugins/sigma.renderers.customShapes.min.js',\r
+      'vendor/pixi/bin/pixi.js'\r
     ],\r
     css: [\r
     'vendor/ng-grid/ng-grid.min.css',\r
index 704f3667d6ba07d2cc32840ca01ecc13d41e1076..2ea8cb5089863b81957899f7dac073d4cd8257d5 100644 (file)
@@ -17,6 +17,7 @@
     "bower": "~1.3.12",
     "grunt": "~0.4.5",
     "grunt-cli": "~0.1.13",
+    "grunt-browserify": "^3.4.0",
     "grunt-contrib-copy": "~0.4.1",
     "grunt-contrib-concat": "~0.3.0",
     "grunt-contrib-coffee": "~0.7.0",
     "karma-coverage": "~0.2.6",
     "karma-phantomjs-launcher": "~0.1.4",
     "angular-mocks": "~1.2.22",
-    "jasmine": "~2.0.1"
+    "jasmine": "~2.0.1",
+    "ngraph.graph": "~0.0.8",
+    "ngraph.forcelayout": "0.0.16",
+    "ngraph.fromjson": "^0.1.4",
+    "ngraph.merge": "0.0.1",
+    "ngraph.physics.simulator": "0.0.10",
+    "node-transform-matrix": "^1.0.2"
   },
   "engines": {
     "node": ">=0.8.0"
index a0d330201cb253c4b04a3a77b2d441af894e0a67..e65132c63390b8f6c242fae401aaa278033b2467 100644 (file)
@@ -59,7 +59,7 @@
         <artifactId>dlux.common.login.resources</artifactId>\r
         <version>${common.login.resources.version}</version>\r
     </dependency>\r
-       <dependency>\r
+    <dependency>\r
         <groupId>org.opendaylight.dlux</groupId>\r
         <artifactId>dlux.common.general.resources</artifactId>\r
         <version>${common.general.resources.version}</version>\r
         <artifactId>dlux.core.resources</artifactId>\r
         <version>${core.resources.version}</version>\r
       </dependency>\r
+      <dependency>\r
+        <groupId>org.opendaylight.dlux</groupId>\r
+        <artifactId>dlux.graph.resources</artifactId>\r
+        <version>${graph.resources.version}</version>\r
+      </dependency>\r
       <dependency>\r
         <groupId>org.opendaylight.dlux</groupId>\r
         <artifactId>dlux.common.topbar.resources</artifactId>\r
                       <include>app/topology/</include>\r
                       <include>app/connection_manager/</include>\r
                       <include>app/core/</include>\r
+                      <include>app/graph/</include>\r
                       <include>common/yangutils/</include>\r
                       <include>common/sigmatopology/</include>\r
                       <include>common/navigation/</include>\r
                   dlux.network.resources,\r
                   dlux.flow.resources,\r
                   dlux.core.resources,\r
+                  dlux.graph.resources,\r
                 </includeArtifactIds>\r
                 <excludes>META-INF\/**</excludes>\r
                 <excludeTransitive>true</excludeTransitive>\r
index 2d6dddb4e805ce7065e01c3fcf9a1229b650be1d..881e6e00917f8aec7aad37fc4e64ff7bd1dbbaff 100644 (file)
@@ -4,11 +4,11 @@
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.2.0 http://karaf.apache.org/xmlns/features/v1.2.0">
     <feature name='odl-dlux-all' version='${project.version}'>
-       <feature>odl-dlux-core</feature>
+        <feature>odl-dlux-core</feature>
         <feature>odl-dlux-minimal</feature>
         <feature>odl-dlux-node</feature>
         <feature>odl-dlux-yangui</feature>
-               <feature>odl-dlux-yangvisualizer</feature>
+        <feature>odl-dlux-yangvisualizer</feature>
     </feature>
 
 
@@ -41,7 +41,7 @@
         <bundle>mvn:org.opendaylight.dlux/dlux.yangui/${project.version}</bundle>
         <bundle>mvn:org.opendaylight.dlux/dlux.common.yangutils/${project.version}</bundle>
     </feature>
-       
+
        <feature name="odl-dlux-yangvisualizer">
         <feature>odl-dlux-core</feature>
         <bundle>mvn:org.opendaylight.dlux/dlux.yangvisualizer/${project.version}</bundle>
diff --git a/modules/graph-resources/pom.xml b/modules/graph-resources/pom.xml
new file mode 100644 (file)
index 0000000..8ac9be3
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (c) 2014 Inocybe Technologies, and others. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-insta
+nce" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.x
+sd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.opendaylight.dlux</groupId>
+    <artifactId>dlux-parent</artifactId>
+    <version>0.2.0-SNAPSHOT</version>
+    <relativePath>../../</relativePath>
+  </parent>
+
+  <artifactId>dlux.graph.resources</artifactId>
+  <name>${project.artifactId}</name>
+  <description>Graph Module Resources</description>
+  <version>${graph.resources.version}</version>
+  <packaging>jar</packaging>
+
+</project>
diff --git a/modules/graph-resources/src/main/resources/graph/eventListener.js b/modules/graph-resources/src/main/resources/graph/eventListener.js
new file mode 100644 (file)
index 0000000..3754fae
--- /dev/null
@@ -0,0 +1,129 @@
+// Math.sign is used a lot here and outdated brownser don't have it.
+if (typeof Math.sign !== 'function') {
+    Math.sign = function(value) {
+        return value ? value < 0 ? -1 : 1 : 0;
+    };
+}
+
+module.exports = function(stage, graph) {
+    // constant TODO: change to use settings
+    var MAX_SCALE = 2.0;
+    var MIN_SCALE = 0.1;
+    //------//
+
+    // pointer options
+    var POINTER_DEFAULT = 'auto';
+    var POINTER_DRAG = 'all-scroll';
+
+    var event = require('events');
+    var emitter = new event.EventEmitter();
+    var mouseDown = false;
+    var draggedNode = null;
+
+    var camera = stage.camera;
+    var oldPos = new PIXI.Point(0, 0);
+
+    // Start dragging the node
+    stage.interactionManager.onMouseDown = function(e) {
+        mouseDown = true;
+        oldPos.set(-1, -1);
+
+        draggedNode = graph.getNodeAt(e.layerX, e.layerY);
+
+        if (draggedNode) {
+            emitter.emit('nodeDown', draggedNode);
+        }
+    };
+
+    // If the node is dragged, update the position
+    // otherwise emit a callable event
+    stage.interactionManager.onMouseMove = function(e) {
+        if (draggedNode) { // drag node
+            setPointerStyle(POINTER_DRAG);
+            var realPos = camera.transform.inverse().transformPoint(e.layerX, e.layerY);
+            draggedNode.pos.x = realPos[0];
+            draggedNode.pos.y = realPos[1];
+        }
+        else if (mouseDown) { // do a pan
+            setPointerStyle(POINTER_DRAG);
+            if (oldPos.x == -1 && oldPos.y == -1) {
+                oldPos.set(e.layerX, e.layerY);
+            }
+
+            var pos = new PIXI.Point(0,0);
+            var deltaX = e.layerX - oldPos.x;
+            var deltaY = e.layerY - oldPos.y;
+
+            pos.set(
+                Math.sign(deltaX) * Math.abs(deltaX),
+                Math.sign(deltaY) * Math.abs(deltaY)
+            );
+
+            oldPos.set(e.layerX, e.layerY);
+            camera.transform.translate(pos.x, pos.y);
+        }
+        else {
+            var node = graph.getNodeAt(e.layerX, e.layerY);
+
+            if (node) {
+                emitter.emit('nodeOver', node);
+                return;
+            }
+
+            emitter.emit('mouseMove', e);
+        }
+    };
+
+    // Stop dragging the node
+    stage.interactionManager.onMouseUp = function(e) {
+        mouseDown = false;
+        setPointerStyle(POINTER_DEFAULT);
+
+        if (draggedNode) {
+            emitter.emit('nodeUp', draggedNode);
+            draggedNode = null;
+        }
+    };
+
+    // Release drag or pan if out of the canvas
+    stage.interactionManager.onMouseOut = function(e) {
+        mouseDown = false;
+        draggedNode = null;
+        setPointerStyle(POINTER_DEFAULT);
+    };
+
+    function setPointerStyle(style) {
+        if (document.body.style.cursor != style) {
+            document.body.style.cursor = style;
+        }
+    }
+
+    function zoom(e) {
+
+        // stop scrolling while zooming
+        e.preventDefault();
+
+        /* Each brownser on each platform
+         * set the wheel distance a different value. That's why the arbitrary value
+        */
+        var factor = 0.10;
+        var sign = Math.sign(e.detail || e.wheelDelta);
+
+        var currentScale = camera.scaleFactor;
+        var scale = currentScale + (sign * factor);
+
+        if (scale > MAX_SCALE) {
+            scale = MAX_SCALE;
+        }
+        else if (scale < MIN_SCALE) {
+            scale = MIN_SCALE;
+        }
+        camera.scaleFactor = scale;
+    }
+
+    // Mouse wheel to Zoom
+    document.getElementById(stage.containerId).addEventListener('mousewheel', zoom, false); // chrome, safari
+    document.getElementById(stage.containerId).addEventListener('DOMMouseScroll', zoom, false); // firefox
+
+    return emitter;
+};
diff --git a/modules/graph-resources/src/main/resources/graph/index.js b/modules/graph-resources/src/main/resources/graph/index.js
new file mode 100644 (file)
index 0000000..9ab0fce
--- /dev/null
@@ -0,0 +1,43 @@
+module.exports = function() {
+    var Graph = require('ngraph.graph');
+    var GraphRenderer = require('./renderer.js');
+    var topology = null;
+
+    // public API
+    return {
+        start : function(id) {
+            try {
+                if (!topology) {
+                    throw new Error('Cannot create a graph, you need to load one first');
+                }
+
+                renderer = new GraphRenderer(id, topology);
+                renderer.run();
+            } catch(e) {
+                console.error(e.name, e.message);
+            }
+        },
+        refresh: function() {
+            //TODO : Implement me (:
+        },
+        loadGraph: function(nodes, links) {
+            topology = new Graph();
+
+            nodes.forEach(function (node) {
+                topology.addNode(node.id, {
+                    title:node.title,
+                    group:node.group,
+                    label:node.label,
+                    value:node.value
+                });
+            });
+
+            links.forEach(function (link) {
+                topology.addLink(link.from, link.to, {
+                    id: link.id,
+                    title: link.title
+                });
+            });
+        }
+    };
+};
diff --git a/modules/graph-resources/src/main/resources/graph/renderer.js b/modules/graph-resources/src/main/resources/graph/renderer.js
new file mode 100644 (file)
index 0000000..6111309
--- /dev/null
@@ -0,0 +1,233 @@
+var defaultConfig = {
+    width: 800,
+    height: 600,
+    switch: {
+        src : '../assets/images/Device_switch_3062_unknown_64.png',
+        width: 64,
+        height: 64
+    },
+    host: {
+        src : '../assets/images/Device_pc_3045_default_64.png',
+        width: 64,
+        height: 64
+    }
+};
+module.exports = function(id, graph, config) {
+    var merge = require('ngraph.merge');
+    var Matrix = require('node-transform-matrix');
+    var container = validateContainer(id);
+
+    // merge current config with default
+    config = merge(config, defaultConfig);
+
+    var stage = new PIXI.Stage(0xFFFFFF, true);
+
+    // use to keep a reference of the node
+    // and not always search in the layout.
+    var nodes = {}, links = {};
+
+    var width = config.width;
+    var height = config.height;
+
+    var renderer = PIXI.autoDetectRenderer(width, height);
+    stage['containerId'] = id;
+    stage['camera'] = {
+        transform :new Matrix(),
+        scaleFactor: 1
+    };
+
+    container.appendChild(renderer.view);
+
+    var emitter = require('./eventListener.js')(stage, graph);
+    var switchTexture = new PIXI.Texture.fromImage(config.switch.src);
+    var hostTexture = new PIXI.Texture.fromImage(config.host.src);
+
+    // add a physic layout to place the node in the screen
+    var layoutCreator = require('ngraph.forcelayout'),
+        physics = require('ngraph.physics.simulator');
+
+    var layout = layoutCreator(graph, physics({
+        springLength: 100,
+        springCoeff: 0.0008,
+        dragCoeff: 0.01,
+        gravity: -1.2,
+        theta: 1
+    }));
+
+    // link renderer
+    var graphics = new PIXI.Graphics();
+
+    graph.forEachNode(initializeNode);
+    graph.forEachLink(initializeLink);
+
+    // loop into the graph to find a node with his position.
+    // certainly a better way to do it
+    graph.getNodeAt = function(x, y) {
+        var half = config.switch.width * 0.5 * stage.camera.scaleFactor;
+        for (var key in nodes) {
+            if (nodes.hasOwnProperty(key)) {
+                var node = nodes[key];
+                var pos = stage.camera.transform.transformPoint(node.pos.x, node.pos.y);
+                var inside = pos[0] - half < x && x < pos[0] + half &&
+                             pos[1] - half < y && y < pos[1] + half;
+                if (inside) {
+                    return nodes[key];
+                }
+            }
+        }
+    };
+
+    // public API
+    return {
+        run : function() {
+            // step the layout to make a good looking graph
+            // remove after cause it's heavy for nothing
+            for (var i = 0; i < 1000; ++i) {
+                layout.step();
+            }
+            layout.dispose();
+            centerGraph();
+            requestAnimFrame(loop);
+        }
+    };
+
+    // add the node sprite to the stage
+    // and add it to the ref array
+    function initializeNode(node) {
+        var nodeSprite = null;
+
+        if (node.data.group === 'switch') {
+            nodeSprite = new PIXI.Sprite(switchTexture);
+        } else if (node.data.group === 'host') {
+            nodeSprite = new PIXI.Sprite(hostTexture);
+        }
+
+        var textOffset = 10;
+        var text = new PIXI.Text(node.data.label, {
+            font : '11px Arial'
+        });
+
+        var centerWidth = width * 0.5;
+        var centerHeight = height * 0.5;
+
+        nodeSprite.anchor.set(0.5, 0.5);
+
+        layout.setNodePosition(node.id, Math.random() * centerWidth, Math.random() * centerHeight);
+        nodeSprite.position.set(centerWidth, centerHeight);
+
+        // text is a child of the sprite, we don't need to apply a transform
+        text.anchor.set(0.5, 0.5);
+
+        text.position.y = config.switch.width * 0.5 + textOffset;
+
+        nodes[node.id] = {
+            sprite: nodeSprite,
+            pos: layout.getNodePosition(node.id)
+        };
+
+        nodeSprite.addChild(text);
+        stage.addChild(nodeSprite);
+    }
+
+    // set the link into the ref array
+    function initializeLink(link) {
+        links[link.id] = {
+            from: layout.getNodePosition(link.fromId),
+            to : layout.getNodePosition(link.toId)
+        };
+        stage.addChild(graphics);
+    }
+
+    // update node position
+    function updateNode(node) {
+        var transPos = stage.camera.transform.transformPoint(node.pos.x, node.pos.y);
+        var scale = stage.camera.scaleFactor;
+        node.sprite.position.set(transPos[0], transPos[1]);
+        node.sprite.scale.set(scale, scale);
+    }
+
+    // draw the link
+    function updateLink(link) {
+        var transFrom = stage.camera.transform.transformPoint(link.from.x, link.from.y);
+        var transTo = stage.camera.transform.transformPoint(link.to.x, link.to.y);
+        graphics.lineStyle(1, 0x000000, 1);
+        graphics.moveTo(transFrom[0], transFrom[1]);
+        graphics.lineTo(transTo[0], transTo[1]);
+    }
+
+    // render loop
+    function loop() {
+        graphics.clear();
+        var key;
+
+        // update link
+        for (key in links) {
+            if (links.hasOwnProperty(key)) {
+                updateLink(links[key]);
+            }
+        }
+
+        // update positions
+        for (key in nodes) {
+            if (nodes.hasOwnProperty(key)) {
+                updateNode(nodes[key]);
+            }
+        }
+
+        // render everything
+        renderer.render(stage);
+
+        // loop
+        requestAnimFrame(loop);
+    }
+
+    // Move the graph near the center.
+    // TODO : use a better algorithm like finding the centroid and move based on that position
+    function centerGraph() {
+        var size = getGraphSize();
+        var moveX = Math.abs(config.width - size[0]) * 0.5;
+        var moveY = Math.abs(config.height - size[1]) * 0.5;
+        stage.camera.transform.translate(moveX, moveY);
+    }
+
+    // get the size of the graph by finding min and max pos
+    function getGraphSize() {
+        var minX = 10000,
+            minY = 10000,
+            maxX = -1,
+            maxY = -1;
+
+        for (var key in nodes) {
+            var node = nodes[key];
+            if (node.pos.x < minX) {
+                minX = node.pos.x;
+            } else if (node.pos.x > maxX) {
+                maxX = node.pos.x;
+            }
+            if (node.pos.y < minY) {
+                minY = node.pos.y;
+            } else if (node.pos.y > maxY) {
+                maxY = node.pos.y;
+            }
+        }
+
+        return [maxX - minX, maxY - minY];
+    }
+
+    // check if the given id exist
+    function validateContainer(id) {
+        var container = document.getElementById(id);
+        if (!container) {
+            throw new NotFoundException('No container found for the givent id ' + id);
+        }
+        return container;
+    }
+
+    // custom exception
+    // TODO : Move to another file
+    function NotFoundException(message) {
+        this.message = message;
+        this.name = 'NotFoundException';
+    }
+
+};
index e79ed9038eff71fdaee01eb8f5174a2566939185..b0da7cb5cd26cddcd9e44e3ef207d8b5e3361ef1 100644 (file)
@@ -16,14 +16,16 @@ require.config({
     'jquery' : '../vendor/jquery/jquery.min',
     'jquery-ui' : '../vendor/jquery-ui/jquery-ui.min',
     'footable' : '../vendor/footable/dist/footable.min',
+    'pixi': '../vendor/pixi/bin/pixi',
     'd3' : '../vendor/d3/d3.min',
-    'vis' : '../vendor/vis/dist/vis.min',
     'ocLazyLoad' : '../vendor/ocLazyLoad/dist/ocLazyLoad',
+    'vis' : '../vendor/vis/dist/vis.min',
     'sigma' : '../vendor/sigma/sigma.min',
     'sigma-parsers-gexf' : '../vendor/sigma/plugins/sigma.parsers.gexf.min',
     'sigma-forceAtlas2' : '../vendor/sigma/plugins/sigma.layout.forceAtlas2.min',
     'sigma-dragNodes' : '../vendor/sigma/plugins/sigma.plugins.dragNodes.min',
-    'sigma-customShapes' : '../vendor/sigma/plugins/sigma.renderers.customShapes.min'
+    'sigma-customShapes' : '../vendor/sigma/plugins/sigma.renderers.customShapes.min',
+    'graphRenderer' : '../assets/js/graphRenderer'
   },
 
   shim : {
@@ -37,6 +39,10 @@ require.config({
     'angular-translate': ['angular'],
     'angular-translate-loader-static-files' : ['angular-translate'],
     'ngload' : ['angularAMD'],
+    'pixi' : {
+        exports: 'PIXI'
+    },
+    'graphRenderer' : ['pixi'],
     'jquery' : {
       exports : '$'
     },
index 5ba82e6b91333d4be601fdd8e1b01e1f87801afa..34aeab74fb850b6db07828cfbcb1107a97384918 100644 (file)
@@ -36,9 +36,11 @@ var test = require.config({
     'angular-translate-loader-static-files' : '../vendor/angular-translate-loader-static-files/angular-translate-loader-static-files.min',
     'jquery' : '../vendor/jquery/jquery',
     'footable' : '../vendor/footable/dist/footable.min',
+    'pixi': '../vendor/pixi/bin/pixi',
     'd3' : '../vendor/d3/d3.min',
     'vis' : '../vendor/vis/dist/vis.min',
-    'ocLazyLoad' : '../vendor/ocLazyLoad/dist/ocLazyLoad' 
+    'ocLazyLoad' : '../vendor/ocLazyLoad/dist/ocLazyLoad',
+    'graphRenderer' : '../assets/js/graphRenderer'
   },
 
   shim : {
@@ -54,6 +56,10 @@ var test = require.config({
     'angular-translate': ['angular'],
     'angular-translate-loader-static-files' : ['angular-translate'],
     'ngload' : ['angularAMD'],
+    'pixi' : {
+        exports: 'PIXI'
+    },
+    'graphRenderer' : ['pixi'],
     'jquery' : {
       exports : '$'
     },
index 2c8b8b98fa2c6bfa9ac4738397074fa8a7d92b76..30c0828e70a730f83bb82f82fb30397369b4e58d 100644 (file)
@@ -41,8 +41,9 @@ sd">
     <module>topology-resources</module>
     <module>loader-resources</module>
     <module>connection_manager-resources</module>
+    <module>graph-resources</module>
     <module>core-resources</module>
     <module>common-topbar-resources</module>
   </modules>
-  
+
 </project>
index 61f80dd80d93b85ab1166edb63899645741e3ed9..82c1f640539d2b294de487a2100acb4be5ef74df 100644 (file)
@@ -23,4 +23,11 @@ sd">
   <version>${topology.resources.version}</version>
   <packaging>jar</packaging>
 
+  <dependencies>
+    <dependency>
+      <groupId>org.opendaylight.dlux</groupId>
+      <artifactId>dlux.graph.resources</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
 </project>
index 86f5a4d5b0fd5ce5066cc343d4580e7168141392..ed8aad78247fff5e6b19325cc19055f2237d3778 100644 (file)
@@ -1,15 +1,23 @@
-define(['app/topology/topology.module', 'app/topology/topology.directives','app/topology/topology.services'], function(topology) {
+define(['app/topology/topology.module','app/topology/topology.services', 'graphRenderer', 'pixi'], function(topology, service, GraphRenderer, PIXI) {
+  window.PIXI = PIXI; // attach pixijs to global scope
+
   topology.register.controller('TopologyCtrl', ['$scope', '$rootScope', 'NetworkTopologySvc' ,  function ($scope, $rootScope, NetworkTopologySvc) {
     $rootScope['section_logo'] = 'logo_topology';
+    var graphRenderer = null;
     $scope.createTopology = function() {
 
         NetworkTopologySvc.getNode("flow:1", function(data) {
-          var x = 50;
+          /*var x = 50;
           var y = 50;
           var step = 30;
           data.nodes.push({id: 1001, x: x, y: y + step, label: 'Switch', group: 'switch',value:20});
-          data.nodes.push({id: 1003, x: x, y: y + 3 * step, label: 'Host', group: 'host',value:20});
-          $scope.topologyData = data;
+          data.nodes.push({id: 1003, x: x, y: y + 3 * step, label: 'Host', group: 'host',value:20});*/
+
+          var inNodes = data.nodes;
+          var inEdges = data.links;
+          graphRenderer = new GraphRenderer();
+          graphRenderer.loadGraph(inNodes, inEdges);
+          graphRenderer.start('topology_simple');
       });
     };
 
index faa185cba251f1226c968b13210f44d80452bfdf..138acc2dc73dd38b326b6b0ea04f8bafa65bab9f 100644 (file)
@@ -12,6 +12,6 @@
       <h3>Controls</h3>
       <button class="btn btn-primary" ng-click="createTopology()">Reload</button>
     </div>
-    <topology-simple class="col-md-10 col-lg-offset-1" topology-data="topologyData"></topology-simple>
+    <div class="col-md-10 col-lg-offset-1" id="topology_simple"></div>
   </div>
 </div>
diff --git a/pom.xml b/pom.xml
index fb4bb4511c6e9a226d7c89f4a3a9e2698974e448..d6be22f7ca624c72259ec89b6c1f8585a018792f 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
       <commons.opendaylight.version>1.5.0-SNAPSHOT</commons.opendaylight.version>
       <loader.resources.version>0.1.0-SNAPSHOT</loader.resources.version>
       <core.resources.version>0.1.0-SNAPSHOT</core.resources.version>
+      <graph.resources.version>0.1.0-SNAPSHOT</graph.resources.version>
       <node.resources.version>0.1.0-SNAPSHOT</node.resources.version>
       <connection_manager.resources.version>0.1.0-SNAPSHOT</connection_manager.resources.version>
       <container.resources.version>0.1.0-SNAPSHOT</container.resources.version>