blob: 97c9e1efbd93bc1d721c6f527162248405b96b3e [file] [log] [blame]
Sean Condonf4f54a12018-10-10 23:25:46 +01001/*
2 * Copyright 2018-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the 'License');
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an 'AS IS' BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Sean Condon0c577f62018-11-18 22:40:05 +000016import {
17 ChangeDetectionStrategy,
18 ChangeDetectorRef,
19 Component,
20 EventEmitter,
21 HostListener,
22 Input,
23 OnChanges,
24 OnInit,
25 Output, QueryList, SimpleChange,
26 SimpleChanges, ViewChildren
27} from '@angular/core';
28import {IconService, LogService} from 'gui2-fw-lib';
29import {
30 Device,
31 ForceDirectedGraph,
32 Host,
33 LabelToggle,
34 LayerType,
35 Region,
36 RegionLink,
37 SubRegion
38} from './models';
39import {DeviceNodeSvgComponent} from './visuals';
Sean Condonaa4366d2018-11-02 14:29:01 +000040
Sean Condonf4f54a12018-10-10 23:25:46 +010041
42/**
43 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000044 *
45 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
46 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010047 */
48@Component({
49 selector: '[onos-forcesvg]',
50 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000051 styleUrls: ['./forcesvg.component.css'],
52 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010053})
Sean Condon0c577f62018-11-18 22:40:05 +000054export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condonaa4366d2018-11-02 14:29:01 +000055 @Input() onosInstMastership: string = '';
Sean Condon0c577f62018-11-18 22:40:05 +000056 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
57 @Output() linkSelected = new EventEmitter<RegionLink>();
58 @Output() selectedNodeEvent = new EventEmitter<Device>();
59 @Input() selectedLink: RegionLink = null;
60 private graph: ForceDirectedGraph;
Sean Condonf4f54a12018-10-10 23:25:46 +010061
Sean Condon0c577f62018-11-18 22:40:05 +000062 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
63 private _options: { width, height } = { width: 800, height: 600 };
Sean Condonf4f54a12018-10-10 23:25:46 +010064
Sean Condon0c577f62018-11-18 22:40:05 +000065 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
66
67 @HostListener('window:resize', ['$event'])
68 onResize(event) {
69 this.graph.initSimulation(this.options);
70 this.log.debug('Simulation reinit after resize', event);
Sean Condonf4f54a12018-10-10 23:25:46 +010071 }
72
Sean Condon0c577f62018-11-18 22:40:05 +000073 constructor(
74 protected log: LogService,
75 protected is: IconService,
76 private ref: ChangeDetectorRef
77 ) {
78 this.selectedLink = null;
79 this.log.debug('ForceSvgComponent constructed');
80 }
81
82 /**
83 * After the component is initialized create the Force simulation
84 */
85 ngOnInit() {
86 // Receiving an initialized simulated graph from our custom d3 service
87 this.graph = new ForceDirectedGraph(this.options);
88
89 /** Binding change detection check on each tick
90 * This along with an onPush change detection strategy should enforce checking only when relevant!
91 * This improves scripting computation duration in a couple of tests I've made, consistently.
92 * Also, it makes sense to avoid unnecessary checks when we are dealing only with simulations data binding.
93 */
94 this.graph.ticker.subscribe((simulation) => {
95 // this.log.debug("Force simulation has ticked", simulation);
96 this.ref.markForCheck();
97 });
98 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
99
100 this.is.loadIconDef('m_switch');
101 }
102
103 /**
104 * When any one of the inputs get changed by a containing component, this gets called automatically
105 * In addition this is called manually by topology.service when a response
106 * is received from the WebSocket from the server
107 *
108 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
109 * The Links are added to the Link list of the simulation.
110 * Before they are added the Links are associated with Nodes based on their endPt
111 *
112 * @param changes - a list of changed @Input(s)
113 */
114 ngOnChanges(changes: SimpleChanges) {
115 if (changes['regionData']) {
116 const devices: Device[] =
117 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
118 const hosts: Host[] =
119 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
120 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
121 this.graph.nodes = [];
122 if (devices) {
123 this.graph.nodes = devices;
124 }
125 if (hosts) {
126 this.graph.nodes = this.graph.nodes.concat(hosts);
127 }
128 if (subRegions) {
129 this.graph.nodes = this.graph.nodes.concat(subRegions);
130 }
131
132 // Associate the endpoints of each link with a real node
133 this.graph.links = [];
134 for (const linkIdx of Object.keys(this.regionData.links)) {
135 this.regionData.links[linkIdx].source =
136 this.graph.nodes.find((node) =>
137 node.id === this.regionData.links[linkIdx].epA);
138 this.regionData.links[linkIdx].target =
139 this.graph.nodes.find((node) =>
140 node.id === this.regionData.links[linkIdx].epB);
141 this.regionData.links[linkIdx].index = Number(linkIdx);
142 }
143
144 this.graph.links = this.regionData.links;
145
146 this.graph.initSimulation(this.options);
147 this.graph.initNodes();
148 this.log.debug('ForceSvgComponent input changed',
149 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
150 }
151 }
152
153 /**
154 * Get the index of LayerType so it can drive the visibility of nodes and
155 * hosts on layers
156 */
157 visibleLayerIdx(): number {
158 const layerKeys: string[] = Object.keys(LayerType);
159 for (const idx in layerKeys) {
160 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
161 return Number(idx);
162 }
163 }
164 return -1;
165 }
166
167 selectLink(link: RegionLink): void {
168 this.selectedLink = link;
169 this.linkSelected.emit(link);
170 }
171
172 get options() {
173 return this._options = {
174 width: window.innerWidth,
175 height: window.innerHeight
176 };
177 }
178
179 updateDeviceLabelToggle() {
180 this.devices.forEach((d) => {
181 const old: LabelToggle = d.labelToggle;
182 const next = LabelToggle.next(old);
183 d.ngOnChanges({'labelToggle': new SimpleChange(old, next, false)});
184 });
185 }
186
187 updateSelected(selectedNodeId: string): void {
188 this.log.debug('Device selected', selectedNodeId);
189 this.devices.filter((d) => d.device.id !== selectedNodeId).forEach((d) => {
190 d.deselect();
191 });
192 const selectedDevice: DeviceNodeSvgComponent =
193 (this.devices.find((d) => d.device.id === selectedNodeId));
194 if (selectedDevice) {
195 this.selectedNodeEvent.emit(selectedDevice.device);
196 } else {
197 this.selectedNodeEvent.emit();
198 }
199 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100200}
Sean Condon0c577f62018-11-18 22:40:05 +0000201