summaryrefslogtreecommitdiffstats
path: root/framework/src/onos/web/gui/src/main/webapp/app/view/topo
diff options
context:
space:
mode:
authorAshlee Young <ashlee@onosfw.com>2015-09-09 22:15:21 -0700
committerAshlee Young <ashlee@onosfw.com>2015-09-09 22:15:21 -0700
commit13d05bc8458758ee39cb829098241e89616717ee (patch)
tree22a4d1ce65f15952f07a3df5af4b462b4697cb3a /framework/src/onos/web/gui/src/main/webapp/app/view/topo
parent6139282e1e93c2322076de4b91b1c85d0bc4a8b3 (diff)
ONOS checkin based on commit tag e796610b1f721d02f9b0e213cf6f7790c10ecd60
Change-Id: Ife8810491034fe7becdba75dda20de4267bd15cd
Diffstat (limited to 'framework/src/onos/web/gui/src/main/webapp/app/view/topo')
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt3
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css729
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html7
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js529
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js576
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js131
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js149
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js1134
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js373
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js338
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js439
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js257
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js418
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js539
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js286
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js262
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js284
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js220
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js159
19 files changed, 6833 insertions, 0 deletions
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt
new file mode 100644
index 00000000..61089bf6
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt
@@ -0,0 +1,3 @@
+# ONOS Topology View
+
+Code and resources for implementing the client-side for the Topology View.
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css
new file mode 100644
index 00000000..f4b089a0
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -0,0 +1,729 @@
+/*
+ * Copyright 2014,2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology View -- CSS file
+ */
+
+/* --- Base SVG Layer --- */
+
+#ov-topo svg {
+ /* prevents the little cut/copy/paste square that would appear on iPad */
+ -webkit-user-select: none;
+}
+.light #ov-topo svg {
+ background-color: #fff;
+}
+.dark #ov-topo svg {
+ background-color: #2b2b2b;
+}
+
+
+/* --- "No Devices" Layer --- */
+
+#ov-topo svg #topo-noDevsLayer {
+ visibility: hidden;
+}
+
+.light #ov-topo svg .noDevsBird {
+ fill: #ecd;
+}
+.dark #ov-topo svg .noDevsBird {
+ fill: #683434;
+}
+
+#ov-topo svg #topo-noDevsLayer text {
+ font-size: 60pt;
+ font-style: italic;
+}
+.light #ov-topo svg #topo-noDevsLayer text {
+ fill: #dde;
+}
+.dark #ov-topo svg #topo-noDevsLayer text {
+ fill: #3b3b4f;
+}
+
+
+/* --- Topo Map --- */
+
+#ov-topo svg #topo-map {
+ stroke-width: 2px;
+ fill: transparent;
+}
+
+.light #ov-topo svg #topo-map {
+ stroke: #ddd;
+}
+.dark #ov-topo svg #topo-map {
+ stroke: #444;
+}
+
+
+/* --- Topo Summary Panel --- */
+
+#topo-p-summary {
+ /* Base css from panel.css */
+}
+
+/* --- Topo Detail Panel --- */
+
+#topo-p-detail {
+ /* Base css from panel.css */
+ top: 320px;
+}
+html[data-platform='iPad'] #topo-p-detail {
+ top: 336px;
+}
+
+#topo-p-detail .actionBtns .actionBtn {
+ display: inline-block;
+}
+#topo-p-detail .actionBtns .actionBtn svg {
+ width: 30px;
+ height: 30px;
+}
+
+/* --- general topo-panel styling --- */
+
+.topo-p div.header div.icon {
+ vertical-align: middle;
+ display: inline-block;
+}
+.topo-p div.body {
+ overflow-y: scroll;
+}
+
+.topo-p div.body::-webkit-scrollbar {
+ display: none;
+}
+
+.topo-p svg {
+ display: inline-block;
+ width: 42px;
+ height: 42px;
+}
+
+.light .topo-p svg .glyph {
+ fill: #222;
+}
+
+.dark .topo-p svg .glyph.overlay {
+ fill: #222;
+}
+
+.dark .topo-p svg .glyph {
+ fill: #ddd;
+}
+.light .topo-p svg .glyph.overlay {
+ fill: #fff;
+}
+
+
+.topo-p h2 {
+ padding: 0 4px;
+ margin: 0;
+ word-wrap: break-word;
+ display: inline-block;
+ width: 210px;
+ vertical-align: middle;
+}
+.light .topo-p h2 {
+ color: black;
+}
+.dark .topo-p h2 {
+ color: #ddd;
+}
+
+.topo-p h3 {
+ padding: 0 4px;
+ margin: 0;
+ word-wrap: break-word;
+ top: 20px;
+ left: 50px;
+}
+.light .topo-p h3 {
+ color: black;
+}
+.dark .topo-p h3 {
+ color: #ddd;
+}
+
+.topo-p p, table {
+ padding: 4px;
+ margin: 0;
+}
+
+.topo-p td {
+ word-wrap: break-word;
+}
+.topo-p td.label {
+ font-style: italic;
+ padding-right: 12px;
+ /* works for both light and dark themes ... */
+ color: #777;
+}
+.topo-p td.value {
+}
+
+.topo-p hr {
+ height: 1px;
+ border: 0;
+}
+.light .topo-p hr {
+ background-color: #ccc;
+ color: #ccc;
+}
+.dark .topo-p hr {
+ background-color: #888;
+ color: #888;
+}
+
+/* --- Topo Instance Panel --- */
+
+#topo-p-instance {
+ height: 100px;
+}
+
+#topo-p-instance div.onosInst {
+ display: inline-block;
+ width: 170px;
+ height: 85px;
+ cursor: pointer;
+}
+
+#topo-p-instance svg rect {
+ stroke-width: 3.5;
+}
+#topo-p-instance .online svg rect {
+ opacity: 1;
+}
+.light #topo-p-instance svg rect {
+ fill: #ccc;
+ stroke: #aaa;
+}
+.light #topo-p-instance .online svg rect {
+ fill: #9cf;
+ stroke: #555;
+}
+.dark #topo-p-instance svg rect {
+ fill: #666;
+ stroke: #222;
+}
+.dark #topo-p-instance .online svg rect {
+ fill: #9cf;
+ stroke: #999;
+}
+
+
+#topo-p-instance svg .glyph {
+ fill: #888;
+ fill-rule: evenodd;
+}
+#topo-p-instance .online svg .glyph {
+ fill: #000;
+}
+
+#topo-p-instance svg .badgeIcon {
+ fill-rule: evenodd;
+ opacity: 0.4;
+}
+.light #topo-p-instance svg .badgeIcon {
+ fill: #777;
+}
+.dark #topo-p-instance svg .badgeIcon {
+ fill: #555;
+}
+
+#topo-p-instance .online svg .badgeIcon {
+ opacity: 1.0;
+}
+.light #topo-p-instance .online svg .badgeIcon {
+ fill: #fff;
+}
+.dark #topo-p-instance .online svg .badgeIcon {
+ fill: #fff;
+}
+
+#topo-p-instance svg text {
+ text-anchor: middle;
+ opacity: 0.3;
+}
+#topo-p-instance .online svg text {
+ opacity: 1.0;
+}
+.light #topo-p-instance svg text {
+ fill: #444;
+}
+.light #topo-p-instance .online svg text {
+ fill: #eee;
+}
+.dark #topo-p-instance svg text {
+ fill: #aaa;
+}
+.dark #topo-p-instance .online svg text {
+ fill: #ccc;
+}
+
+#topo-p-instance svg text.instTitle {
+ font-size: 11pt;
+ font-weight: bold;
+}
+#topo-p-instance svg text.instLabel {
+ font-size: 9pt;
+ font-style: italic;
+}
+
+#topo-p-instance .onosInst.mastership {
+ opacity: 0.3;
+}
+#topo-p-instance .onosInst.mastership.affinity {
+ opacity: 1.0;
+}
+.light #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#blue-glow);
+}
+.dark #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#yellow-glow);
+}
+.light.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+.dark.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");
+}
+
+
+/* --- Toolbar --- */
+
+#toolbar-topo-tbar .tbar-row.right {
+ width: 100%;
+}
+
+#toolbar-topo-tbar .tbar-row-text {
+ height: 21px;
+ text-align: right;
+ padding: 8px 60px 0 0;
+ font-style: italic;
+}
+
+
+/* --- Topo Nodes --- */
+
+#ov-topo svg .suppressed {
+ opacity: 0.5 !important;
+}
+
+#ov-topo svg .suppressedmax {
+ opacity: 0.2 !important;
+}
+
+#ov-topo svg .node {
+ cursor: pointer;
+}
+
+.light #ov-topo svg .node.selected rect,
+.light #ov-topo svg .node.selected circle {
+ fill: #f90;
+ filter: url(#blue-glow);
+}
+.dark #ov-topo svg .node.selected rect,
+.dark #ov-topo svg .node.selected circle {
+ fill: #f90;
+ filter: url(#yellow-glow);
+}
+.light.firefox #ov-topo svg .node.selected rect,
+.light.firefox #ov-topo svg .node.selected circle {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+.dark.firefox #ov-topo svg .node.selected rect,
+.dark.firefox #ov-topo svg .node.selected circle {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");
+}
+
+#ov-topo svg .node text {
+ pointer-events: none;
+}
+
+/* Device Nodes */
+
+#ov-topo svg .node.device {
+}
+
+#ov-topo svg .node.device rect {
+ stroke-width: 1.5;
+}
+
+#ov-topo svg .node.device.fixed rect {
+ stroke-width: 1.5;
+}
+.light #ov-topo svg .node.device.fixed rect {
+ stroke: #aaa;
+}
+.dark #ov-topo svg .node.device.fixed rect {
+ stroke: #999;
+}
+
+/* note: device is offline without the 'online' class */
+.light #ov-topo svg .node.device {
+ fill: #777;
+}
+.dark #ov-topo svg .node.device {
+ fill: #555;
+}
+
+.light #ov-topo svg .node.device rect {
+ stroke: #666;
+}
+.light #ov-topo svg .node.device rect {
+ stroke: #999;
+}
+
+.light #ov-topo svg .node.device.online {
+ fill: #6e7fa3;
+}
+.dark #ov-topo svg .node.device.online {
+ fill: #4E5C7F;
+}
+
+/* note: device is offline without the 'online' class */
+#ov-topo svg .node.device text {
+ fill: #bbb;
+ font: 10pt sans-serif;
+}
+
+#ov-topo svg .node.device.online text {
+ fill: white;
+}
+
+#ov-topo svg .node.device .svgIcon rect {
+ fill: #aaa;
+}
+#ov-topo svg .node.device .svgIcon use {
+ fill: #777;
+}
+#ov-topo svg .node.device.selected .svgIcon rect {
+ fill: #f90;
+}
+#ov-topo svg .node.device.online .svgIcon rect {
+ fill: #ccc;
+}
+#ov-topo svg .node.device.online .svgIcon use {
+ fill: #000;
+}
+#ov-topo svg .node.device.online.selected .svgIcon rect {
+ fill: #f90;
+}
+
+
+/* Host Nodes */
+
+#ov-topo svg .node.host {
+}
+
+#ov-topo svg .node.host text {
+ stroke: none;
+ font: 9pt sans-serif;
+}
+.light #ov-topo svg .node.host text {
+ fill: #846;
+}
+.dark #ov-topo svg .node.host text {
+ fill: #BB809D;
+}
+
+.light svg .node.host circle {
+ stroke: #000;
+ fill: #edb;
+}
+.dark svg .node.host circle {
+ stroke: #eee;
+ fill: #B2A180;
+}
+
+.light svg .node.host .svgIcon {
+ fill: #444;
+}
+.dark svg .node.host .svgIcon {
+ fill: #222;
+}
+
+/* --- Topo Links --- */
+
+#ov-topo svg .link {
+ opacity: .9;
+}
+
+#ov-topo svg .link.selected,
+#ov-topo svg .link.enhanced {
+ stroke-width: 4.5px;
+}
+.light #ov-topo svg .link.selected,
+.light #ov-topo svg .link.enhanced {
+ filter: url(#blue-glow);
+}
+.dark #ov-topo svg .link.selected,
+.dark #ov-topo svg .link.enhanced {
+ filter: url(#yellow-glow);
+}
+.light.firefox #ov-topo svg .link.selected,
+.light.firefox #ov-topo svg .link.enhanced {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+.dark.firefox #ov-topo svg .link.selected,
+.dark.firefox #ov-topo svg .link.enhanced {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");
+
+}
+
+#ov-topo svg .link.inactive {
+ opacity: .5;
+ stroke-dasharray: 8 4;
+}
+
+#ov-topo svg .link.secondary {
+ stroke-width: 3px;
+}
+.light #ov-topo svg .link.secondary {
+ stroke: rgba(0,153,51,0.5);
+}
+.dark #ov-topo svg .link.secondary {
+ stroke: rgba(121,231,158,0.5);
+}
+
+/* Port traffic color visualization for Kbps, Mbps, and Gbps */
+
+.light #ov-topo svg .link.secondary.port-traffic-Kbps {
+ stroke: rgb(0,153,51);
+ stroke-width: 5.0;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Kbps {
+ stroke: rgb(98, 153, 118);
+ stroke-width: 5.0;
+}
+
+.light #ov-topo svg .link.secondary.port-traffic-Mbps {
+ stroke: rgb(128,145,27);
+ stroke-width: 6.5;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Mbps {
+ stroke: rgb(91, 109, 54);
+ stroke-width: 6.5;
+}
+
+.light #ov-topo svg .link.secondary.port-traffic-Gbps {
+ stroke: rgb(255, 137, 3);
+ stroke-width: 8.0;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Gbps {
+ stroke: rgb(174, 119, 55);
+ stroke-width: 8.0;
+}
+
+.light #ov-topo svg .link.secondary.port-traffic-Gbps-choked {
+ stroke: rgb(183, 30, 21);
+ stroke-width: 8.0;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Gbps-choked {
+ stroke: rgb(127, 40, 39);
+ stroke-width: 8.0;
+}
+
+
+#ov-topo svg .link.animated {
+ stroke-dasharray: 8 5;
+ animation: ants 5s infinite linear;
+ /* below line will be added via Javascript based on path */
+ /*animation-direction: reverse;*/
+}
+@keyframes ants {
+ from {
+ stroke-dashoffset: 0;
+ }
+ to {
+ stroke-dashoffset: 400;
+ }
+}
+
+#ov-topo svg .link.primary {
+ stroke-width: 4px;
+}
+.light #ov-topo svg .link.primary {
+ stroke: #ffA300;
+}
+.dark #ov-topo svg .link.primary {
+ stroke: #D58E0F;
+}
+
+#ov-topo svg .link.secondary.optical {
+ stroke-width: 4px;
+}
+.light #ov-topo svg .link.secondary.optical {
+ stroke: rgba(128,64,255,0.5);
+}
+.dark #ov-topo svg .link.secondary.optical {
+ stroke: rgba(164,139,215,0.5);
+}
+
+#ov-topo svg .link.primary.optical {
+ stroke-width: 6px;
+}
+.light #ov-topo svg .link.primary.optical {
+ stroke: #74f;
+}
+.dark #ov-topo svg .link.primary.optical {
+ stroke: #7352CD;
+}
+
+/* Link Labels */
+
+#ov-topo svg .linkLabel rect {
+ stroke: none;
+}
+.light #ov-topo svg .linkLabel rect {
+ fill: #eee;
+}
+.dark #ov-topo svg .linkLabel rect {
+ fill: #555;
+}
+
+#ov-topo svg .linkLabel text {
+ text-anchor: middle;
+ stroke-width: 0.1;
+ font-size: 9pt;
+}
+.light #ov-topo svg .linkLabel text {
+ fill: #444;
+}
+.dark #ov-topo svg .linkLabel text {
+ fill: #eee;
+}
+
+/* Port Labels */
+
+#ov-topo svg .portLabel rect {
+ stroke: none;
+}
+.light #ov-topo svg .portLabel rect {
+ fill: #eee;
+}
+.dark #ov-topo svg .portLabel rect {
+ fill: #222;
+}
+
+#ov-topo svg .portLabel text {
+ text-anchor: middle;
+ stroke-width: 0.1;
+ font-size: 11pt;
+}
+.light #ov-topo svg .portLabel text {
+ fill: #444;
+}
+.dark #ov-topo svg .portLabel text {
+ fill: #eee;
+}
+
+/* Number of Links Labels */
+#ov-topo line.numLinkHash {
+ stroke-width: 3;
+}
+
+#ov-topo text.numLinkText {
+ font-size: 15pt;
+}
+
+#ov-topo text.numLinkText {
+ text-anchor: middle;
+}
+
+.light #ov-topo text.numLinkText {
+ fill: #444;
+}
+.dark #ov-topo text.numLinkText {
+ fill: #eee;
+}
+
+/* ------------------------------------------------- */
+/* Sprite Layer */
+
+#ov-topo svg #topo-sprites use {
+ stroke-width: 2;
+}
+#ov-topo svg #topo-sprites text {
+ text-anchor: middle;
+ font-size: 20pt;
+ font-style: italic;
+}
+
+.light #ov-topo svg #topo-sprites .gold1 use {
+ stroke: #fda;
+ fill: none;
+}
+.dark #ov-topo svg #topo-sprites .gold1 use {
+ stroke: #541;
+ fill: none;
+}
+.light #ov-topo svg #topo-sprites .gold1 text {
+ fill: #eda;
+}
+.dark #ov-topo svg #topo-sprites .gold1 text {
+ fill: #543;
+}
+
+.light #ov-topo svg #topo-sprites .blue1 use {
+ stroke: #bbd;
+ fill: none;
+}
+.dark #ov-topo svg #topo-sprites .blue1 use {
+ stroke: #445;
+ fill: none;
+}
+.light #ov-topo svg #topo-sprites .blue1 text {
+ fill: #cce;
+}
+.dark #ov-topo svg #topo-sprites .blue1 text {
+ fill: #446;
+}
+
+.light #ov-topo svg #topo-sprites .gray1 use {
+ stroke: #ccc;
+ fill: none;
+}
+.dark #ov-topo svg #topo-sprites .gray1 use {
+ stroke: #333;
+ fill: none;
+}
+.light #ov-topo svg #topo-sprites .gray1 text {
+ fill: #ddd;
+}
+.dark #ov-topo svg #topo-sprites .gray1 text {
+ fill: #444;
+}
+
+/* fills */
+.light #ov-topo svg #topo-sprites use.fill-gray2 {
+ fill: #eee;
+}
+.dark #ov-topo svg #topo-sprites use.fill-gray2 {
+ fill: #444;
+}
+
+.light #ov-topo svg #topo-sprites use.fill-blue2 {
+ fill: #bce;
+}
+.dark #ov-topo svg #topo-sprites use.fill-blue2 {
+ fill: #447;
+}
+
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html
new file mode 100644
index 00000000..8a64abb5
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html
@@ -0,0 +1,7 @@
+<!-- Topology View partial HTML -->
+<div id="ov-topo">
+ <svg viewBox="0 0 1000 1000"
+ resize offset-height="56" offset-width="12"
+ notifier="notifyResize()">
+ </svg>
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js
new file mode 100644
index 00000000..21894100
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -0,0 +1,529 @@
+/*
+ * Copyright 2014,2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology View Module
+ */
+
+(function () {
+ 'use strict';
+
+ var moduleDependencies = [
+ 'ngCookies',
+ 'onosUtil',
+ 'onosSvg',
+ 'onosRemote'
+ ];
+
+ // references to injected services etc.
+ var $scope, $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps,
+ tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, ttip, tov;
+
+ // DOM elements
+ var ovtopo, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer;
+
+ // Internal state
+ var zoomer, actionMap;
+
+ // --- Short Cut Keys ------------------------------------------------
+
+ function setUpKeys(overlayKeys) {
+ // key bindings need to be made after the services have been injected
+ // thus, deferred to here...
+ actionMap = {
+ I: [toggleInstances, 'Toggle ONOS instances panel'],
+ O: [toggleSummary, 'Toggle ONOS summary panel'],
+ D: [toggleUseDetailsFlag, 'Disable / enable details panel'],
+
+ H: [toggleHosts, 'Toggle host visibility'],
+ M: [toggleOffline, 'Toggle offline visibility'],
+ P: [togglePorts, 'Toggle Port Highlighting'],
+ dash: [tfs.showBadLinks, 'Show bad links'],
+ B: [toggleMap, 'Toggle background map'],
+ S: [toggleSprites, 'Toggle sprite layer'],
+
+ //X: [toggleNodeLock, 'Lock / unlock node positions'],
+ Z: [tos.toggleOblique, 'Toggle oblique view (Experimental)'],
+ N: [fltr.clickAction, 'Cycle node layers'],
+ L: [tfs.cycleDeviceLabels, 'Cycle device labels'],
+ U: [tfs.unpin, 'Unpin node (hover mouse over)'],
+ R: [resetZoom, 'Reset pan / zoom'],
+ dot: [ttbs.toggleToolbar, 'Toggle Toolbar'],
+
+ E: [equalizeMasters, 'Equalize mastership roles'],
+
+ esc: handleEscape,
+
+ _keyListener: ttbs.keyListener,
+
+ _helpFormat: [
+ ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B', 'S' ],
+ ['X', 'Z', 'N', 'L', 'U', 'R', '-', 'E', '-', 'dot'],
+ [] // this column reserved for overlay actions
+ ]
+ };
+
+ if (fs.isO(overlayKeys)) {
+ mergeKeys(overlayKeys);
+ }
+
+ ks.keyBindings(actionMap);
+
+ ks.gestureNotes([
+ ['click', 'Select the item and show details'],
+ ['shift-click', 'Toggle selection state'],
+ ['drag', 'Reposition (and pin) device / host'],
+ ['cmd-scroll', 'Zoom in / out'],
+ ['cmd-drag', 'Pan']
+ ]);
+ }
+
+ // when a topology overlay is activated, we need to bind their keystrokes
+ // and include them in the quick-help panel
+ function mergeKeys(extra) {
+ var _hf = actionMap._helpFormat[2];
+ extra._keyOrder.forEach(function (k) {
+ var d = extra[k],
+ cb = d && d.cb,
+ tt = d && d.tt;
+ // NOTE: ignore keys that are already defined
+ if (d && !actionMap[k]) {
+ actionMap[k] = [cb, tt];
+ _hf.push(k);
+ }
+ });
+ }
+
+ // --- Keystroke functions -------------------------------------------
+
+ function toggleInstances(x) {
+ updatePrefsState('insts', tis.toggle(x));
+ tfs.updateDeviceColors();
+ }
+
+ function toggleSummary(x) {
+ updatePrefsState('summary', tps.toggleSummary(x));
+ }
+
+ function toggleUseDetailsFlag(x) {
+ updatePrefsState('detail', tps.toggleUseDetailsFlag(x));
+ }
+
+ function toggleHosts(x) {
+ updatePrefsState('hosts', tfs.toggleHosts(x));
+ }
+
+ function toggleOffline(x) {
+ updatePrefsState('offdev', tfs.toggleOffline(x));
+ }
+
+ function togglePorts(x) {
+ updatePrefsState('porthl', tfs.togglePorts(x));
+ }
+
+ function _togSvgLayer(x, G, tag, what) {
+ var on = (x === 'keyev') ? !sus.visible(G) : !!x,
+ verb = on ? 'Show' : 'Hide';
+ sus.visible(G, on);
+ updatePrefsState(tag, on);
+ flash.flash(verb + ' ' + what);
+ }
+
+ function toggleMap(x) {
+ _togSvgLayer(x, mapG, 'bg', 'background map');
+ }
+
+ function toggleSprites(x) {
+ _togSvgLayer(x, spriteG, 'spr', 'sprite layer');
+ }
+
+ function resetZoom() {
+ zoomer.reset();
+ flash.flash('Pan and zoom reset');
+ }
+
+ function equalizeMasters() {
+ wss.sendEvent('equalizeMasters');
+ flash.flash('Equalizing master roles');
+ }
+
+ function handleEscape() {
+ if (tis.showMaster()) {
+ // if an instance is selected, cancel the affinity mapping
+ tis.cancelAffinity()
+
+ } else if (tov.hooks.escape()) {
+ // else if the overlay consumed the ESC event...
+ // (work already done)
+
+ } else if (tss.deselectAll()) {
+ // else if we have node selections, deselect them all
+ // (work already done)
+
+ } else if (tls.deselectLink()) {
+ // else if we have a link selected, deselect it
+ // (work already done)
+
+ } else if (tis.isVisible()) {
+ // else if the Instance Panel is visible, hide it
+ tis.hide();
+ tfs.updateDeviceColors();
+
+ } else if (tps.summaryVisible()) {
+ // else if the Summary Panel is visible, hide it
+ tps.hideSummaryPanel();
+ }
+ }
+
+ // --- Toolbar Functions ---------------------------------------------
+
+ function notValid(what) {
+ $log.warn('topo.js getActionEntry(): Not a valid ' + what);
+ }
+
+ function getActionEntry(key) {
+ var entry;
+
+ if (!key) {
+ notValid('key');
+ return null;
+ }
+
+ entry = actionMap[key];
+
+ if (!entry) {
+ notValid('actionMap entry');
+ return null;
+ }
+ return fs.isA(entry) || [entry, ''];
+ }
+
+ function setUpToolbar() {
+ ttbs.init({
+ getActionEntry: getActionEntry,
+ setUpKeys: setUpKeys
+ });
+ ttbs.createToolbar();
+ }
+
+ // --- Glyphs, Icons, and the like -----------------------------------
+
+ function setUpDefs() {
+ defs = svg.append('defs');
+ gs.loadDefs(defs);
+ sus.loadGlowDefs(defs);
+ }
+
+
+ // --- Pan and Zoom --------------------------------------------------
+
+ // zoom enabled predicate. ev is a D3 source event.
+ function zoomEnabled(ev) {
+ return fs.isMobile() || (ev.metaKey || ev.altKey);
+ }
+
+ function zoomCallback() {
+ var sc = zoomer.scale(),
+ tr = zoomer.translate();
+
+ ps.setPrefs('topo_zoom', {tx:tr[0], ty:tr[1], sc:sc});
+
+ // keep the map lines constant width while zooming
+ mapG.style('stroke-width', (2.0 / sc) + 'px');
+ }
+
+ function setUpZoom() {
+ zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer,
+ zoomEnabled: zoomEnabled,
+ zoomCallback: zoomCallback
+ });
+ }
+
+
+ // callback invoked when the SVG view has been resized..
+ function svgResized(s) {
+ tfs.newDim([s.width, s.height]);
+ }
+
+ // --- Background Map ------------------------------------------------
+
+ function setUpNoDevs() {
+ var g, box;
+ noDevsLayer = svg.append('g').attr({
+ id: 'topo-noDevsLayer',
+ transform: sus.translate(500,500)
+ });
+ // Note, SVG viewbox is '0 0 1000 1000', defined in topo.html.
+ // We are translating this layer to have its origin at the center
+
+ g = noDevsLayer.append('g');
+ gs.addGlyph(g, 'bird', 100).attr('class', 'noDevsBird');
+ g.append('text').text('No devices are connected')
+ .attr({ x: 120, y: 80});
+
+ box = g.node().getBBox();
+ box.x -= box.width/2;
+ box.y -= box.height/2;
+ g.attr('transform', sus.translate(box.x, box.y));
+
+ showNoDevs(true);
+ }
+
+ function showNoDevs(b) {
+ sus.visible(noDevsLayer, b);
+ }
+
+
+ var countryFilters = {
+ world: function (c) {
+ return c.properties.continent !== 'Antarctica';
+ },
+
+ // NOTE: for "usa" we are using our hand-crafted topojson file
+
+ s_america: function (c) {
+ return c.properties.continent === 'South America';
+ },
+
+ japan: function (c) {
+ return c.properties.geounit === 'Japan';
+ },
+
+ europe: function (c) {
+ return c.properties.continent === 'Europe';
+ },
+
+ italy: function (c) {
+ return c.properties.geounit === 'Italy';
+ },
+
+ uk: function (c) {
+ // technically, Ireland is not part of the United Kingdom,
+ // but the map looks weird without it showing.
+ return c.properties.adm0_a3 === 'GBR' ||
+ c.properties.adm0_a3 === 'IRL';
+ },
+
+ s_korea: function (c) {
+ return c.properties.adm0_a3 === 'KOR';
+ },
+
+ australia: function (c) {
+ return c.properties.adm0_a3 === 'AUS';
+ }
+ };
+
+
+ function setUpMap($loc) {
+ var s1 = $loc.search().mapid,
+ s2 = ps.getPrefs('topo_mapid'),
+ mapId = s1 || (s2 && s2.id) || 'usa',
+ promise,
+ cfilter,
+ opts;
+
+ mapG = zoomLayer.append('g').attr('id', 'topo-map');
+ if (mapId === 'usa') {
+ promise = ms.loadMapInto(mapG, '*continental_us');
+ } else {
+ ps.setPrefs('topo_mapid', {id:mapId});
+ cfilter = countryFilters[mapId] || countryFilters.world;
+ opts = { countryFilter: cfilter };
+ promise = ms.loadMapRegionInto(mapG, opts);
+ }
+ return promise;
+ }
+
+ function opacifyMap(b) {
+ mapG.transition()
+ .duration(1000)
+ .attr('opacity', b ? 1 : 0);
+ }
+
+ function setUpSprites($loc, tspr) {
+ var s1 = $loc.search().sprites,
+ s2 = ps.getPrefs('topo_sprites'),
+ sprId = s1 || (s2 && s2.id);
+
+ spriteG = zoomLayer.append ('g').attr('id', 'topo-sprites');
+ if (sprId) {
+ ps.setPrefs('topo_sprites', {id:sprId});
+ tspr.loadSprites(spriteG, defs, sprId);
+ }
+ }
+
+ // --- User Preferemces ----------------------------------------------
+
+ var prefsState = {};
+
+ function updatePrefsState(what, b) {
+ prefsState[what] = b ? 1 : 0;
+ ps.setPrefs('topo_prefs', prefsState);
+ }
+
+
+ function restoreConfigFromPrefs() {
+ // NOTE: toolbar will have set this for us..
+ prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+
+ $log.debug('TOPO- Prefs State:', prefsState);
+
+ flash.enable(false);
+ toggleInstances(prefsState.insts);
+ toggleSummary(prefsState.summary);
+ toggleUseDetailsFlag(prefsState.detail);
+ toggleHosts(prefsState.hosts);
+ toggleOffline(prefsState.offdev);
+ togglePorts(prefsState.porthl);
+ toggleMap(prefsState.bg);
+ toggleSprites(prefsState.spr);
+ flash.enable(true);
+ }
+
+
+ // somewhat hackish, because summary update cannot happen until we
+ // have opened the websocket to the server; hence this extra function
+ // invoked after tes.start()
+ function restoreSummaryFromPrefs() {
+ prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+ $log.debug('TOPO- Prefs SUMMARY State:', prefsState.summary);
+
+ flash.enable(false);
+ toggleSummary(prefsState.summary);
+ flash.enable(true);
+ }
+
+
+ // --- Controller Definition -----------------------------------------
+
+ angular.module('ovTopo', moduleDependencies)
+ .controller('OvTopoCtrl', ['$scope', '$log', '$location', '$timeout',
+ '$cookies', 'FnService', 'MastService', 'KeyService', 'ZoomService',
+ 'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
+ 'WebSocketService', 'PrefsService',
+ 'TopoEventService', 'TopoForceService', 'TopoPanelService',
+ 'TopoInstService', 'TopoSelectService', 'TopoLinkService',
+ 'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService',
+ 'TopoToolbarService', 'TopoSpriteService', 'TooltipService',
+ 'TopoOverlayService',
+
+ function (_$scope_, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_,
+ _zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _tes_, _tfs_,
+ _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_, _ttbs_, tspr,
+ _ttip_, _tov_) {
+ var projection,
+ dim,
+ uplink = {
+ // provides function calls back into this space
+ showNoDevs: showNoDevs,
+ projection: function () { return projection; },
+ zoomLayer: function () { return zoomLayer; },
+ zoomer: function () { return zoomer; },
+ opacifyMap: opacifyMap
+ };
+
+ $scope = _$scope_;
+ $log = _$log_;
+ $cookies = _$cookies_;
+ fs = _fs_;
+ ks = _ks_;
+ zs = _zs_;
+ gs = _gs_;
+ ms = _ms_;
+ sus = _sus_;
+ flash = _flash_;
+ wss = _wss_;
+ ps = _ps_;
+ tes = _tes_;
+ tfs = _tfs_;
+ // TODO: consider funnelling actions through TopoForceService...
+ // rather than injecting references to these 'sub-modules',
+ // just so we can invoke functions on them.
+ tps = _tps_;
+ tis = _tis_;
+ tss = _tss_;
+ tls = _tls_;
+ tts = _tts_;
+ tos = _tos_;
+ fltr = _fltr_;
+ ttbs = _ttbs_;
+ ttip = _ttip_;
+ tov = _tov_;
+
+ $scope.notifyResize = function () {
+ svgResized(fs.windowSize(mast.mastHeight()));
+ };
+
+ // Cleanup on destroyed scope..
+ $scope.$on('$destroy', function () {
+ $log.log('OvTopoCtrl is saying Buh-Bye!');
+ tes.stop();
+ ks.unbindKeys();
+ tps.destroyPanels();
+ tis.destroyInst();
+ tfs.destroyForce();
+ ttbs.destroyToolbar();
+ });
+
+ // svg layer and initialization of components
+ ovtopo = d3.select('#ov-topo');
+ svg = ovtopo.select('svg');
+ // set the svg size to match that of the window, less the masthead
+ svg.attr(fs.windowSize(mast.mastHeight()));
+ dim = [svg.attr('width'), svg.attr('height')];
+
+ setUpKeys();
+ setUpToolbar();
+ setUpDefs();
+ setUpZoom();
+ setUpNoDevs();
+ setUpMap($loc).then(
+ function (proj) {
+ var z = ps.getPrefs('topo_zoom') || {tx:0, ty:0, sc:1};
+ zoomer.panZoom([z.tx, z.ty], z.sc);
+ $log.debug('** Zoom restored:', z);
+
+ projection = proj;
+ $log.debug('** We installed the projection:', proj);
+ flash.enable(false);
+ toggleMap(prefsState.bg);
+ flash.enable(true);
+
+ // now we have the map projection, we are ready for
+ // the server to send us device/host data...
+ tes.start();
+ // need to do the following so we immediately get
+ // the summary panel data back from the server
+ restoreSummaryFromPrefs();
+ }
+ );
+ setUpSprites($loc, tspr);
+
+ forceG = zoomLayer.append('g').attr('id', 'topo-force');
+ tfs.initForce(svg, forceG, uplink, dim);
+ tis.initInst({ showMastership: tfs.showMastership });
+ tps.initPanels();
+
+ // temporary solution for persisting user settings
+ restoreConfigFromPrefs();
+
+ $log.debug('registered overlays...', tov.list());
+ $log.log('OvTopoCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js
new file mode 100644
index 00000000..d29748b1
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -0,0 +1,576 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology D3 Module.
+ Functions for manipulating the D3 visualizations of the Topology
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, sus, is, ts;
+
+ // api to topoForce
+ var api;
+ /*
+ node() // get ref to D3 selection of nodes
+ link() // get ref to D3 selection of links
+ linkLabel() // get ref to D3 selection of link labels
+ instVisible() // true if instances panel is visible
+ posNode() // position node
+ showHosts() // true if hosts are to be shown
+ restyleLinkElement() // update link styles based on backing data
+ updateLinkLabelModel() // update backing data for link labels
+ */
+
+ // configuration
+ var devCfg = {
+ xoff: -20,
+ yoff: -18
+ },
+ labelConfig = {
+ imgPad: 16,
+ padLR: 4,
+ padTB: 3,
+ marginLR: 3,
+ marginTB: 2,
+ port: {
+ gap: 3,
+ width: 18,
+ height: 14
+ }
+ },
+ icfg;
+
+ // internal state
+ var deviceLabelIndex = 0,
+ hostLabelIndex = 0;
+
+
+ var dCol = {
+ black: '#000',
+ paleblue: '#acf',
+ offwhite: '#ddd',
+ darkgrey: '#444',
+ midgrey: '#888',
+ lightgrey: '#bbb',
+ orange: '#f90'
+ };
+
+ // note: these are the device icon colors without affinity
+ var dColTheme = {
+ light: {
+ rfill: dCol.offwhite,
+ online: {
+ glyph: dCol.darkgrey,
+ rect: dCol.paleblue
+ },
+ offline: {
+ glyph: dCol.midgrey,
+ rect: dCol.lightgrey
+ }
+ },
+ dark: {
+ rfill: dCol.midgrey,
+ online: {
+ glyph: dCol.darkgrey,
+ rect: dCol.paleblue
+ },
+ offline: {
+ glyph: dCol.midgrey,
+ rect: dCol.darkgrey
+ }
+ }
+ };
+
+ function devBaseColor(d) {
+ var o = d.online ? 'online' : 'offline';
+ return dColTheme[ts.theme()][o];
+ }
+
+ function setDeviceColor(d) {
+ var o = d.online,
+ s = d.el.classed('selected'),
+ c = devBaseColor(d),
+ a = instColor(d.master, o),
+ icon = d.el.select('g.deviceIcon'),
+ g, r;
+
+ if (s) {
+ g = c.glyph;
+ r = dCol.orange;
+ } else if (api.instVisible()) {
+ g = o ? a : c.glyph;
+ r = o ? c.rfill : a;
+ } else {
+ g = c.glyph;
+ r = c.rect;
+ }
+
+ icon.select('use').style('fill', g);
+ icon.select('rect').style('fill', r);
+ }
+
+ function instColor(id, online) {
+ return sus.cat7().getColor(id, !online, ts.theme());
+ }
+
+ // ====
+
+ function incDevLabIndex() {
+ deviceLabelIndex = (deviceLabelIndex+1) % 3;
+ switch(deviceLabelIndex) {
+ case 0: return 'Hide device labels';
+ case 1: return 'Show friendly device labels';
+ case 2: return 'Show device ID labels';
+ }
+ }
+
+ // Returns the newly computed bounding box of the rectangle
+ function adjustRectToFitText(n) {
+ var text = n.select('text'),
+ box = text.node().getBBox(),
+ lab = labelConfig;
+
+ text.attr('text-anchor', 'middle')
+ .attr('y', '-0.8em')
+ .attr('x', lab.imgPad/2);
+
+ // translate the bbox so that it is centered on [x,y]
+ box.x = -box.width / 2;
+ box.y = -box.height / 2;
+
+ // add padding
+ box.x -= (lab.padLR + lab.imgPad/2);
+ box.width += lab.padLR * 2 + lab.imgPad;
+ box.y -= lab.padTB;
+ box.height += lab.padTB * 2;
+
+ return box;
+ }
+
+ function hostLabel(d) {
+ var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
+ return d.labels[idx];
+ }
+ function deviceLabel(d) {
+ var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
+ return d.labels[idx];
+ }
+ function trimLabel(label) {
+ return (label && label.trim()) || '';
+ }
+
+ function emptyBox() {
+ return {
+ x: -2,
+ y: -2,
+ width: 4,
+ height: 4
+ };
+ }
+
+
+ function updateDeviceLabel(d) {
+ var label = trimLabel(deviceLabel(d)),
+ noLabel = !label,
+ node = d.el,
+ dim = icfg.device.dim,
+ box, dx, dy;
+
+ node.select('text')
+ .text(label)
+ .style('opacity', 0)
+ .transition()
+ .style('opacity', 1);
+
+ if (noLabel) {
+ box = emptyBox();
+ dx = -dim/2;
+ dy = -dim/2;
+ } else {
+ box = adjustRectToFitText(node);
+ dx = box.x + devCfg.xoff;
+ dy = box.y + devCfg.yoff;
+ }
+
+ node.select('rect')
+ .transition()
+ .attr(box);
+
+ node.select('g.deviceIcon')
+ .transition()
+ .attr('transform', sus.translate(dx, dy));
+ }
+
+ function updateHostLabel(d) {
+ var label = trimLabel(hostLabel(d));
+ d.el.select('text').text(label);
+ }
+
+ function updateDeviceColors(d) {
+ if (d) {
+ setDeviceColor(d);
+ } else {
+ api.node().filter('.device').each(function (d) {
+ setDeviceColor(d);
+ });
+ }
+ }
+
+
+ // ==========================
+ // updateNodes - subfunctions
+
+ function deviceExisting(d) {
+ var node = d.el;
+ node.classed('online', d.online);
+ updateDeviceLabel(d);
+ api.posNode(d, true);
+ }
+
+ function hostExisting(d) {
+ updateHostLabel(d);
+ api.posNode(d, true);
+ }
+
+ function deviceEnter(d) {
+ var node = d3.select(this),
+ glyphId = d.type || 'unknown',
+ label = trimLabel(deviceLabel(d)),
+ //devCfg = deviceIconConfig,
+ noLabel = !label,
+ box, dx, dy, icon;
+
+ d.el = node;
+
+ node.append('rect').attr({ rx: 5, ry: 5 });
+ node.append('text').text(label).attr('dy', '1.1em');
+ box = adjustRectToFitText(node);
+ node.select('rect').attr(box);
+
+ icon = is.addDeviceIcon(node, glyphId);
+
+ if (noLabel) {
+ dx = -icon.dim/2;
+ dy = -icon.dim/2;
+ } else {
+ box = adjustRectToFitText(node);
+ dx = box.x + devCfg.xoff;
+ dy = box.y + devCfg.yoff;
+ }
+
+ icon.attr('transform', sus.translate(dx, dy));
+ }
+
+ function hostEnter(d) {
+ var node = d3.select(this),
+ gid = d.type || 'unknown',
+ rad = icfg.host.radius,
+ r = d.type ? rad.withGlyph : rad.noGlyph,
+ textDy = r + 10;
+
+ d.el = node;
+ sus.visible(node, api.showHosts());
+
+ is.addHostIcon(node, r, gid);
+
+ node.append('text')
+ .text(hostLabel)
+ .attr('dy', textDy)
+ .attr('text-anchor', 'middle');
+ }
+
+ function hostExit(d) {
+ var node = d.el;
+ node.select('use')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.select('text')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.select('circle')
+ .style('stroke-fill', '#555')
+ .style('fill', '#888')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(1500)
+ .attr('r', 0);
+ }
+
+ function deviceExit(d) {
+ var node = d.el;
+ node.select('use')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.selectAll('rect')
+ .style('stroke-fill', '#555')
+ .style('fill', '#888')
+ .style('opacity', 0.5);
+ }
+
+
+ // ==========================
+ // updateLinks - subfunctions
+
+ function linkEntering(d) {
+ var link = d3.select(this);
+ d.el = link;
+ api.restyleLinkElement(d);
+ if (d.type() === 'hostLink') {
+ sus.visible(link, api.showHosts());
+ }
+ }
+
+ var linkLabelOffset = '0.3em';
+
+ function applyLinkLabels() {
+ var entering;
+
+ api.updateLinkLabelModel();
+
+ // for elements already existing, we need to update the text
+ // and adjust the rectangle size to fit
+ api.linkLabel().each(function (d) {
+ var el = d3.select(this),
+ rect = el.select('rect'),
+ text = el.select('text');
+ text.text(d.label);
+ rect.attr(rectAroundText(el));
+ });
+
+ entering = api.linkLabel().enter().append('g')
+ .classed('linkLabel', true)
+ .attr('id', function (d) { return d.id; });
+
+ entering.each(function (d) {
+ var el = d3.select(this),
+ rect,
+ text;
+
+ if (d.ldata.type() === 'hostLink') {
+ el.classed('hostLinkLabel', true);
+ sus.visible(el, api.showHosts());
+ }
+
+ d.el = el;
+ rect = el.append('rect');
+ text = el.append('text').text(d.label);
+ rect.attr(rectAroundText(el));
+ text.attr('dy', linkLabelOffset);
+
+ el.attr('transform', transformLabel(d.ldata.position));
+ });
+
+ // Remove any labels that are no longer required.
+ api.linkLabel().exit().remove();
+ }
+
+ function rectAroundText(el) {
+ var text = el.select('text'),
+ box = text.node().getBBox();
+
+ // translate the bbox so that it is centered on [x,y]
+ box.x = -box.width / 2;
+ box.y = -box.height / 2;
+
+ // add padding
+ box.x -= 1;
+ box.width += 2;
+ return box;
+ }
+
+ function transformLabel(p) {
+ var dx = p.x2 - p.x1,
+ dy = p.y2 - p.y1,
+ xMid = dx/2 + p.x1,
+ yMid = dy/2 + p.y1;
+ return sus.translate(xMid, yMid);
+ }
+
+ function applyPortLabels(data, portLabelG) {
+ var entering = portLabelG.selectAll('.portLabel')
+ .data(data).enter().append('g')
+ .classed('portLabel', true)
+ .attr('id', function (d) { return d.id; });
+
+ entering.each(function (d) {
+ var el = d3.select(this),
+ rect = el.append('rect'),
+ text = el.append('text').text(d.num);
+
+ rect.attr(rectAroundText(el));
+ text.attr('dy', linkLabelOffset);
+ el.attr('transform', sus.translate(d.x, d.y));
+ });
+ }
+
+ function labelPoint(linkPos) {
+ var lengthUpLine = 1 / 3,
+ dx = linkPos.x2 - linkPos.x1,
+ dy = linkPos.y2 - linkPos.y1,
+ movedX = dx * lengthUpLine,
+ movedY = dy * lengthUpLine;
+
+ return {
+ x: movedX,
+ y: movedY
+ };
+ }
+
+ function calcGroupPos(linkPos) {
+ var moved = labelPoint(linkPos);
+ return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
+ }
+
+ // calculates where on the link that the hash line for 5+ label appears
+ function hashAttrs(linkPos) {
+ var hashLength = 25,
+ halfLength = hashLength / 2,
+ dx = linkPos.x2 - linkPos.x1,
+ dy = linkPos.y2 - linkPos.y1,
+ length = Math.sqrt((dx * dx) + (dy * dy)),
+ moveAmtX = (dx / length) * halfLength,
+ moveAmtY = (dy / length) * halfLength,
+ mid = labelPoint(linkPos),
+ angle = Math.atan(dy / dx) + 45;
+
+ return {
+ x1: mid.x - moveAmtX,
+ y1: mid.y - moveAmtY,
+ x2: mid.x + moveAmtX,
+ y2: mid.y + moveAmtY,
+ stroke: api.linkConfig()[ts.theme()].baseColor,
+ transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
+ };
+ }
+
+ function textLabelPos(linkPos) {
+ var point = labelPoint(linkPos),
+ dist = 20;
+ return {
+ x: point.x + dist,
+ y: point.y + dist
+ };
+ }
+
+ function applyNumLinkLabels(data, lblsG) {
+ var labels = lblsG.selectAll('g.numLinkLabel')
+ .data(data, function (d) { return 'pair-' + d.id; }),
+ entering;
+
+ // update existing labels
+ labels.each(function (d) {
+ var el = d3.select(this);
+
+ el.attr({
+ transform: function (d) { return calcGroupPos(d.linkCoords); }
+ });
+ el.select('line')
+ .attr(hashAttrs(d.linkCoords));
+ el.select('text')
+ .attr(textLabelPos(d.linkCoords))
+ .text(d.num);
+ });
+
+ // add new labels
+ entering = labels
+ .enter()
+ .append('g')
+ .attr({
+ transform: function (d) { return calcGroupPos(d.linkCoords); },
+ id: function (d) { return 'pair-' + d.id; }
+ })
+ .classed('numLinkLabel', true);
+
+ entering.each(function (d) {
+ var el = d3.select(this);
+
+ el.append('line')
+ .classed('numLinkHash', true)
+ .attr(hashAttrs(d.linkCoords));
+ el.append('text')
+ .classed('numLinkText', true)
+ .attr(textLabelPos(d.linkCoords))
+ .text(d.num);
+ });
+
+ // remove old labels
+ labels.exit().remove();
+ }
+
+ // ==========================
+ // Module definition
+
+ angular.module('ovTopo')
+ .factory('TopoD3Service',
+ ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
+
+ function (_$log_, _fs_, _sus_, _is_, _ts_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+ is = _is_;
+ ts = _ts_;
+
+ icfg = is.iconConfig();
+
+ function initD3(_api_) {
+ api = _api_;
+ }
+
+ function destroyD3() { }
+
+ return {
+ initD3: initD3,
+ destroyD3: destroyD3,
+
+ incDevLabIndex: incDevLabIndex,
+ adjustRectToFitText: adjustRectToFitText,
+ hostLabel: hostLabel,
+ deviceLabel: deviceLabel,
+ trimLabel: trimLabel,
+
+ updateDeviceLabel: updateDeviceLabel,
+ updateHostLabel: updateHostLabel,
+ updateDeviceColors: updateDeviceColors,
+
+ deviceExisting: deviceExisting,
+ hostExisting: hostExisting,
+ deviceEnter: deviceEnter,
+ hostEnter: hostEnter,
+ hostExit: hostExit,
+ deviceExit: deviceExit,
+
+ linkEntering: linkEntering,
+ applyLinkLabels: applyLinkLabels,
+ transformLabel: transformLabel,
+ applyPortLabels: applyPortLabels,
+ applyNumLinkLabels: applyNumLinkLabels
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js
new file mode 100644
index 00000000..5fd38bf6
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Event Module.
+
+ Defines the conduit between the client and the server:
+ - provides a clean API for sending events to the server
+ - dispatches incoming events from the server to the appropriate sub-module
+
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $interval, wss, tps, tis, tfs, tss, tov, tspr;
+
+ // internal state
+ var handlerMap,
+ openListener,
+ heartbeatTimer;
+
+ var heartbeatPeriod = 9000; // 9 seconds
+
+ // ==========================
+
+ function createHandlerMap() {
+ handlerMap = {
+ showSummary: tps,
+
+ showDetails: tss,
+
+ showHighlights: tov,
+
+ addInstance: tis,
+ updateInstance: tis,
+ removeInstance: tis,
+
+ addDevice: tfs,
+ updateDevice: tfs,
+ removeDevice: tfs,
+ addHost: tfs,
+ updateHost: tfs,
+ removeHost: tfs,
+ addLink: tfs,
+ updateLink: tfs,
+ removeLink: tfs,
+
+ spriteListResponse: tspr,
+ spriteDataResponse: tspr
+ };
+ }
+
+ function wsOpen(host, url) {
+ $log.debug('TOPO: web socket open - cluster node:', host, 'URL:', url);
+ // Request batch of initial data from the new server
+ wss.sendEvent('topoStart');
+ }
+
+ function cancelHeartbeat() {
+ if (heartbeatTimer) {
+ $interval.cancel(heartbeatTimer);
+ }
+ heartbeatTimer = null;
+ }
+
+ function scheduleHeartbeat() {
+ cancelHeartbeat();
+ heartbeatTimer = $interval(function () {
+ wss.sendEvent('topoHeartbeat');
+ }, heartbeatPeriod);
+ }
+
+
+ angular.module('ovTopo')
+ .factory('TopoEventService',
+ ['$log', '$interval', 'WebSocketService',
+ 'TopoPanelService', 'TopoInstService', 'TopoForceService',
+ 'TopoSelectService', 'TopoOverlayService', 'TopoSpriteService',
+
+ function (_$log_, _$interval_, _wss_,
+ _tps_, _tis_, _tfs_, _tss_, _tov_, _tspr_) {
+ $log = _$log_;
+ $interval = _$interval_;
+ wss = _wss_;
+ tps = _tps_;
+ tis = _tis_;
+ tfs = _tfs_;
+ tss = _tss_;
+ tov = _tov_;
+ tspr = _tspr_;
+
+ createHandlerMap();
+
+ function start() {
+ openListener = wss.addOpenListener(wsOpen);
+ wss.bindHandlers(handlerMap);
+ wss.sendEvent('topoStart');
+ scheduleHeartbeat();
+ $log.debug('topo comms started');
+ }
+
+ function stop() {
+ cancelHeartbeat();
+ wss.sendEvent('topoStop');
+ wss.unbindHandlers(handlerMap);
+ wss.removeOpenListener(openListener);
+ openListener = null;
+ $log.debug('topo comms stopped');
+ }
+
+ return {
+ start: start,
+ stop: stop
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js
new file mode 100644
index 00000000..f9b96ae8
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology layer filtering Module.
+ Provides functionality to visually differentiate between the packet and
+ optical layers of the topology.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, flash, tps, tts;
+
+ // api to topoForce
+ var api;
+ /*
+ node() // get ref to D3 selection of nodes
+ link() // get ref to D3 selection of links
+ */
+
+ var smax = 'suppressedmax';
+
+ // which "layer" a particular item "belongs to"
+ var layerLookup = {
+ host: {
+ endstation: 'pkt', // default, if host event does not define type
+ router: 'pkt',
+ bgpSpeaker: 'pkt'
+ },
+ device: {
+ switch: 'pkt',
+ roadm: 'opt'
+ },
+ link: {
+ hostLink: 'pkt',
+ direct: 'pkt',
+ indirect: '',
+ tunnel: '',
+ optical: 'opt'
+ }
+ },
+ // order of layer cycling in button
+ dispatch = [
+ {
+ type: 'all',
+ action: function () { suppressLayers(false); },
+ msg: 'All Layers Shown'
+ },
+ {
+ type: 'pkt',
+ action: function () { showLayer('pkt'); },
+ msg: 'Packet Layer Shown'
+ },
+ {
+ type: 'opt',
+ action: function () { showLayer('opt'); },
+ msg: 'Optical Layer Shown'
+ }
+ ],
+ layer = 0;
+
+ function clickAction() {
+ layer = (layer + 1) % dispatch.length;
+ dispatch[layer].action();
+ flash.flash(dispatch[layer].msg);
+ }
+
+ function selected() {
+ return dispatch[layer].type;
+ }
+
+ function inLayer(d, layer) {
+ var type = d.class === 'link' ? d.type() : d.type,
+ look = layerLookup[d.class],
+ lyr = look && look[type];
+ return lyr === layer;
+ }
+
+ function unsuppressLayer(which) {
+ api.node().each(function (d) {
+ var node = d.el;
+ if (inLayer(d, which)) {
+ node.classed(smax, false);
+ }
+ });
+
+ api.link().each(function (d) {
+ var link = d.el;
+ if (inLayer(d, which)) {
+ link.classed(smax, false);
+ }
+ });
+ }
+
+ function suppressLayers(b) {
+ api.node().classed(smax, b);
+ api.link().classed(smax, b);
+ }
+
+ function showLayer(which) {
+ suppressLayers(true);
+ unsuppressLayer(which);
+ }
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoFilterService',
+ ['$log', 'FnService',
+ 'FlashService',
+ 'TopoPanelService',
+ 'TopoTrafficService',
+
+ function (_$log_, _fs_, _flash_, _tps_, _tts_) {
+ $log = _$log_;
+ fs = _fs_;
+ flash = _flash_;
+ tps = _tps_;
+ tts = _tts_;
+
+ function initFilter(_api_) {
+ api = _api_;
+ }
+
+ return {
+ initFilter: initFilter,
+
+ clickAction: clickAction,
+ selected: selected,
+ inLayer: inLayer
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js
new file mode 100644
index 00000000..dbe8f9f5
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -0,0 +1,1134 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Force Module.
+ Visualization of the topology in an SVG layer, using a D3 Force Layout.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $timeout, fs, sus, ts, flash, wss, tov,
+ tis, tms, td3, tss, tts, tos, fltr, tls, uplink, svg;
+
+ // configuration
+ var linkConfig = {
+ light: {
+ baseColor: '#666',
+ inColor: '#66f',
+ outColor: '#f00'
+ },
+ dark: {
+ baseColor: '#aaa',
+ inColor: '#66f',
+ outColor: '#f66'
+ },
+ inWidth: 12,
+ outWidth: 10
+ };
+
+ // internal state
+ var settings, // merged default settings and options
+ force, // force layout object
+ drag, // drag behavior handler
+ network = {
+ nodes: [],
+ links: [],
+ linksByDevice: {},
+ lookup: {},
+ revLinkToKey: {}
+ },
+ lu, // shorthand for lookup
+ rlk, // shorthand for revLinktoKey
+ showHosts = false, // whether hosts are displayed
+ showOffline = true, // whether offline devices are displayed
+ nodeLock = false, // whether nodes can be dragged or not (locked)
+ fTimer, // timer for delayed force layout
+ fNodesTimer, // timer for delayed nodes update
+ fLinksTimer, // timer for delayed links update
+ dim, // the dimensions of the force layout [w,h]
+ linkNums = []; // array of link number labels
+
+ // SVG elements;
+ var linkG, linkLabelG, numLinkLblsG, portLabelG, nodeG;
+
+ // D3 selections;
+ var link, linkLabel, node;
+
+ // default settings for force layout
+ var defaultSettings = {
+ gravity: 0.4,
+ friction: 0.7,
+ charge: {
+ // note: key is node.class
+ device: -8000,
+ host: -5000,
+ _def_: -12000
+ },
+ linkDistance: {
+ // note: key is link.type
+ direct: 100,
+ optical: 120,
+ hostLink: 3,
+ _def_: 50
+ },
+ linkStrength: {
+ // note: key is link.type
+ // range: {0.0 ... 1.0}
+ //direct: 1.0,
+ //optical: 1.0,
+ //hostLink: 1.0,
+ _def_: 1.0
+ }
+ };
+
+
+ // ==========================
+ // === EVENT HANDLERS
+
+ function addDevice(data) {
+ var id = data.id,
+ d;
+
+ uplink.showNoDevs(false);
+
+ // although this is an add device event, if we already have the
+ // device, treat it as an update instead..
+ if (lu[id]) {
+ updateDevice(data);
+ return;
+ }
+
+ d = tms.createDeviceNode(data);
+ network.nodes.push(d);
+ lu[id] = d;
+ updateNodes();
+ fStart();
+ }
+
+ function updateDevice(data) {
+ var id = data.id,
+ d = lu[id],
+ wasOnline;
+
+ if (d) {
+ wasOnline = d.online;
+ angular.extend(d, data);
+ if (tms.positionNode(d, true)) {
+ sendUpdateMeta(d);
+ }
+ updateNodes();
+ if (wasOnline !== d.online) {
+ tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
+ updateOfflineVisibility(d);
+ }
+ }
+ }
+
+ function removeDevice(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ removeDeviceElement(d);
+ }
+ }
+
+ function addHost(data) {
+ var id = data.id,
+ d, lnk;
+
+ // although this is an add host event, if we already have the
+ // host, treat it as an update instead..
+ if (lu[id]) {
+ updateHost(data);
+ return;
+ }
+
+ d = tms.createHostNode(data);
+ network.nodes.push(d);
+ lu[id] = d;
+ updateNodes();
+
+ lnk = tms.createHostLink(data);
+ if (lnk) {
+ d.linkData = lnk; // cache ref on its host
+ network.links.push(lnk);
+ lu[d.ingress] = lnk;
+ lu[d.egress] = lnk;
+ updateLinks();
+ }
+ fStart();
+ }
+
+ function updateHost(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ angular.extend(d, data);
+ if (tms.positionNode(d, true)) {
+ sendUpdateMeta(d);
+ }
+ updateNodes();
+ }
+ }
+
+ function removeHost(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ removeHostElement(d, true);
+ }
+ }
+
+ function addLink(data) {
+ var result = tms.findLink(data, 'add'),
+ bad = result.badLogic,
+ d = result.ldata;
+
+ if (bad) {
+ //logicError(bad + ': ' + link.id);
+ return;
+ }
+
+ if (d) {
+ // we already have a backing store link for src/dst nodes
+ addLinkUpdate(d, data);
+ return;
+ }
+
+ // no backing store link yet
+ d = tms.createLink(data);
+ if (d) {
+ network.links.push(d);
+ aggregateLink(d, data);
+ lu[d.key] = d;
+ updateLinks();
+ fStart();
+ }
+ }
+
+ function updateLink(data) {
+ var result = tms.findLink(data, 'update'),
+ bad = result.badLogic;
+ if (bad) {
+ //logicError(bad + ': ' + link.id);
+ return;
+ }
+ result.updateWith(link);
+ }
+
+ function removeLink(data) {
+ var result = tms.findLink(data, 'remove');
+
+ if (!result.badLogic) {
+ result.removeRawLink();
+ }
+ }
+
+ // ========================
+
+ function nodeById(id) {
+ return lu[id];
+ }
+
+ function makeNodeKey(node1, node2) {
+ return node1 + '-' + node2;
+ }
+
+ function findNodePair(key, keyRev) {
+ if (network.linksByDevice[key]) {
+ return key;
+ } else if (network.linksByDevice[keyRev]) {
+ return keyRev;
+ } else {
+ return false;
+ }
+ }
+
+ function aggregateLink(ldata, link) {
+ var key = makeNodeKey(link.src, link.dst),
+ keyRev = makeNodeKey(link.dst, link.src),
+ found = findNodePair(key, keyRev);
+
+ if (found) {
+ network.linksByDevice[found].push(ldata);
+ ldata.devicePair = found;
+ } else {
+ network.linksByDevice[key] = [ ldata ];
+ ldata.devicePair = key;
+ }
+ }
+
+ function addLinkUpdate(ldata, link) {
+ // add link event, but we already have the reverse link installed
+ ldata.fromTarget = link;
+ rlk[link.id] = ldata.key;
+ // possible solution to el being undefined in restyleLinkElement:
+ //_updateLinks();
+ restyleLinkElement(ldata);
+ }
+
+
+ var widthRatio = 1.4,
+ linkScale = d3.scale.linear()
+ .domain([1, 12])
+ .range([widthRatio, 12 * widthRatio])
+ .clamp(true),
+ allLinkTypes = 'direct indirect optical tunnel';
+
+ function restyleLinkElement(ldata, immediate) {
+ // this fn's job is to look at raw links and decide what svg classes
+ // need to be applied to the line element in the DOM
+ var th = ts.theme(),
+ el = ldata.el,
+ type = ldata.type(),
+ lw = ldata.linkWidth(),
+ online = ldata.online(),
+ delay = immediate ? 0 : 1000;
+
+ // FIXME: understand why el is sometimes undefined on addLink events...
+ // Investigated:
+ // el is undefined when it's a reverse link that is being added.
+ // updateLinks (which sets ldata.el) isn't called before this is called.
+ // Calling _updateLinks in addLinkUpdate fixes it, but there might be
+ // a more efficient way to fix it.
+ if (el && !el.empty()) {
+ el.classed('link', true);
+ el.classed('inactive', !online);
+ el.classed(allLinkTypes, false);
+ if (type) {
+ el.classed(type, true);
+ }
+ el.transition()
+ .duration(delay)
+ .attr('stroke-width', linkScale(lw))
+ .attr('stroke', linkConfig[th].baseColor);
+ }
+ }
+
+ function removeLinkElement(d) {
+ var idx = fs.find(d.key, network.links, 'key'),
+ removed;
+ if (idx >=0) {
+ // remove from links array
+ removed = network.links.splice(idx, 1);
+ // remove from lookup cache
+ delete lu[removed[0].key];
+ updateLinks();
+ fResume();
+ }
+ }
+
+ function removeHostElement(d, upd) {
+ // first, remove associated hostLink...
+ removeLinkElement(d.linkData);
+
+ // remove hostLink bindings
+ delete lu[d.ingress];
+ delete lu[d.egress];
+
+ // remove from lookup cache
+ delete lu[d.id];
+ // remove from nodes array
+ var idx = fs.find(d.id, network.nodes);
+ network.nodes.splice(idx, 1);
+
+ // remove from SVG
+ // NOTE: upd is false if we were called from removeDeviceElement()
+ if (upd) {
+ updateNodes();
+ fResume();
+ }
+ }
+
+ function removeDeviceElement(d) {
+ var id = d.id,
+ idx;
+ // first, remove associated hosts and links..
+ tms.findAttachedHosts(id).forEach(removeHostElement);
+ tms.findAttachedLinks(id).forEach(removeLinkElement);
+
+ // remove from lookup cache
+ delete lu[id];
+ // remove from nodes array
+ idx = fs.find(id, network.nodes);
+ if (idx > -1) {
+ network.nodes.splice(idx, 1);
+ }
+
+ if (!network.nodes.length) {
+ uplink.showNoDevs(true);
+ }
+
+ // remove from SVG
+ updateNodes();
+ fResume();
+ }
+
+ function updateHostVisibility() {
+ sus.visible(nodeG.selectAll('.host'), showHosts);
+ sus.visible(linkG.selectAll('.hostLink'), showHosts);
+ sus.visible(linkLabelG.selectAll('.hostLinkLabel'), showHosts);
+ }
+
+ function updateOfflineVisibility(dev) {
+ function updDev(d, show) {
+ var b;
+ sus.visible(d.el, show);
+
+ tms.findAttachedLinks(d.id).forEach(function (link) {
+ b = show && ((link.type() !== 'hostLink') || showHosts);
+ sus.visible(link.el, b);
+ });
+ tms.findAttachedHosts(d.id).forEach(function (host) {
+ b = show && showHosts;
+ sus.visible(host.el, b);
+ });
+ }
+
+ if (dev) {
+ // updating a specific device that just toggled off/on-line
+ updDev(dev, dev.online || showOffline);
+ } else {
+ // updating all offline devices
+ tms.findDevices(true).forEach(function (d) {
+ updDev(d, showOffline);
+ });
+ }
+ }
+
+
+ function sendUpdateMeta(d, clearPos) {
+ var metaUi = {},
+ ll;
+
+ // if we are not clearing the position data (unpinning),
+ // attach the x, y, longitude, latitude...
+ if (!clearPos) {
+ ll = tms.lngLatFromCoord([d.x, d.y]);
+ metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]};
+ }
+ d.metaUi = metaUi;
+ wss.sendEvent('updateMeta', {
+ id: d.id,
+ class: d.class,
+ memento: metaUi
+ });
+ }
+
+
+ function mkSvgClass(d) {
+ return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
+ }
+
+ function vis(b) {
+ return b ? 'visible' : 'hidden';
+ }
+
+ function toggleHosts(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showHosts : !!x;
+
+ showHosts = on;
+ updateHostVisibility();
+ flash.flash('Hosts ' + vis(on));
+ return on;
+ }
+
+ function toggleOffline(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showOffline : !!x;
+
+ showOffline = on;
+ updateOfflineVisibility();
+ flash.flash('Offline devices ' + vis(on));
+ return on;
+ }
+
+ function cycleDeviceLabels() {
+ flash.flash(td3.incDevLabIndex());
+ tms.findDevices().forEach(function (d) {
+ td3.updateDeviceLabel(d);
+ });
+ }
+
+ function unpin() {
+ var hov = tss.hovered();
+ if (hov) {
+ sendUpdateMeta(hov, true);
+ hov.fixed = false;
+ hov.el.classed('fixed', false);
+ fResume();
+ }
+ }
+
+ function showMastership(masterId) {
+ if (!masterId) {
+ restoreLayerState();
+ } else {
+ showMastershipFor(masterId);
+ }
+ }
+
+ function restoreLayerState() {
+ // NOTE: this level of indirection required, for when we have
+ // the layer filter functionality re-implemented
+ suppressLayers(false);
+ }
+
+ function showMastershipFor(id) {
+ suppressLayers(true);
+ node.each(function (n) {
+ if (n.master === id) {
+ n.el.classed('suppressedmax', false);
+ }
+ });
+ }
+
+ function supAmt(less) {
+ return less ? "suppressed" : "suppressedmax";
+ }
+
+ function suppressLayers(b, less) {
+ var cls = supAmt(less);
+ node.classed(cls, b);
+ link.classed(cls, b);
+ }
+
+ function unsuppressNode(id, less) {
+ var cls = supAmt(less);
+ node.each(function (n) {
+ if (n.id === id) {
+ n.el.classed(cls, false);
+ }
+ });
+ }
+
+ function unsuppressLink(key, less) {
+ var cls = supAmt(less);
+ link.each(function (n) {
+ if (n.key === key) {
+ n.el.classed(cls, false);
+ }
+ });
+ }
+
+ function showBadLinks() {
+ var badLinks = tms.findBadLinks();
+ flash.flash('Bad Links: ' + badLinks.length);
+ $log.debug('Bad Link List (' + badLinks.length + '):');
+ badLinks.forEach(function (d) {
+ $log.debug('bad link: (' + d.bad + ') ' + d.key, d);
+ if (d.el) {
+ d.el.attr('stroke-width', linkScale(2.8))
+ .attr('stroke', 'red');
+ }
+ });
+ // back to normal after 2 seconds...
+ $timeout(updateLinks, 2000);
+ }
+
+ // ==========================================
+
+ function updateNodes() {
+ if (fNodesTimer) {
+ $timeout.cancel(fNodesTimer);
+ }
+ fNodesTimer = $timeout(_updateNodes, 150);
+ }
+
+ // IMPLEMENTATION NOTE: _updateNodes() should NOT stop, start, or resume
+ // the force layout; that needs to be determined and implemented elsewhere
+ function _updateNodes() {
+ // select all the nodes in the layout:
+ node = nodeG.selectAll('.node')
+ .data(network.nodes, function (d) { return d.id; });
+
+ // operate on existing nodes:
+ node.filter('.device').each(td3.deviceExisting);
+ node.filter('.host').each(td3.hostExisting);
+
+ // operate on entering nodes:
+ var entering = node.enter()
+ .append('g')
+ .attr({
+ id: function (d) { return sus.safeId(d.id); },
+ class: mkSvgClass,
+ transform: function (d) {
+ // Need to guard against NaN here ??
+ return sus.translate(d.x, d.y);
+ },
+ opacity: 0
+ })
+ .call(drag)
+ .on('mouseover', tss.nodeMouseOver)
+ .on('mouseout', tss.nodeMouseOut)
+ .transition()
+ .attr('opacity', 1);
+
+ // augment entering nodes:
+ entering.filter('.device').each(td3.deviceEnter);
+ entering.filter('.host').each(td3.hostEnter);
+
+ // operate on both existing and new nodes:
+ td3.updateDeviceColors();
+
+ // operate on exiting nodes:
+ // Note that the node is removed after 2 seconds.
+ // Sub element animations should be shorter than 2 seconds.
+ var exiting = node.exit()
+ .transition()
+ .duration(2000)
+ .style('opacity', 0)
+ .remove();
+
+ // exiting node specifics:
+ exiting.filter('.host').each(td3.hostExit);
+ exiting.filter('.device').each(td3.deviceExit);
+ }
+
+ // ==========================
+
+ function getDefaultPos(link) {
+ return {
+ x1: link.source.x,
+ y1: link.source.y,
+ x2: link.target.x,
+ y2: link.target.y
+ };
+ }
+
+ // returns amount of adjustment along the normal for given link
+ function amt(numLinks, linkIdx) {
+ var gap = 6;
+ return (linkIdx - ((numLinks - 1) / 2)) * gap;
+ }
+
+ function calcMovement(d, amt, flipped) {
+ var pos = getDefaultPos(d),
+ mult = flipped ? -amt : amt,
+ dx = pos.x2 - pos.x1,
+ dy = pos.y2 - pos.y1,
+ length = Math.sqrt((dx * dx) + (dy * dy));
+
+ return {
+ x1: pos.x1 + (mult * dy / length),
+ y1: pos.y1 + (mult * -dx / length),
+ x2: pos.x2 + (mult * dy / length),
+ y2: pos.y2 + (mult * -dx / length)
+ };
+ }
+
+ function calcPosition() {
+ var lines = this,
+ linkSrcId;
+ linkNums = [];
+ lines.each(function (d) {
+ if (d.type() === 'hostLink') {
+ d.position = getDefaultPos(d);
+ }
+ });
+
+ function normalizeLinkSrc(link) {
+ // ensure source device is consistent across set of links
+ // temporary measure until link modeling is refactored
+ if (!linkSrcId) {
+ linkSrcId = link.source.id;
+ return false;
+ }
+
+ return link.source.id !== linkSrcId;
+ }
+
+ angular.forEach(network.linksByDevice, function (linkArr, key) {
+ var numLinks = linkArr.length,
+ link;
+
+ if (numLinks === 1) {
+ link = linkArr[0];
+ link.position = getDefaultPos(link);
+ link.position.multiLink = false;
+ } else if (numLinks >= 5) {
+ // this code is inefficient, in the future the way links
+ // are modeled will be changed
+ angular.forEach(linkArr, function (link) {
+ link.position = getDefaultPos(link);
+ link.position.multiLink = true;
+ });
+ linkNums.push({
+ id: key,
+ num: numLinks,
+ linkCoords: linkArr[0].position
+ });
+ } else {
+ linkSrcId = null;
+ angular.forEach(linkArr, function (link, index) {
+ var offsetAmt = amt(numLinks, index),
+ needToFlip = normalizeLinkSrc(link);
+ link.position = calcMovement(link, offsetAmt, needToFlip);
+ link.position.multiLink = false;
+ });
+ }
+ });
+ }
+
+ function updateLinks() {
+ if (fLinksTimer) {
+ $timeout.cancel(fLinksTimer);
+ }
+ fLinksTimer = $timeout(_updateLinks, 150);
+ }
+
+ // IMPLEMENTATION NOTE: _updateLinks() should NOT stop, start, or resume
+ // the force layout; that needs to be determined and implemented elsewhere
+ function _updateLinks() {
+ var th = ts.theme();
+
+ link = linkG.selectAll('.link')
+ .data(network.links, function (d) { return d.key; });
+
+ // operate on existing links:
+ link.each(function (d) {
+ // this is supposed to be an existing link, but we have observed
+ // occasions (where links are deleted and added rapidly?) where
+ // the DOM element has not been defined. So protect against that...
+ if (d.el) {
+ restyleLinkElement(d, true);
+ }
+ });
+
+ // operate on entering links:
+ var entering = link.enter()
+ .append('line')
+ .call(calcPosition)
+ .attr({
+ x1: function (d) { return d.position.x1; },
+ y1: function (d) { return d.position.y1; },
+ x2: function (d) { return d.position.x2; },
+ y2: function (d) { return d.position.y2; },
+ stroke: linkConfig[th].inColor,
+ 'stroke-width': linkConfig.inWidth
+ });
+
+ // augment links
+ entering.each(td3.linkEntering);
+
+ // operate on both existing and new links:
+ //link.each(...)
+
+ // add labels for how many links are in a thick line
+ td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+
+ // apply or remove labels
+ td3.applyLinkLabels();
+
+ // operate on exiting links:
+ link.exit()
+ .attr('stroke-dasharray', '3 3')
+ .attr('stroke', linkConfig[th].outColor)
+ .style('opacity', 0.5)
+ .transition()
+ .duration(1500)
+ .attr({
+ 'stroke-dasharray': '3 12',
+ 'stroke-width': linkConfig.outWidth
+ })
+ .style('opacity', 0.0)
+ .remove();
+ }
+
+
+ // ==========================
+ // force layout tick function
+
+ function fResume() {
+ if (!tos.isOblique()) {
+ force.resume();
+ }
+ }
+
+ function fStart() {
+ if (!tos.isOblique()) {
+ if (fTimer) {
+ $timeout.cancel(fTimer);
+ }
+ fTimer = $timeout(function () {
+ $log.debug("Starting force-layout");
+ force.start();
+ }, 200);
+ }
+ }
+
+ var tickStuff = {
+ nodeAttr: {
+ transform: function (d) {
+ var dx = isNaN(d.x) ? 0 : d.x,
+ dy = isNaN(d.y) ? 0 : d.y;
+ return sus.translate(dx, dy);
+ }
+ },
+ linkAttr: {
+ x1: function (d) { return d.position.x1; },
+ y1: function (d) { return d.position.y1; },
+ x2: function (d) { return d.position.x2; },
+ y2: function (d) { return d.position.y2; }
+ },
+ linkLabelAttr: {
+ transform: function (d) {
+ var lnk = tms.findLinkById(d.key);
+ if (lnk) {
+ return td3.transformLabel(lnk.position);
+ }
+ }
+ }
+ };
+
+ function tick() {
+ // guard against null (which can happen when our view pages out)...
+ if (node && node.size()) {
+ node.attr(tickStuff.nodeAttr);
+ }
+ if (link && link.size()) {
+ link.call(calcPosition)
+ .attr(tickStuff.linkAttr);
+ td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+ }
+ if (linkLabel && linkLabel.size()) {
+ linkLabel.attr(tickStuff.linkLabelAttr);
+ }
+ }
+
+
+ // ==========================
+ // === MOUSE GESTURE HANDLERS
+
+ function zoomingOrPanning(ev) {
+ return ev.metaKey || ev.altKey;
+ }
+
+ function atDragEnd(d) {
+ // once we've finished moving, pin the node in position
+ d.fixed = true;
+ d3.select(this).classed('fixed', true);
+ sendUpdateMeta(d);
+ tss.clickConsumed(true);
+ }
+
+ // predicate that indicates when dragging is active
+ function dragEnabled() {
+ var ev = d3.event.sourceEvent;
+ // nodeLock means we aren't allowing nodes to be dragged...
+ return !nodeLock && !zoomingOrPanning(ev);
+ }
+
+ // predicate that indicates when clicking is active
+ function clickEnabled() {
+ return true;
+ }
+
+ // =============================================
+ // function entry points for overlay module
+
+ // TODO: find an automatic way of tracking via the "showHighlights" events
+ var allTrafficClasses = 'primary secondary optical animated ' +
+ 'port-traffic-Kbps port-traffic-Mbps port-traffic-Gbps ' +
+ 'port-traffic-Gbps-choked';
+
+ function clearLinkTrafficStyle() {
+ link.style('stroke-width', null)
+ .classed(allTrafficClasses, false);
+ }
+
+ function removeLinkLabels() {
+ network.links.forEach(function (d) {
+ d.label = '';
+ });
+ }
+
+ function updateLinkLabelModel() {
+ // create the backing data for showing labels..
+ var data = [];
+ link.each(function (d) {
+ if (d.label) {
+ data.push({
+ id: 'lab-' + d.key,
+ key: d.key,
+ label: d.label,
+ ldata: d
+ });
+ }
+ });
+
+ linkLabel = linkLabelG.selectAll('.linkLabel')
+ .data(data, function (d) { return d.id; });
+ }
+
+ // ==========================
+ // Module definition
+
+ function mkModelApi(uplink) {
+ return {
+ projection: uplink.projection,
+ network: network,
+ restyleLinkElement: restyleLinkElement,
+ removeLinkElement: removeLinkElement
+ };
+ }
+
+ function mkD3Api() {
+ return {
+ node: function () { return node; },
+ link: function () { return link; },
+ linkLabel: function () { return linkLabel; },
+ instVisible: function () { return tis.isVisible(); },
+ posNode: tms.positionNode,
+ showHosts: function () { return showHosts; },
+ restyleLinkElement: restyleLinkElement,
+ updateLinkLabelModel: updateLinkLabelModel,
+ linkConfig: function () { return linkConfig; }
+ };
+ }
+
+ function mkSelectApi() {
+ return {
+ node: function () { return node; },
+ zoomingOrPanning: zoomingOrPanning,
+ updateDeviceColors: td3.updateDeviceColors,
+ deselectLink: tls.deselectLink
+ };
+ }
+
+ function mkTrafficApi() {
+ return {
+ hovered: tss.hovered,
+ somethingSelected: tss.somethingSelected,
+ selectOrder: tss.selectOrder
+ };
+ }
+
+ function mkOverlayApi() {
+ return {
+ clearLinkTrafficStyle: clearLinkTrafficStyle,
+ removeLinkLabels: removeLinkLabels,
+ findLinkById: tms.findLinkById,
+ findNodeById: nodeById,
+ updateLinks: updateLinks,
+ updateNodes: updateNodes,
+ supLayers: suppressLayers,
+ unsupNode: unsuppressNode,
+ unsupLink: unsuppressLink
+ };
+ }
+
+ function mkObliqueApi(uplink, fltr) {
+ return {
+ force: function() { return force; },
+ zoomLayer: uplink.zoomLayer,
+ nodeGBBox: function() { return nodeG.node().getBBox(); },
+ node: function () { return node; },
+ link: function () { return link; },
+ linkLabel: function () { return linkLabel; },
+ nodes: function () { return network.nodes; },
+ tickStuff: tickStuff,
+ nodeLock: function (b) {
+ var old = nodeLock;
+ nodeLock = b;
+ return old;
+ },
+ opacifyMap: uplink.opacifyMap,
+ inLayer: fltr.inLayer,
+ calcLinkPos: calcPosition,
+ applyNumLinkLabels: function () {
+ td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+ }
+ };
+ }
+
+ function mkFilterApi() {
+ return {
+ node: function () { return node; },
+ link: function () { return link; }
+ };
+ }
+
+ function mkLinkApi(svg, uplink) {
+ return {
+ svg: svg,
+ zoomer: uplink.zoomer(),
+ network: network,
+ portLabelG: function () { return portLabelG; },
+ showHosts: function () { return showHosts; }
+ };
+ }
+
+ angular.module('ovTopo')
+ .factory('TopoForceService',
+ ['$log', '$timeout', 'FnService', 'SvgUtilService',
+ 'ThemeService', 'FlashService', 'WebSocketService',
+ 'TopoOverlayService', 'TopoInstService', 'TopoModelService',
+ 'TopoD3Service', 'TopoSelectService', 'TopoTrafficService',
+ 'TopoObliqueService', 'TopoFilterService', 'TopoLinkService',
+
+ function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_, _tov_,
+ _tis_, _tms_, _td3_, _tss_, _tts_, _tos_, _fltr_, _tls_) {
+ $log = _$log_;
+ $timeout = _$timeout_;
+ fs = _fs_;
+ sus = _sus_;
+ ts = _ts_;
+ flash = _flash_;
+ wss = _wss_;
+ tov = _tov_;
+ tis = _tis_;
+ tms = _tms_;
+ td3 = _td3_;
+ tss = _tss_;
+ tts = _tts_;
+ tos = _tos_;
+ fltr = _fltr_;
+ tls = _tls_;
+
+ var themeListener = ts.addListener(function () {
+ updateLinks();
+ updateNodes();
+ });
+
+ // forceG is the SVG group to display the force layout in
+ // uplink is the api from the main topo source file
+ // dim is the initial dimensions of the SVG as [w,h]
+ // opts are, well, optional :)
+ function initForce(_svg_, forceG, _uplink_, _dim_, opts) {
+ uplink = _uplink_;
+ dim = _dim_;
+ svg = _svg_;
+
+ lu = network.lookup;
+ rlk = network.revLinkToKey;
+
+ $log.debug('initForce().. dim = ' + dim);
+
+ tov.setApi(mkOverlayApi(), tss);
+ tms.initModel(mkModelApi(uplink), dim);
+ td3.initD3(mkD3Api());
+ tss.initSelect(mkSelectApi());
+ tts.initTraffic(mkTrafficApi());
+ tos.initOblique(mkObliqueApi(uplink, fltr));
+ fltr.initFilter(mkFilterApi());
+ tls.initLink(mkLinkApi(svg, uplink), td3);
+
+ settings = angular.extend({}, defaultSettings, opts);
+
+ linkG = forceG.append('g').attr('id', 'topo-links');
+ linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
+ numLinkLblsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
+ nodeG = forceG.append('g').attr('id', 'topo-nodes');
+ portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
+
+ link = linkG.selectAll('.link');
+ linkLabel = linkLabelG.selectAll('.linkLabel');
+ node = nodeG.selectAll('.node');
+
+ force = d3.layout.force()
+ .size(dim)
+ .nodes(network.nodes)
+ .links(network.links)
+ .gravity(settings.gravity)
+ .friction(settings.friction)
+ .charge(settings.charge._def_)
+ .linkDistance(settings.linkDistance._def_)
+ .linkStrength(settings.linkStrength._def_)
+ .on('tick', tick);
+
+ drag = sus.createDragBehavior(force,
+ tss.selectObject, atDragEnd, dragEnabled, clickEnabled);
+ }
+
+ function newDim(_dim_) {
+ dim = _dim_;
+ force.size(dim);
+ tms.newDim(dim);
+ }
+
+ function destroyForce() {
+ force.stop();
+
+ tls.destroyLink();
+ tos.destroyOblique();
+ tts.destroyTraffic();
+ tss.destroySelect();
+ td3.destroyD3();
+ tms.destroyModel();
+ // note: no need to destroy overlay service
+ ts.removeListener(themeListener);
+ themeListener = null;
+
+ // clean up the DOM
+ svg.selectAll('g').remove();
+ svg.selectAll('defs').remove();
+
+ // clean up internal state
+ network.nodes = [];
+ network.links = [];
+ network.linksByDevice = {};
+ network.lookup = {};
+ network.revLinkToKey = {};
+
+ linkNums = [];
+
+ linkG = linkLabelG = numLinkLblsG = nodeG = portLabelG = null;
+ link = linkLabel = node = null;
+ force = drag = null;
+
+ // clean up $timeout promises
+ if (fTimer) {
+ $timeout.cancel(fTimer);
+ }
+ if (fNodesTimer) {
+ $timeout.cancel(fNodesTimer);
+ }
+ if (fLinksTimer) {
+ $timeout.cancel(fLinksTimer);
+ }
+ }
+
+ return {
+ initForce: initForce,
+ newDim: newDim,
+ destroyForce: destroyForce,
+
+ updateDeviceColors: td3.updateDeviceColors,
+ toggleHosts: toggleHosts,
+ togglePorts: tls.togglePorts,
+ toggleOffline: toggleOffline,
+ cycleDeviceLabels: cycleDeviceLabels,
+ unpin: unpin,
+ showMastership: showMastership,
+ showBadLinks: showBadLinks,
+
+ addDevice: addDevice,
+ updateDevice: updateDevice,
+ removeDevice: removeDevice,
+ addHost: addHost,
+ updateHost: updateHost,
+ removeHost: removeHost,
+ addLink: addLink,
+ updateLink: updateLink,
+ removeLink: removeLink
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js
new file mode 100644
index 00000000..7e929977
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Instances Module.
+ Defines modeling of ONOS instances.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, ps, sus, gs, ts, fs, flash;
+
+ // api from topo
+ var api;
+ /*
+ showMastership( id )
+ */
+
+ // configuration
+ var instCfg = {
+ rectPad: 8,
+ nodeOx: 9,
+ nodeOy: 9,
+ nodeDim: 40,
+ birdOx: 19,
+ birdOy: 21,
+ birdDim: 21,
+ uiDy: 45,
+ titleDy: 30,
+ textYOff: 20,
+ textYSpc: 15
+ },
+ showLogicErrors = true,
+ idIns = 'topo-p-instance',
+ instOpts = {
+ edge: 'left',
+ width: 20
+ };
+
+ // internal state
+ var onosInstances,
+ onosOrder,
+ oiShowMaster,
+ oiBox,
+ themeListener;
+
+
+ // ==========================
+
+ function addInstance(data) {
+ var id = data.id;
+
+ if (onosInstances[id]) {
+ updateInstance(data);
+ return;
+ }
+ onosInstances[id] = data;
+ onosOrder.push(data);
+ updateInstances();
+ }
+
+ function updateInstance(data) {
+ var id = data.id,
+ d = onosInstances[id];
+ if (d) {
+ angular.extend(d, data);
+ updateInstances();
+ } else {
+ logicError('updateInstance: lookup fail: ID = "' + id + '"');
+ }
+ }
+
+ function removeInstance(data) {
+ var id = data.id,
+ d = onosInstances[id];
+ if (d) {
+ var idx = fs.find(id, onosOrder);
+ if (idx >= 0) {
+ onosOrder.splice(idx, 1);
+ }
+ delete onosInstances[id];
+ updateInstances();
+ } else {
+ logicError('removeInstance lookup fail. ID = "' + id + '"');
+ }
+ }
+
+ // ==========================
+
+ function computeDim(self) {
+ var css = window.getComputedStyle(self);
+ return {
+ w: sus.stripPx(css.width),
+ h: sus.stripPx(css.height)
+ };
+ }
+
+ function clickInst(d) {
+ var el = d3.select(this),
+ aff = el.classed('affinity');
+ if (!aff) {
+ setAffinity(el, d);
+ } else {
+ cancelAffinity();
+ }
+ }
+
+ function setAffinity(el, d) {
+ d3.selectAll('.onosInst')
+ .classed('mastership', true)
+ .classed('affinity', false);
+ el.classed('affinity', true);
+
+ // suppress all elements except nodes whose master is this instance
+ api.showMastership(d.id);
+ oiShowMaster = true;
+ }
+
+ function cancelAffinity() {
+ d3.selectAll('.onosInst')
+ .classed('mastership affinity', false);
+
+ api.showMastership(null);
+ oiShowMaster = false;
+ }
+
+ function instRectAttr(dim) {
+ var pad = instCfg.rectPad;
+ return {
+ x: pad,
+ y: pad,
+ width: dim.w - pad*2,
+ height: dim.h - pad*2,
+ rx: 6
+ };
+ }
+
+ function viewBox(dim) {
+ return '0 0 ' + dim.w + ' ' + dim.h;
+ }
+
+ function attachUiBadge(svg) {
+ gs.addGlyph(svg, 'uiAttached', 30, true, [12, instCfg.uiDy])
+ .classed('badgeIcon uiBadge', true);
+ }
+
+ function instColor(id, online) {
+ return sus.cat7().getColor(id, !online, ts.theme());
+ }
+
+ // ==============================
+
+ function updateInstances() {
+ var onoses = oiBox.el().selectAll('.onosInst')
+ .data(onosOrder, function (d) { return d.id; }),
+ instDim = {w:0,h:0},
+ c = instCfg;
+
+ function nSw(n) {
+ return '# Switches: ' + n;
+ }
+
+ // operate on existing onos instances if necessary
+ onoses.each(function (d) {
+ var el = d3.select(this),
+ svg = el.select('svg');
+ instDim = computeDim(this);
+
+ // update online state
+ el.classed('online', d.online);
+
+ // update ui-attached state
+ svg.select('use.uiBadge').remove();
+ if (d.uiAttached) {
+ attachUiBadge(svg);
+ }
+
+ function updAttr(id, value) {
+ svg.select('text.instLabel.'+id).text(value);
+ }
+
+ updAttr('ip', d.ip);
+ updAttr('ns', nSw(d.switches));
+ });
+
+
+ // operate on new onos instances
+ var entering = onoses.enter()
+ .append('div')
+ .attr('class', 'onosInst')
+ .classed('online', function (d) { return d.online; })
+ .on('click', clickInst);
+
+ entering.each(function (d) {
+ var el = d3.select(this),
+ rectAttr,
+ svg;
+ instDim = computeDim(this);
+ rectAttr = instRectAttr(instDim);
+
+ svg = el.append('svg').attr({
+ width: instDim.w,
+ height: instDim.h,
+ viewBox: viewBox(instDim)
+ });
+
+ svg.append('rect').attr(rectAttr);
+
+ gs.addGlyph(svg, 'bird', 28, true, [14, 14])
+ .classed('badgeIcon', true);
+
+ if (d.uiAttached) {
+ attachUiBadge(svg);
+ }
+
+ var left = c.nodeOx + c.nodeDim,
+ len = rectAttr.width - left,
+ hlen = len / 2,
+ midline = hlen + left;
+
+ // title
+ svg.append('text')
+ .attr({
+ class: 'instTitle',
+ x: midline,
+ y: c.titleDy
+ })
+ .text(d.id);
+
+ // a couple of attributes
+ var ty = c.titleDy + c.textYOff;
+
+ function addAttr(id, label) {
+ svg.append('text').attr({
+ class: 'instLabel ' + id,
+ x: midline,
+ y: ty
+ }).text(label);
+ ty += c.textYSpc;
+ }
+
+ addAttr('ip', d.ip);
+ addAttr('ns', nSw(d.switches));
+ });
+
+ // operate on existing + new onoses here
+ // set the affinity colors...
+ onoses.each(function (d) {
+ var el = d3.select(this),
+ rect = el.select('svg').select('rect'),
+ col = instColor(d.id, d.online);
+ rect.style('fill', col);
+ });
+
+ // adjust the panel size appropriately...
+ oiBox.width(instDim.w * onosOrder.length);
+ oiBox.height(instDim.h);
+
+ // remove any outgoing instances
+ onoses.exit().remove();
+ }
+
+
+ // ==========================
+
+ function logicError(msg) {
+ if (showLogicErrors) {
+ $log.warn('TopoInstService: ' + msg);
+ }
+ }
+
+ function initInst(_api_) {
+ api = _api_;
+ oiBox = ps.createPanel(idIns, instOpts);
+ oiBox.show();
+
+ onosInstances = {};
+ onosOrder = [];
+ oiShowMaster = false;
+
+ // we want to update the instances, each time the theme changes
+ themeListener = ts.addListener(updateInstances);
+ }
+
+ function destroyInst() {
+ ts.removeListener(themeListener);
+ themeListener = null;
+
+ ps.destroyPanel(idIns);
+ oiBox = null;
+
+ onosInstances = {};
+ onosOrder = [];
+ oiShowMaster = false;
+ }
+
+ function showInsts() {
+ oiBox.show();
+ }
+
+ function hideInsts() {
+ oiBox.hide();
+ }
+
+ function toggleInsts(x) {
+ var kev = (x === 'keyev'),
+ on,
+ verb;
+
+ if (kev) {
+ on = oiBox.toggle();
+ } else {
+ on = !!x;
+ if (on) {
+ showInsts();
+ } else {
+ hideInsts();
+ }
+ }
+ verb = on ? 'Show' : 'Hide';
+ flash.flash(verb + ' instances panel');
+ return on;
+ }
+
+ // ==========================
+
+ angular.module('ovTopo')
+ .factory('TopoInstService',
+ ['$log', 'PanelService', 'SvgUtilService', 'GlyphService',
+ 'ThemeService', 'FnService', 'FlashService',
+
+ function (_$log_, _ps_, _sus_, _gs_, _ts_, _fs_, _flash_) {
+ $log = _$log_;
+ ps = _ps_;
+ sus = _sus_;
+ gs = _gs_;
+ ts = _ts_;
+ fs = _fs_;
+ flash = _flash_;
+
+ return {
+ initInst: initInst,
+ destroyInst: destroyInst,
+
+ addInstance: addInstance,
+ updateInstance: updateInstance,
+ removeInstance: removeInstance,
+
+ cancelAffinity: cancelAffinity,
+
+ isVisible: function () { return oiBox.isVisible(); },
+ show: showInsts,
+ hide: hideInsts,
+ toggle: toggleInsts,
+ showMaster: function () { return oiShowMaster; }
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js
new file mode 100644
index 00000000..38f3a6c3
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Link Module.
+ Functions for highlighting/selecting links
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, sus, ts, flash, tss, tps;
+
+ // internal state
+ var api,
+ td3,
+ network,
+ showPorts = true, // enable port highlighting by default
+ enhancedLink = null, // the link over which the mouse is hovering
+ selectedLink = null; // the link which is currently selected
+
+ // SVG elements;
+ var svg;
+
+
+ // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ========
+
+ function getLogicalMousePosition(container) {
+ var m = d3.mouse(container),
+ sc = api.zoomer.scale(),
+ tr = api.zoomer.translate(),
+ mx = (m[0] - tr[0]) / sc,
+ my = (m[1] - tr[1]) / sc;
+ return {x: mx, y: my};
+ }
+
+
+ function sq(x) { return x * x; }
+
+ function mdist(p, m) {
+ return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
+ }
+
+ function prox(dist) {
+ return dist / api.zoomer.scale();
+ }
+
+ function computeNearestNode(mouse) {
+ var proximity = prox(30),
+ nearest = null,
+ minDist;
+
+ if (network.nodes.length) {
+ minDist = proximity * 2;
+
+ network.nodes.forEach(function (d) {
+ var dist;
+
+ if (!api.showHosts() && d.class === 'host') {
+ return; // skip hidden hosts
+ }
+
+ dist = mdist({x: d.x, y: d.y}, mouse);
+ if (dist < minDist && dist < proximity) {
+ minDist = dist;
+ nearest = d;
+ }
+ });
+ }
+ return nearest;
+ }
+
+
+ function computeNearestLink(mouse) {
+ var proximity = prox(30),
+ nearest = null,
+ minDist;
+
+ function pdrop(line, mouse) {
+ var x1 = line.x1,
+ y1 = line.y1,
+ x2 = line.x2,
+ y2 = line.y2,
+ x3 = mouse.x,
+ y3 = mouse.y,
+ k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) /
+ (sq(y2-y1) + sq(x2-x1)),
+ x4 = x3 - k * (y2-y1),
+ y4 = y3 + k * (x2-x1);
+ return {x:x4, y:y4};
+ }
+
+ function lineHit(line, p, m) {
+ if (p.x < line.x1 && p.x < line.x2) return false;
+ if (p.x > line.x1 && p.x > line.x2) return false;
+ if (p.y < line.y1 && p.y < line.y2) return false;
+ if (p.y > line.y1 && p.y > line.y2) return false;
+ // line intersects, but are we close enough?
+ return mdist(p, m) <= proximity;
+ }
+
+ if (network.links.length) {
+ minDist = proximity * 2;
+
+ network.links.forEach(function (d) {
+ if (!api.showHosts() && d.type() === 'hostLink') {
+ return; // skip hidden host links
+ }
+
+ var line = d.position,
+ point = pdrop(line, mouse),
+ hit = lineHit(line, point, mouse),
+ dist;
+
+ if (hit) {
+ dist = mdist(point, mouse);
+ if (dist < minDist) {
+ minDist = dist;
+ nearest = d;
+ }
+ }
+ });
+ }
+ return nearest;
+ }
+
+ function enhanceLink(ldata) {
+ // if the new link is same as old link, do nothing
+ if (enhancedLink && ldata && enhancedLink.key === ldata.key) return;
+
+ // first, unenhance the currently enhanced link
+ if (enhancedLink) {
+ unenhance(enhancedLink);
+ }
+ enhancedLink = ldata;
+ if (enhancedLink) {
+ enhance(enhancedLink);
+ }
+ }
+
+ function unenhance(d) {
+ // guard against link element not set
+ if (d.el) {
+ d.el.classed('enhanced', false);
+ }
+ api.portLabelG().selectAll('.portLabel').remove();
+ }
+
+ function enhance(d) {
+ var data = [],
+ point;
+
+ // guard against link element not set
+ if (!d.el) return;
+
+ d.el.classed('enhanced', true);
+
+ // Define port label data objects.
+ // NOTE: src port is absent in the case of host-links.
+
+ point = locatePortLabel(d);
+ angular.extend(point, {
+ id: 'topo-port-tgt',
+ num: d.tgtPort
+ });
+ data.push(point);
+
+ if (d.srcPort) {
+ point = locatePortLabel(d, 1);
+ angular.extend(point, {
+ id: 'topo-port-src',
+ num: d.srcPort
+ });
+ data.push(point);
+ }
+
+ td3.applyPortLabels(data, api.portLabelG());
+ }
+
+ function locatePortLabel(link, src) {
+ var offset = 32,
+ pos = link.position,
+ nearX = src ? pos.x1 : pos.x2,
+ nearY = src ? pos.y1 : pos.y2,
+ farX = src ? pos.x2 : pos.x1,
+ farY = src ? pos.y2 : pos.y1;
+
+ function dist(x, y) { return Math.sqrt(x*x + y*y); }
+
+ var dx = farX - nearX,
+ dy = farY - nearY,
+ k = offset / dist(dx, dy);
+
+ return {x: k * dx + nearX, y: k * dy + nearY};
+ }
+
+ function selectLink(ldata) {
+ // if the new link is same as old link, do nothing
+ if (selectedLink && ldata && selectedLink.key === ldata.key) return;
+
+ // make sure no nodes are selected
+ tss.deselectAll();
+
+ // first, unenhance the currently enhanced link
+ if (selectedLink) {
+ unselLink(selectedLink);
+ }
+ selectedLink = ldata;
+ if (selectedLink) {
+ selLink(selectedLink);
+ }
+ }
+
+ function unselLink(d) {
+ // guard against link element not set
+ if (d.el) {
+ d.el.classed('selected', false);
+ }
+ }
+
+ function selLink(d) {
+ // guard against link element not set
+ if (!d.el) return;
+
+ d.el.classed('selected', true);
+
+ tps.displayLink(d);
+ tps.displaySomething();
+ }
+
+ // ====== MOUSE EVENT HANDLERS ======
+
+ function mouseMoveHandler() {
+ var mp = getLogicalMousePosition(this),
+ link = computeNearestLink(mp);
+ enhanceLink(link);
+ }
+
+ function mouseClickHandler() {
+ var mp, link, node;
+
+ if (!tss.clickConsumed()) {
+ mp = getLogicalMousePosition(this);
+ node = computeNearestNode(mp);
+ if (node) {
+ $log.debug('found nearest node:', node.labels[1]);
+ tss.selectObject(node);
+ } else {
+ link = computeNearestLink(mp);
+ selectLink(link);
+ }
+ }
+ }
+
+
+ // ======================
+
+ function togglePorts(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showPorts : !!x,
+ what = on ? 'Enable' : 'Disable',
+ handler = on ? mouseMoveHandler : null;
+
+ showPorts = on;
+
+ if (!on) {
+ enhanceLink(null);
+ }
+ svg.on('mousemove', handler);
+ flash.flash(what + ' port highlighting');
+ return on;
+ }
+
+ function deselectLink() {
+ if (selectedLink) {
+ unselLink(selectedLink);
+ selectedLink = null;
+ return true;
+ }
+ return false;
+ }
+
+ // ==========================
+ // Module definition
+
+ angular.module('ovTopo')
+ .factory('TopoLinkService',
+ ['$log', 'FnService', 'SvgUtilService', 'ThemeService', 'FlashService',
+ 'TopoSelectService', 'TopoPanelService',
+
+ function (_$log_, _fs_, _sus_, _ts_, _flash_, _tss_, _tps_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+ ts = _ts_;
+ flash = _flash_;
+ tss = _tss_;
+ tps = _tps_;
+
+ function initLink(_api_, _td3_) {
+ api = _api_;
+ td3 = _td3_;
+ svg = api.svg;
+ network = api.network;
+ if (showPorts && !fs.isMobile()) {
+ svg.on('mousemove', mouseMoveHandler);
+ }
+ svg.on('click', mouseClickHandler);
+ }
+
+ function destroyLink() {
+ // unconditionally remove any event handlers
+ svg.on('mousemove', null);
+ svg.on('click', null);
+ }
+
+ return {
+ initLink: initLink,
+ destroyLink: destroyLink,
+ togglePorts: togglePorts,
+ deselectLink: deselectLink
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js
new file mode 100644
index 00000000..fb98fc2b
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js
@@ -0,0 +1,439 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Model Module.
+ Auxiliary functions for the model of the topology; that is, our internal
+ representations of devices, hosts, links, etc.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, rnd;
+
+ // api to topoForce
+ var api;
+ /*
+ projection()
+ network {...}
+ restyleLinkElement( ldata )
+ removeLinkElement( ldata )
+ */
+
+ // shorthand
+ var lu, rlk, nodes, links, linksByDevice;
+
+ var dim; // dimensions of layout [w,h]
+
+ // configuration 'constants'
+ var defaultLinkType = 'direct',
+ nearDist = 15;
+
+
+ function coordFromLngLat(loc) {
+ var p = api.projection();
+ // suspected cause of ONOS-2109
+ return p ? p([loc.lng, loc.lat]) : [0, 0];
+ }
+
+ function lngLatFromCoord(coord) {
+ var p = api.projection();
+ return p ? p.invert(coord) : [0, 0];
+ }
+
+ function positionNode(node, forUpdate) {
+ var meta = node.metaUi,
+ x = meta && meta.x,
+ y = meta && meta.y,
+ xy;
+
+ // If we have [x,y] already, use that...
+ if (x && y) {
+ node.fixed = true;
+ node.px = node.x = x;
+ node.py = node.y = y;
+ return;
+ }
+
+ var location = node.location,
+ coord;
+
+ if (location && location.type === 'latlng') {
+ coord = coordFromLngLat(location);
+ node.fixed = true;
+ node.px = node.x = coord[0];
+ node.py = node.y = coord[1];
+ return true;
+ }
+
+ // if this is a node update (not a node add).. skip randomizer
+ if (forUpdate) {
+ return;
+ }
+
+ // Note: Placing incoming unpinned nodes at exactly the same point
+ // (center of the view) causes them to explode outwards when
+ // the force layout kicks in. So, we spread them out a bit
+ // initially, to provide a more serene layout convergence.
+ // Additionally, if the node is a host, we place it near
+ // the device it is connected to.
+
+ function rand() {
+ return {
+ x: rnd.randDim(dim[0]),
+ y: rnd.randDim(dim[1])
+ };
+ }
+
+ function near(node) {
+ return {
+ x: node.x + nearDist + rnd.spread(nearDist),
+ y: node.y + nearDist + rnd.spread(nearDist)
+ };
+ }
+
+ function getDevice(cp) {
+ var d = lu[cp.device];
+ return d || rand();
+ }
+
+ xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+ angular.extend(node, xy);
+ }
+
+ function mkSvgCls(dh, t, on) {
+ var ndh = 'node ' + dh,
+ ndht = t ? ndh + ' ' + t : ndh;
+ return on ? ndht + ' online' : ndht;
+ }
+
+ function createDeviceNode(device) {
+ var node = device;
+
+ // Augment as needed...
+ node.class = 'device';
+ node.svgClass = mkSvgCls('device', device.type, device.online);
+ positionNode(node);
+ return node;
+ }
+
+ function createHostNode(host) {
+ var node = host;
+
+ // Augment as needed...
+ node.class = 'host';
+ if (!node.type) {
+ node.type = 'endstation';
+ }
+ node.svgClass = mkSvgCls('host', node.type);
+ positionNode(node);
+ return node;
+ }
+
+ function createHostLink(host) {
+ var src = host.id,
+ dst = host.cp.device,
+ id = host.ingress,
+ lnk = linkEndPoints(src, dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ // Synthesize link ...
+ angular.extend(lnk, {
+ key: id,
+ class: 'link',
+ // NOTE: srcPort left undefined (host end of the link)
+ tgtPort: host.cp.port,
+
+ type: function () { return 'hostLink'; },
+ online: function () {
+ // hostlink target is edge switch
+ return lnk.target.online;
+ },
+ linkWidth: function () { return 1; }
+ });
+ return lnk;
+ }
+
+ function createLink(link) {
+ var lnk = linkEndPoints(link.src, link.dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ angular.extend(lnk, {
+ key: link.id,
+ class: 'link',
+ fromSource: link,
+ srcPort: link.srcPort,
+ tgtPort: link.dstPort,
+ position: {
+ x1: 0,
+ y1: 0,
+ x2: 0,
+ y2: 0
+ },
+
+ // functions to aggregate dual link state
+ type: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget;
+ return (s && s.type) || (t && t.type) || defaultLinkType;
+ },
+ online: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ both = lnk.source.online && lnk.target.online;
+ return both && ((s && s.online) || (t && t.online));
+ },
+ linkWidth: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ ws = (s && s.linkWidth) || 0,
+ wt = (t && t.linkWidth) || 0;
+ return lnk.position.multiLink ? 5 : Math.max(ws, wt);
+ }
+ });
+ return lnk;
+ }
+
+
+ function linkEndPoints(srcId, dstId) {
+ var srcNode = lu[srcId],
+ dstNode = lu[dstId],
+ sMiss = !srcNode ? missMsg('src', srcId) : '',
+ dMiss = !dstNode ? missMsg('dst', dstId) : '';
+
+ if (sMiss || dMiss) {
+ $log.error('Node(s) not on map for link:' + sMiss + dMiss);
+ //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+ return null;
+ }
+ return {
+ source: srcNode,
+ target: dstNode
+ };
+ }
+
+ function missMsg(what, id) {
+ return '\n[' + what + '] "' + id + '" missing';
+ }
+
+
+ function makeNodeKey(d, what) {
+ var port = what + 'Port';
+ return d[what] + '/' + d[port];
+ }
+
+ function makeLinkKey(d, flipped) {
+ var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
+ two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
+ return one + '-' + two;
+ }
+
+ function findLinkById(id) {
+ // check to see if this is a reverse lookup, else default to given id
+ var key = rlk[id] || id;
+ return key && lu[key];
+ }
+
+ function findLink(linkData, op) {
+ var key = makeLinkKey(linkData),
+ keyrev = makeLinkKey(linkData, 1),
+ link = lu[key],
+ linkRev = lu[keyrev],
+ result = {},
+ ldata = link || linkRev,
+ rawLink;
+
+ if (op === 'add') {
+ if (link) {
+ // trying to add a link that we already know about
+ result.ldata = link;
+ result.badLogic = 'addLink: link already added';
+
+ } else if (linkRev) {
+ // we found the reverse of the link to be added
+ result.ldata = linkRev;
+ if (linkRev.fromTarget) {
+ result.badLogic = 'addLink: link already added';
+ }
+ }
+ } else if (op === 'update') {
+ if (!ldata) {
+ result.badLogic = 'updateLink: link not found';
+ } else {
+ rawLink = link ? ldata.fromSource : ldata.fromTarget;
+ result.updateWith = function (data) {
+ angular.extend(rawLink, data);
+ api.restyleLinkElement(ldata);
+ }
+ }
+ } else if (op === 'remove') {
+ if (!ldata) {
+ result.badLogic = 'removeLink: link not found';
+ } else {
+ rawLink = link ? ldata.fromSource : ldata.fromTarget;
+
+ if (!rawLink) {
+ result.badLogic = 'removeLink: link not found';
+
+ } else {
+ result.removeRawLink = function () {
+ // remove link out of aggregate linksByDevice list
+ var linksForDevPair = linksByDevice[ldata.devicePair],
+ rmvIdx = fs.find(ldata.key, linksForDevPair, 'key');
+ if (rmvIdx >= 0) {
+ linksForDevPair.splice(rmvIdx, 1);
+ }
+ ldata.position.multilink = linksForDevPair.length >= 5;
+
+ if (link) {
+ // remove fromSource
+ ldata.fromSource = null;
+ if (ldata.fromTarget) {
+ // promote target into source position
+ ldata.fromSource = ldata.fromTarget;
+ ldata.fromTarget = null;
+ ldata.key = keyrev;
+ delete lu[key];
+ lu[keyrev] = ldata;
+ delete rlk[keyrev];
+ }
+ } else {
+ // remove fromTarget
+ ldata.fromTarget = null;
+ delete rlk[keyrev];
+ }
+ if (ldata.fromSource) {
+ api.restyleLinkElement(ldata);
+ } else {
+ api.removeLinkElement(ldata);
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ function findDevices(offlineOnly) {
+ var a = [];
+ nodes.forEach(function (d) {
+ if (d.class === 'device' && !(offlineOnly && d.online)) {
+ a.push(d);
+ }
+ });
+ return a;
+ }
+
+ function findAttachedHosts(devId) {
+ var hosts = [];
+ nodes.forEach(function (d) {
+ if (d.class === 'host' && d.cp.device === devId) {
+ hosts.push(d);
+ }
+ });
+ return hosts;
+ }
+
+ function findAttachedLinks(devId) {
+ var lnks = [];
+ links.forEach(function (d) {
+ if (d.source.id === devId || d.target.id === devId) {
+ lnks.push(d);
+ }
+ });
+ return lnks;
+ }
+
+ // returns one-way links or where the internal link types differ
+ function findBadLinks() {
+ var lnks = [],
+ src, tgt;
+ links.forEach(function (d) {
+ // NOTE: skip edge links, which are synthesized
+ if (d.type() !== 'hostLink') {
+ delete d.bad;
+ src = d.fromSource;
+ tgt = d.fromTarget;
+ if (src && !tgt) {
+ d.bad = 'missing link';
+ } else if (src.type !== tgt.type) {
+ d.bad = 'type mismatch';
+ }
+ if (d.bad) {
+ lnks.push(d);
+ }
+ }
+ });
+ return lnks;
+ }
+
+ // ==========================
+ // Module definition
+
+ angular.module('ovTopo')
+ .factory('TopoModelService',
+ ['$log', 'FnService', 'RandomService',
+
+ function (_$log_, _fs_, _rnd_) {
+ $log = _$log_;
+ fs = _fs_;
+ rnd = _rnd_;
+
+ function initModel(_api_, _dim_) {
+ api = _api_;
+ dim = _dim_;
+ lu = api.network.lookup;
+ rlk = api.network.revLinkToKey;
+ nodes = api.network.nodes;
+ links = api.network.links;
+ linksByDevice = api.network.linksByDevice;
+ }
+
+ function newDim(_dim_) {
+ dim = _dim_;
+ }
+
+ function destroyModel() { }
+
+ return {
+ initModel: initModel,
+ newDim: newDim,
+ destroyModel: destroyModel,
+
+ positionNode: positionNode,
+ createDeviceNode: createDeviceNode,
+ createHostNode: createHostNode,
+ createHostLink: createHostLink,
+ createLink: createLink,
+ coordFromLngLat: coordFromLngLat,
+ lngLatFromCoord: lngLatFromCoord,
+ findLink: findLink,
+ findLinkById: findLinkById,
+ findDevices: findDevices,
+ findAttachedHosts: findAttachedHosts,
+ findAttachedLinks: findAttachedLinks,
+ findBadLinks: findBadLinks
+ }
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js
new file mode 100644
index 00000000..e84b1173
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Oblique View Module.
+ Provides functionality to view the topology as two planes (packet & optical)
+ from an oblique (side-on) perspective.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, sus, flash;
+
+ // api to topoForce
+ var api;
+ /*
+ force() // get ref to force layout object
+ zoomLayer() // get ref to zoom layer
+ nodeGBBox() // get bounding box of node group layer
+ node() // get ref to D3 selection of nodes
+ link() // get ref to D3 selection of links
+ nodes() // get ref to network nodes array
+ tickStuff // ref to tick functions
+ nodeLock(b) // test-and-set nodeLock state
+ opacifyMap(b) // show or hide map layer
+ inLayer(d, layer) // return true if d in layer {'pkt'|'opt'}
+ calcLinkPos() // recomputes link pos based on node data
+ */
+
+ // configuration
+ var xsky = -.7, // x skew y factor
+ xsk = -35, // x skew angle
+ ysc = .5, // y scale
+ pad = 50,
+ time = 1500,
+ fill = {
+ pkt: 'rgba(130,130,170,0.3)', // blue-ish
+ opt: 'rgba(170,130,170,0.3)' // magenta-ish
+ };
+
+ // internal state
+ var oblique = false,
+ xffn = null,
+ plane = {},
+ oldNodeLock;
+
+
+ function planeId(tag) {
+ return 'topo-obview-' + tag + 'Plane';
+ }
+
+ function ytfn(h, dir) {
+ return h * ysc * dir * 1.1;
+ }
+
+ function obXform(h, dir) {
+ var yt = ytfn(h, dir);
+ return sus.scale(1, ysc) + sus.translate(0, yt) + sus.skewX(xsk);
+ }
+
+ function noXform() {
+ return sus.skewX(0) + sus.translate(0,0) + sus.scale(1,1);
+ }
+
+ function padBox(box, p) {
+ box.x -= p;
+ box.y -= p;
+ box.width += p*2;
+ box.height += p*2;
+ }
+
+ function toObliqueView() {
+ var box = api.nodeGBBox(),
+ ox, oy;
+
+ padBox(box, pad);
+
+ ox = box.x + box.width / 2;
+ oy = box.y + box.height / 2;
+
+ // remember node lock state, then lock the nodes down
+ oldNodeLock = api.nodeLock(true);
+ api.opacifyMap(false);
+
+ insertPlanes(ox, oy);
+
+ xffn = function (xy, dir) {
+ var yt = ytfn(box.height, dir),
+ ax = xy.x - ox,
+ ay = xy.y - oy,
+ x = ax + ay * xsky,
+ y = (ay + yt) * ysc;
+ return {x: ox + x, y: oy + y};
+ };
+
+ showPlane('pkt', box, -1);
+ showPlane('opt', box, 1);
+ obTransitionNodes();
+ }
+
+ function toNormalView() {
+ xffn = null;
+
+ hidePlane('pkt');
+ hidePlane('opt');
+ obTransitionNodes();
+
+ removePlanes();
+
+ // restore node lock state
+ api.nodeLock(oldNodeLock);
+ api.opacifyMap(true);
+ }
+
+ function obTransitionNodes() {
+ // return the direction for the node
+ // -1 for pkt layer, 1 for optical layer
+ function dir(d) {
+ return api.inLayer(d, 'pkt') ? -1 : 1;
+ }
+
+ if (xffn) {
+ api.nodes().forEach(function (d) {
+ var oldxy = {x: d.x, y: d.y},
+ coords = xffn(oldxy, dir(d));
+ d.oldxy = oldxy;
+ d.px = d.x = coords.x;
+ d.py = d.y = coords.y;
+ });
+ } else {
+ api.nodes().forEach(function (d) {
+ var old = d.oldxy || {x: d.x, y: d.y};
+ d.px = d.x = old.x;
+ d.py = d.y = old.y;
+ delete d.oldxy;
+ });
+ }
+
+ api.node().transition()
+ .duration(time)
+ .attr(api.tickStuff.nodeAttr);
+ api.link().transition()
+ .duration(time)
+ .call(api.calcLinkPos)
+ .attr(api.tickStuff.linkAttr)
+ .call(api.applyNumLinkLabels);
+ api.linkLabel().transition()
+ .duration(time)
+ .attr(api.tickStuff.linkLabelAttr);
+ }
+
+ function showPlane(tag, box, dir) {
+ // set box origin at center..
+ box.x = -box.width/2;
+ box.y = -box.height/2;
+
+ plane[tag].select('rect')
+ .attr(box)
+ .attr('opacity', 0)
+ .transition()
+ .duration(time)
+ .attr('opacity', 1)
+ .attr('transform', obXform(box.height, dir));
+ }
+
+ function hidePlane(tag) {
+ plane[tag].select('rect')
+ .transition()
+ .duration(time)
+ .attr('opacity', 0)
+ .attr('transform', noXform());
+ }
+
+ function insertPlanes(ox, oy) {
+ function ins(tag) {
+ var id = planeId(tag),
+ g = api.zoomLayer().insert('g', '#topo-G')
+ .attr('id', id)
+ .attr('transform', sus.translate(ox,oy));
+ g.append('rect')
+ .attr('fill', fill[tag])
+ .attr('opacity', 0);
+ plane[tag] = g;
+ }
+ ins('opt');
+ ins('pkt');
+ }
+
+ function removePlanes() {
+ function rem(tag) {
+ var id = planeId(tag);
+ api.zoomLayer().select('#'+id)
+ .transition()
+ .duration(time + 50)
+ .remove();
+ delete plane[tag];
+ }
+ rem('opt');
+ rem('pkt');
+ }
+
+
+// === -----------------------------------------------------
+// === MODULE DEFINITION ===
+
+angular.module('ovTopo')
+ .factory('TopoObliqueService',
+ ['$log', 'FnService', 'SvgUtilService', 'FlashService',
+
+ function (_$log_, _fs_, _sus_, _flash_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+ flash = _flash_;
+
+ function initOblique(_api_) {
+ api = _api_;
+ }
+
+ function destroyOblique() { }
+
+ function toggleOblique() {
+ oblique = !oblique;
+ if (oblique) {
+ api.force().stop();
+ flash.flash('Oblique view');
+ toObliqueView();
+ } else {
+ flash.flash('Normal view');
+ toNormalView();
+ }
+ }
+
+ return {
+ initOblique: initOblique,
+ destroyOblique: destroyOblique,
+
+ isOblique: function () { return oblique; },
+ toggleOblique: toggleOblique
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
new file mode 100644
index 00000000..7eb45ba4
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ ONOS GUI -- Topology Overlay Module.
+
+ Provides overlay capabilities, allowing ONOS apps to provide additional
+ custom data/behavior for the topology view.
+
+ */
+
+(function () {
+ 'use strict';
+
+ // constants
+ var tos = 'TopoOverlayService: ';
+
+ // injected refs
+ var $log, fs, gs, wss, ns, tss, tps, api;
+
+ // internal state
+ var overlays = {},
+ current = null;
+
+ function error(fn, msg) {
+ $log.error(tos + fn + '(): ' + msg);
+ }
+
+ function warn(fn, msg) {
+ $log.warn(tos + fn + '(): ' + msg);
+ }
+
+ function mkGlyphId(oid, gid) {
+ return (gid[0] === '*') ? oid + '-' + gid.slice(1) : gid;
+ }
+
+ function handleGlyphs(o) {
+ var gdata = fs.isO(o.glyphs),
+ oid = o.overlayId,
+ gid = o.glyphId || 'unknown',
+ data = {},
+ note = [];
+
+ o._glyphId = mkGlyphId(oid, gid);
+
+ o.mkGid = function (g) {
+ return mkGlyphId(oid, g);
+ };
+ o.mkId = function (s) {
+ return oid + '-' + s;
+ };
+
+ // process glyphs if defined
+ if (gdata) {
+ angular.forEach(gdata, function (value, key) {
+ var fullkey = oid + '-' + key;
+ data['_' + fullkey] = value.vb;
+ data[fullkey] = value.d;
+ note.push('*' + key);
+ });
+ gs.registerGlyphs(data);
+ $log.debug('registered overlay glyphs:', oid, note);
+ }
+ }
+
+ function register(overlay) {
+ var r = 'register',
+ over = fs.isO(overlay),
+ kb = over ? fs.isO(overlay.keyBindings) : null,
+ id = over ? over.overlayId : '';
+
+ if (!id) {
+ return error(r, 'not a recognized overlay');
+ }
+ if (overlays[id]) {
+ return warn(r, 'already registered: "' + id + '"');
+ }
+ overlays[id] = overlay;
+ handleGlyphs(overlay);
+
+ if (kb) {
+ if (!fs.isA(kb._keyOrder)) {
+ warn(r, 'no _keyOrder array defined on keyBindings');
+ } else {
+ kb._keyOrder.forEach(function (k) {
+ if (k !== '-' && !kb[k]) {
+ warn(r, 'no "' + k + '" property defined on keyBindings');
+ }
+ });
+ }
+ }
+
+ $log.debug(tos + 'registered overlay: ' + id, overlay);
+ }
+
+ // TODO: remove this redundant code.......
+ // NOTE: unregister needs to be called if an app is ever
+ // deactivated/uninstalled via the applications view
+/*
+ function unregister(overlay) {
+ var u = 'unregister',
+ over = fs.isO(overlay),
+ id = over ? over.overlayId : '';
+
+ if (!id) {
+ return error(u, 'not a recognized overlay');
+ }
+ if (!overlays[id]) {
+ return warn(u, 'not registered: "' + id + "'")
+ }
+ delete overlays[id];
+ $log.debug(tos + 'unregistered overlay: ' + id);
+ }
+*/
+
+
+ // returns the list of overlay identifiers
+ function list() {
+ return d3.map(overlays).keys();
+ }
+
+ // add a radio button for each registered overlay
+ function augmentRbset(rset, switchFn) {
+ angular.forEach(overlays, function (ov) {
+ rset.push({
+ gid: ov._glyphId,
+ tooltip: (ov.tooltip || '(no tooltip)'),
+ cb: function () {
+ tbSelection(ov.overlayId, switchFn);
+ }
+ });
+ });
+ }
+
+ // an overlay was selected via toolbar radio button press from user
+ function tbSelection(id, switchFn) {
+ var same = current && current.overlayId === id,
+ payload = {},
+ actions;
+
+ function doop(op) {
+ var oid = current.overlayId;
+ $log.debug('Overlay:', op, oid);
+ current[op]();
+ payload[op] = oid;
+ }
+
+ if (!same) {
+ current && doop('deactivate');
+ current = overlays[id];
+ current && doop('activate');
+ actions = current && fs.isO(current.keyBindings);
+ switchFn(id, actions);
+
+ wss.sendEvent('topoSelectOverlay', payload);
+
+ // Ensure summary and details panels are updated immediately..
+ wss.sendEvent('requestSummary');
+ tss.updateDetail();
+ }
+ }
+
+ var coreButtons = {
+ showDeviceView: {
+ gid: 'switch',
+ tt: 'Show Device View',
+ path: 'device'
+ },
+ showFlowView: {
+ gid: 'flowTable',
+ tt: 'Show Flow View for this Device',
+ path: 'flow'
+ },
+ showPortView: {
+ gid: 'portTable',
+ tt: 'Show Port View for this Device',
+ path: 'port'
+ },
+ showGroupView: {
+ gid: 'groupTable',
+ tt: 'Show Group View for this Device',
+ path: 'group'
+ }
+ };
+
+ // retrieves a button definition from the current overlay and generates
+ // a button descriptor to be added to the panel, with the data baked in
+ function _getButtonDef(id, data) {
+ var btns = current && current.buttons,
+ b = btns && btns[id],
+ cb = fs.isF(b.cb),
+ f = cb ? function () { cb(data); } : function () {};
+
+ return b ? {
+ id: current.mkId(id),
+ gid: current.mkGid(b.gid),
+ tt: b.tt,
+ cb: f
+ } : null;
+ }
+
+ // install core buttons, and include any additional from the current overlay
+ function installButtons(buttons, data, devId) {
+ buttons.forEach(function (id) {
+ var btn = coreButtons[id],
+ gid = btn && btn.gid,
+ tt = btn && btn.tt,
+ path = btn && btn.path;
+
+ if (btn) {
+ tps.addAction({
+ id: 'core-' + id,
+ gid: gid,
+ tt: tt,
+ cb: function () { ns.navTo(path, {devId: devId }); }
+ });
+ } else if (btn = _getButtonDef(id, data)) {
+ tps.addAction(btn);
+ }
+ });
+ }
+
+ function addDetailButton(id) {
+ var b = _getButtonDef(id);
+ if (b) {
+ tps.addAction({
+ id: current.mkId(id),
+ gid: current.mkGid(b.gid),
+ cb: b.cb,
+ tt: b.tt
+ });
+ }
+ }
+
+
+ // === -----------------------------------------------------
+ // Hooks for overlays
+
+ function _hook(x) {
+ var h = current && current.hooks;
+ return h && fs.isF(h[x]);
+ }
+
+ function escapeHook() {
+ var eh = _hook('escape');
+ return eh ? eh() : false;
+ }
+
+ function emptySelectHook() {
+ var cb = _hook('empty');
+ cb && cb();
+ }
+
+ function singleSelectHook(data) {
+ var cb = _hook('single');
+ cb && cb(data);
+ }
+
+ function multiSelectHook(selectOrder) {
+ var cb = _hook('multi');
+ cb && cb(selectOrder);
+ }
+
+ function mouseOverHook(what) {
+ var cb = _hook('mouseover');
+ cb && cb(what);
+ }
+
+ function mouseOutHook() {
+ var cb = _hook('mouseout');
+ cb && cb();
+ }
+
+ // === -----------------------------------------------------
+ // Event (from server) Handlers
+
+ function setApi(_api_, _tss_) {
+ api = _api_;
+ tss = _tss_;
+ }
+
+ function showHighlights(data) {
+ var less;
+
+ /*
+ API to topoForce
+ clearLinkTrafficStyle()
+ removeLinkLabels()
+ findLinkById( id )
+ findNodeById( id )
+ updateLinks()
+ updateNodes()
+ supLayers( bool, [less] )
+ unsupNode( id, [less] )
+ unsupLink( key, [less] )
+ */
+
+ // TODO: clear node highlighting
+ api.clearLinkTrafficStyle();
+ api.removeLinkLabels();
+
+ // handle element suppression
+ if (data.subdue) {
+ less = data.subdue === 'min';
+ api.supLayers(true, less);
+
+ } else {
+ api.supLayers(false);
+ api.supLayers(false, true);
+ }
+
+ data.hosts.forEach(function (host) {
+ var hdata = api.findNodeById(host.id);
+ if (hdata && !hdata.el.empty()) {
+ if (!host.subdue) {
+ api.unsupNode(hdata.id, less);
+ }
+ // TODO: further highlighting?
+ }
+ });
+
+ data.devices.forEach(function (device) {
+ var ddata = api.findNodeById(device.id);
+ if (ddata && !ddata.el.empty()) {
+ if (!device.subdue) {
+ api.unsupNode(ddata.id, less);
+ }
+ // TODO: further highlighting?
+ }
+ });
+
+ data.links.forEach(function (link) {
+ var ldata = api.findLinkById(link.id),
+ lab = link.label,
+ units, portcls, magnitude;
+
+ if (ldata && !ldata.el.empty()) {
+ if (!link.subdue) {
+ api.unsupLink(ldata.key, less);
+ }
+ ldata.el.classed(link.css, true);
+ ldata.label = lab;
+
+ // TODO: this needs to be pulled out into traffic overlay
+ // inject additional styling for port-based traffic
+ if (fs.endsWith(lab, 'bps')) {
+ units = lab.substring(lab.length-4);
+ portcls = 'port-traffic-' + units;
+
+ // for GBps
+ if (units.substring(0,1) === 'G') {
+ magnitude = fs.parseBitRate(lab);
+ if (magnitude >= 9) {
+ portcls += '-choked'
+ }
+ }
+ ldata.el.classed(portcls, true);
+ }
+ }
+ });
+
+ api.updateNodes();
+ api.updateLinks();
+ }
+
+ // ========================================================================
+
+ angular.module('ovTopo')
+ .factory('TopoOverlayService',
+ ['$log', 'FnService', 'GlyphService', 'WebSocketService', 'NavService',
+ 'TopoPanelService',
+
+ function (_$log_, _fs_, _gs_, _wss_, _ns_, _tps_) {
+ $log = _$log_;
+ fs = _fs_;
+ gs = _gs_;
+ wss = _wss_;
+ ns = _ns_;
+ tps = _tps_;
+
+ return {
+ register: register,
+ //unregister: unregister,
+ setApi: setApi,
+ list: list,
+ augmentRbset: augmentRbset,
+ mkGlyphId: mkGlyphId,
+ tbSelection: tbSelection,
+ installButtons: installButtons,
+ addDetailButton: addDetailButton,
+ hooks: {
+ escape: escapeHook,
+ emptySelect: emptySelectHook,
+ singleSelect: singleSelectHook,
+ multiSelect: multiSelectHook,
+ mouseOver: mouseOverHook,
+ mouseOut: mouseOutHook
+ },
+
+ showHighlights: showHighlights
+ }
+ }]);
+
+}()); \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js
new file mode 100644
index 00000000..7db17f0d
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js
@@ -0,0 +1,539 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Panel Module.
+ Defines functions for manipulating the summary, detail, and instance panels.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $window, $rootScope, fs, ps, gs, flash, wss, bns, mast, ns;
+
+ // constants
+ var pCls = 'topo-p',
+ idSum = 'topo-p-summary',
+ idDet = 'topo-p-detail',
+ panelOpts = {
+ width: 260
+ },
+ sumMax = 240,
+ padTop = 20,
+ devPath = 'device';
+
+ // internal state
+ var useDetails = true, // should we show details if we have 'em?
+ haveDetails = false, // do we have details that we could show?
+ sumFromTop, // summary panel distance from top of screen
+ unbindWatch;
+
+ // panels
+ var summary, detail;
+
+ // === -----------------------------------------------------
+ // Panel API
+ function createTopoPanel(id, opts) {
+ var p = ps.createPanel(id, opts),
+ pid = id,
+ header, body, footer;
+ p.classed(pCls, true);
+
+ function panel() {
+ return p;
+ }
+
+ function hAppend(x) {
+ return header.append(x);
+ }
+
+ function bAppend(x) {
+ return body.append(x);
+ }
+
+ function fAppend(x) {
+ return footer.append(x);
+ }
+
+ function setup() {
+ p.empty();
+
+ p.append('div').classed('header', true);
+ p.append('div').classed('body', true);
+ p.append('div').classed('footer', true);
+
+ header = p.el().select('.header');
+ body = p.el().select('.body');
+ footer = p.el().select('.footer');
+ }
+
+ function destroy() {
+ ps.destroyPanel(pid);
+ }
+
+ // fromTop is how many pixels from the top of the page the panel is
+ // max is the max height of the panel in pixels
+ // only adjusts if the body content would be 10px or larger
+ function adjustHeight(fromTop, max) {
+ var totalPHeight, avSpace,
+ overflow = 0,
+ pdg = 30;
+
+ if (!fromTop) {
+ $log.warn('adjustHeight: height from top of page not given');
+ return null;
+ } else if (!body || !p) {
+ $log.warn('adjustHeight: panel contents are not defined');
+ return null;
+ }
+
+ p.el().style('height', null);
+ body.style('height', null);
+
+ totalPHeight = fromTop + p.height();
+ avSpace = fs.windowSize(pdg).height;
+
+ if (totalPHeight >= avSpace) {
+ overflow = totalPHeight - avSpace;
+ }
+
+ function _adjustBody(height) {
+ if (height < 10) {
+ return false;
+ } else {
+ body.style('height', height + 'px');
+ }
+ return true;
+ }
+
+ if (!_adjustBody(fs.noPxStyle(body, 'height') - overflow)) {
+ return;
+ }
+
+ if (max && p.height() > max) {
+ _adjustBody(fs.noPxStyle(body, 'height') - (p.height() - max));
+ }
+ }
+
+ return {
+ panel: panel,
+ setup: setup,
+ destroy: destroy,
+ appendHeader: hAppend,
+ appendBody: bAppend,
+ appendFooter: fAppend,
+ adjustHeight: adjustHeight
+ };
+ }
+
+ // === -----------------------------------------------------
+ // Utility functions
+
+ function addSep(tbody) {
+ tbody.append('tr').append('td').attr('colspan', 2).append('hr');
+ }
+
+ function addBtnFooter() {
+ detail.appendFooter('hr');
+ detail.appendFooter('div').classed('actionBtns', true);
+ }
+
+ function addProp(tbody, label, value) {
+ var tr = tbody.append('tr'),
+ lab;
+ if (typeof label === 'string') {
+ lab = label.replace(/_/g, ' ');
+ } else {
+ lab = label;
+ }
+
+ function addCell(cls, txt) {
+ tr.append('td').attr('class', cls).html(txt);
+ }
+ addCell('label', lab + ' :');
+ addCell('value', value);
+ }
+
+ function listProps(tbody, data) {
+ data.propOrder.forEach(function (p) {
+ if (p === '-') {
+ addSep(tbody);
+ } else {
+ addProp(tbody, p, data.props[p]);
+ }
+ });
+ }
+
+ function watchWindow() {
+ unbindWatch = $rootScope.$watchCollection(
+ function () {
+ return {
+ h: $window.innerHeight,
+ w: $window.innerWidth
+ };
+ }, function () {
+ summary.adjustHeight(sumFromTop, sumMax);
+ detail.adjustHeight(detail.ypos.current);
+ }
+ );
+ }
+
+ // === -----------------------------------------------------
+ // Functions for populating the summary panel
+
+ function populateSummary(data) {
+ summary.setup();
+
+ var svg = summary.appendHeader('div')
+ .classed('icon', true)
+ .append('svg'),
+ title = summary.appendHeader('h2'),
+ table = summary.appendBody('table'),
+ tbody = table.append('tbody'),
+ glyphId = data.type || 'node';
+
+ gs.addGlyph(svg, glyphId, 40);
+
+ if (glyphId === 'node') {
+ gs.addGlyph(svg, 'bird', 24, true, [8,12]);
+ }
+
+ title.text(data.title);
+ listProps(tbody, data);
+ }
+
+ // === -----------------------------------------------------
+ // Functions for populating the detail panel
+
+ var isDevice = {
+ switch: 1,
+ roadm: 1
+ };
+
+ function displaySingle(data) {
+ detail.setup();
+
+ var svg = detail.appendHeader('div')
+ .classed('icon clickable', true)
+ .append('svg'),
+ title = detail.appendHeader('h2')
+ .classed('clickable', true),
+ table = detail.appendBody('table'),
+ tbody = table.append('tbody'),
+ navFn;
+
+ gs.addGlyph(svg, (data.type || 'unknown'), 40);
+ title.text(data.title);
+
+ // only add navigation when displaying a device
+ if (isDevice[data.type]) {
+ navFn = function () {
+ ns.navTo(devPath, { devId: data.id });
+ };
+
+ svg.on('click', navFn);
+ title.on('click', navFn);
+ }
+
+ listProps(tbody, data);
+ addBtnFooter();
+ }
+
+ function displayMulti(ids) {
+ detail.setup();
+
+ var title = detail.appendHeader('h3'),
+ table = detail.appendBody('table'),
+ tbody = table.append('tbody');
+
+ title.text('Selected Nodes');
+ ids.forEach(function (d, i) {
+ addProp(tbody, i+1, d);
+ });
+ addBtnFooter();
+ }
+
+ function addAction(o) {
+ var btnDiv = d3.select('#' + idDet)
+ .select('.actionBtns')
+ .append('div')
+ .classed('actionBtn', true);
+ bns.button(btnDiv, idDet + '-' + o.id, o.gid, o.cb, o.tt);
+ }
+
+ var friendlyIndex = {
+ device: 1,
+ host: 0
+ };
+
+ function friendly(d) {
+ var i = friendlyIndex[d.class] || 0;
+ return (d.labels && d.labels[i]) || '';
+ }
+
+ function linkSummary(d) {
+ var o = d && d.online ? 'online' : 'offline';
+ return d ? d.type + ' / ' + o : '-';
+ }
+
+ // provided to change presentation of internal type name
+ var linkTypePres = {
+ hostLink: 'edge link'
+ };
+
+ function linkType(d) {
+ return linkTypePres[d.type()] || d.type();
+ }
+
+ var coreOrder = [
+ 'Type', '-',
+ 'A_type', 'A_id', 'A_label', 'A_port', '-',
+ 'B_type', 'B_id', 'B_label', 'B_port', '-'
+ ],
+ edgeOrder = [
+ 'Type', '-',
+ 'A_type', 'A_id', 'A_label', '-',
+ 'B_type', 'B_id', 'B_label', 'B_port'
+ ];
+
+ function displayLink(data) {
+ detail.setup();
+
+ var svg = detail.appendHeader('div')
+ .classed('icon', true)
+ .append('svg'),
+ title = detail.appendHeader('h2'),
+ table = detail.appendBody('table'),
+ tbody = table.append('tbody'),
+ edgeLink = data.type() === 'hostLink',
+ order = edgeLink ? edgeOrder : coreOrder;
+
+ gs.addGlyph(svg, 'ports', 40);
+ title.text('Link');
+
+
+ listProps(tbody, {
+ propOrder: order,
+ props: {
+ Type: linkType(data),
+
+ A_type: data.source.class,
+ A_id: data.source.id,
+ A_label: friendly(data.source),
+ A_port: data.srcPort,
+
+ B_type: data.target.class,
+ B_id: data.target.id,
+ B_label: friendly(data.target),
+ B_port: data.tgtPort
+ }
+ });
+
+ if (!edgeLink) {
+ addProp(tbody, 'A &rarr; B', linkSummary(data.fromSource));
+ addProp(tbody, 'B &rarr; A', linkSummary(data.fromTarget));
+ }
+ }
+
+ function displayNothing() {
+ haveDetails = false;
+ hideDetailPanel();
+ }
+
+ function displaySomething() {
+ haveDetails = true;
+ if (useDetails) {
+ showDetailPanel();
+ }
+ }
+
+ // === -----------------------------------------------------
+ // Event Handlers
+
+ function showSummary(data) {
+ populateSummary(data);
+ showSummaryPanel();
+ }
+
+ function toggleSummary(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !summary.panel().isVisible() : !!x,
+ verb = on ? 'Show' : 'Hide';
+
+ if (on) {
+ // ask server to start sending summary data.
+ wss.sendEvent('requestSummary');
+ // note: the summary panel will appear, once data arrives
+ } else {
+ hideSummaryPanel();
+ }
+ flash.flash(verb + ' summary panel');
+ return on;
+ }
+
+ // === -----------------------------------------------------
+ // === LOGIC For showing/hiding summary and detail panels...
+
+ function showSummaryPanel() {
+ function _show() {
+ summary.panel().show();
+ summary.adjustHeight(sumFromTop, sumMax);
+ }
+ if (detail.panel().isVisible()) {
+ detail.down(_show);
+ } else {
+ _show();
+ }
+ }
+
+ function hideSummaryPanel() {
+ // instruct server to stop sending summary data
+ wss.sendEvent("cancelSummary");
+ summary.panel().hide(detail.up);
+ }
+
+ function showDetailPanel() {
+ if (summary.panel().isVisible()) {
+ detail.down(detail.panel().show);
+ } else {
+ detail.up(detail.panel().show);
+ }
+ }
+
+ function hideDetailPanel() {
+ detail.panel().hide();
+ }
+
+ // ==========================
+
+ function augmentDetailPanel() {
+ var d = detail,
+ downPos = sumFromTop + sumMax + 20;
+ d.ypos = { up: sumFromTop, down: downPos, current: downPos};
+
+ d._move = function (y, cb) {
+ var yp = d.ypos,
+ endCb;
+
+ if (fs.isF(cb)) {
+ endCb = function () {
+ cb();
+ d.adjustHeight(d.ypos.current);
+ }
+ } else {
+ endCb = function () {
+ d.adjustHeight(d.ypos.current);
+ }
+ }
+ if (yp.current !== y) {
+ yp.current = y;
+ d.panel().el().transition().duration(300)
+ .each('end', endCb)
+ .style('top', yp.current + 'px');
+ } else {
+ endCb();
+ }
+ };
+
+ d.down = function (cb) { d._move(d.ypos.down, cb); };
+ d.up = function (cb) { d._move(d.ypos.up, cb); };
+ }
+
+ function toggleUseDetailsFlag(x) {
+ var kev = (x === 'keyev'),
+ verb;
+
+ useDetails = kev ? !useDetails : !!x;
+ verb = useDetails ? 'Enable' : 'Disable';
+
+ if (useDetails) {
+ if (haveDetails) {
+ showDetailPanel();
+ }
+ } else {
+ hideDetailPanel();
+ }
+ flash.flash(verb + ' details panel');
+ return useDetails;
+ }
+
+ // ==========================
+
+ function initPanels() {
+ sumFromTop = mast.mastHeight() + padTop;
+ summary = createTopoPanel(idSum, panelOpts);
+ detail = createTopoPanel(idDet, panelOpts);
+
+ augmentDetailPanel();
+ watchWindow();
+ }
+
+ function destroyPanels() {
+ summary.destroy();
+ summary = null;
+
+ detail.destroy();
+ detail = null;
+ haveDetails = false;
+ unbindWatch();
+ }
+
+ // ==========================
+
+ angular.module('ovTopo')
+ .factory('TopoPanelService',
+ ['$log', '$window', '$rootScope', 'FnService', 'PanelService', 'GlyphService',
+ 'FlashService', 'WebSocketService', 'ButtonService', 'MastService',
+ 'NavService',
+
+ function (_$log_, _$window_, _$rootScope_,
+ _fs_, _ps_, _gs_, _flash_, _wss_, _bns_, _mast_, _ns_) {
+ $log = _$log_;
+ $window = _$window_;
+ $rootScope = _$rootScope_;
+ fs = _fs_;
+ ps = _ps_;
+ gs = _gs_;
+ flash = _flash_;
+ wss = _wss_;
+ bns = _bns_;
+ mast = _mast_;
+ ns = _ns_;
+
+ return {
+ initPanels: initPanels,
+ destroyPanels: destroyPanels,
+ createTopoPanel: createTopoPanel,
+
+ showSummary: showSummary,
+ toggleSummary: toggleSummary,
+
+ toggleUseDetailsFlag: toggleUseDetailsFlag,
+ displaySingle: displaySingle,
+ displayMulti: displayMulti,
+ displayLink: displayLink,
+ displayNothing: displayNothing,
+ displaySomething: displaySomething,
+ addAction: addAction,
+
+ hideSummaryPanel: hideSummaryPanel,
+
+ detailVisible: function () { return detail.panel().isVisible(); },
+ summaryVisible: function () { return summary.panel().isVisible(); }
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js
new file mode 100644
index 00000000..483c4baa
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Selection Module.
+ Defines behavior when selecting nodes.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, wss, tov, tps, tts, ns;
+
+ // api to topoForce
+ var api;
+ /*
+ node() // get ref to D3 selection of nodes
+ zoomingOrPanning( ev )
+ updateDeviceColors( [dev] )
+ deselectLink()
+ */
+
+ // internal state
+ var hovered, // the node over which the mouse is hovering
+ selections = {}, // currently selected nodes (by id)
+ selectOrder = [], // the order in which we made selections
+ consumeClick = false; // used to coordinate with SVG click handler
+
+ // ==========================
+
+ function nSel() {
+ return selectOrder.length;
+ }
+ function getSel(idx) {
+ return selections[selectOrder[idx]];
+ }
+ function allSelectionsClass(cls) {
+ for (var i=0, n=nSel(); i<n; i++) {
+ if (getSel(i).obj.class !== cls) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // ==========================
+
+ function nodeMouseOver(m) {
+ if (!m.dragStarted) {
+ if (hovered != m) {
+ hovered = m;
+ tov.hooks.mouseOver({
+ id: m.id,
+ class: m.class,
+ type: m.type
+ });
+ }
+ }
+ }
+
+ function nodeMouseOut(m) {
+ if (!m.dragStarted) {
+ if (hovered) {
+ hovered = null;
+ tov.hooks.mouseOut();
+ }
+ }
+ }
+
+ // ==========================
+
+ function selectObject(obj) {
+ var el = this,
+ nodeEv = el && el.tagName === 'g',
+ ev = d3.event.sourceEvent || {},
+ n;
+
+ if (api.zoomingOrPanning(ev)) {
+ return;
+ }
+
+ if (nodeEv) {
+ n = d3.select(el);
+ } else {
+ api.node().each(function (d) {
+ if (d == obj) {
+ n = d3.select(el = this);
+ }
+ });
+ }
+ if (!n) return;
+
+ if (nodeEv) {
+ consumeClick = true;
+ }
+ api.deselectLink();
+
+ if (ev.shiftKey && n.classed('selected')) {
+ deselectObject(obj.id);
+ updateDetail();
+ return;
+ }
+
+ if (!ev.shiftKey) {
+ deselectAll(true);
+ }
+
+ selections[obj.id] = { obj: obj, el: el };
+ selectOrder.push(obj.id);
+
+ n.classed('selected', true);
+ api.updateDeviceColors(obj);
+ updateDetail();
+ }
+
+ function deselectObject(id) {
+ var obj = selections[id];
+ if (obj) {
+ d3.select(obj.el).classed('selected', false);
+ delete selections[id];
+ fs.removeFromArray(id, selectOrder);
+ api.updateDeviceColors(obj.obj);
+ }
+ }
+
+ function deselectAll(skipUpdate) {
+ var something = (selectOrder.length > 0);
+
+ // deselect all nodes in the network...
+ api.node().classed('selected', false);
+ selections = {};
+ selectOrder = [];
+ api.updateDeviceColors();
+ if (!skipUpdate) {
+ updateDetail();
+ }
+
+ // return true if something was selected
+ return something;
+ }
+
+ // === -----------------------------------------------------
+
+ function requestDetails(data) {
+ wss.sendEvent('requestDetails', {
+ id: data.id,
+ class: data.class
+ });
+ }
+
+ // === -----------------------------------------------------
+
+ function updateDetail() {
+ var nSel = selectOrder.length;
+ if (!nSel) {
+ emptySelect();
+ } else if (nSel === 1) {
+ singleSelect();
+ } else {
+ multiSelect();
+ }
+ }
+
+ function emptySelect() {
+ tov.hooks.emptySelect();
+ tps.displayNothing();
+ }
+
+ function singleSelect() {
+ var data = getSel(0).obj;
+ requestDetails(data);
+ // NOTE: detail panel is shown as a response to receiving
+ // a 'showDetails' event from the server. See 'showDetails'
+ // callback function below...
+ }
+
+ function multiSelect() {
+ // display the selected nodes in the detail panel
+ tps.displayMulti(selectOrder);
+ addHostSelectionActions();
+ tov.hooks.multiSelect(selectOrder);
+ tps.displaySomething();
+ }
+
+ function addHostSelectionActions() {
+ if (allSelectionsClass('host')) {
+ if (nSel() === 2) {
+ tps.addAction({
+ id: 'host-flow-btn',
+ gid: 'endstation',
+ cb: tts.addHostIntent,
+ tt: 'Create Host-to-Host Flow'
+ });
+ } else if (nSel() >= 2) {
+ tps.addAction({
+ id: 'mult-src-flow-btn',
+ gid: 'flows',
+ cb: tts.addMultiSourceIntent,
+ tt: 'Create Multi-Source Flow'
+ });
+ }
+ }
+ }
+
+
+ // === -----------------------------------------------------
+ // Event Handlers
+
+ // display the data for the single selected node
+ function showDetails(data) {
+ var buttons = fs.isA(data.buttons) || [];
+ tps.displaySingle(data);
+ tov.installButtons(buttons, data, data.props['URI']);
+ tov.hooks.singleSelect(data);
+ tps.displaySomething();
+ }
+
+ // returns true if one or more nodes are selected.
+ function somethingSelected() {
+ return nSel();
+ }
+
+ function clickConsumed(x) {
+ var cc = consumeClick;
+ consumeClick = !!x;
+ return cc;
+ }
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoSelectService',
+ ['$log', 'FnService', 'WebSocketService', 'TopoOverlayService',
+ 'TopoPanelService', 'TopoTrafficService', 'NavService',
+
+ function (_$log_, _fs_, _wss_, _tov_, _tps_, _tts_, _ns_) {
+ $log = _$log_;
+ fs = _fs_;
+ wss = _wss_;
+ tov = _tov_;
+ tps = _tps_;
+ tts = _tts_;
+ ns = _ns_;
+
+ function initSelect(_api_) {
+ api = _api_;
+ }
+
+ function destroySelect() { }
+
+ return {
+ initSelect: initSelect,
+ destroySelect: destroySelect,
+
+ showDetails: showDetails,
+
+ nodeMouseOver: nodeMouseOver,
+ nodeMouseOut: nodeMouseOut,
+ selectObject: selectObject,
+ deselectObject: deselectObject,
+ deselectAll: deselectAll,
+ updateDetail: updateDetail,
+
+ hovered: function () { return hovered; },
+ selectOrder: function () { return selectOrder; },
+ somethingSelected: somethingSelected,
+
+ clickConsumed: clickConsumed
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js
new file mode 100644
index 00000000..1c957412
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Sprite Module.
+ Defines behavior for loading sprites into the sprite layer.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $http, fs, gs, sus, wss;
+
+ // constants
+ var tssid = 'TopoSpriteService: ',
+ fontsize = 20; // default font size 20pt.
+
+ // internal state
+ var spriteLayer, defsElement;
+
+
+ function registerPathsAsGlyphs(paths) {
+ var custom = {},
+ ids = [];
+
+ function mkd(d) {
+ return fs.isA(d) ? d.join('') : d;
+ }
+
+ paths.forEach(function (path) {
+ var tag = 'spr_' + path.tag;
+
+ if (path.glyph) {
+ // assumption is that we are using a built-in glyph
+ return;
+ }
+
+ custom['_' + tag] = path.viewbox || '0 0 1000 1000';
+ custom[tag] = mkd(path.d);
+ ids.push(tag);
+ });
+
+ gs.registerGlyphs(custom);
+ gs.loadDefs(defsElement, ids, true);
+ }
+
+ function applyStrokeStyle(s, use) {
+ var style;
+ if (s) {
+ style = {};
+ angular.forEach(s, function (value, key) {
+ style['stroke-' + key] = value;
+ });
+ use.style(style);
+ }
+ }
+
+ function applyFillClass(f, use) {
+ use.classed('fill-' + f, true);
+ }
+
+ function doSprite(spr, def, pathmeta) {
+ var pmeta = pathmeta[def.path],
+ c = spr.class || 'gray1',
+ p = spr.pos || [0,0],
+ lab = spr.label,
+ dim = def.dim || [40,40],
+ w = dim[0],
+ h = dim[1],
+ dy = def.labelyoff || 1,
+ sc = def.scale,
+ xfm = sus.translate(p),
+ g, attr, use;
+
+ if (sc) {
+ xfm += sus.scale(sc, sc);
+ }
+
+ g = spriteLayer.append('g')
+ .classed(c, true)
+ .attr('transform', xfm);
+
+ attr = {
+ width: w,
+ height: h,
+ 'xlink:href': '#' + pmeta.u
+ };
+
+ use = g.append('use').attr(attr);
+ applyStrokeStyle(pmeta.s, use);
+ applyFillClass(def.fill, use);
+
+
+ // add subpaths if they have been defined
+ if (fs.isA(def.subpaths)) {
+ def.subpaths.forEach(function (v) {
+ pmeta = pathmeta[v.path];
+ attr = {
+ width: w,
+ height: h,
+ 'xlink:href': '#' + pmeta.u,
+ transform: sus.translate(v.pos)
+ };
+ use = g.append('use').attr(attr);
+ applyStrokeStyle(pmeta.s, use);
+ applyFillClass(def.subpathfill, use);
+ });
+ }
+
+ if (lab) {
+ g.append('text')
+ .text(lab)
+ .attr({ x: w / 2, y: h * dy });
+ }
+ }
+
+ function doLabel(label) {
+ var c = label.class || 'gray1',
+ p = label.pos || [0,0],
+ sz = label.size || 1.0,
+ g = spriteLayer.append('g')
+ .classed(c, true)
+ .attr('transform', sus.translate(p))
+ .append('text')
+ .text(label.text)
+ .style('font-size', (fontsize * sz)+'pt');
+ }
+
+
+ // ==========================
+ // event handlers
+
+ // Handles response from 'spriteListRequest' which lists all the
+ // registered sprite definitions on the server.
+ // (see onos-upload-sprites)
+ function inList(payload) {
+ $log.debug(tssid + 'Registered sprite definitions:', payload.names);
+ // Some day, we will make this list available to the user in
+ // a dropdown selection box...
+ }
+
+ // Handles response from 'spriteDataRequest' which provides the
+ // data for the requested sprite definition.
+ function inData(payload) {
+ var data = payload.data,
+ name, desc, pfx, sprites, labels, alpha,
+ paths, defn, load,
+ pathmeta = {},
+ defs = {},
+ warn = [];
+
+ if (!data) {
+ $log.warn(tssid + 'No sprite data loaded.');
+ return;
+ }
+ name = data.defn_name;
+ desc = data.defn_desc;
+ paths = data.paths;
+ defn = data.defn;
+ load = data.load;
+ pfx = tssid + '[' + name + ']: ';
+
+ $log.debug("Loading sprites...[" + name + "]", desc);
+
+ function no(what) {
+ warn.push(pfx + 'No ' + what + ' property defined');
+ }
+
+ if (!paths) no('paths');
+ if (!defn) no('defn');
+ if (!load) no('load');
+
+ if (warn.length) {
+ $log.error(warn.join('\n'));
+ return;
+ }
+
+ // any custom paths need to be added to the glyph DB, and imported
+ registerPathsAsGlyphs(paths);
+
+ paths.forEach(function (p) {
+ pathmeta[p.tag] = {
+ s: p.stroke,
+ u: p.glyph || 'spr_' + p.tag
+ };
+ });
+
+ defn.forEach(function (d) {
+ defs[d.id] = d;
+ });
+
+ // sprites, labels and alpha are each optional components of the load
+ sprites = load.sprites;
+ labels = load.labels;
+ alpha = load.alpha;
+
+ if (alpha) {
+ spriteLayer.style('opacity', alpha);
+ }
+
+ if (sprites) {
+ sprites.forEach(function (spr) {
+ var def = defs[spr.id];
+ doSprite(spr, def, pathmeta);
+ });
+ }
+
+ if (labels) {
+ labels.forEach(doLabel);
+ }
+ }
+
+
+ function loadSprites(layer, defsElem, defname) {
+ var name = defname || 'sprites';
+ spriteLayer = layer;
+ defsElement = defsElem;
+
+ $log.info(tssid + 'Requesting sprite definition ['+name+']...');
+
+ wss.sendEvent('spriteListRequest');
+ wss.sendEvent('spriteDataRequest', {name: name});
+ }
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoSpriteService',
+ ['$log', '$http', 'FnService', 'GlyphService',
+ 'SvgUtilService', 'WebSocketService',
+
+ function (_$log_, _$http_, _fs_, _gs_, _sus_, _wss_) {
+ $log = _$log_;
+ $http = _$http_;
+ fs = _fs_;
+ gs = _gs_;
+ sus = _sus_;
+ wss = _wss_;
+
+ return {
+ loadSprites: loadSprites,
+ spriteListResponse: inList,
+ spriteDataResponse: inData
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
new file mode 100644
index 00000000..84de261b
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Toolbar Module.
+ Functions for creating and interacting with the toolbar.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, fs, tbs, ps, tov, api;
+
+ // API:
+ // getActionEntry
+ // setUpKeys
+
+ // internal state
+ var toolbar, keyData, cachedState, thirdRow;
+
+ // constants
+ var name = 'topo-tbar',
+ cooktag = 'topo_prefs',
+ soa = 'switchOverlayActions: ',
+ selOver = 'Select overlay here &#x21e7;';
+
+
+ // key to button mapping data
+ var k2b = {
+ O: { id: 'summary-tog', gid: 'summary', isel: true},
+ I: { id: 'instance-tog', gid: 'uiAttached', isel: true },
+ D: { id: 'details-tog', gid: 'details', isel: true },
+ H: { id: 'hosts-tog', gid: 'endstation', isel: false },
+ M: { id: 'offline-tog', gid: 'switch', isel: true },
+ P: { id: 'ports-tog', gid: 'ports', isel: true },
+ B: { id: 'bkgrnd-tog', gid: 'map', isel: false },
+ S: { id: 'sprite-tog', gid: 'cloud', isel: false },
+
+ //X: { id: 'nodelock-tog', gid: 'lock', isel: false },
+ Z: { id: 'oblique-tog', gid: 'oblique', isel: false },
+ N: { id: 'filters-btn', gid: 'filters' },
+ L: { id: 'cycleLabels-btn', gid: 'cycleLabels' },
+ R: { id: 'resetZoom-btn', gid: 'resetZoom' },
+
+ E: { id: 'eqMaster-btn', gid: 'eqMaster' }
+ };
+
+ var prohibited = [
+ 'T', 'backSlash', 'slash',
+ 'X' // needed until we re-instate X above.
+ ];
+ prohibited = prohibited.concat(d3.map(k2b).keys());
+
+
+ // initial toggle state: default settings and tag to key mapping
+ var defaultPrefsState = {
+ summary: 1,
+ insts: 1,
+ detail: 1,
+ hosts: 0,
+ offdev: 1,
+ porthl: 1,
+ bg: 0,
+ spr: 0,
+ toolbar: 0
+ },
+ prefsMap = {
+ summary: 'O',
+ insts: 'I',
+ detail: 'D',
+ hosts: 'H',
+ offdev: 'M',
+ porthl: 'P',
+ bg: 'B',
+ spr: 'S'
+ // NOTE: toolbar state is handled separately
+ };
+
+ function init(_api_) {
+ api = _api_;
+
+ // retrieve initial toggle button settings from user prefs
+ setInitToggleState();
+ }
+
+ function topoDefPrefs() {
+ return angular.extend({}, defaultPrefsState);
+ }
+
+ function setInitToggleState() {
+ cachedState = ps.asNumbers(ps.getPrefs(cooktag));
+ $log.debug('TOOLBAR---- read prefs state:', cachedState);
+
+ if (!cachedState) {
+ cachedState = topoDefPrefs();
+ ps.setPrefs(cooktag, cachedState);
+ $log.debug('TOOLBAR---- Set default prefs state:', cachedState);
+ }
+
+ angular.forEach(prefsMap, function (v, k) {
+ var cfg = k2b[v];
+ cfg && (cfg.isel = !!cachedState[k]);
+ });
+ }
+
+ function initKeyData() {
+ // TODO: use angular forEach instead of d3.map
+ keyData = d3.map(k2b);
+ keyData.forEach(function(key, value) {
+ var data = api.getActionEntry(key);
+ value.cb = data[0]; // on-click callback
+ value.tt = data[1] + ' (' + key + ')'; // tooltip
+ });
+ }
+
+ function addButton(key) {
+ var v = keyData.get(key);
+ v.btn = toolbar.addButton(v.id, v.gid, v.cb, v.tt);
+ }
+
+ function addToggle(key, suppressIfMobile) {
+ var v = keyData.get(key);
+ if (suppressIfMobile && fs.isMobile()) { return; }
+ v.tog = toolbar.addToggle(v.id, v.gid, v.isel, v.cb, v.tt);
+ }
+
+ function addFirstRow() {
+ addToggle('I');
+ addToggle('O');
+ addToggle('D');
+ toolbar.addSeparator();
+
+ addToggle('H');
+ addToggle('M');
+ addToggle('P', true);
+ addToggle('B');
+ addToggle('S', true);
+ }
+
+ function addSecondRow() {
+ //addToggle('X');
+ addToggle('Z');
+ addButton('N');
+ addButton('L');
+ addButton('R');
+ toolbar.addSeparator();
+ addButton('E');
+ }
+
+ function addOverlays() {
+ toolbar.addSeparator();
+
+ // generate radio button set for overlays; start with 'none'
+ var rset = [{
+ gid: 'topo',
+ tooltip: 'No Overlay',
+ cb: function () {
+ tov.tbSelection(null, switchOverlayActions);
+ }
+ }];
+ tov.augmentRbset(rset, switchOverlayActions);
+ toolbar.addRadioSet('topo-overlays', rset);
+ }
+
+ // invoked by overlay service to switch out old buttons and switch in new
+ function switchOverlayActions(oid, keyBindings) {
+ var prohibits = [],
+ kb = fs.isO(keyBindings) || {},
+ order = fs.isA(kb._keyOrder) || [];
+
+ if (keyBindings && !keyBindings._keyOrder) {
+ $log.warn(soa + 'no _keyOrder property defined');
+ } else {
+ // sanity removal of reserved property names
+ ['esc', '_keyListener', '_helpFormat'].forEach(function (k) {
+ fs.removeFromArray(k, order);
+ });
+ }
+
+ thirdRow.clear();
+
+ if (!order.length) {
+ thirdRow.setText(selOver);
+ thirdRow.classed('right', true);
+ api.setUpKeys(); // clear previous overlay key bindings
+
+ } else {
+ thirdRow.classed('right', false);
+ angular.forEach(order, function (key) {
+ var value, bid, gid, tt;
+
+ if (prohibited.indexOf(key) > -1) {
+ prohibits.push(key);
+
+ } else {
+ value = keyBindings[key];
+ bid = oid + '-' + key;
+ gid = tov.mkGlyphId(oid, value.gid);
+ tt = value.tt + ' (' + key + ')';
+ thirdRow.addButton(bid, gid, value.cb, tt);
+ }
+ });
+ api.setUpKeys(keyBindings); // add overlay key bindings
+ }
+
+ if (prohibits.length) {
+ $log.warn(soa + 'Prohibited key bindings ignored:', prohibits);
+ }
+ }
+
+ function createToolbar() {
+ initKeyData();
+ toolbar = tbs.createToolbar(name);
+ addFirstRow();
+ toolbar.addRow();
+ addSecondRow();
+ addOverlays();
+ thirdRow = toolbar.addRow();
+ thirdRow.setText(selOver);
+ thirdRow.classed('right', true);
+
+ if (cachedState.toolbar) {
+ toolbar.show();
+ } else {
+ toolbar.hide();
+ }
+ }
+
+ function destroyToolbar() {
+ tbs.destroyToolbar(name);
+ }
+
+ // allows us to ensure the button states track key strokes
+ function keyListener(key) {
+ var v = keyData.get(key);
+
+ if (v) {
+ // we have a valid button mapping
+ if (v.tog) {
+ // it's a toggle button
+ v.tog.toggleNoCb();
+ }
+ }
+ }
+
+ function toggleToolbar() {
+ toolbar.toggle();
+ }
+
+ angular.module('ovTopo')
+ .factory('TopoToolbarService',
+ ['$log', 'FnService', 'ToolbarService', 'PrefsService',
+ 'TopoOverlayService',
+
+ function (_$log_, _fs_, _tbs_, _ps_, _tov_) {
+ $log = _$log_;
+ fs = _fs_;
+ tbs = _tbs_;
+ ps = _ps_;
+ tov = _tov_;
+
+ return {
+ init: init,
+ createToolbar: createToolbar,
+ destroyToolbar: destroyToolbar,
+ keyListener: keyListener,
+ toggleToolbar: toggleToolbar
+ };
+ }]);
+}()); \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
new file mode 100644
index 00000000..ca379360
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Traffic Module.
+ Defines behavior for viewing different traffic modes.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, flash, wss, api;
+
+ /*
+ API to topoForce
+ hovered()
+ somethingSelected()
+ selectOrder()
+ */
+
+ // internal state
+ var trafficMode = null,
+ hoverMode = null;
+
+
+ // === -----------------------------------------------------
+ // Helper functions
+
+ // invoked in response to change in selection and/or mouseover/out:
+ function requestTrafficForMode(mouse) {
+ if (trafficMode === 'flows') {
+ requestDeviceLinkFlows();
+ } else if (trafficMode === 'intents') {
+ if (!mouse || hoverMode === 'intents') {
+ requestRelatedIntents();
+ }
+ } else {
+ // do nothing
+ }
+ }
+
+ function requestDeviceLinkFlows() {
+ // generates payload based on current hover-state
+ var hov = api.hovered();
+
+ function hoverValid() {
+ return hoverMode === 'flows' &&
+ hov && (hov.class === 'device');
+ }
+
+ if (api.somethingSelected()) {
+ wss.sendEvent('requestDeviceLinkFlows', {
+ ids: api.selectOrder(),
+ hover: hoverValid() ? hov.id : ''
+ });
+ }
+ }
+
+ function requestRelatedIntents() {
+ // generates payload based on current hover-state
+ var hov = api.hovered();
+
+ function hoverValid() {
+ return hoverMode === 'intents' &&
+ hov && (hov.class === 'host' || hov.class === 'device');
+ }
+
+ if (api.somethingSelected()) {
+ wss.sendEvent('requestRelatedIntents', {
+ ids: api.selectOrder(),
+ hover: hoverValid() ? hov.id : ''
+ });
+ }
+ }
+
+
+ // === -------------------------------------------------------------
+ // Traffic requests invoked from keystrokes or toolbar buttons...
+
+ function cancelTraffic(forced) {
+ if (!trafficMode || (!forced && trafficMode === 'allFlowPort')) {
+ return false;
+ }
+
+ trafficMode = hoverMode = null;
+ wss.sendEvent('cancelTraffic');
+ flash.flash('Traffic monitoring canceled');
+ return true;
+ }
+
+ function showAllFlowTraffic() {
+ trafficMode = 'allFlowPort';
+ hoverMode = null;
+ wss.sendEvent('requestAllFlowTraffic');
+ flash.flash('All Flow Traffic');
+ }
+
+ function showAllPortTraffic() {
+ trafficMode = 'allFlowPort';
+ hoverMode = null;
+ wss.sendEvent('requestAllPortTraffic');
+ flash.flash('All Port Traffic');
+ }
+
+ function showDeviceLinkFlows () {
+ trafficMode = hoverMode = 'flows';
+ requestDeviceLinkFlows();
+ flash.flash('Device Flows');
+ }
+
+ function showRelatedIntents () {
+ trafficMode = hoverMode = 'intents';
+ requestRelatedIntents();
+ flash.flash('Related Paths');
+ }
+
+ function showPrevIntent() {
+ if (trafficMode === 'intents') {
+ hoverMode = null;
+ wss.sendEvent('requestPrevRelatedIntent');
+ flash.flash('Previous related intent');
+ }
+ }
+
+ function showNextIntent() {
+ if (trafficMode === 'intents') {
+ hoverMode = null;
+ wss.sendEvent('requestNextRelatedIntent');
+ flash.flash('Next related intent');
+ }
+ }
+
+ function showSelectedIntentTraffic() {
+ if (trafficMode === 'intents') {
+ hoverMode = null;
+ wss.sendEvent('requestSelectedIntentTraffic');
+ flash.flash('Traffic on Selected Path');
+ }
+ }
+
+
+ // === ------------------------------------------------------
+ // action buttons on detail panel (multiple selection)
+
+ function addHostIntent () {
+ var so = api.selectOrder();
+ wss.sendEvent('addHostIntent', {
+ one: so[0],
+ two: so[1],
+ ids: so
+ });
+ trafficMode = 'intents';
+ hoverMode = null;
+ flash.flash('Host-to-Host flow added');
+ }
+
+ function addMultiSourceIntent () {
+ var so = api.selectOrder();
+ wss.sendEvent('addMultiSourceIntent', {
+ src: so.slice(0, so.length - 1),
+ dst: so[so.length - 1],
+ ids: so
+ });
+ trafficMode = 'intents';
+ hoverMode = null;
+ flash.flash('Multi-Source flow added');
+ }
+
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoTrafficService',
+ ['$log', 'FnService', 'FlashService', 'WebSocketService',
+
+ function (_$log_, _fs_, _flash_, _wss_) {
+ $log = _$log_;
+ fs = _fs_;
+ flash = _flash_;
+ wss = _wss_;
+
+ return {
+ initTraffic: function (_api_) { api = _api_; },
+ destroyTraffic: function () { },
+
+ // invoked from toolbar overlay buttons or keystrokes
+ cancelTraffic: cancelTraffic,
+ showAllFlowTraffic: showAllFlowTraffic,
+ showAllPortTraffic: showAllPortTraffic,
+ showDeviceLinkFlows: showDeviceLinkFlows,
+ showRelatedIntents: showRelatedIntents,
+ showPrevIntent: showPrevIntent,
+ showNextIntent: showNextIntent,
+ showSelectedIntentTraffic: showSelectedIntentTraffic,
+
+ // invoked from mouseover/mouseout and selection change
+ requestTrafficForMode: requestTrafficForMode,
+
+ // TODO: these should move to new UI demo app
+ // invoked from buttons on detail (multi-select) panel
+ addHostIntent: addHostIntent,
+ addMultiSourceIntent: addMultiSourceIntent
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
new file mode 100644
index 00000000..cb4bc49a
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ ONOS GUI -- Topology Traffic Overlay Module.
+ Defines behavior for viewing different traffic modes.
+ Installed as a Topology Overlay.
+ */
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, tov, tts;
+
+ // NOTE: no internal state here -- see TopoTrafficService for that
+
+ // NOTE: providing button disabling requires too big a refactoring of
+ // the button factory etc. Will have to be done another time.
+
+
+ // traffic overlay definition
+ var overlay = {
+ overlayId: 'traffic',
+ glyphId: 'allTraffic',
+ tooltip: 'Traffic Overlay',
+
+ // NOTE: Traffic glyphs already installed as part of the base ONOS set.
+
+ activate: function () {
+ $log.debug("Traffic overlay ACTIVATED");
+ },
+
+ deactivate: function () {
+ tts.cancelTraffic(true);
+ $log.debug("Traffic overlay DEACTIVATED");
+ },
+
+ // detail panel button definitions
+ // (keys match button identifiers, also defined in TrafficOverlay.java)
+ buttons: {
+ showDeviceFlows: {
+ gid: 'flows',
+ tt: 'Show Device Flows',
+ cb: function (data) { tts.showDeviceLinkFlows(); }
+ },
+
+ showRelatedTraffic: {
+ gid: 'relatedIntents',
+ tt: 'Show Related Traffic',
+ cb: function (data) { tts.showRelatedIntents(); }
+ }
+ },
+
+ // key bindings for traffic overlay toolbar buttons
+ // NOTE: fully qual. button ID is derived from overlay-id and key-name
+ keyBindings: {
+ 0: {
+ cb: function () { tts.cancelTraffic(true); },
+ tt: 'Cancel traffic monitoring',
+ gid: 'xMark'
+ },
+
+ A: {
+ cb: function () { tts.showAllFlowTraffic(); },
+ tt: 'Monitor all traffic using flow stats',
+ gid: 'allTraffic'
+ },
+ Q: {
+ cb: function () { tts.showAllPortTraffic(); },
+ tt: 'Monitor all traffic using port stats',
+ gid: 'allTraffic'
+ },
+ F: {
+ cb: function () { tts.showDeviceLinkFlows(); },
+ tt: 'Show device link flows',
+ gid: 'flows'
+ },
+ V: {
+ cb: function () { tts.showRelatedIntents(); },
+ tt: 'Show all related intents',
+ gid: 'relatedIntents'
+ },
+ leftArrow: {
+ cb: function () { tts.showPrevIntent(); },
+ tt: 'Show previous related intent',
+ gid: 'prevIntent'
+ },
+ rightArrow: {
+ cb: function () { tts.showNextIntent(); },
+ tt: 'Show next related intent',
+ gid: 'nextIntent'
+ },
+ W: {
+ cb: function () { tts.showSelectedIntentTraffic(); },
+ tt: 'Monitor traffic of selected intent',
+ gid: 'intentTraffic'
+ },
+
+ _keyOrder: [
+ '0', 'A', 'Q', 'F', 'V', 'leftArrow', 'rightArrow', 'W'
+ ]
+ },
+
+ hooks: {
+ // hook for handling escape key
+ escape: function () {
+ // Must return true to consume ESC, false otherwise.
+ return tts.cancelTraffic(true);
+ },
+
+ // hooks for when the selection changes...
+ empty: function () {
+ tts.cancelTraffic();
+ },
+ single: function (data) {
+ tts.requestTrafficForMode();
+ },
+ multi: function (selectOrder) {
+ tts.requestTrafficForMode();
+ tov.addDetailButton('showRelatedTraffic');
+ },
+
+ // mouse hooks
+ mouseover: function (m) {
+ // m has id, class, and type properties
+ tts.requestTrafficForMode(true);
+ },
+ mouseout: function () {
+ tts.requestTrafficForMode(true);
+ }
+ }
+ };
+
+ // invoke code to register with the overlay service
+ angular.module('ovTopo')
+ .run(['$log', 'TopoOverlayService', 'TopoTrafficService',
+
+ function (_$log_, _tov_, _tts_) {
+ $log = _$log_;
+ tov = _tov_;
+ tts = _tts_;
+ tov.register(overlay);
+ }]);
+
+}());