1/* 2 * Copyright (c) 2017, The OpenThread Authors. 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 3. Neither the name of the copyright holder nor the 13 * names of its contributors may be used to endorse or promote products 14 * derived from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 * POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29(function() { 30 angular 31 .module('StarterApp', ['ngMaterial', 'ngMessages']) 32 .controller('AppCtrl', AppCtrl) 33 .service('sharedProperties', function() { 34 var index = 0; 35 var networkInfo; 36 37 return { 38 getIndex: function() { 39 return index; 40 }, 41 setIndex: function(value) { 42 index = value; 43 }, 44 getNetworkInfo: function() { 45 return networkInfo; 46 }, 47 setNetworkInfo: function(value) { 48 networkInfo = value 49 }, 50 }; 51 }); 52 53 function AppCtrl($scope, $http, $mdDialog, $interval, sharedProperties) { 54 $scope.menu = [{ 55 title: 'Home', 56 icon: 'home', 57 show: true, 58 }, 59 { 60 title: 'Join', 61 icon: 'add_circle_outline', 62 show: false, 63 }, 64 { 65 title: 'Form', 66 icon: 'open_in_new', 67 show: false, 68 }, 69 { 70 title: 'Status', 71 icon: 'info_outline', 72 show: false, 73 }, 74 { 75 title: 'Settings', 76 icon: 'settings', 77 show: false, 78 }, 79 { 80 title: 'Commission', 81 icon: 'add_circle_outline', 82 show: false, 83 }, 84 { 85 title: 'Topology', 86 icon: 'add_circle_outline', 87 show: false, 88 }, 89 90 ]; 91 92 $scope.thread = { 93 networkName: 'OpenThreadDemo', 94 extPanId: '1111111122222222', 95 panId: '0x1234', 96 passphrase: 'j01Nme', 97 networkKey: '00112233445566778899aabbccddeeff', 98 channel: 15, 99 prefix: 'fd11:22::', 100 defaultRoute: true, 101 }; 102 103 $scope.setting = { 104 prefix: 'fd11:22::', 105 defaultRoute: true, 106 }; 107 108 $scope.headerTitle = 'Home'; 109 $scope.status = []; 110 111 $scope.isLoading = false; 112 113 $scope.showScanAlert = function(ev) { 114 $mdDialog.show( 115 $mdDialog.alert() 116 .parent(angular.element(document.querySelector('#popupContainer'))) 117 .clickOutsideToClose(true) 118 .title('Information') 119 .textContent('There is no available Thread network currently, please \ 120 wait a moment and retry it.') 121 .ariaLabel('Alert Dialog Demo') 122 .ok('Okay') 123 ); 124 }; 125 $scope.showPanels = function(index) { 126 $scope.headerTitle = $scope.menu[index].title; 127 for (var i = 0; i < 7; i++) { 128 $scope.menu[i].show = false; 129 } 130 $scope.menu[index].show = true; 131 if (index == 1) { 132 $scope.isLoading = true; 133 $http.get('available_network').then(function(response) { 134 $scope.isLoading = false; 135 if (response.data.error == 0) { 136 $scope.networksInfo = response.data.result; 137 } else { 138 $scope.showScanAlert(event); 139 } 140 }); 141 } 142 if (index == 3) { 143 $http.get('get_properties').then(function(response) { 144 console.log(response); 145 if (response.data.error == 0) { 146 var statusJson = response.data.result; 147 $scope.status = []; 148 for (var i = 0; i < Object.keys(statusJson).length; i++) { 149 $scope.status.push({ 150 name: Object.keys(statusJson)[i], 151 value: statusJson[Object.keys(statusJson)[i]], 152 icon: 'res/img/icon-info.png', 153 }); 154 } 155 } 156 }); 157 } 158 if (index == 6) { 159 $scope.dataInit(); 160 $scope.showTopology(); 161 } 162 }; 163 164 $scope.showJoinDialog = function(ev, index, item) { 165 sharedProperties.setIndex(index); 166 sharedProperties.setNetworkInfo(item); 167 $scope.index = index; 168 $mdDialog.show({ 169 controller: DialogController, 170 templateUrl: 'join.dialog.html', 171 parent: angular.element(document.body), 172 targetEvent: ev, 173 clickOutsideToClose: true, 174 fullscreen: $scope.customFullscreen, 175 }); 176 }; 177 178 function DialogController($scope, $mdDialog, $http, $interval, sharedProperties) { 179 var index = sharedProperties.getIndex(); 180 $scope.isDisplay = false; 181 $scope.thread = { 182 networkKey: '00112233445566778899aabbccddeeff', 183 prefix: 'fd11:22::', 184 defaultRoute: true, 185 }; 186 187 $scope.showAlert = function(ev, message) { 188 $mdDialog.show( 189 $mdDialog.alert() 190 .parent(angular.element(document.querySelector('#popupContainer'))) 191 .clickOutsideToClose(true) 192 .title('Information') 193 .textContent(message) 194 .ariaLabel('Alert Dialog Demo') 195 .ok('Okay') 196 .targetEvent(ev) 197 ); 198 }; 199 200 $scope.showQRAlert = function(ev, message) { 201 $mdDialog.show( 202 $mdDialog.alert() 203 .parent(angular.element(document.querySelector('#popupContainer'))) 204 .clickOutsideToClose(true) 205 .title('Information') 206 .textContent(message) 207 .ariaLabel('Alert Dialog Demo') 208 .ok('Okay') 209 .targetEvent(ev) 210 .multiple(true) 211 ); 212 }; 213 214 $scope.showQRCode = function(ev, image) { 215 $mdDialog.show({ 216 targetEvent: ev, 217 parent: angular.element(document.querySelector('#popupContainer')), 218 template: 219 '<md-dialog>' + 220 ' <md-dialog-content>' + 221 ' <h6 style="margin: 10px 10px 10px 10px; "><b>Open your OT Commissioner Android App to scan the Connect QR Code</b></h6>' + 222 ' <div layout="row">' + 223 ' <img ng-src="' + image + '" alt="qr code" style="display: block; margin-left: auto; margin-right: auto; width:40%"></img>' + 224 ' </div>' + 225 ' <md-dialog-actions>' + 226 ' <md-button ng-click="closeDialog()" class="md-primary">' + 227 ' Close' + 228 ' </md-button>' + 229 ' </md-dialog-actions>' + 230 '</md-dialog>', 231 controller: function DialogController($scope, $mdDialog) { 232 $scope.closeDialog = function() { 233 $mdDialog.hide(); 234 } 235 }, 236 multiple: true 237 }); 238 }; 239 240 $scope.join = function(valid) { 241 if (!valid) 242 { 243 return; 244 } 245 246 if ($scope.thread.defaultRoute == null) { 247 $scope.thread.defaultRoute = false; 248 }; 249 $scope.isDisplay = true; 250 var data = { 251 credentialType: $scope.thread.credentialType, 252 networkKey: $scope.thread.networkKey, 253 pskd: $scope.thread.pskd, 254 prefix: $scope.thread.prefix, 255 defaultRoute: $scope.thread.defaultRoute, 256 index: index, 257 }; 258 var httpRequest = $http({ 259 method: 'POST', 260 url: 'join_network', 261 data: data, 262 }); 263 264 httpRequest.then(function successCallback(response) { 265 $scope.res = response.data.result; 266 if (response.data.result == 'successful') { 267 $mdDialog.hide(); 268 } 269 $scope.isDisplay = false; 270 $scope.showAlert(event, "Join operation is " + response.data.result + ". " + response.data.message); 271 }); 272 }; 273 274 $scope.cancel = function() { 275 $mdDialog.cancel(); 276 }; 277 278 $scope.qrcode = function() { 279 $scope.isLoading = false; 280 $http.get('get_qrcode').then(function(response) { 281 console.log(response); 282 $scope.res = response.data.result; 283 if (response.data.result == 'successful') { 284 var image = "http://api.qrserver.com/v1/create-qr-code/?color=000000&bgcolor=FFFFFF&data=v%3D1%26%26eui%3D" + response.data.eui64 +"%26%26cc%3D" + $scope.thread.pskd +"&qzone=1&margin=0&size=400x400&ecc=L"; 285 $scope.showQRCode(event, image); 286 } else { 287 $scope.showQRAlert(event, "sorry, can not generate the QR code."); 288 } 289 $scope.isDisplay = true; 290 291 }); 292 }; 293 }; 294 295 296 $scope.showConfirm = function(ev, valid) { 297 if (!valid) 298 { 299 return; 300 } 301 302 var confirm = $mdDialog.confirm() 303 .title('Are you sure you want to Form the Thread Network?') 304 .textContent('') 305 .targetEvent(ev) 306 .ok('Okay') 307 .cancel('Cancel'); 308 309 $mdDialog.show(confirm).then(function() { 310 if ($scope.thread.defaultRoute == null) { 311 $scope.thread.defaultRoute = false; 312 }; 313 var data = { 314 networkKey: $scope.thread.networkKey, 315 prefix: $scope.thread.prefix, 316 defaultRoute: $scope.thread.defaultRoute, 317 extPanId: $scope.thread.extPanId, 318 panId: $scope.thread.panId, 319 passphrase: $scope.thread.passphrase, 320 channel: $scope.thread.channel, 321 networkName: $scope.thread.networkName, 322 }; 323 $scope.isForming = true; 324 var httpRequest = $http({ 325 method: 'POST', 326 url: 'form_network', 327 data: data, 328 }); 329 330 httpRequest.then(function successCallback(response) { 331 $scope.res = response.data.result; 332 if (response.data.result == 'successful') { 333 $mdDialog.hide(); 334 } 335 $scope.isForming = false; 336 $scope.showAlert(event, 'FORM', response.data.result); 337 }); 338 }, function() { 339 $mdDialog.cancel(); 340 }); 341 }; 342 343 $scope.showAlert = function(ev, operation, result) { 344 $mdDialog.show( 345 $mdDialog.alert() 346 .parent(angular.element(document.querySelector('#popupContainer'))) 347 .clickOutsideToClose(true) 348 .title('Information') 349 .textContent(operation + ' operation is ' + result) 350 .ariaLabel('Alert Dialog Demo') 351 .ok('Okay') 352 .targetEvent(ev) 353 ); 354 }; 355 356 $scope.showAddConfirm = function(ev) { 357 var confirm = $mdDialog.confirm() 358 .title('Are you sure you want to Add this On-Mesh Prefix?') 359 .textContent('') 360 .targetEvent(ev) 361 .ok('Okay') 362 .cancel('Cancel'); 363 364 $mdDialog.show(confirm).then(function() { 365 if ($scope.setting.defaultRoute == null) { 366 $scope.setting.defaultRoute = false; 367 }; 368 var data = { 369 prefix: $scope.setting.prefix, 370 defaultRoute: $scope.setting.defaultRoute, 371 }; 372 var httpRequest = $http({ 373 method: 'POST', 374 url: 'add_prefix', 375 data: data, 376 }); 377 378 httpRequest.then(function successCallback(response) { 379 $scope.showAlert(event, 'Add', response.data.result); 380 }); 381 }, function() { 382 $mdDialog.cancel(); 383 }); 384 }; 385 386 $scope.showDeleteConfirm = function(ev) { 387 var confirm = $mdDialog.confirm() 388 .title('Are you sure you want to Delete this On-Mesh Prefix?') 389 .textContent('') 390 .targetEvent(ev) 391 .ok('Okay') 392 .cancel('Cancel'); 393 394 $mdDialog.show(confirm).then(function() { 395 var data = { 396 prefix: $scope.setting.prefix, 397 }; 398 var httpRequest = $http({ 399 method: 'POST', 400 url: 'delete_prefix', 401 data: data, 402 }); 403 404 httpRequest.then(function successCallback(response) { 405 $scope.showAlert(event, 'Delete', response.data.result); 406 }); 407 }, function() { 408 $mdDialog.cancel(); 409 }); 410 }; 411 412 $scope.startCommission = function(ev) { 413 var data = { 414 pskd: $scope.commission.pskd, 415 passphrase: $scope.commission.passphrase, 416 }; 417 var httpRequest = $http({ 418 method: 'POST', 419 url: 'commission', 420 data: data, 421 }); 422 423 ev.target.disabled = true; 424 425 httpRequest.then(function successCallback(response) { 426 if (response.data.error == 0) { 427 $scope.showAlert(event, 'Commission', 'success'); 428 } else { 429 $scope.showAlert(event, 'Commission', 'failed'); 430 } 431 ev.target.disabled = false; 432 }); 433 }; 434 435 $scope.restServerPort = '8081'; 436 $scope.ipAddr = window.location.hostname + ':' + $scope.restServerPort; 437 438 // Basic information line 439 $scope.basicInfo = { 440 'NetworkName' : 'Unknown', 441 'LeaderData' :{'LeaderRouterId' : 'Unknown'} 442 } 443 // Num of router calculated by diagnostic 444 $scope.NumOfRouter = 'Unknown'; 445 446 // Diagnostic information for detailed display 447 $scope.nodeDetailInfo = 'Unknown'; 448 // For response of Diagnostic 449 $scope.networksDiagInfo = ''; 450 $scope.graphisReady = false; 451 $scope.detailList = { 452 'ExtAddress': { 'title': false, 'content': true }, 453 'Rloc16': { 'title': false, 'content': true }, 454 'Mode': { 'title': false, 'content': false }, 455 'Connectivity': { 'title': false, 'content': false }, 456 'Route': { 'title': false, 'content': false }, 457 'LeaderData': { 'title': false, 'content': false }, 458 'NetworkData': { 'title': false, 'content': true }, 459 'IP6Address List': { 'title': false, 'content': true }, 460 'MACCounters': { 'title': false, 'content': false }, 461 'ChildTable': { 'title': false, 'content': false }, 462 'ChannelPages': { 'title': false, 'content': false } 463 }; 464 $scope.graphInfo = { 465 'nodes': [], 466 'links': [] 467 } 468 469 $scope.dataInit = function() { 470 471 $http.get('http://' + $scope.ipAddr + '/node').then(function(response) { 472 473 $scope.basicInfo = response.data; 474 console.log(response.data); 475 $scope.basicInfo.Rloc16 = $scope.intToHexString($scope.basicInfo.Rloc16,4); 476 $scope.basicInfo.LeaderData.LeaderRouterId = '0x' + $scope.intToHexString($scope.basicInfo.LeaderData.LeaderRouterId,2); 477 }); 478 } 479 $scope.isObject = function(obj) { 480 return obj.constructor === Object; 481 } 482 $scope.isArray = function(arr) { 483 return !!arr && arr.constructor === Array; 484 } 485 486 $scope.clickList = function(key) { 487 $scope.detailList[key]['content'] = !$scope.detailList[key]['content'] 488 } 489 490 $scope.intToHexString = function(num, len){ 491 var value; 492 value = num.toString(16); 493 494 while( value.length < len ){ 495 value = '0' + value; 496 } 497 return value; 498 } 499 $scope.showTopology = function() { 500 var nodeMap = {} 501 var count, src, dist, rloc, child, rlocOfParent, rlocOfChild, diagOfNode, linkNode, childInfo; 502 503 $scope.graphisReady = false; 504 $scope.graphInfo = { 505 'nodes': [], 506 'links': [] 507 }; 508 $http.get('http://' + $scope.ipAddr + '/diagnostics').then(function(response) { 509 510 511 $scope.networksDiagInfo = response.data; 512 for (diagOfNode of $scope.networksDiagInfo){ 513 514 diagOfNode['RouteId'] = '0x' + $scope.intToHexString(diagOfNode['Rloc16'] >> 10,2); 515 516 diagOfNode['Rloc16'] = '0x' + $scope.intToHexString(diagOfNode['Rloc16'],4); 517 518 diagOfNode['LeaderData']['LeaderRouterId'] = '0x' + $scope.intToHexString(diagOfNode['LeaderData']['LeaderRouterId'],2); 519 for (linkNode of diagOfNode['Route']['RouteData']){ 520 linkNode['RouteId'] = '0x' + $scope.intToHexString(linkNode['RouteId'],2); 521 } 522 } 523 524 count = 0; 525 526 for (diagOfNode of $scope.networksDiagInfo) { 527 if ('ChildTable' in diagOfNode) { 528 529 rloc = parseInt(diagOfNode['Rloc16'],16).toString(16); 530 nodeMap[rloc] = count; 531 532 if ( diagOfNode['RouteId'] == diagOfNode['LeaderData']['LeaderRouterId']) { 533 diagOfNode['Role'] = 'Leader'; 534 } else { 535 diagOfNode['Role'] = 'Router'; 536 } 537 538 $scope.graphInfo.nodes.push(diagOfNode); 539 540 if (diagOfNode['Rloc16'] === $scope.basicInfo.rloc16) { 541 $scope.nodeDetailInfo = diagOfNode 542 } 543 count = count + 1; 544 } 545 } 546 // Num of Router is based on the diagnostic information 547 $scope.NumOfRouter = count; 548 549 // Index for a second loop 550 src = 0; 551 // Construct links 552 for (diagOfNode of $scope.networksDiagInfo) { 553 if ('ChildTable' in diagOfNode) { 554 // Link bewtwen routers 555 for (linkNode of diagOfNode['Route']['RouteData']) { 556 rloc = ( parseInt(linkNode['RouteId'],16) << 10).toString(16); 557 if (rloc in nodeMap) { 558 dist = nodeMap[rloc]; 559 if (src < dist) { 560 $scope.graphInfo.links.push({ 561 'source': src, 562 'target': dist, 563 'weight': 1, 564 'type': 0, 565 'linkInfo': { 566 'inQuality': linkNode['LinkQualityIn'], 567 'outQuality': linkNode['LinkQualityOut'] 568 } 569 }); 570 } 571 } 572 } 573 574 // Link between router and child 575 for (childInfo of diagOfNode['ChildTable']) { 576 child = {}; 577 rlocOfParent = parseInt(diagOfNode['Rloc16'],16).toString(16); 578 rlocOfChild = (parseInt(diagOfNode['Rloc16'],16) + childInfo['ChildId']).toString(16); 579 580 src = nodeMap[rlocOfParent]; 581 582 child['Rloc16'] = '0x' + rlocOfChild; 583 child['RouteId'] = diagOfNode['RouteId']; 584 nodeMap[rlocOfChild] = count; 585 child['Role'] = 'Child'; 586 $scope.graphInfo.nodes.push(child); 587 $scope.graphInfo.links.push({ 588 'source': src, 589 'target': count, 590 'weight': 1, 591 'type': 1, 592 'linkInfo': { 593 'Timeout': childInfo['Timeout'], 594 'Mode': childInfo['Mode'] 595 } 596 597 }); 598 599 count = count + 1; 600 } 601 } 602 src = src + 1; 603 } 604 605 $scope.drawGraph(); 606 }) 607 } 608 609 610 $scope.updateDetailLabel = function() { 611 for (var detailInfoKey in $scope.detailList) { 612 $scope.detailList[detailInfoKey]['title'] = false; 613 } 614 for (var diagInfoKey in $scope.nodeDetailInfo) { 615 if (diagInfoKey in $scope.detailList) { 616 $scope.detailList[diagInfoKey]['title'] = true; 617 } 618 619 } 620 } 621 622 623 $scope.drawGraph = function() { 624 var json, svg, tooltip, force; 625 var scale, len; 626 627 document.getElementById('topograph').innerHTML = ''; 628 scale = $scope.graphInfo.nodes.length; 629 len = 125 * Math.sqrt(scale); 630 631 // Topology graph 632 svg = d3.select('.d3graph').append('svg') 633 .attr('preserveAspectRatio', 'xMidYMid meet') 634 .attr('viewBox', '0, 0, ' + len.toString(10) + ', ' + (len / (3 / 2)).toString(10)); 635 636 // Legend 637 svg.append('circle') 638 .attr('cx',len-20) 639 .attr('cy',10).attr('r', 3) 640 .style('fill', "#7e77f8") 641 .style('stroke', '#484e46') 642 .style('stroke-width', '0.4px'); 643 644 svg.append('circle') 645 .attr("cx",len-20) 646 .attr('cy',20) 647 .attr('r', 3) 648 .style('fill', '#03e2dd') 649 .style('stroke', '#484e46') 650 .style('stroke-width', '0.4px'); 651 652 svg.append('circle') 653 .attr('cx',len-20) 654 .attr('cy',30) 655 .attr('r', 3) 656 .style('fill', '#aad4b0') 657 .style('stroke', '#484e46') 658 .style('stroke-width', '0.4px') 659 .style('stroke-dasharray','2 1'); 660 661 svg.append('circle') 662 .attr('cx',len-50) 663 .attr('cy',10).attr('r', 3) 664 .style('fill', '#ffffff') 665 .style('stroke', '#f39191') 666 .style('stroke-width', '0.4px'); 667 668 svg.append('text') 669 .attr('x', len-15) 670 .attr('y', 10) 671 .text('Leader') 672 .style('font-size', '4px') 673 .attr('alignment-baseline','middle'); 674 675 svg.append('text') 676 .attr('x', len-15) 677 .attr('y',20 ) 678 .text('Router') 679 .style('font-size', '4px') 680 .attr('alignment-baseline','middle'); 681 682 svg.append('text') 683 .attr('x', len-15) 684 .attr('y',30 ) 685 .text('Child') 686 .style('font-size', '4px') 687 .attr('alignment-baseline','middle'); 688 689 svg.append('text') 690 .attr('x', len-45) 691 .attr('y',10 ) 692 .text('Selected') 693 .style('font-size', '4px') 694 .attr('alignment-baseline','middle'); 695 696 // Tooltip style for each node 697 tooltip = d3.select('body') 698 .append('div') 699 .attr('class', 'tooltip') 700 .style('position', 'absolute') 701 .style('z-index', '10') 702 .style('visibility', 'hidden') 703 .text('a simple tooltip'); 704 705 force = d3.layout.force() 706 .distance(40) 707 .size([len, len / (3 / 2)]); 708 709 710 json = $scope.graphInfo; 711 712 force 713 .nodes(json.nodes) 714 .links(json.links) 715 .start(); 716 717 718 var link = svg.selectAll('.link') 719 .data(json.links) 720 .enter().append('line') 721 .attr('class', 'link') 722 .style('stroke', '#908484') 723 // Dash line for link between child and parent 724 .style('stroke-dasharray', function(item) { 725 if ('Timeout' in item.linkInfo) return '4 4'; 726 else return '0 0' 727 }) 728 // Line width representing link quality 729 .style('stroke-width', function(item) { 730 if ('inQuality' in item.linkInfo) 731 return Math.sqrt(item.linkInfo.inQuality/2); 732 else return Math.sqrt(0.5) 733 }) 734 // Effect of mouseover on a line 735 .on('mouseover', function(item) { 736 return tooltip.style('visibility', 'visible') 737 .text(item.linkInfo); 738 }) 739 .on('mousemove', function() { 740 return tooltip.style('top', (d3.event.pageY - 10) + 'px') 741 .style('left', (d3.event.pageX + 10) + 'px'); 742 }) 743 .on('mouseout', function() { 744 return tooltip.style('visibility', 'hidden'); 745 }); 746 747 748 var node = svg.selectAll('.node') 749 .data(json.nodes) 750 .enter().append('g') 751 .attr('class', function(item) { 752 return item.Role; 753 }) 754 .call(force.drag) 755 // Tooltip effect of mouseover on a node 756 .on('mouseover', function(item) { 757 return tooltip.style('visibility', 'visible') 758 .text(item.Rloc16 ); 759 }) 760 .on('mousemove', function() { 761 return tooltip.style('top', (d3.event.pageY - 10) + 'px') 762 .style('left', (d3.event.pageX + 10) + 'px'); 763 }) 764 .on('mouseout', function() { 765 return tooltip.style('visibility', 'hidden'); 766 }); 767 768 d3.selectAll('.Child') 769 .append('circle') 770 .attr('r', '6') 771 .attr('fill', '#aad4b0') 772 .style('stroke', '#484e46') 773 .style('stroke-dasharray','2 1') 774 .style('stroke-width', '0.5px') 775 .attr('class', function(item) { 776 return item.Rloc16; 777 }) 778 .on('mouseover', function(item) { 779 return tooltip.style('visibility', 'visible') 780 .text(item.Rloc16 ); 781 }) 782 .on('mousemove', function() { 783 return tooltip.style('top', (d3.event.pageY - 10) + 'px') 784 .style('left', (d3.event.pageX + 10) + 'px'); 785 }) 786 .on('mouseout', function() { 787 return tooltip.style('visibility', 'hidden'); 788 }); 789 790 791 d3.selectAll('.Leader') 792 .append('circle') 793 .attr('r', '8') 794 .attr('fill', '#7e77f8') 795 .style('stroke', '#484e46') 796 .style('stroke-width', '1px') 797 .attr('class', function(item) { 798 return 'Stroke'; 799 }) 800 // Effect that node will become bigger when mouseover 801 .on('mouseover', function(item) { 802 d3.select(this) 803 .transition() 804 .attr('r','9'); 805 return tooltip.style('visibility', 'visible') 806 .text(item.Rloc16); 807 }) 808 .on('mousemove', function() { 809 return tooltip.style('top', (d3.event.pageY - 10) + 'px') 810 .style('left', (d3.event.pageX + 10) + 'px'); 811 }) 812 .on('mouseout', function() { 813 d3.select(this).transition().attr('r','8'); 814 return tooltip.style('visibility', 'hidden'); 815 }) 816 // Effect that node will have a yellow edge when clicked 817 .on('click', function(item) { 818 d3.selectAll('.Stroke') 819 .style('stroke', '#484e46') 820 .style('stroke-width', '1px'); 821 d3.select(this) 822 .style('stroke', '#f39191') 823 .style('stroke-width', '1px'); 824 $scope.$apply(function() { 825 $scope.nodeDetailInfo = item; 826 $scope.updateDetailLabel(); 827 }); 828 }); 829 d3.selectAll('.Router') 830 .append('circle') 831 .attr('r', '8') 832 .style('stroke', '#484e46') 833 .style('stroke-width', '1px') 834 .attr('fill', '#03e2dd') 835 .attr('class','Stroke') 836 .on('mouseover', function(item) { 837 d3.select(this) 838 .transition() 839 .attr('r','8'); 840 return tooltip.style('visibility', 'visible') 841 .text(item.Rloc16); 842 }) 843 .on('mousemove', function() { 844 return tooltip.style('top', (d3.event.pageY - 10) + 'px') 845 .style('left', (d3.event.pageX + 10) + 'px'); 846 }) 847 .on('mouseout', function() { 848 d3.select(this) 849 .transition() 850 .attr('r','7'); 851 return tooltip.style('visibility', 'hidden'); 852 }) 853 // The same effect as Leader 854 .on('click', function(item) { 855 d3.selectAll('.Stroke') 856 .style('stroke', '#484e46') 857 .style('stroke-width', '1px'); 858 d3.select(this) 859 .style('stroke', '#f39191') 860 .style('stroke-width', '1px'); 861 $scope.$apply(function() { 862 $scope.nodeDetailInfo = item; 863 $scope.updateDetailLabel(); 864 }); 865 }); 866 867 force.on('tick', function() { 868 link.attr('x1', function(item) { return item.source.x; }) 869 .attr('y1', function(item) { return item.source.y; }) 870 .attr('x2', function(item) { return item.target.x; }) 871 .attr('y2', function(item) { return item.target.y; }); 872 node.attr('transform', function(item) { 873 return 'translate(' + item.x + ',' + item.y + ')'; 874 }); 875 }); 876 877 $scope.updateDetailLabel(); 878 $scope.graphisReady = true; 879 880 } 881 }; 882})(); 883