GUI2 Extract Topology view in to its own library
Change-Id: I45597d0902c99b5b3d606966866cc518011c54a0
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
new file mode 100644
index 0000000..204b85c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 (forces device visual) -- CSS file
+ */
+g.node.device rect {
+ fill: #f0f0f070;
+}
+g.node.device text {
+ fill: #bbb;
+}
+g.node.device use {
+ fill: #777;
+}
+
+
+g.node.device.online rect {
+ fill: #fafafad0;
+}
+g.node.device.online text {
+ fill: #3c3a3a;
+}
+g.node.device.online use {
+ /* NOTE: this gets overridden programatically */
+ fill: #ffffff;
+}
+
+g.node.selected .node-container {
+ stroke-width: 2.0;
+ stroke: #009fdb;
+}
+
+g.node.hovered .node-container {
+ stroke-width: 2.0;
+ stroke: #454545;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
new file mode 100644
index 0000000..399955c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
@@ -0,0 +1,91 @@
+<!--
+~ Copyright 2019-present Open Networking Foundation
+~
+~ 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.
+-->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+ <!-- Template explanation: Define an SVG Filter that in
+ line 0) creates a box big enough to accommodate the drop shadow
+ line 1) render the target object in to a bit map and apply a blur to it
+ based on its alpha channel
+ line 2) take that blurred layer and shift it down and to the right by 4
+ line 3) Merge this blurred and shifted layer and overlay it with the
+ original target object
+ -->
+ <svg:filter id="drop-shadow" x="-25%" y="-25%" width="200%" height="200%">
+ <svg:feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur" />
+ <svg:feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>
+ <svg:feMerge >
+ <svg:feMergeNode in="offsetBlur" />
+ <svg:feMergeNode in="SourceGraphic" />
+ </svg:feMerge>
+ </svg:filter>
+ <!-- Template explanation: Define a colour gradient that can be used in icons -->
+ <svg:linearGradient id="diagonal_blue" x1="0%" y1="0%" x2="100%" y2="100%">
+ <svg:stop offset= "0%" style="stop-color: #7fabdb;" />
+ <svg:stop offset= "100%" style="stop-color: #5b99d2;" />
+ </svg:linearGradient>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Group and in
+ line 1) transform it to the position calculated by the d3 force graph engine
+ and scale it inversely to the zoom level
+ line 2) Give it various CSS styles depending on attributes
+ line 3) When it is clicked, call the method that toggles the selection and
+ emits an event.
+ Other child objects have their own description
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.transform]="'translate(' + device?.x + ',' + device?.y + '), scale(' + scale + ')'"
+ [ngClass]="['node', 'device', device.online?'online':'', selected?'selected':'']"
+ (click)="toggleSelected(device)">
+ <svg:desc>Device {{device.id}}</svg:desc>
+ <!-- Template explanation: Creates an SVG Rectangle and in
+ line 1) set a css style and shift so that it's centred
+ line 2) set the initial width and height - width changes with label
+ line 3) link to the animation 'deviceLabelToggle', pass in to it a width
+ calculated from the width of the text, and additional padding at the end
+ line 4) Apply the filter defined above to this rectangle (even as its
+ width changes
+ -->
+ <svg:rect
+ class="node-container" x="-18" y="-18"
+ width="36" height="36"
+ [@deviceLabelToggle]="{ value: labelToggle, params: {txtWidth: (36 + labelTextLen() * 1.1)+'px' }}"
+ filter= "url(#drop-shadow)">
+ </svg:rect>
+ <!-- Template explanation: Creates an SVG Rectangle slightly smaller and
+ overlaid on the above. This is the blue box, and its width and height does
+ not change
+ -->
+ <svg:rect x="-16" y="-16" width="32" height="32" style="fill: url(#diagonal_blue)">
+ </svg:rect>
+ <!-- Template explanation: Creates an SVG Text element and in
+ line 1) make it left aligned and slightly down and to the right of the last rect
+ line 2) set its text length to be the calculated value - see that function
+ line 3) because of kerning the actual text might be shorter or longer than
+ the pre-calculated value - if so change the spacing between the letters
+ (and not the letter width to compensate)
+ line 4) link to the animation deviceLabelToggleTxt, so that the text appears
+ in gently
+ line 5) The text will be one of 3 values - blank, the id or the name
+ -->
+ <svg:text
+ text-anchor="start" y="0.3em" x="22"
+ [attr.textLength]= "labelTextLen()"
+ lengthAdjust= "spacing"
+ [@deviceLabelToggleTxt]="labelToggle">
+ {{ labelToggle == 0 ? '': labelToggle == 1 ? device.id:device.props.name }}
+ </svg:text>
+ <svg:use [attr.xlink:href]="'#' + deviceIcon()" width="36" height="36" x="-18" y="-18">
+ </svg:use>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
new file mode 100644
index 0000000..b490605
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeviceNodeSvgComponent } from './devicenodesvg.component';
+import {IconService, LogService} from 'gui2-fw-lib';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {ChangeDetectorRef} from '@angular/core';
+import {Device} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+class MockIconService {
+ loadIconDef() { }
+}
+
+describe('DeviceNodeSvgComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: DeviceNodeSvgComponent;
+ let fixture: ComponentFixture<DeviceNodeSvgComponent>;
+ let ar: MockActivatedRoute;
+ let testDevice: Device;
+
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+ testDevice = new Device('test:1');
+ testDevice.online = true;
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ DeviceNodeSvgComponent ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ { provide: ChangeDetectorRef, useClass: ChangeDetectorRef },
+ { provide: IconService, useClass: MockIconService }
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DeviceNodeSvgComponent);
+ component = fixture.componentInstance;
+ component.device = testDevice;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
new file mode 100644
index 0000000..ce6bd46
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges, Output,
+ SimpleChanges,
+} from '@angular/core';
+import {Device, LabelToggle, UiElement} from '../../models';
+import {IconService, LocMeta, LogService, MetaUi, ZoomUtils} from 'gui2-fw-lib';
+import {NodeVisual} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+import {LocationType} from '../../../backgroundsvg/backgroundsvg.component';
+
+/**
+ * The Device node in the force graph
+ *
+ * Note: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ */
+@Component({
+ selector: '[onos-devicenodesvg]',
+ templateUrl: './devicenodesvg.component.html',
+ styleUrls: ['./devicenodesvg.component.css'],
+ changeDetection: ChangeDetectionStrategy.Default,
+ animations: [
+ trigger('deviceLabelToggle', [
+ state('0', style({ // none
+ width: '36px',
+ })),
+ state('1, 2', // id
+ style({ width: '{{ txtWidth }}'}),
+ { params: {'txtWidth': '36px'}}
+ ), // default
+ transition('0 => 1', animate('250ms ease-in')),
+ transition('1 => 2', animate('250ms ease-in')),
+ transition('* => 0', animate('250ms ease-out'))
+ ]),
+ trigger('deviceLabelToggleTxt', [
+ state('0', style( {
+ opacity: 0,
+ })),
+ state( '1,2', style({
+ opacity: 1.0
+ })),
+ transition('0 => 1', animate('250ms ease-in')),
+ transition('* => 0', animate('250ms ease-out'))
+ ])
+ ]
+})
+export class DeviceNodeSvgComponent extends NodeVisual implements OnChanges {
+ @Input() device: Device;
+ @Input() scale: number = 1.0;
+ @Input() labelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
+ @Output() selectedEvent = new EventEmitter<UiElement>();
+ textWidth: number = 36;
+ constructor(
+ protected log: LogService,
+ private is: IconService,
+ private ref: ChangeDetectorRef
+ ) {
+ super();
+ }
+
+ /**
+ * Called by parent (forcesvg) when a change happens
+ *
+ * There is a difficulty in passing the SVG text object to the animation
+ * directly, to get its width, so we capture it here and update textWidth
+ * local variable here and use it in the animation
+ */
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['device']) {
+ if (!this.device.x) {
+ this.device.x = 0;
+ this.device.y = 0;
+ }
+ }
+ this.ref.markForCheck();
+ }
+
+ /**
+ * Calculate the text length in advance as well as possible
+ *
+ * The length of SVG text cannot be exactly estimated, because depending on
+ * the letters kerning might mean that it is shorter or longer than expected
+ *
+ * This takes the approach of 8px width per letter of this size, that on average
+ * evens out over words. A word like 'ilj' will be much shorter than 'wm0'
+ * because of kerning
+ *
+ *
+ * In addition in the template, the <svg:text> properties
+ * textLength and lengthAdjust ensure that the text becomes long with extra
+ * wide spacing created as necessary.
+ *
+ * Other approaches like getBBox() of the text
+ */
+ labelTextLen() {
+ if (this.labelToggle === 1) {
+ return this.device.id.length * 8;
+ } else if (this.labelToggle === 2 && this.device &&
+ this.device.props.name && this.device.props.name.trim().length > 0) {
+ return this.device.props.name.length * 8;
+ } else {
+ return 0;
+ }
+ }
+
+ deviceIcon(): string {
+ if (this.device.props && this.device.props.uiType) {
+ this.is.loadIconDef(this.device.props.uiType);
+ return this.device.props.uiType;
+ } else {
+ return 'm_' + this.device.type;
+ }
+ }
+
+ resetNodeLocation(): void {
+ this.log.debug('Resetting device', this.device.id, this.device.type);
+ let origLoc: MetaUi;
+
+ if (!this.device.location || this.device.location.locType === LocationType.NONE) {
+ // No location - nothing to do
+ return;
+ } else if (this.device.location.locType === LocationType.GEO) {
+ origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+ lng: this.device.location.longOrX,
+ lat: this.device.location.latOrY
+ });
+ } else if (this.device.location.locType === LocationType.GRID) {
+ origLoc = ZoomUtils.convertXYtoGeo(
+ this.device.location.longOrX, this.device.location.latOrY);
+ }
+ this.device.metaUi = origLoc;
+ this.device['fx'] = origLoc.x;
+ this.device['fy'] = origLoc.y;
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css
new file mode 100644
index 0000000..92a114f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 (forces host visual) -- CSS file
+ */
+.node.host text {
+ stroke: none;
+ font-size: 13px;
+ fill: #846;
+}
+
+.node.host circle {
+ stroke: #a3a596;
+ fill: #e0dfd6;
+}
+
+.node.host.selected > circle {
+ stroke-width: 2.0;
+ stroke: #009fdb;
+}
+
+.node.host use {
+ fill: #3c3a3a;
+}
+
+.node.host rect {
+ fill: #ffffff;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
new file mode 100644
index 0000000..ccce65a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
@@ -0,0 +1,70 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ 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.
+-->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+ <!-- Template explanation: Define an SVG Filter that in
+ line 1) render the target object in to a bit map and apply a blur to it
+ based on its alpha channel
+ line 2) take that blurred layer and shift it down and to the right by 4
+ line 3) Merge this blurred and shifted layer and overlay it with the
+ original target object
+ -->
+ <svg:filter id="drop-shadow-host" x="-25%" y="-25%" width="200%" height="200%">
+ <svg:feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur" />
+ <svg:feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>
+ <svg:feMerge >
+ <svg:feMergeNode in="offsetBlur" />
+ <svg:feMergeNode in="SourceGraphic" />
+ </svg:feMerge>
+ </svg:filter>
+ <svg:radialGradient id="three_stops_radial">
+ <svg:stop offset= "0%" style="stop-color: #e3e5d6;" />
+ <svg:stop offset= "70%" style="stop-color: #c3c5b6;" />
+ <svg:stop offset="100%" style="stop-color: #a3a596;" />
+ </svg:radialGradient>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Group and in
+ line 1) transform it to the position calculated by the d3 force graph engine
+ line 2) Give it various CSS styles depending on attributes
+ line 3) When it is clicked, call the method that toggles the selection and
+ emits an event.
+ Other child objects have their own description
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.transform]="'translate(' + host?.x + ',' + host?.y + '), scale(' + scale + ')'"
+ [ngClass]="['node', 'host', 'endstation', 'fixed', selected?'selected':'', 'hovered']"
+ (click)="toggleSelected(host)">
+ <svg:desc>Host {{host.id}}</svg:desc>
+ <!-- Template explanation: Creates an SVG Circle and in
+ line 1) Apply the drop shadow defined above to this circle
+ line 2) Apply the radial gradient defined above to the circle
+ -->
+ <svg:circle r="15"
+ filter="url(#drop-shadow-host)"
+ style="fill: url(#three_stops_radial)">
+ </svg:circle>
+ <svg:use xlink:href="#m_endstation" width="22.5" height="22.5" x="-11.25" y="-11.25">
+ </svg:use>
+ <!-- Template explanation: Creates an SVG Text
+ line 1) if the labelToggle is not 0
+ line 2) shift it below the circle, and have it centred with the circle
+ line 3) apply a scale and call on the hostName(0 method to get the
+ displayed value
+ -->
+ <svg:text
+ *ngIf="labelToggle != 0"
+ dy="30" text-anchor="middle"
+ >{{hostName()}}</svg:text>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
new file mode 100644
index 0000000..e3efb3f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HostNodeSvgComponent } from './hostnodesvg.component';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {LogService} from 'gui2-fw-lib';
+import {Host} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {ChangeDetectorRef} from '@angular/core';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('HostNodeSvgComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: HostNodeSvgComponent;
+ let fixture: ComponentFixture<HostNodeSvgComponent>;
+ let ar: MockActivatedRoute;
+ let testHost: Host;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+ testHost = new Host('host:1');
+ testHost.ips = ['10.205.86.123', '192.168.56.10'];
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ HostNodeSvgComponent ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ { provide: ChangeDetectorRef, useClass: ChangeDetectorRef }
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HostNodeSvgComponent);
+ component = fixture.componentInstance;
+ component.host = testHost;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts
new file mode 100644
index 0000000..26c105a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ Output,
+ SimpleChanges
+} from '@angular/core';
+import {Host, HostLabelToggle, Node} from '../../models';
+import {LogService} from 'gui2-fw-lib';
+import {NodeVisual} from '../nodevisual';
+
+/**
+ * The Host node in the force graph
+ *
+ * Note: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ */
+@Component({
+ selector: '[onos-hostnodesvg]',
+ templateUrl: './hostnodesvg.component.html',
+ styleUrls: ['./hostnodesvg.component.css']
+})
+export class HostNodeSvgComponent extends NodeVisual implements OnChanges {
+ @Input() host: Host;
+ @Input() scale: number = 1.0;
+ @Input() labelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.IP;
+ @Output() selectedEvent = new EventEmitter<Node>();
+
+ constructor(
+ protected log: LogService
+ ) {
+ super();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (!this.host.x) {
+ this.host.x = 0;
+ this.host.y = 0;
+ }
+ }
+
+ hostName(): string {
+ if (this.host === undefined) {
+ return undefined;
+ } else if (this.labelToggle === HostLabelToggle.Enum.IP) {
+ return this.host.ips.join(',');
+ } else if (this.labelToggle === HostLabelToggle.Enum.MAC) {
+ return this.host.id;
+ } else {
+ return this.host.id; // Todo - replace with a friendly name
+ }
+
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css
new file mode 100644
index 0000000..58548c4
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 (forces link svg) -- CSS file
+ */
+/* --- Topo Links --- */
+line {
+ stroke: #888888;
+ stroke-width: 2px;
+}
+
+.link {
+ opacity: .9;
+}
+
+.link.selected {
+ stroke: #009fdb;
+}
+.link.enhanced {
+ stroke: #009fdb;
+ stroke-width: 4px;
+ cursor: pointer;
+}
+
+.link.inactive {
+ opacity: .5;
+ stroke-dasharray: 4 2;
+}
+/* TODO: Review for not-permitted links */
+.link.not-permitted {
+ stroke: rgb(255,0,0);
+ stroke-dasharray: 8 4;
+}
+
+.link.secondary {
+ stroke: rgba(0,153,51,0.5);
+}
+
+.link.secondary.port-traffic-green {
+ stroke: rgb(0,153,51);
+}
+
+.link.secondary.port-traffic-yellow {
+ stroke: rgb(128,145,27);
+}
+
+.link.secondary.port-traffic-orange {
+ stroke: rgb(255, 137, 3);
+}
+
+.link.secondary.port-traffic-red {
+ stroke: rgb(183, 30, 21);
+}
+
+/* Port traffic color visualization for Kbps, Mbps, and Gbps */
+
+.link.secondary.port-traffic-Kbps {
+ stroke: rgb(0,153,51);
+}
+
+.link.secondary.port-traffic-Mbps {
+ stroke: rgb(128,145,27);
+}
+
+.link.secondary.port-traffic-Gbps {
+ stroke: rgb(255, 137, 3);
+}
+
+.link.secondary.port-traffic-Gbps-choked {
+ stroke: rgb(183, 30, 21);
+}
+
+.link.animated {
+ stroke-dasharray: 8 5;
+ animation: ants 5s infinite linear;
+ /* below line could be added via Javascript, based on path, if we cared
+ * enough about the direction of ant-flow
+ */
+ /*animation-direction: reverse;*/
+}
+@keyframes ants {
+ from {
+ stroke-dashoffset: 0;
+ }
+ to {
+ stroke-dashoffset: 400;
+ }
+}
+
+.link.primary {
+ stroke-width: 4px;
+ stroke: #ffA300;
+}
+
+.link.secondary.optical {
+ stroke-width: 4px;
+ stroke: rgba(128,64,255,0.5);
+}
+
+.link.primary.optical {
+ stroke-width: 6px;
+ stroke: #74f;
+}
+
+/* Link Labels */
+.linkLabel rect {
+ stroke: none;
+ fill: #ffffff;
+}
+
+.linkLabel text {
+ fill: #444;
+ text-anchor: middle;
+}
+
+
+/* Port Labels */
+.portLabel rect {
+ stroke: #a3a596;
+ fill: #ffffff;
+}
+
+.portLabel {
+ fill: #444;
+ alignment-baseline: middle;
+ dominant-baseline: middle;
+}
+
+/* Number of Links Labels */
+
+
+#ov-topo2 text.numLinkText {
+ fill: #444;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html
new file mode 100644
index 0000000..c856763
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html
@@ -0,0 +1,100 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ 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.
+-->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+ <svg:filter id="glow">
+ <svg:feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0.9 0 0 0 0 0.9 0 0 0 0 1 0" />
+ <svg:feGaussianBlur stdDeviation="2.5" result="coloredBlur" />
+ <svg:feMerge>
+ <svg:feMergeNode in="coloredBlur" />
+ <svg:feMergeNode in="SourceGraphic"/>
+ </svg:feMerge>
+ </svg:filter>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Line and in
+ line 1) transform end A to the position calculated by the d3 force graph engine
+ line 2) transform end B to the position calculated by the d3 force graph engine
+ line 3) Give it various CSS styles depending on attributes
+ ling 4) Change the line width depending on the scale
+ line 4) When it is clicked, call the method that toggles the selection and
+ emits an event.
+ line 5) When the mouse is moved over call on enhance() function. This will
+ flash up the port labels, and display the link in blue for 1 second
+ Other child objects have their own description
+-->
+<svg:line xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.x1]="link.source?.x" [attr.y1]="link.source?.y"
+ [attr.x2]="link.target?.x" [attr.y2]="link.target?.y"
+ [ngClass]="['link', selected?'selected':'', enhanced?'enhanced':'', highlighted]"
+ [ngStyle]="{'stroke-width': (enhanced ? 4 : 2) * scale + 'px'}"
+ (click)="toggleSelected(link)"
+ (mouseover)="enhance()"
+ [attr.filter]="highlighted?'url(#glow)':'none'">
+</svg:line>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ [ngClass]="['linkLabel']"
+ [attr.transform]="'scale(' + scale + ')'">
+ <!-- Template explanation: Creates SVG Text in the middle of the link to
+ show traffic and in:
+ line 1) Performs the animation 'linkLabelVisible' whenever the isHighlighted
+ boolean value changes
+ line 2 & 3) Sets the text at half way between the 2 end points of the line
+ Note: we do not use an *ngIf to enable or disable this, because that would
+ cause the fade out of the text to not work
+ -->
+ <svg:text xmlns:svg="http://www.w3.org/2000/svg"
+ [@linkLabelVisible]="isHighlighted"
+ [attr.x]="link.source?.x + (link.target?.x - link.source?.x)/2"
+ [attr.y]="link.source?.y + (link.target?.y - link.source?.y)/2"
+ >{{ label }}</svg:text>
+</svg:g>
+<!-- Template explanation: Creates an SVG Group if
+ line 1) 'enhanced' is active and port text exists
+ line 2) assigns classes to it
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ *ngIf="enhanced && link.portA"
+ class="portLabel"
+ [attr.transform]="'translate(' + labelPosSrc.x + ',' + labelPosSrc.y + '),scale(' + scale + ')'">
+ <!-- Template explanation: Creates an SVG Rectangle and in
+ line 1) transform end A to the position calculated by the d3 force graph engine
+ line 2) assigns classes to it
+ -->
+ <svg:rect
+ [attr.x]="2 - textLength(link.portA)/2" y="-8"
+ [attr.width]="4 + textLength(link.portA)" height="16" >
+ </svg:rect>
+ <!-- Template explanation: Creates SVG Text and in
+ line 1) transform it to the position calculated by the method labelPosSrc()
+ line 2) centre aligns it
+ line 3) ensures that the text fills the rectangle by adjusting spacing
+ -->
+ <svg:text y="2" text-anchor="middle"
+ [attr.textLength]= "textLength(link.portA)" lengthAdjust="spacing"
+ >{{ link.portA }}</svg:text>
+</svg:g>
+<!-- A repeat of the above, but for the other end of the line -->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ *ngIf="enhanced && link.portB"
+ class="portLabel"
+ [attr.transform]="'translate(' + labelPosTgt.x + ',' + labelPosTgt.y + '),scale(' + scale + ')'">
+ <svg:rect
+ [attr.x]="2 - textLength(link.portB)/2" y="-8"
+ [attr.width]="4 + textLength(link.portB)" height="16">
+ </svg:rect>
+ <svg:text x="2" y="2" text-anchor="middle"
+ [attr.textLength]= "textLength(link.portB)" lengthAdjust="spacing"
+ >{{ link.portB }}</svg:text>
+</svg:g>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts
new file mode 100644
index 0000000..7626ff5
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LinkSvgComponent } from './linksvg.component';
+import {LogService} from 'gui2-fw-lib';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {Device, Link, RegionLink, LinkType} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('LinkVisualComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: LinkSvgComponent;
+ let fixture: ComponentFixture<LinkSvgComponent>;
+ let ar: MockActivatedRoute;
+ let testLink: Link;
+ let testDeviceA: Device;
+ let testDeviceB: Device;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ testDeviceA = new Device('test:A');
+ testDeviceA.online = true;
+
+ testDeviceB = new Device('test:B');
+ testDeviceB.online = true;
+
+ testLink = new RegionLink(LinkType.UiDeviceLink, testDeviceA, testDeviceB);
+ testLink.id = 'test:A/1-test:B/1';
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ LinkSvgComponent ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LinkSvgComponent);
+ component = fixture.componentInstance;
+ component.link = testLink;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts
new file mode 100644
index 0000000..eab533f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import {
+ ChangeDetectorRef,
+ Component, EventEmitter,
+ Input, OnChanges, Output, SimpleChanges,
+} from '@angular/core';
+import {Link, LinkHighlight, UiElement} from '../../models';
+import {LogService} from 'gui2-fw-lib';
+import {NodeVisual} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+
+interface Point {
+ x: number;
+ y: number;
+}
+
+@Component({
+ selector: '[onos-linksvg]',
+ templateUrl: './linksvg.component.html',
+ styleUrls: ['./linksvg.component.css'],
+ animations: [
+ trigger('linkLabelVisible', [
+ state('true', style( {
+ opacity: 1.0,
+ })),
+ state( 'false', style({
+ opacity: 0
+ })),
+ transition('false => true', animate('500ms ease-in')),
+ transition('true => false', animate('1000ms ease-out'))
+ ])
+ ]
+})
+export class LinkSvgComponent extends NodeVisual implements OnChanges {
+ @Input() link: Link;
+ @Input() highlighted: string = '';
+ @Input() highlightsEnabled: boolean = true;
+ @Input() label: string;
+ @Input() scale = 1.0;
+ isHighlighted: boolean = false;
+ @Output() selectedEvent = new EventEmitter<UiElement>();
+ @Output() enhancedEvent = new EventEmitter<Link>();
+ enhanced: boolean = false;
+ labelPosSrc: Point = {x: 0, y: 0};
+ labelPosTgt: Point = {x: 0, y: 0};
+
+ constructor(
+ protected log: LogService,
+ private ref: ChangeDetectorRef
+ ) {
+ super();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['linkHighlight']) {
+ const hl: LinkHighlight = changes['linkHighlight'].currentValue;
+ this.highlighted = hl.css;
+ this.label = hl.label;
+ this.isHighlighted = true;
+ setTimeout(() => {
+ this.isHighlighted = false;
+ this.highlighted = '';
+ this.ref.markForCheck();
+ }, 4990);
+
+ }
+
+ this.ref.markForCheck();
+ }
+
+ enhance() {
+ if (!this.highlightsEnabled) {
+ return;
+ }
+ this.enhancedEvent.emit(this.link);
+ this.enhanced = true;
+ this.repositionLabels();
+ setTimeout(() => {
+ this.enhanced = false;
+ this.ref.markForCheck();
+ }, 1000);
+ }
+
+ /**
+ * We want to place the label for the port about 40 px from the node.
+ * If the distance between the nodes is less than 100, then just place the
+ * label 1/3 of the way from the node
+ */
+ repositionLabels(): void {
+ const x1: number = this.link.source.x;
+ const y1: number = this.link.source.y;
+ const x2: number = this.link.target.x;
+ const y2: number = this.link.target.y;
+
+ const dist = Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
+ const offset = dist > 100 ? 40 : dist / 3;
+ this.labelPosSrc = <Point>{
+ x: x1 + (x2 - x1) * offset / dist,
+ y: y1 + (y2 - y1) * offset / dist
+ };
+
+ this.labelPosTgt = <Point>{
+ x: x2 - (x2 - x1) * offset / dist,
+ y: y2 - (y2 - y1) * offset / dist
+ };
+ }
+
+ /**
+ * For the 14pt font we are using, the average width seems to be about 8px
+ * @param text The string we want to calculate a width for
+ */
+ textLength(text: string) {
+ return text.length * 8;
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/nodevisual.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/nodevisual.ts
new file mode 100644
index 0000000..a41df62
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/nodevisual.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import {EventEmitter} from '@angular/core';
+import {UiElement} from '../models';
+
+/**
+ * A base class for the Host and Device components
+ */
+export abstract class NodeVisual {
+ selected: boolean;
+ selectedEvent = new EventEmitter<UiElement>();
+
+ toggleSelected(uiElement: UiElement) {
+ this.selected = !this.selected;
+ if (this.selected) {
+ this.selectedEvent.emit(uiElement);
+ } else {
+ this.selectedEvent.emit();
+ }
+ }
+
+ deselect() {
+ this.selected = false;
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css
new file mode 100644
index 0000000..87c23bd
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 (forces subRegion visual) -- CSS file
+ */
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html
new file mode 100644
index 0000000..5760634
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html
@@ -0,0 +1,23 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ 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.
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" [attr.transform]="'translate(' + subRegion?.x + ',' + subRegion?.y + ')'">>
+ <svg:circle
+ cx="0"
+ cy="0"
+ r="5">
+ </svg:circle>
+ <svg:text>{{subRegion?.id}}</svg:text>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts
new file mode 100644
index 0000000..d6f6446
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SubRegionNodeSvgComponent } from './subregionnodesvg.component';
+import {SubRegion} from '../../models';
+
+describe('SubRegionNodeSvgComponent', () => {
+ let component: SubRegionNodeSvgComponent;
+ let fixture: ComponentFixture<SubRegionNodeSvgComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SubRegionNodeSvgComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubRegionNodeSvgComponent);
+ component = fixture.debugElement.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should create with an input', () => {
+ component.subRegion = new SubRegion('testId');
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts
new file mode 100644
index 0000000..5dd4736
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {SubRegion} from '../../models';
+
+/**
+ * The SubRegion node in the force graph
+ *
+ * Note 1: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ * Note 2: the selector is exactly the same as the @Input alias to make this
+ * directive trick work
+ */
+@Component({
+ selector: '[onos-subregionnodesvg]',
+ templateUrl: './subregionnodesvg.component.html',
+ styleUrls: ['./subregionnodesvg.component.css']
+})
+export class SubRegionNodeSvgComponent implements OnChanges {
+ @Input() subRegion: SubRegion;
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (!this.subRegion.x) {
+ this.subRegion.x = 0;
+ this.subRegion.y = 0;
+ }
+ }
+
+}