GUI2 Absolute locations for Devices and Hosts

Change-Id: I172020a19004b559ae740478d30a2cf9ce08091e
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
index 3ba641f..9e62410 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
@@ -66,6 +66,7 @@
 
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static org.onosproject.net.AnnotationKeys.DRIVER;
+import static org.onosproject.net.AnnotationKeys.UI_TYPE;
 import static org.onosproject.net.PortNumber.portNumber;
 import static org.onosproject.net.config.basics.BasicElementConfig.LOC_TYPE_GEO;
 import static org.onosproject.net.config.basics.BasicElementConfig.LOC_TYPE_GRID;
@@ -438,7 +439,7 @@
     // Create models of the data to return, that overlays can adjust / augment
 
     private String lookupGlyph(Device device) {
-        String uiType = device.annotations().value("uiType");
+        String uiType = device.annotations().value(UI_TYPE);
         if (uiType != null && !uiType.equalsIgnoreCase("undefined")) {
             return uiType;
         } else {
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 8e45d49..58be4ab 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
@@ -37,6 +37,7 @@
 tbtt_cyc_layers=Cycle node layers
 tbtt_cyc_dev_labs=Cycle device labels
 tbtt_cyc_host_labs=Cycle host labels
+tbtt_cyc_grid_display=Cycle grid display
 tbtt_unpin_node=Unpin node (hover mouse over)
 tbtt_reset_zoom=Reset pan / zoom
 tbtt_tog_toolbar=Toggle Toolbar
@@ -63,6 +64,11 @@
 fl_host_labels_show_mac=Show host MAC addresses
 fl_host_labels_hide=Hide host labels
 
+fl_grid_display_hide=Hide grid
+fl_grid_display_1000=Show XY grid
+fl_grid_display_geo=Show Geo grid
+fl_grid_display_both=Show both grids
+
 fl_offline_devices=Offline Devices
 fl_bad_links=Bad Links
 fl_reset_node_locations=Reset Node Locations
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.ts
index a3cbb43..be2ddb4 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.ts
@@ -877,6 +877,20 @@
     'M11.6,51.4a1.2,1.2,0,0,0-1.2,1.2V63.2a1.2,1.2,0,0,0,1.2,1.2' +
     'H33.8A1.2,1.2,0,0,0,35,63.2V52.6a1.2,1.2,0,0,0-1.2-1.2H11.6Z'],
 
+    ['m_cycleGridDisplay', 'M 78.5,74.7 A 34.2,34.2 0 0 1 30.8,81.5 ' +
+    'L 30.5,81.2 30.4,81 v -0.2 a 1.8,1.8 0 0 1 0.8,-2.5 L 31.9,78 26.7,74.6 ' +
+    'v 6.2 l 0.7,-0.3 a 1.8560711,1.8560711 0 1 1 1.7,3.3 ' +
+    'l -3.4,1.8 -0.9,0.2 -1,-0.3 A 1.9,1.9 0 0 1 22.9,83.9 V 71.1 ' +
+    'a 1.8,1.8 0 0 1 2.8,-1.5 l 10.7,7 a 1.9,1.9 0 0 1 0.8,1.6 1.9,1.9 0 0 1 -1,1.5 ' +
+    'l -0.7,0.4 a 30.5,30.5 0 0 0 40.1,-7.7 1.8506756,1.8506756 0 0 1 2.9,2.3 z ' +
+    'M 18.500163,7.4998005 V 20.50004 H 2.5001058 v 2.999817 H 18.500163 V 50.499758 ' +
+    'H 2.5001058 v 3.000335 H 18.500163 v 13.999663 h 2.999816 V 53.500093 h 26.999902 ' +
+    'v 13.999663 h 3.000333 V 53.500093 h 26.999903 v 13.999663 h 2.999817 V 53.500093 ' +
+    'H 97.499992 V 50.499758 H 81.499934 V 23.499857 H 97.499992 V 20.50004 H 81.499934 ' +
+    'V 7.4998005 H 78.500117 V 20.50004 H 51.500214 V 7.4998005 H 48.499881 V 20.50004 ' +
+    'H 21.499979 V 7.4998005 Z M 21.499979,23.499857 H 48.499881 V 50.499758 H 21.499979 ' +
+    'Z m 30.000235,0 H 78.500117 V 50.499758 H 51.500214 Z'],
+
     ['m_prev', 'M59.8,72l-0.9-.2L21.8,51.3a1.8,1.8,0,0,1,0-3.2L58.9,28.2' +
     'a1.8,1.8,0,0,1,2.7,1.6V40.7H77.3a1.8,1.8,0,0,1,1.8,1.8V57.3' +
     'a1.8,1.8,0,0,1-1.8,1.8h-7a1.8,1.8,0,1,1,0-3.7h5.2v-11H59.8' +
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts
index f2497f7..51e4f6b 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.ts
@@ -55,6 +55,7 @@
     ['m_oblique', 'm_oblique'],
     ['m_filters', 'm_filters'],
     ['m_cycleLabels', 'm_cycleLabels'],
+    ['m_cycleGridDisplay', 'm_cycleGridDisplay'],
     ['m_prev', 'm_prev'],
     ['m_next', 'm_next'],
     ['m_flows', 'm_flows'],
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
index bb9e638..b6290b6 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.html
@@ -13,10 +13,17 @@
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
-<svg:g xmlns:svg="http://www.w3.org/2000/svg" onos-mapsvg [map]="map"/>
-<svg:g  xmlns:svg="http://www.w3.org/2000/svg">
-    <svg:text>Layout: {{ layoutData.id }} {{ layoutData.bgDesc }}</svg:text>
-    <svg:text>Region: {{ layoutData.region }} {{ layoutData.regionName }}</svg:text>
-    <svg:text>Parent {{ layoutData.parent }}</svg:text>
-    <svg:text *ngFor="let crumb of layoutData.crumbs">{{ crumb.id }} {{ crumb.name }}</svg:text>
-</svg:g>
+<!-- The transform here goes from a 0,0 centred grid of -180 to 180 of
+    longitude to -75 to 75 of latitude
+     It is mapped to a 2000x1000 SVG grid with -500,0 at the top left
+     (The SVG viewbox of ONOS is 1000x1000 - for the geo grid we wanted
+     to keep it the same height 1000 representing +75 latitude down to
+     -75 latitude, but double the width. Why 75? There's no city in the
+     world above 70 - Murmansk)
+     The 6.66 represents 1000/150 and the 5.55 represents 2000/360
+     The reason for the difference is that mercator projection widens
+     countries in the northern and southern extremities, and so
+     the map is squashed horizontally slightly here to compensate
+     (with no squashing the width would be 2400)-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" onos-mapsvg [map]="map"
+       transform="translate(500,500), scale(5.5555,6.666666)"/>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts
index a3eed06..0732018 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.spec.ts
@@ -21,6 +21,13 @@
 import {HttpClient} from '@angular/common/http';
 import {LogService} from 'gui2-fw-lib';
 import {MapObject} from '../maputils';
+import {LocMeta} from '../forcesvg/models';
+import {ForceSvgComponent} from '../forcesvg/forcesvg.component';
+import {
+    DeviceNodeSvgComponent,
+    HostNodeSvgComponent, LinkSvgComponent, SubRegionNodeSvgComponent
+} from '../forcesvg/visuals';
+import {DraggableDirective} from '../forcesvg/draggable/draggable.directive';
 
 class MockHttpClient {
     get() {
@@ -46,7 +53,13 @@
         TestBed.configureTestingModule({
             declarations: [
                 BackgroundSvgComponent,
-                MapSvgComponent
+                MapSvgComponent,
+                ForceSvgComponent,
+                DeviceNodeSvgComponent,
+                HostNodeSvgComponent,
+                SubRegionNodeSvgComponent,
+                LinkSvgComponent,
+                DraggableDirective
             ],
             providers: [
                 { provide: LogService, useValue: logSpy },
@@ -68,4 +81,33 @@
     it('should create', () => {
         expect(component).toBeTruthy();
     });
+
+    it('should convert latlong to xy', () => {
+        const result = BackgroundSvgComponent.convertGeoToCanvas(<LocMeta>{lat: 52, lng: -8});
+        expect(Math.round(result.x * 100)).toEqual(45556);
+        expect(Math.round(result.y * 100)).toEqual(15333);
+    });
+
+    /**
+     * For some reason including the following causes "ForceSvgComponent should create error
+     * TODO: Investigate
+     */
+
+    // it('should convert xy random extents to latlong', () => {
+    //     const result = BackgroundSvgComponent.convertXYtoGeo(455.556, 153.33);
+    //     expect(Math.round(result.equivLoc.lng)).toEqual(-8);
+    //     expect(Math.round(result.equivLoc.lat)).toEqual(52);
+    // });
+
+    // it('should convert xy min extents to latlong', () => {
+    //     const result = BackgroundSvgComponent.convertXYtoGeo(-500, 0);
+    //     expect(Math.round(result.equivLoc.lng)).toEqual(-180);
+    //     expect(Math.round(result.equivLoc.lat)).toEqual(75);
+    // });
+
+    // it('should convert xy full extents to latlong', () => {
+    //     const result = BackgroundSvgComponent.convertXYtoGeo(1500, 1000);
+    //     expect(Math.round(result.equivLoc.lng)).toEqual(180);
+    //     expect(Math.round(result.equivLoc.lat)).toEqual(-75);
+    // });
 });
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
index 5536784..cc41fb6 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/backgroundsvg/backgroundsvg.component.ts
@@ -15,6 +15,7 @@
  */
 import {Component, Input, OnInit} from '@angular/core';
 import {MapObject} from '../maputils';
+import {LocMeta, MetaUi} from '../forcesvg/models';
 
 /**
  * model of the topo2CurrentLayout attrs from BgZoom below
@@ -68,6 +69,9 @@
     regionName: string;
 }
 
+const LONGITUDE_EXTENT = 180;
+const LATITUDE_EXTENT = 75;
+
 /**
  * ONOS GUI -- Topology Background Layer View.
  */
@@ -81,6 +85,32 @@
 
     layoutData: Layout = <Layout>{};
 
+    static convertGeoToCanvas(location: LocMeta): MetaUi {
+        const calcX = (LONGITUDE_EXTENT + location.lng) / ( LONGITUDE_EXTENT * 2 ) * 2000 - 500;
+        const calcY = (LATITUDE_EXTENT - location.lat) / ( LATITUDE_EXTENT * 2 ) * 1000;
+        return <MetaUi>{
+            x: calcX,
+            y: calcY,
+            equivLoc: {
+                lat: location.lat,
+                lng: location.lng
+            }
+        };
+    }
+
+    static convertXYtoGeo(x: number, y: number): MetaUi {
+        const calcLong: number = (x + 500) * 2 * LONGITUDE_EXTENT / 2000 - LONGITUDE_EXTENT;
+        const calcLat: number = -(y * 2 * LATITUDE_EXTENT / 1000 - LATITUDE_EXTENT);
+        return <MetaUi>{
+            x: x,
+            y: y,
+            equivLoc: <LocMeta>{
+                lat: calcLat,
+                lng: calcLong
+            }
+        };
+    }
+
     constructor() { }
 
     ngOnInit() {
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.spec.ts
index bb6b4d0..94c61db 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.spec.ts
@@ -16,15 +16,18 @@
 import { DraggableDirective } from './draggable.directive';
 import {inject, TestBed} from '@angular/core/testing';
 import {ElementRef} from '@angular/core';
+import {LogService} from 'gui2-fw-lib';
 
 export class MockElementRef extends ElementRef {
     nativeElement = {};
 }
 
 describe('DraggableDirective', () => {
+    let logServiceSpy: jasmine.SpyObj<LogService>;
     let mockWindow: Window;
 
     beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
         mockWindow = <any>{
             navigator: {
                 userAgent: 'HeadlessChrome',
@@ -34,10 +37,12 @@
 
         TestBed.configureTestingModule({
             providers: [DraggableDirective,
+                { provide: LogService, useValue: logSpy },
                 { provide: 'Window', useFactory: (() => mockWindow ) },
                 { provide: ElementRef, useValue: mockWindow }
             ]
         });
+        logServiceSpy = TestBed.get(LogService);
     });
 
     it('should create an instance', inject([DraggableDirective], (directive: DraggableDirective) => {
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts
index 88faa37..ef99728 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/draggable/draggable.directive.ts
@@ -13,9 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
-import { ForceDirectedGraph, Node } from '../models';
+import {
+    Directive,
+    ElementRef,
+    EventEmitter,
+    Input,
+    OnChanges, Output
+} from '@angular/core';
+import {ForceDirectedGraph, LocMeta, MetaUi, Node} from '../models';
 import * as d3 from 'd3';
+import {LogService} from 'gui2-fw-lib';
+import {BackgroundSvgComponent} from '../../backgroundsvg/backgroundsvg.component';
 
 @Directive({
   selector: '[onosDraggableNode]'
@@ -23,23 +31,27 @@
 export class DraggableDirective implements OnChanges {
     @Input() draggableNode: Node;
     @Input() draggableInGraph: ForceDirectedGraph;
+    @Output() newLocation = new EventEmitter<MetaUi>();
 
     constructor(
-        private _element: ElementRef
+        private _element: ElementRef,
+        private log: LogService
     ) {
+        this.log.debug('DraggableDirective constructed');
     }
 
     ngOnChanges() {
         this.applyDraggableBehaviour(
             this._element.nativeElement,
             this.draggableNode,
-            this.draggableInGraph);
+            this.draggableInGraph,
+            this.newLocation);
     }
 
     /**
      * A method to bind a draggable behaviour to an svg element
      */
-    applyDraggableBehaviour(element, node: Node, graph: ForceDirectedGraph) {
+    applyDraggableBehaviour(element, node: Node, graph: ForceDirectedGraph, newLocation: EventEmitter<MetaUi>) {
         const d3element = d3.select(element);
 
         function started() {
@@ -50,7 +62,7 @@
                 graph.simulation.alphaTarget(0.3).restart();
             }
 
-            d3.event.on('drag', dragged).on('end', ended);
+            d3.event.on('drag', () => dragged()).on('end', () => ended());
 
             function dragged() {
                 node.fx = d3.event.x;
@@ -61,9 +73,10 @@
                 if (!d3.event.active) {
                     graph.simulation.alphaTarget(0);
                 }
+                newLocation.emit(BackgroundSvgComponent.convertXYtoGeo(node.fx, node.fy));
 
-                node.fx = null;
-                node.fy = null;
+                // node.fx = null;
+                // node.fy = null;
             }
         }
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
index 517c3e7..6bbf85b 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.html
@@ -41,12 +41,15 @@
         line 3) Use the onosDraggable directive and pass this device in to
          its draggableNode Input parameter and setting the draggableInGraph
          Input parameter to 'graph'
-        line 4) when the onos-devicenodesvg component emits the selectedEvent
+        line 4) event handler of the draggable directive - causes the new location
+         to be written back to the server
+        line 5) when the onos-devicenodesvg component emits the selectedEvent
          call the updateSelected() method of this (forcesvg) component
     -->
     <svg:g onos-devicenodesvg [device]="device"
            *ngFor="let device of regionData.devices[visibleLayerIdx()]"
            onosDraggableNode [draggableNode]="device" [draggableInGraph]="graph"
+               (newLocation)="nodeMoved('device', device.id, $event)"
            (selectedEvent)="updateSelected($event)"
             [labelToggle]="deviceLabelToggle">
         <svg:desc>Device nodes</svg:desc>
@@ -62,12 +65,15 @@
             line 3) Use the onosDraggable directive and pass this host in to
              its draggableNode Input parameter and setting the draggableInGraph
              Input parameter to 'graph'
-            line 4) when the onos-hostnodesvg component emits the selectedEvent
+            line 4) event handler of the draggable directive - causes the new location
+             to be written back to the server
+            line 5) when the onos-hostnodesvg component emits the selectedEvent
              call the updateSelected() method of this (forcesvg) component
         -->
         <svg:g onos-hostnodesvg [host]="host"
                *ngFor="let host of regionData.hosts[visibleLayerIdx()]"
                onosDraggableNode [draggableNode]="host" [draggableInGraph]="graph"
+                   (newLocation)="nodeMoved('host', device.id, $event)"
                (selectedEvent)="updateSelected($event)"
                [labelToggle]="hostLabelToggle">
             <svg:desc>Host nodes</svg:desc>
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts
index 7a30d75..a834e73 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.spec.ts
@@ -16,7 +16,7 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { ForceSvgComponent } from './forcesvg.component';
-import {LogService} from 'gui2-fw-lib';
+import {FnService, LogService} from 'gui2-fw-lib';
 import {
     DeviceNodeSvgComponent,
     HostNodeSvgComponent, LinkSvgComponent,
@@ -25,6 +25,7 @@
 import {DraggableDirective} from './draggable/draggable.directive';
 import {ActivatedRoute, Params} from '@angular/router';
 import {of} from 'rxjs';
+import {MapSvgComponent} from '../mapsvg/mapsvg.component';
 
 class MockActivatedRoute extends ActivatedRoute {
     constructor(params: Params) {
@@ -34,6 +35,7 @@
 }
 
 describe('ForceSvgComponent', () => {
+    let fs: FnService;
     let ar: MockActivatedRoute;
     let windowMock: Window;
     let logServiceSpy: jasmine.SpyObj<LogService>;
@@ -56,6 +58,8 @@
             }
         };
 
+        fs = new FnService(ar, logSpy, windowMock);
+
         TestBed.configureTestingModule({
             declarations: [
                 ForceSvgComponent,
@@ -63,10 +67,13 @@
                 HostNodeSvgComponent,
                 SubRegionNodeSvgComponent,
                 LinkSvgComponent,
-                DraggableDirective
+                DraggableDirective,
+                MapSvgComponent
             ],
             providers: [
+                { provide: FnService, useValue: fs },
                 { provide: LogService, useValue: logSpy },
+                { provide: 'Window', useValue: windowMock },
             ]
         })
         .compileComponents();
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
index d2a0b18..7b66f36 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
@@ -28,7 +28,7 @@
     SimpleChanges,
     ViewChildren
 } from '@angular/core';
-import {LogService} from 'gui2-fw-lib';
+import {LogService, WebSocketService} from 'gui2-fw-lib';
 import {
     Device,
     ForceDirectedGraph,
@@ -38,6 +38,8 @@
     LayerType,
     Link,
     LinkHighlight,
+    Location, LocMeta,
+    MetaUi,
     ModelEventMemo,
     ModelEventType,
     Region,
@@ -50,6 +52,16 @@
     HostNodeSvgComponent,
     LinkSvgComponent
 } from './visuals';
+import {
+    BackgroundSvgComponent,
+    LocationType
+} from '../backgroundsvg/backgroundsvg.component';
+
+interface UpdateMeta {
+    id: string;
+    class: string;
+    memento: MetaUi;
+}
 
 /**
  * ONOS GUI -- Topology Forces Graph Layer View.
@@ -85,7 +97,8 @@
 
     constructor(
         protected log: LogService,
-        private ref: ChangeDetectorRef
+        private ref: ChangeDetectorRef,
+        protected wss: WebSocketService
     ) {
         this.selectedLink = null;
         this.log.debug('ForceSvgComponent constructed');
@@ -172,6 +185,21 @@
                 this.graph.nodes = this.graph.nodes.concat(subRegions);
             }
 
+            // If a node has a fixed location then assign it to fx and fy so
+            // that it doesn't get affected by forces
+            this.graph.nodes
+            .forEach((n) => {
+                const loc: Location = <Location>n['location'];
+                if (loc && loc.locType === LocationType.GEO) {
+                    const position: MetaUi =
+                        BackgroundSvgComponent.convertGeoToCanvas(
+                            <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
+                    n.fx = position.x;
+                    n.fy = position.y;
+                    this.log.debug('Found node', n.id, 'with', loc.locType);
+                }
+            });
+
             // Associate the endpoints of each link with a real node
             this.graph.links = [];
             for (const linkIdx of Object.keys(this.regionData.links)) {
@@ -394,5 +422,21 @@
             });
         }
     }
+
+    /**
+     * As nodes are dragged around the graph, their new location should be sent
+     * back to server
+     * @param klass The class of node e.g. 'host' or 'device'
+     * @param id - the ID of the node
+     * @param newLocation - the new Location of the node
+     */
+    nodeMoved(klass: string, id: string, newLocation: MetaUi) {
+        this.wss.sendEvent('updateMeta', <UpdateMeta>{
+            id: id,
+            class: klass,
+            memento: newLocation
+        });
+        this.log.debug(klass, id, 'has been moved to', newLocation);
+    }
 }
 
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
index de70eb4..5f8d8c6 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/models/node.ts
@@ -79,6 +79,33 @@
 }
 
 /**
+ * Toggle state for how the grid should be displayed
+ */
+export enum GridDisplayToggle {
+    GRIDNONE,
+    GRID1000,
+    GRIDGEO,
+    GRIDBOTH
+}
+
+/**
+ * Add the method 'next()' to the GridDisplayToggle enum above
+ */
+export namespace GridDisplayToggle {
+    export function next(current: GridDisplayToggle) {
+        if (current === GridDisplayToggle.GRIDNONE) {
+            return GridDisplayToggle.GRID1000;
+        } else if (current === GridDisplayToggle.GRID1000) {
+            return GridDisplayToggle.GRIDGEO;
+        } else if (current === GridDisplayToggle.GRIDGEO) {
+            return GridDisplayToggle.GRIDBOTH;
+        } else if (current === GridDisplayToggle.GRIDBOTH) {
+            return GridDisplayToggle.GRIDNONE;
+        }
+    }
+}
+
+/**
  * model of the topo2CurrentRegion device props from Device below
  */
 export interface DeviceProps {
@@ -143,7 +170,7 @@
 export class Device extends Node {
     id: string;
     layer: LayerType;
-    location: LocationType;
+    location: Location;
     metaUi: MetaUi;
     master: string;
     online: boolean;
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.css b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.css
new file mode 100644
index 0000000..056a0a0
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.css
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+.gridrect {
+    stroke-width: 1;
+    fill: none;
+}
+
+.gridtext {
+    fill: lightgray;
+    text-anchor: middle;
+    dominant-baseline: middle;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.html
new file mode 100644
index 0000000..c183ac1
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.html
@@ -0,0 +1,48 @@
+<!--
+~ 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:g *ngFor="let pt of gridPointsHoriz" xmlns:svg="http://www.w3.org/2000/svg"
+       [attr.transform]="'translate(' + horizCentreOffset + ',' + vertCentreOffset + '), ' +
+        'scale(' + gridScaleX + ',' + gridScaleY +')'">
+    <svg:desc>Vertical grid lines</svg:desc>
+    <svg:rect id="gridRectVert" class="gridrect"
+              [ngStyle]="{'stroke': gridcolor, 'stroke-width': 1/gridScaleX }"
+            [attr.width]="spacing"
+            [attr.height]="vertUpperLimit-vertLowerLimit"
+            [attr.x]="pt"
+            [attr.y]="vertLowerLimit">
+    </svg:rect>
+    <svg:text id="gridTextVert" class="gridtext"
+              [ngStyle]="{'stroke': gridcolor, 'font-size': 100/gridScaleX+'%', 'stroke-width': 1/gridScaleX }"
+            [attr.x]="pt"
+            [attr.y]="(vertUpperLimit - vertLowerLimit)/2">{{pt}}</svg:text>
+</svg:g>
+
+<svg:g *ngFor="let pt of gridPointsVert" xmlns:svg="http://www.w3.org/2000/svg"
+       [attr.transform]="'translate(' + horizCentreOffset + ',' + vertCentreOffset + '), ' +
+        'scale(' + gridScaleX + ',' + gridScaleY + ')'">
+    <svg:desc>Horizontal grid lines</svg:desc>
+    <svg:rect id="gridRectHoriz" class="gridrect"
+              [ngStyle]="{'stroke': gridcolor, 'stroke-width': 1/gridScaleY }"
+            [attr.width]="horizUpperLimit-horizLowerLimit"
+            [attr.height]="spacing"
+            [attr.x]="horizLowerLimit"
+            [attr.y]="pt">
+    </svg:rect>
+    <svg:text id="gridTextHoriz" class="gridtext"
+              [ngStyle]="{'stroke': gridcolor, 'font-size': 100/gridScaleY+'%', 'stroke-width': 1/gridScaleY }"
+            [attr.x]="(horizUpperLimit - horizLowerLimit)/2"
+            [attr.y]="invertVertical ? -pt : pt">{{pt}}</svg:text>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.spec.ts
new file mode 100644
index 0000000..48a031a
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { GridsvgComponent } from './gridsvg.component';
+
+describe('GridsvgComponent', () => {
+    let component: GridsvgComponent;
+    let fixture: ComponentFixture<GridsvgComponent>;
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            declarations: [ GridsvgComponent ]
+        })
+        .compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(GridsvgComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.ts
new file mode 100644
index 0000000..e0934be
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/gridsvg/gridsvg.component.ts
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+import {
+    Component,
+    Input,
+    OnChanges,
+    OnInit,
+    SimpleChanges
+} from '@angular/core';
+
+/**
+ * How to fit in to the 1000 by 100 SVG viewbox
+ */
+export enum FitOption {
+    FIT1000WIDE = 'fit1000wide',
+    FIT1000HIGH = 'fit1000high',
+    FITNONE = 'fitnone'// 1:1 ratio
+}
+
+const SVG_VIEWBOX_CENTRE = 500; // View box is 0,0,1000,1000
+
+@Component({
+    selector: '[onos-gridsvg]',
+    templateUrl: './gridsvg.component.html',
+    styleUrls: ['./gridsvg.component.css']
+})
+export class GridsvgComponent implements OnInit, OnChanges {
+    @Input() horizLowerLimit: number = 0;
+    @Input() horizUpperLimit: number = 1000;
+    @Input() vertLowerLimit: number = 0;
+    @Input() vertUpperLimit: number = 1000;
+    @Input() spacing: number = 100;
+    @Input() invertVertical: boolean = false;
+    @Input() gridcolor: string = '#e8e7e1'; // If specifying this in a template use [gridcolor]="'#e8e7e1'"
+    @Input() centre: boolean = true;
+    @Input() fit: FitOption = FitOption.FITNONE;
+    @Input() aspectRatio: number = 1.0;
+
+    gridPointsHoriz: number[];
+    gridPointsVert: number[];
+    horizCentreOffset: number = 0;
+    vertCentreOffset: number = 0;
+    gridScaleX: number = 1.0;
+    gridScaleY: number = 1.0;
+
+    public static calculateGridPoints(lwr: number, upper: number, step: number): number[] {
+        const gridPoints = new Array<number>();
+        for (let i = lwr; i < upper; i += step) {
+            gridPoints.push(i);
+        }
+        return gridPoints;
+    }
+
+    public static calcOffset(lwr: number, upper: number): number {
+        return -((upper + lwr) * (upper - lwr) / ((upper - lwr) * 2) - SVG_VIEWBOX_CENTRE);
+    }
+
+    public static calcScale(lwr: number, upper: number): number {
+        return SVG_VIEWBOX_CENTRE * 2 / Math.abs(upper - lwr);
+    }
+
+    constructor() { }
+
+    ngOnInit() {
+        this.gridPointsHoriz = GridsvgComponent.calculateGridPoints(
+            this.horizLowerLimit, this.horizUpperLimit, this.spacing);
+        this.gridPointsVert = GridsvgComponent.calculateGridPoints(
+            this.vertLowerLimit, this.vertUpperLimit, this.spacing);
+        this.horizCentreOffset = GridsvgComponent.calcOffset(this.horizUpperLimit, this.horizLowerLimit);
+        this.vertCentreOffset = GridsvgComponent.calcOffset(this.vertUpperLimit, this.vertLowerLimit);
+        this.gridScaleX = this.whichScale(this.fit, true);
+        this.gridScaleY = this.whichScale(this.fit, false);
+    }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes['horizLowerLimit'] ||
+            changes['horizUpperLimit'] ||
+            changes['horizSpacing']) {
+            this.gridPointsHoriz = GridsvgComponent.calculateGridPoints(
+                this.horizLowerLimit, this.horizUpperLimit, this.spacing);
+            this.horizCentreOffset = GridsvgComponent.calcOffset(this.horizUpperLimit, this.horizLowerLimit);
+        }
+        if (changes['vertLowerLimit'] ||
+            changes['vertUpperLimit'] ||
+            changes['vertSpacing'] ) {
+            this.gridPointsVert = GridsvgComponent.calculateGridPoints(
+                this.vertLowerLimit, this.vertUpperLimit, this.spacing);
+            this.vertCentreOffset = GridsvgComponent.calcOffset(this.vertUpperLimit, this.vertLowerLimit);
+        }
+    }
+
+    whichScale(fit: FitOption, isX: boolean): number {
+        if (fit === FitOption.FIT1000HIGH) {
+            return GridsvgComponent.calcScale(
+                    this.vertUpperLimit, this.vertLowerLimit) * (isX ? this.aspectRatio : 1.0);
+        } else if (fit === FitOption.FIT1000WIDE) {
+            return GridsvgComponent.calcScale(
+                this.horizUpperLimit, this.horizLowerLimit) * (isX ? 1.0 : this.aspectRatio);
+        } else {
+            return 1.0;
+        }
+    }
+}
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css
index 0dac42a..d35aa7b 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.css
@@ -20,7 +20,7 @@
 /* --- Topo Map --- */
 
 path.topo-map {
-    stroke-width: 2px;
+    stroke-width: 0.05px;
     stroke: #f4f4f4;
     fill: #e5e5e6;
 }
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html
index adc0346..cc2a794 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.html
@@ -15,9 +15,8 @@
 -->
 <svg:desc xmlns:svg="http://www.w3.org/2000/svg">Map of {{map.id}} in SVG format</svg:desc>
 <svg:path class="topo-map"
-        *ngFor="let f of mapPathGenerator?.geodata.features"
+        *ngFor="let f of geodata?.features"
         xmlns:svg="http://www.w3.org/2000/svg"
         [attr.d]="pathGenerator(f)">
-    <svg:title>{{ f.id }}</svg:title>
+    <svg:title>{{ f.id }} {{f.properties?.name}}</svg:title>
 </svg:path>
-
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts
index a10cf4c..23dbeb1 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/mapsvg/mapsvg.component.ts
@@ -46,15 +46,6 @@
 }
 
 /**
- * Model of the Path Generator
- */
-interface PathGenerator {
-    geodata: FeatureCollection;
-    pathgen: (Feature) => string;
-    settings: GeneratorSettings;
-}
-
-/**
  * Model of the Feature returned prom topojson library
  */
 interface Feature {
@@ -83,16 +74,6 @@
     transform: TopoDataTransform; // scale and translate
 }
 
-/**
- * Default settings for the path generator for TopoJson
- */
-const DEFAULT_GEN_SETTINGS: GeneratorSettings = <GeneratorSettings>{
-    objectTag: 'states',
-    projection: d3.geoMercator(),
-    logicalSize: 1000,
-    mapFillScale: .95,
-};
-
 @Component({
     selector: '[onos-mapsvg]',
     templateUrl: './mapsvg.component.html',
@@ -101,14 +82,33 @@
 export class MapSvgComponent implements  OnChanges {
     @Input() map: MapObject = <MapObject>{id: 'none'};
 
-    cache = new Map<string, TopoData>();
-    topodata: TopoData;
-    mapPathGenerator: PathGenerator;
+    geodata: FeatureCollection;
+    pathgen: (Feature) => string;
+    // testPath: string;
+    // testFeature = <Feature>{
+    //     id: 'test',
+    //     type: 'Feature',
+    //     geometry: {
+    //         coordinates: [
+    //             [[-15, 60], [45, 60], [45, 45], [-15, 45], [-15, 60]],
+    //             [[-10, 55], [45, 55], [45, 50], [-10, 50], [-10, 55]],
+    //         ],
+    //         type: 'Polygon'
+    //     },
+    //     properties: { name: 'Test'}
+    // };
 
     constructor(
         private log: LogService,
         private httpClient: HttpClient,
     ) {
+        this.pathgen = d3.geoPath().projection(
+            MapSvgComponent.scale(1, 360, 150));
+
+        // this.log.debug('Feature Test',this.testFeature);
+        // this.testPath = this.pathgen(this.testFeature);
+        // this.log.debug('Feature Path', this.testPath);
+
         this.log.debug('MapSvgComponent constructed');
     }
 
@@ -119,24 +119,13 @@
         return id + '.topojson';
     }
 
-    ngOnChanges(changes: SimpleChanges): void {
-        this.log.debug('Change detected', changes);
-        if (changes['map']) {
-            const map: MapObject = <MapObject>(changes['map'].currentValue);
-            if (map.id) {
-                if (this.cache.get(map.id)) {
-                    this.topodata = this.cache.get(map.id);
-                } else {
-                    this.httpClient
-                        .get(MapSvgComponent.getUrl(map.filePath))
-                        .subscribe((topoData: TopoData) => {
-                            this.mapPathGenerator = this.handleTopoJson(map, topoData);
-                            this.log.debug('Path Generated for', map.id,
-                                'from', MapSvgComponent.getUrl(map.filePath));
-                        });
-                }
+    static scale (scaleFactor: number, width: number, height: number) {
+        return d3.geoTransform({
+            point: function(x, y) {
+                this.stream.point( (x - width / 2) * scaleFactor + width / 2,
+                    (-y - height / 2) * scaleFactor + height / 2);
             }
-        }
+        });
     }
 
     /**
@@ -144,7 +133,24 @@
      * @param feature The county or state within the map
      */
     pathGenerator(feature: Feature): string {
-        return this.mapPathGenerator.pathgen(feature);
+        return this.pathgen(feature);
+    }
+
+    ngOnChanges(changes: SimpleChanges): void {
+        this.log.debug('Change detected', changes);
+        if (changes['map']) {
+            const map: MapObject = <MapObject>(changes['map'].currentValue);
+            if (map.id) {
+                this.httpClient
+                    .get(MapSvgComponent.getUrl(map.filePath))
+                    .subscribe((topoData: TopoData) => {
+                        // this.mapPathGenerator =
+                        this.handleTopoJson(map, topoData);
+                        this.log.debug('Path Generated for', map.id,
+                            'from', MapSvgComponent.getUrl(map.filePath));
+                    });
+            }
+        }
     }
 
     /**
@@ -156,63 +162,15 @@
      * @param map The Map chosen in the GUI
      * @param topoData The data in the TopoJson file
      */
-    handleTopoJson(map: MapObject, topoData: TopoData): PathGenerator {
-        this.topodata = topoData;
-        this.cache.set(map.id, topoData);
-        this.log.debug('Map retrieved', topoData);
+    handleTopoJson(map: MapObject, topoData: TopoData): void {
 
-        const topoObject = topoData.objects[map.id];
-        const geoData: FeatureCollection = <FeatureCollection>topojson.feature(topoData, topoObject);
-        this.log.debug('Map retrieved', topoData, geoData);
+        let topoObject = topoData.objects[map.id];
+        if (!topoObject) {
+            topoObject = topoData.objects['states'];
+        }
+        this.log.debug('Topo obj', topoObject, 'topodata', topoData);
+        this.geodata = <FeatureCollection>topojson.feature(topoData, topoObject);
+        this.log.debug('Map retrieved', topoData, this.geodata);
 
-        const settings: GeneratorSettings = Object.assign({}, DEFAULT_GEN_SETTINGS);
-        const path = d3.geoPath().projection(settings.projection);
-        this.rescaleProjection(
-            settings.projection,
-            settings.mapFillScale,
-            settings.logicalSize,
-            path,
-            geoData);
-        this.log.debug('Scale adjusted');
-
-        return <PathGenerator>{
-            geodata: geoData,
-            pathgen: path,
-            settings: settings
-        };
-    }
-
-    /**
-     * Adjust projection scale and translation to fill the view
-     * with the map
-     * @param proj
-     * @param mfs
-     * @param dim
-     * @param path
-     * @param geoData
-     * @param adjustScale
-     */
-    rescaleProjection(proj: any, mfs: number, dim: number, path: any,
-                      geoData: FeatureCollection, adjustScale: number = 1.0) {
-        // start with unit scale, no translation..
-        proj.scale(1).translate([0, 0]);
-
-        // figure out dimensions of map data..
-        const b = path.bounds(geoData);
-        const x1 = b[0][0];
-        const y1 = b[0][1];
-        const x2 = b[1][0];
-        const y2 = b[1][1];
-        const dx = x2 - x1;
-        const dy = y2 - y1;
-        const x = (x1 + x2) / 2;
-        const y = (y1 + y2) / 2;
-
-        // size map to 95% of minimum dimension to fill space..
-        const s = (mfs / Math.min(dx / dim, dy / dim)) * adjustScale;
-        const t = [dim / 2 - s * x, dim / 2 - s * y];
-
-        // set new scale, translation on the projection..
-        proj.scale(s).translate(t);
     }
 }
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html
index fa7fd01..623c425 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.html
@@ -70,5 +70,8 @@
         <div class="button" id="toolbar-topo2-toolbar-topo2-quickhelp" (click)="buttonClicked('quickhelp-btn')">
             <onos-icon [iconSize]="25" iconId="query" [toolTip]="lionFn('qh_title')" classes="button"></onos-icon>
         </div>
+        <div class="button" id="toolbar-topo2-toolbar-topo2-cycleGrid-btn" (click)="buttonClicked('cycleGridDisplay-btn')">
+            <onos-icon [iconSize]="25" iconId="m_cycleGridDisplay" [toolTip]="lionFn('tbtt_cyc_grid_display')" classes="button"></onos-icon>
+        </div>
     </div>
 </div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts
index ed9e2ee..0615530 100644
--- a/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/panel/toolbar/toolbar.component.ts
@@ -33,6 +33,7 @@
 export const BKGRND_SELECT = 'bkgrnd-sel';
 export const CYCLELABELS_BTN = 'cycleLabels-btn';
 export const CYCLEHOSTLABEL_BTN = 'cycleHostLabel-btn';
+export const CYCLEGRIDDISPLAY_BTN = 'cycleGridDisplay-btn';
 export const RESETZOOM_BTN = 'resetZoom-btn';
 export const EQMASTER_BTN = 'eqMaster-btn';
 export const CANCEL_TRAFFIC = 'cancel-traffic';
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
index b6c79a1..02e39c8 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology.module.ts
@@ -36,6 +36,7 @@
 } from './layer/forcesvg/visuals';
 import { MapSelectorComponent } from './panel/mapselector/mapselector.component';
 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import { GridsvgComponent } from './layer/gridsvg/gridsvg.component';
 
 /**
  * ONOS GUI -- Topology View Module
@@ -69,7 +70,8 @@
         DeviceNodeSvgComponent,
         HostNodeSvgComponent,
         SubRegionNodeSvgComponent,
-        MapSelectorComponent
+        MapSelectorComponent,
+        GridsvgComponent
     ],
     providers: [
         TopologyService
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
index 95d5d9e..b6f511a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.html
@@ -55,7 +55,8 @@
         whose contents are supplied through the Topology Service, and whose positions
         are driven by the d3.force engine
     -->
-    <svg:svg #svgZoom xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" id="topo2">
+    <svg:svg #svgZoom xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" id="topo2"
+        preserveAspectRatio="xMaxYMax none">
         <svg:desc>The main SVG canvas of the Topology View</svg:desc>
         <svg:g *ngIf="force.regionData?.devices[0].length +
                         force.regionData?.devices[1].length +
@@ -63,6 +64,14 @@
                onos-nodeviceconnected />
         <svg:g id="topo-zoomlayer" onosZoomableOf [zoomableOf]="svgZoom">
             <svg:desc>A logical layer that allows the main SVG canvas to be zoomed and panned</svg:desc>
+            <svg:g #gridFull *ngIf="prefsState.grid == 1 || prefsState.grid == 3" onos-gridsvg>
+            </svg:g>
+            <svg:g #geoGrid *ngIf="prefsState.grid == 2 || prefsState.grid == 3"
+                   onos-gridsvg [horizLowerLimit]="-180" [horizUpperLimit]="180"
+                   [vertLowerLimit]="-75" [vertUpperLimit]="75" [spacing]="15"
+                   [invertVertical]="true" [fit]="'fit1000high'" [aspectRatio]="0.83333"
+                   [gridcolor]="'#bfe7fb'">
+            </svg:g>
             <svg:g *ngIf="prefsState.bg" onos-backgroundsvg [map]="mapIdState">
                 <svg:desc>The Background SVG component - contains maps</svg:desc>
             </svg:g>
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts
index 0c0a6dd..ed65f5a 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.spec.ts
@@ -49,6 +49,7 @@
 import {BackgroundSvgComponent} from '../layer/backgroundsvg/backgroundsvg.component';
 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {MapSvgComponent} from '../layer/mapsvg/mapsvg.component';
+import {GridsvgComponent} from '../layer/gridsvg/gridsvg.component';
 
 
 class MockActivatedRoute extends ActivatedRoute {
@@ -119,6 +120,10 @@
         this.listeners = this.listeners.filter((obj) => obj !== listener);
     }
 
+    setPrefs(name: string, obj: Object) {
+
+    }
+
 }
 
 /**
@@ -185,7 +190,8 @@
                 SubRegionNodeSvgComponent,
                 MapSelectorComponent,
                 BackgroundSvgComponent,
-                MapSvgComponent
+                MapSvgComponent,
+                GridsvgComponent
             ],
             providers: [
                 { provide: FnService, useValue: fs },
diff --git a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
index 750162f..2de3c8b 100644
--- a/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/topology/topology.component.ts
@@ -35,6 +35,7 @@
 import {ForceSvgComponent} from '../layer/forcesvg/forcesvg.component';
 import {TopologyService} from '../topology.service';
 import {
+    GridDisplayToggle,
     HostLabelToggle,
     LabelToggle,
     UiElement
@@ -43,7 +44,7 @@
     INSTANCE_TOGGLE, SUMMARY_TOGGLE, DETAILS_TOGGLE,
     HOSTS_TOGGLE, OFFLINE_TOGGLE, PORTS_TOGGLE,
     BKGRND_TOGGLE, CYCLELABELS_BTN, CYCLEHOSTLABEL_BTN,
-    RESETZOOM_BTN, EQMASTER_BTN,
+    CYCLEGRIDDISPLAY_BTN, RESETZOOM_BTN, EQMASTER_BTN,
     CANCEL_TRAFFIC, ALL_TRAFFIC, QUICKHELP_BTN, BKGRND_SELECT
 } from '../panel/toolbar/toolbar.component';
 import {TrafficService} from '../traffic.service';
@@ -57,6 +58,7 @@
 const PREF_DETAIL = 'detail';
 const PREF_DLBLS = 'dlbls';
 const PREF_HLBLS = 'hlbls';
+const PREF_GRID = 'grid';
 const PREF_HOSTS = 'hosts';
 const PREF_INSTS = 'insts';
 const PREF_OFFDEV = 'offdev';
@@ -81,6 +83,7 @@
     ovid: string;
     summary: number;
     toolbar: number;
+    grid: number;
 }
 
 /**
@@ -135,6 +138,7 @@
         spr: 0,
         summary: 1,
         toolbar: 0,
+        grid: 0
     };
 
     mapIdState: MapObject = <MapObject>{
@@ -144,6 +148,9 @@
     mapSelShown: boolean = false;
     lionFn; // Function
 
+    gridShown: boolean = true;
+    geoGridShown: boolean = true;
+
     constructor(
         protected log: LogService,
         protected fs: FnService,
@@ -177,6 +184,7 @@
         this.is.loadIconDef('m_map');
         this.is.loadIconDef('m_selectMap');
         this.is.loadIconDef('m_cycleLabels');
+        this.is.loadIconDef('m_cycleGridDisplay');
         this.is.loadIconDef('m_resetZoom');
         this.is.loadIconDef('m_eqMaster');
         this.is.loadIconDef('m_unknown');
@@ -211,6 +219,15 @@
         }
     }
 
+    private static gridDisplayFlashMessage(index: number): string {
+        switch (index) {
+            case 0: return 'fl_grid_display_hide';
+            case 1: return 'fl_grid_display_1000';
+            case 2: return 'fl_grid_display_geo';
+            case 3: return 'fl_grid_display_both';
+        }
+    }
+
     /**
      * Pass the list of Key Commands to the KeyService, and initialize the Topology
      * Service - which communicates with through the WebSocket to the ONOS server
@@ -241,9 +258,6 @@
         if (data[TOPO2_PREFS]) {
             this.prefsState = data[TOPO2_PREFS];
         }
-        if (data[TOPO_MAPID_PREFS]) {
-            this.mapIdState = data[TOPO_MAPID_PREFS];
-        }
         this.log.debug('Updated topo2 prefs', this.prefsState, this.mapIdState);
     }
 
@@ -294,6 +308,9 @@
             case CYCLEHOSTLABEL_BTN:
                 this.cycleHostLabels();
                 break;
+            case CYCLEGRIDDISPLAY_BTN:
+                this.cycleGridDisplay();
+                break;
             case RESETZOOM_BTN:
                 this.resetZoom();
                 break;
@@ -323,19 +340,20 @@
     actionMap() {
         return {
             A: [() => {this.monitorAllTraffic(); }, 'Monitor all traffic'],
-            L: [() => {this.cycleDeviceLabels(); }, 'Cycle device labels'],
             B: [(token) => {this.toggleBackground(token); }, 'Toggle background'],
             D: [(token) => {this.toggleDetails(token); }, 'Toggle details panel'],
+            E: [() => {this.equalizeMasters(); }, 'Equalize mastership roles'],
+            H: [() => {this.toggleHosts(); }, 'Toggle host visibility'],
             I: [(token) => {this.toggleInstancePanel(token); }, 'Toggle ONOS Instance Panel'],
             G: [() => {this.mapSelShown = !this.mapSelShown; }, 'Show map selection dialog'],
-            O: [() => {this.toggleSummary(); }, 'Toggle the Summary Panel'],
-            R: [() => {this.resetZoom(); }, 'Reset pan / zoom'],
-            P: [(token) => {this.togglePorts(token); }, 'Toggle Port Highlighting'],
-            E: [() => {this.equalizeMasters(); }, 'Equalize mastership roles'],
-            X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
-            U: [() => {this.unpinNode(); }, 'Unpin node (mouse over)'],
-            H: [() => {this.toggleHosts(); }, 'Toggle host visibility'],
+            L: [() => {this.cycleDeviceLabels(); }, 'Cycle device labels'],
             M: [() => {this.toggleOfflineDevices(); }, 'Toggle offline visibility'],
+            O: [() => {this.toggleSummary(); }, 'Toggle the Summary Panel'],
+            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)'],
+            X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
             dot: [() => {this.toggleToolbar(); }, 'Toggle Toolbar'],
             0: [() => {this.cancelTraffic(); }, 'Cancel traffic monitoring'],
             'shift-L': [() => {this.cycleHostLabels(); }, 'Cycle host labels'],
@@ -443,6 +461,14 @@
         this.log.debug('Cycling host labels', old, next);
     }
 
+    protected cycleGridDisplay() {
+        const old: GridDisplayToggle = this.prefsState.grid;
+        const next = GridDisplayToggle.next(old);
+        this.flashMsg = this.lionFn(TopologyComponent.gridDisplayFlashMessage(next));
+        this.updatePrefsState(PREF_GRID, next);
+        this.log.debug('Cycling grid display', old, next);
+    }
+
     /**
      * When the button is clicked on the toolbar or the B key is pressed
      * 1) Find the inverse of the current state (held as 1 or 0)