GUI2 Command to Unpin or Freeze selected or all nodes

Change-Id: I4f0494a3fadc04dd09afbd096ea1f0d4f73d5c4f
diff --git a/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/Topo.properties b/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/Topo.properties
index 58be4ab..c2cdfae 100644
--- a/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/Topo.properties
+++ b/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/Topo.properties
@@ -72,6 +72,8 @@
 fl_offline_devices=Offline Devices
 fl_bad_links=Bad Links
 fl_reset_node_locations=Reset Node Locations
+fl_unpinned_floating_nodes=Unpinned floating nodes
+fl_pinned_floating_nodes=Pinned floating nodes
 
 fl_layer_all=All Layers Shown
 fl_layer_pkt=Packet Layer Shown
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
index 85f5de7..b759997 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
@@ -553,10 +553,74 @@
         this.log.debug(klass, id, 'has been moved to', newLocation);
     }
 
-    resetNodeLocations() {
-        this.devices.forEach((d) => {
-            d.resetNodeLocation();
-        });
+    /**
+     * If any nodes with fixed positions had been dragged out of place
+     * then put back where they belong
+     * If there are some devices selected reset only these
+     */
+    resetNodeLocations(): number {
+        let numbernodes = 0;
+        if (this.selectedNodes.length > 0) {
+            this.devices
+                .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
+                .forEach((dev) => {
+                    Node.resetNodeLocation(<Node>dev.device);
+                    numbernodes++;
+                });
+            this.hosts
+                .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
+                .forEach((h) => {
+                    Host.resetNodeLocation(<Host>h.host);
+                    numbernodes++;
+                });
+        } else {
+            this.devices.forEach((dev) => {
+                Node.resetNodeLocation(<Node>dev.device);
+                numbernodes++;
+            });
+            this.hosts.forEach((h) => {
+                Host.resetNodeLocation(<Host>h.host);
+                numbernodes++;
+            });
+        }
+        this.graph.reinitSimulation();
+        return numbernodes;
     }
+
+    /**
+     * Toggle floating nodes between unpinned and frozen
+     * There may be frozen and unpinned in the selection
+     *
+     * If there are nodes selected toggle only these
+     */
+    unpinOrFreezeNodes(freeze: boolean): number {
+        let numbernodes = 0;
+        if (this.selectedNodes.length > 0) {
+            this.devices
+                .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
+                .forEach((d) => {
+                    Node.unpinOrFreezeNode(<Node>d.device, freeze);
+                    numbernodes++;
+                });
+            this.hosts
+                .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
+                .forEach((h) => {
+                    Node.unpinOrFreezeNode(<Node>h.host, freeze);
+                    numbernodes++;
+                });
+        } else {
+            this.devices.forEach((d) => {
+                Node.unpinOrFreezeNode(<Node>d.device, freeze);
+                numbernodes++;
+            });
+            this.hosts.forEach((h) => {
+                Node.unpinOrFreezeNode(<Node>h.host, freeze);
+                numbernodes++;
+            });
+        }
+        this.graph.reinitSimulation();
+        return numbernodes;
+    }
+
 }
 
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
index b4e62cc..5b8f48d 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
@@ -16,7 +16,7 @@
 import * as d3 from 'd3';
 import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
 import {LayerType, Location, NodeType, RegionProps} from './regions';
-import {MetaUi} from 'gui2-fw-lib';
+import {LocMeta, MetaUi, ZoomUtils} from 'gui2-fw-lib';
 
 export interface UiElement {
     index?: number;
@@ -129,7 +129,7 @@
 /**
  * Implementing SimulationNodeDatum interface into our custom Node class
  */
-export abstract class Node implements UiElement, d3.SimulationNodeDatum {
+export class Node implements UiElement, d3.SimulationNodeDatum {
     // Optional - defining optional implementation properties - required for relevant typing assistance
     index?: number;
     x: number;
@@ -139,6 +139,7 @@
     fx?: number | null;
     fy?: number | null;
     nodeType: NodeType;
+    location: Location;
     id: string;
 
     protected constructor(id) {
@@ -146,6 +147,58 @@
         this.x = 0;
         this.y = 0;
     }
+
+    /**
+     * Static method to reset the node's position to that specified in its
+     * coordinates
+     * This is overridden for host
+     * @param node The node to reset
+     */
+    static resetNodeLocation(node: Node): void {
+        let origLoc: MetaUi;
+
+        if (!node.location || node.location.locType === LocationType.NONE) {
+            // No location - nothing to do
+            return;
+        } else if (node.location.locType === LocationType.GEO) {
+            origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+                lng: node.location.longOrX,
+                lat: node.location.latOrY
+            });
+        } else if (node.location.locType === LocationType.GRID) {
+            origLoc = ZoomUtils.convertXYtoGeo(
+                node.location.longOrX, node.location.latOrY);
+        }
+        Node.moveNodeTo(node, origLoc);
+    }
+
+    protected static moveNodeTo(node: Node, origLoc: MetaUi) {
+        const currentX = node.fx;
+        const currentY = node.fy;
+        const distX = origLoc.x - node.fx;
+        const distY = origLoc.y - node.fy;
+        let count = 0;
+        const task = setInterval(() => {
+            count++;
+            if (count >= 10) {
+                clearInterval(task);
+            }
+            node.fx = currentX + count * distX / 10;
+            node.fy = currentY + count * distY / 10;
+        }, 50);
+    }
+
+    static unpinOrFreezeNode(node: Node, freeze: boolean): void {
+        if (!node.location || node.location.locType === LocationType.NONE) {
+            if (freeze) {
+                node.fx = node.x;
+                node.fy = node.y;
+            } else {
+                node.fx = null;
+                node.fy = null;
+            }
+        }
+    }
 }
 
 /**
@@ -154,7 +207,6 @@
 export class Device extends Node {
     id: string;
     layer: LayerType;
-    location: Location;
     metaUi: MetaUi;
     master: string;
     online: boolean;
@@ -179,6 +231,24 @@
     constructor(id: string) {
         super(id);
     }
+
+    static resetNodeLocation(host: Host): void {
+        let origLoc: MetaUi;
+
+        if (!host.props || host.props.locType === LocationType.NONE) {
+            // No location - nothing to do
+            return;
+        } else if (host.props.locType === LocationType.GEO) {
+            origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+                lng: host.props.longitude,
+                lat: host.props.latitude
+            });
+        } else if (host.props.locType === LocationType.GRID) {
+            origLoc = ZoomUtils.convertXYtoGeo(
+                host.props.gridX, host.props.gridY);
+        }
+        Node.moveNodeTo(host, origLoc);
+    }
 }
 
 
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
index e87e1a8..8a5e1b5 100644
--- 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
@@ -69,6 +69,10 @@
     -->
     <svg:rect x="-16" y="-16" width="32" height="32" style="fill: url(#diagonal_blue)">
     </svg:rect>
+    <svg:path *ngIf="device.location && device.location.locType != 'none'"
+              d="M-15 12 v3 h5" style="stroke: white; stroke-width: 1; fill: none"></svg:path>
+    <svg:path *ngIf="device.fx != null"
+              d="M15 -12 v-3 h-5" style="stroke: white; stroke-width: 1; fill: none"></svg:path>
     <!-- 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
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
index adcd788..90bd2d3 100644
--- 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
@@ -131,25 +131,4 @@
             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/topology/topology.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
index 10bc846..601159d 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
@@ -84,6 +84,7 @@
 const PREF_PORTHL = 'porthl';
 const PREF_SUMMARY = 'summary';
 const PREF_TOOLBAR = 'toolbar';
+const PREF_PINNED = 'pinned';
 
 /**
  * Model of the topo2_prefs object - this is a subset of the overall Prefs returned
@@ -103,6 +104,7 @@
     summary: number;
     toolbar: number;
     grid: number;
+    pinned: number;
 }
 
 /**
@@ -402,7 +404,7 @@
             P: [(token) => {this.togglePorts(token); }, 'Toggle Port Highlighting'],
             Q: [() => {this.cycleGridDisplay(); }, 'Cycle grid display'],
             R: [() => {this.resetZoom(); }, 'Reset pan / zoom'],
-            U: [() => {this.unpinNode(); }, 'Unpin node (mouse over)'],
+            U: [() => {this.unpinOrFreezeNodes(); }, 'Unpin or freeze nodes'],
             X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
             dot: [() => {this.toggleToolbar(); }, 'Toggle Toolbar'],
             0: [() => {this.cancelTraffic(); }, 'Cancel traffic monitoring'],
@@ -602,16 +604,30 @@
         this.log.debug('equalizing masters');
     }
 
+    /**
+     * If any nodes with fixed positions had been dragged out of place
+     * then put back where they belong
+     * If there are some devices selected reset only these
+     */
     protected resetNodeLocation() {
-        // TODO: Implement reset locations
-        this.force.resetNodeLocations();
-        this.flashMsg = this.lionFn('fl_reset_node_locations');
-        this.log.debug('resetting node location');
+        const numNodes = this.force.resetNodeLocations();
+        this.flashMsg = this.lionFn('fl_reset_node_locations') +
+            '(' + String(numNodes) + ')';
+        this.log.debug('resetting ', numNodes, 'node(s) location');
     }
 
-    protected unpinNode() {
-        // TODO: Implement this
-        this.log.debug('unpinning node');
+    /**
+     * Toggle floating nodes between pinned and frozen
+     * If there are floating nodes selected toggle only these
+     */
+    protected unpinOrFreezeNodes() {
+        const pinned: boolean = !Boolean(this.prefsState.pinned);
+        const numNodes = this.force.unpinOrFreezeNodes(pinned);
+        this.flashMsg = this.lionFn(pinned ?
+            'fl_pinned_floating_nodes' : 'fl_unpinned_floating_nodes') +
+            '(' + String(numNodes) + ')';
+        this.updatePrefsState(PREF_PINNED, pinned ? 1 : 0);
+        this.log.debug('Toggling pinning for floating ', numNodes, 'nodes', pinned);
     }
 
     /**
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.module.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.module.ts
index 2fbba74..af3af4e 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.module.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.module.ts
@@ -10,7 +10,7 @@
 
 const appRoutes: Routes = [
     { path: '**', component: AppComponent }
-]
+];
 
 @NgModule({
     declarations: [