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;
+ }
+}