GUI2 Command to Unpin or Freeze selected or all nodes

Change-Id: I4f0494a3fadc04dd09afbd096ea1f0d4f73d5c4f
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);
     }
 
     /**