First part of migrating Topo2 to GUI2

Change-Id: I316dd34cba161688e01dfb7b340bff5f2c3c57d4
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/consolelogger.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/consolelogger.service.spec.ts
new file mode 100644
index 0000000..bbb8974
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/consolelogger.service.spec.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { ConsoleLoggerService } from './consolelogger.service';
+
+/**
+ * ONOS GUI -- Console Logger Service - Unit Tests
+ */
+describe('ConsoleloggerService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [ConsoleLoggerService]
+    });
+  });
+
+  it('should be created', inject([ConsoleLoggerService], (service: ConsoleLoggerService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/detectbrowser.directive.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/detectbrowser.directive.spec.ts
new file mode 100644
index 0000000..3faa7f1
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/detectbrowser.directive.spec.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from './log.service';
+import { ConsoleLoggerService } from './consolelogger.service';
+import { DetectBrowserDirective } from './detectbrowser.directive';
+import { ActivatedRoute, Params } from '@angular/router';
+import { FnService } from './util/fn.service';
+import { OnosService } from './onos.service';
+import { of } from 'rxjs';
+
+class MockFnService extends FnService {
+    constructor(ar: ActivatedRoute, log: LogService, w: Window) {
+        super(ar, log, w);
+    }
+}
+
+class MockOnosService {}
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Detect Browser Directive - Unit Tests
+ */
+describe('DetectBrowserDirective', () => {
+    let log: LogService;
+    let ar: ActivatedRoute;
+    let mockWindow: Window;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute(['debug', 'DetectBrowserDirective']);
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            }
+        };
+
+        TestBed.configureTestingModule({
+            providers: [ DetectBrowserDirective,
+                { provide: FnService, useValue: new MockFnService(ar, log, mockWindow) },
+                { provide: LogService, useValue: log },
+                { provide: OnosService, useClass: MockOnosService },
+                { provide: Document, useValue: document },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+    });
+
+    afterEach(() => {
+        log = null;
+    });
+
+    it('should create an instance', inject([DetectBrowserDirective], (directive: DetectBrowserDirective) => {
+        expect(directive).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/layer/loading.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/layer/loading.service.spec.ts
new file mode 100644
index 0000000..404439f
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/layer/loading.service.spec.ts
@@ -0,0 +1,56 @@
+/*
+ *  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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { LoadingService } from './loading.service';
+import { FnService } from '../util/fn.service';
+import { ThemeService } from '../util/theme.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+class MockFnService {
+    debug() {
+    }
+}
+
+class MockThemeService {}
+
+class MockWebSocketService {}
+
+/**
+ * ONOS GUI -- Layer -- Loading Service - Unit Tests
+ */
+describe('LoadingService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [LoadingService,
+                { provide: LogService, useValue: log },
+                { provide: FnService, useClass: MockFnService },
+                { provide: ThemeService, useClass: MockThemeService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+            ]
+        });
+    });
+
+    it('should be created', inject([LoadingService], (service: LoadingService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/log.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/log.service.spec.ts
new file mode 100644
index 0000000..e2998f8
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/log.service.spec.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from './log.service';
+
+/**
+ * ONOS GUI -- Log Service - Unit Tests
+ */
+describe('LogService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [LogService]
+    });
+  });
+
+  it('should be created', inject([LogService], (service: LogService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast.service.spec.ts
new file mode 100644
index 0000000..ca99af8
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast.service.spec.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { MastService } from './mast.service';
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {
+    isMobile() {}
+}
+
+/**
+ * ONOS GUI -- Masthead Service - Unit Tests
+ */
+describe('MastService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [MastService,
+                { provide: FnService, useClass: MockFnService },
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([MastService], (service: MastService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.css b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.css
index 5b2d464..a767b2e 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.css
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.css
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014-present Open Networking Foundation
+ * 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.
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.html b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.html
index 2d4e606..437f96d 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.html
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.html
@@ -1,5 +1,5 @@
 <!--
-~ Copyright 2014-present Open Networking Foundation
+~ 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.
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.spec.ts
index fe424e7..fca2dd9 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.spec.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.spec.ts
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * 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.
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.ts
index e8e5fec..41e2f3e 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.component.ts
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014-present Open Networking Foundation
+ * 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.
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.theme.css b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.theme.css
index 6b92beb..968aefa 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.theme.css
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/mast/mast/mast.theme.css
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-present Open Networking Foundation
+ * 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.
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/nav/nav.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/nav/nav.service.spec.ts
new file mode 100644
index 0000000..88ab5fa
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/nav/nav.service.spec.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import { HttpClient, HttpErrorResponse } from '@angular/common/http';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { NavService } from './nav.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+class MockHttpClient {}
+
+
+/**
+ * ONOS GUI -- Util -- Navigation Service - Unit Tests
+ */
+describe('NavService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [NavService,
+                { provide: HttpClient, useClass: MockHttpClient },
+                { provide: FnService, useClass: MockFnService },
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([NavService], (service: NavService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/nav/nav.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/nav/nav.service.ts
index a24f242..6e96309 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/nav/nav.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/nav/nav.service.ts
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-present Open Networking Foundation
+ * 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.
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.spec.ts
new file mode 100644
index 0000000..5b4669d
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyph.service.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { GlyphService } from './glyph.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+/**
+ * ONOS GUI -- SVG -- Glyph Service - Unit Tests
+ */
+describe('GlyphService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [GlyphService,
+                { provide: FnService, useClass: MockFnService },
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([GlyphService], (service: GlyphService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.spec.ts
new file mode 100644
index 0000000..ab770d5
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/glyphdata.service.spec.ts
@@ -0,0 +1,41 @@
+/*
+ *  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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '..//log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { GlyphDataService } from './glyphdata.service';
+
+/**
+ * ONOS GUI -- SVG -- Glyph Data Service - Unit Tests
+ */
+describe('GlyphDataService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [GlyphDataService,
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([GlyphDataService], (service: GlyphDataService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.spec.ts
new file mode 100644
index 0000000..094baef
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon.service.spec.ts
@@ -0,0 +1,50 @@
+/*
+ *  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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { IconService } from './icon.service';
+import { GlyphService } from './glyph.service';
+import { SvgUtilService } from './svgutil.service';
+
+class MockGlyphService {}
+
+class MockSvgUtilService {}
+
+/**
+ * ONOS GUI -- SVG -- Icon Service - Unit Tests
+ */
+describe('IconService', () => {
+
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [IconService,
+                { provide: LogService, useValue: log },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: SvgUtilService, useClass: MockSvgUtilService },
+            ]
+        });
+    });
+
+    it('should be created', inject([IconService], (service: IconService) => {
+        expect(service).toBeTruthy();
+    }));
+});
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 6ced162..73480ed 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
@@ -43,6 +43,7 @@
     ['m_ports', 'm_ports'],
 
     ['topo', 'topo'],
+    ['bird', 'bird'],
 
     ['refresh', 'refresh'],
     ['query', 'query'],
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon/icon.component.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon/icon.component.spec.ts
new file mode 100644
index 0000000..8234551
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/icon/icon.component.spec.ts
@@ -0,0 +1,30 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LogService } from '../../log.service';
+import { ConsoleLoggerService } from '../../consolelogger.service';
+import { IconComponent } from './icon.component';
+import { IconService } from '../icon.service';
+
+class MockIconService {}
+
+describe('IconComponent', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            declarations: [ IconComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                { provide: IconService, useClass: MockIconService },
+            ]
+        });
+    });
+
+    it('should create', () => {
+        const fixture = TestBed.createComponent(IconComponent);
+        const component = fixture.componentInstance;
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.spec.ts
new file mode 100644
index 0000000..7165b33
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { SvgUtilService } from './svgutil.service';
+import { FnService } from '../util/fn.service';
+
+class MockFnService {}
+
+/**
+ * ONOS GUI -- SVG -- Svg Util Service - Unit Tests
+ */
+describe('SvgUtilService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [SvgUtilService,
+                { provide: LogService, useValue: log },
+                { provide: FnService, useClass: MockFnService },
+            ]
+        });
+    });
+
+    it('should be created', inject([SvgUtilService], (service: SvgUtilService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
index 13327fe..6107d16 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/svgutil.service.ts
@@ -16,6 +16,7 @@
 import { Injectable } from '@angular/core';
 import { FnService } from '../util/fn.service';
 import { LogService } from '../log.service';
+import * as d3 from 'd3';
 
 /**
  * ONOS GUI -- SVG -- Util Service
@@ -26,11 +27,45 @@
     providedIn: 'root',
 })
 export class SvgUtilService {
+    lightNorm: string[];
+    lightMute: string[];
+    darkNorm: string[];
+    darkMute: string[];
+    colors: any;
 
     constructor(
         private fs: FnService,
         private log: LogService
     ) {
+
+        // --- Ordinal scales for 7 values.
+        // TODO: migrate these colors to the theme service.
+
+        // Colors per Mojo-Design's color palette.. (version one)
+        //               blue       red        dk grey    steel      lt blue    lt red     lt grey
+        // var lightNorm = ['#5b99d2', '#d05a55', '#716b6b', '#7e9aa8', '#66cef6', '#db7773', '#aeada8' ],
+        //     lightMute = ['#a8cceb', '#f1a7a7', '#b9b5b5', '#bdcdd5', '#a8e9fd', '#f8c9c9', '#d7d6d4' ],
+
+        // Colors per Mojo-Design's color palette.. (version two)
+        //               blue       lt blue    red        green      brown      teal       lime
+        this.lightNorm = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
+        this.lightMute = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
+
+        this.darkNorm = ['#5b99d2', '#66cef6', '#d05a55', '#0f9d58', '#ba7941', '#3dc0bf', '#56af00'];
+        this.darkMute = ['#9ebedf', '#abdef5', '#d79a96', '#7cbe99', '#cdab8d', '#96d5d5', '#a0c96d'];
+
+
+        this.colors = {
+            light: {
+                norm: d3.scaleOrdinal().range(this.lightNorm),
+                mute: d3.scaleOrdinal().range(this.lightMute),
+            },
+            dark: {
+                norm: d3.scaleOrdinal().range(this.darkNorm),
+                mute: d3.scaleOrdinal().range(this.darkMute),
+            },
+        };
+
         this.log.debug('SvgUtilService constructed');
     }
 
@@ -40,4 +75,91 @@
         }
         return 'translate(' + x + ',' + y + ')';
     }
+
+    scale(x, y) {
+        return 'scale(' + x + ',' + y + ')';
+    }
+
+    skewX(x) {
+        return 'skewX(' + x + ')';
+    }
+
+    rotate(deg) {
+        return 'rotate(' + deg + ')';
+    }
+
+    cat7() {
+        const tcid = 'd3utilTestCard';
+
+        function getColor(id, muted, theme) {
+            // NOTE: since we are lazily assigning domain ids, we need to
+            //       get the color from all 4 scales, to keep the domains
+            //       in sync.
+            const ln = this.colors.light.norm(id);
+            const lm = this.colors.light.mute(id);
+            const dn = this.colors.dark.norm(id);
+            const dm = this.colors.dark.mute(id);
+            if (theme === 'dark') {
+                return muted ? dm : dn;
+            } else {
+                return muted ? lm : ln;
+            }
+        }
+
+        function testCard(svg) {
+            let g = svg.select('g#' + tcid);
+            const dom = d3.range(7);
+            let k;
+            let muted;
+            let theme;
+            let what;
+
+            if (!g.empty()) {
+                g.remove();
+
+            } else {
+                g = svg.append('g')
+                    .attr('id', tcid)
+                    .attr('transform', 'scale(4)translate(20,20)');
+
+                for (k = 0; k < 4; k++) {
+                    muted = k % 2;
+                    what = muted ? ' muted' : ' normal';
+                    theme = k < 2 ? 'light' : 'dark';
+                    dom.forEach(function (id, i) {
+                        const x = i * 20;
+                        const y = k * 20;
+                        const f = getColor(id, muted, theme);
+                        g.append('circle').attr({
+                            cx: x,
+                            cy: y,
+                            r: 5,
+                            fill: f,
+                        });
+                    });
+                    g.append('rect').attr({
+                        x: 140,
+                        y: k * 20 - 5,
+                        width: 32,
+                        height: 10,
+                        rx: 2,
+                        fill: '#888',
+                    });
+                    g.append('text').text(theme + what)
+                        .attr({
+                            x: 142,
+                            y: k * 20 + 2,
+                            fill: 'white',
+                        })
+                        .style('font-size', '4pt');
+                }
+            }
+        }
+
+        return {
+            testCard: testCard,
+            getColor: getColor,
+        };
+    }
+
 }
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.spec.ts
new file mode 100644
index 0000000..fe25860
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.spec.ts
@@ -0,0 +1,193 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import * as d3 from 'd3';
+
+import { LogService } from '../log.service';
+import { FnService } from '../util/fn.service';
+
+import { ZoomService, CZ, D3S, ZoomOpts, Zoomer } from './zoom.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+
+/**
+ * ONOS GUI -- SVG -- Zoom Service - Unit Tests
+ */
+describe('ZoomService', () => {
+    let zs: ZoomService;
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let mockWindow: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const svg = d3.select('body').append('svg').attr('id', 'mySvg');
+    const zoomLayer = svg.append('g').attr('id', 'myZoomlayer');
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            }
+        };
+        fs = new FnService(ar, logSpy, mockWindow);
+
+        TestBed.configureTestingModule({
+            providers: [ ZoomService,
+                { provide: FnService, useValue: fs },
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+
+        zs = TestBed.get(ZoomService);
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    it('should be created', () => {
+        expect(zs).toBeTruthy();
+    });
+
+    it('should define ZoomService', function () {
+        expect(zs).toBeDefined();
+    });
+
+    it('should define api functions', function () {
+        expect(fs.areFunctions(zs, [
+            'createZoomer',
+            'zoomed',
+            'adjustZoomLayer'
+        ])).toBeTruthy();
+    });
+
+    function verifyZoomerApi() {
+        expect(fs.areFunctions(zs.zoomer, [
+            'panZoom', 'reset', 'translate', 'scale', 'scaleExtent'
+        ])).toBeTruthy();
+    }
+
+    it('should fail gracefully with no option object', function () {
+        expect(() => zs.createZoomer(<ZoomOpts>{}))
+            .toThrow(new Error(CZ + 'No "svg" (svg tag)' + D3S));
+        expect(logServiceSpy.error)
+            .toHaveBeenCalledWith(CZ + 'No "svg" (svg tag)' + D3S);
+    });
+
+    it('should complain if we miss required options', function () {
+        expect(() => zs.createZoomer(<ZoomOpts>{svg: svg}))
+            .toThrow(new Error(CZ + 'No "zoomLayer" (g tag)' + D3S));
+        expect(logServiceSpy.error).toHaveBeenCalledWith(CZ + 'No "zoomLayer" (g tag)' + D3S);
+    });
+
+    it('should work with minimal parameters', function () {
+        const zoomer = zs.createZoomer(<ZoomOpts>{
+            svg: svg,
+            zoomLayer: zoomLayer
+        });
+        expect(logServiceSpy.error).not.toHaveBeenCalled();
+        verifyZoomerApi();
+    });
+
+    it('should start at scale 1 and translate 0,0', function () {
+        const zoomer = zs.createZoomer(<ZoomOpts>{
+            svg: svg,
+            zoomLayer: zoomLayer
+        });
+        verifyZoomerApi();
+        expect(zoomer.translate()).toEqual([0, 0]);
+        expect(zoomer.scale()).toEqual(1);
+    });
+
+    it('should allow programmatic pan/zoom', function () {
+        const zoomer: Zoomer = zs.createZoomer(<ZoomOpts>{
+            svg: svg,
+            zoomLayer: zoomLayer
+        });
+        verifyZoomerApi();
+
+        expect(zoomer.translate()).toEqual([0, 0]);
+        expect(zoomer.scale()).toEqual(1);
+
+        zoomer.panZoom([20, 30], 1);
+        expect(zoomer.translate()).toEqual([20, 30]);
+        expect(zoomer.scale()).toEqual(1);
+
+        zoomer.reset();
+        expect(zoomer.translate()).toEqual([0, 0]);
+        expect(zoomer.scale()).toEqual(1);
+
+
+    });
+
+    it('should provide default scale extent', function () {
+        const zoomer = zs.createZoomer(<ZoomOpts>{
+            svg: svg,
+            zoomLayer: zoomLayer
+        });
+        expect(zoomer.scaleExtent()).toEqual([0.05, 50]);
+    });
+
+    it('should allow us to override the minimum zoom', function () {
+        const zoomer = zs.createZoomer(<ZoomOpts>{
+            svg: svg,
+            zoomLayer: zoomLayer,
+            zoomMin: 1.23
+        });
+        expect(zoomer.scaleExtent()).toEqual([1.23, 50]);
+    });
+
+    it('should allow us to override the maximum zoom', function () {
+        const zoomer = zs.createZoomer(<ZoomOpts>{
+            svg: svg,
+            zoomLayer: zoomLayer,
+            zoomMax: 13
+        });
+        expect(zoomer.scaleExtent()).toEqual([0.05, 13]);
+    });
+
+    // TODO: test zoomed() where we fake out the d3.event.sourceEvent etc...
+    //  need to check default enabled (true) and custom enabled predicate
+    //  need to check that the callback is invoked also
+
+    it('should invoke the callback on programmatic pan/zoom', function () {
+        const foo = { cb() { return; } };
+        spyOn(foo, 'cb');
+
+        const zoomer = zs.createZoomer(<ZoomOpts>{
+            svg: svg,
+            zoomMin: 0.25,
+            zoomMax: 10,
+            zoomLayer: zoomLayer,
+            zoomEnabled: (ev) => true,
+            zoomCallback: foo.cb,
+        });
+
+        zoomer.panZoom([0, 0], 2);
+        expect(foo.cb).toHaveBeenCalled();
+    });
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
new file mode 100644
index 0000000..fdf08f9
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/svg/zoom.service.ts
@@ -0,0 +1,148 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import * as d3 from 'd3';
+import { LogService } from '../log.service';
+
+export interface ZoomOpts {
+    svg: any;                         // D3 selection of <svg> element
+    zoomLayer: any;                   // D3 selection of <g> element
+    zoomMin: number;                  // Min zoom level - usually 0.25
+    zoomMax: number;                  // Max zoom level - usually 10
+    zoomEnabled(ev: any): boolean;   // Function that takes event and returns boolean
+    zoomCallback(translate: number[], scale: number): void; // Function that is called on zoom
+}
+
+export interface Zoomer {
+    panZoom(translate: number[], scale: number, transition?: number): void;
+    reset(): void;
+    translate(): number[];
+    scale(): number;
+    scaleExtent(): number[];
+}
+
+export const CZ: string = 'ZoomService.createZoomer(): ';
+export const D3S: string = ' (D3 selection) property defined';
+
+/**
+ * ONOS GUI -- Topology Zoom Service Module.
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class ZoomService {
+    defaultSettings: ZoomOpts;
+
+    zoom: any;
+    public zoomer: Zoomer;
+    settings: ZoomOpts;
+
+    constructor(
+        protected log: LogService,
+    ) {
+        this.defaultSettings = <ZoomOpts>{
+            zoomMin: 0.05,
+            zoomMax: 50,
+            zoomEnabled: (ev) => true,
+            zoomCallback: (t, s) => { return; }
+        };
+
+        this.log.debug('ZoomService constructed');
+    }
+
+    createZoomer(opts: ZoomOpts): Zoomer {
+        this.settings = Object.assign(this.defaultSettings, opts);
+
+        if (!this.settings.svg) {
+            this.log.error(CZ + 'No "svg" (svg tag)' + D3S);
+            throw new Error(CZ + 'No "svg" (svg tag)' + D3S);
+        }
+        if (!this.settings.zoomLayer) {
+            this.log.error(CZ + 'No "zoomLayer" (g tag)' + D3S);
+            throw new Error(CZ + 'No "zoomLayer" (g tag)' + D3S);
+        }
+
+        this.zoom = d3.zoom()
+            .scaleExtent([this.settings.zoomMin, this.settings.zoomMax])
+            .extent([[0, 0], [1000, 1000]])
+            .on('zoom', () => this.zoomed);
+
+
+        this.zoomer = <Zoomer>{
+            panZoom: (translate: number[], scale: number, transition?: number) => {
+                this.settings.svg.call(this.zoom.translateBy, translate[0], translate[1]);
+                this.settings.svg.call(this.zoom.scaleTo, scale);
+                this.adjustZoomLayer(translate, scale, transition);
+            },
+
+            reset: () => {
+                this.settings.svg.call(this.zoom.translateTo, 500, 500);
+                this.settings.svg.call(this.zoom.scaleTo, 1);
+                this.adjustZoomLayer([0, 0], 1, 0);
+            },
+
+            translate: () => {
+                const trans = d3.zoomTransform(this.settings.svg.node());
+                return [trans.x, trans.y];
+            },
+
+            scale: () => {
+                const trans = d3.zoomTransform(this.settings.svg.node());
+                return trans.k;
+            },
+
+            scaleExtent: () => {
+                return this.zoom.scaleExtent();
+            },
+        };
+
+        // apply the zoom behavior to the SVG element
+/*
+        if (this.settings.svg ) {
+            this.settings.svg.call(this.zoom);
+        }
+*/
+        // Remove zoom on double click (prevents a
+        // false zoom navigating regions)
+        this.settings.svg.on('dblclick.zoom', null);
+
+        return this.zoomer;
+    }
+
+    /**
+     * zoom events from mouse gestures...
+     */
+    zoomed() {
+        const ev = d3.event.sourceEvent;
+        if (this.settings.zoomEnabled(ev)) {
+            this.adjustZoomLayer(d3.event.translate, d3.event.scale);
+        }
+    }
+
+    /**
+     * Adjust the zoom layer
+     */
+    adjustZoomLayer(translate: number[], scale: number, transition?: any): void {
+
+        this.settings.zoomLayer.transition()
+            .duration(transition || 0)
+            .attr('transform',
+                'translate(' + translate + ') scale(' + scale + ')');
+
+        this.settings.zoomCallback(translate, scale);
+    }
+
+}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.spec.ts
index 5b86d56..e9b7c2a 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.spec.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.spec.ts
@@ -235,7 +235,8 @@
             'isFirefox', 'parseDebugFlags',
             'debugOn', 'debug', 'find', 'inArray', 'removeFromArray',
             'isEmptyObject', 'cap', 'noPx', 'noPxStyle', 'endsWith',
-            'inEvilList', 'analyze', 'sanitize', 'sameObjProps', 'containsObj'
+            'inEvilList', 'analyze', 'sanitize', 'sameObjProps', 'containsObj',
+            'addToTrie', 'removeFromTrie', 'trieLookup'
 //            'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
 //            'eecode', 'noPx', 'noPxStyle', 'endsWith', 'addToTrie', 'removeFromTrie', 'trieLookup',
 //            'classNames', 'extend', 'sanitize'
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.ts
index 506ce74..60d4950 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.ts
@@ -13,9 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Injectable, Inject } from '@angular/core';
-import { ActivatedRoute, Router} from '@angular/router';
-import { LogService } from '../log.service';
+import {Inject, Injectable} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {LogService} from '../log.service';
+import {Trie, TrieOp} from './trie';
 
 // Angular>=2 workaround for missing definition
 declare const InstallTrigger: any;
@@ -40,112 +41,6 @@
     name: string;
 }
 
-// TODO Move all this trie stuff to its own class
-// Angular>=2 Tightened up on types to avoid compiler errors
-interface TrieC {
-    p: any;
-    s: string[];
-}
-// trie operation
-function _trieOp(op: string, trie, word: string, data) {
-    const p = trie;
-    const w: string = word.toUpperCase();
-    const s: Array<string> = w.split('');
-    let c: TrieC = { p: p, s: s };
-    let t = [];
-    let  x = 0;
-    const f1 = op === '+' ? add : probe;
-    const f2 = op === '+' ? insert : remove;
-
-    function add(cAdded): TrieC {
-        const q = cAdded.s.shift();
-        let np = cAdded.p[q];
-
-        if (!np) {
-            cAdded.p[q] = {};
-            np = cAdded.p[q];
-            x = 1;
-        }
-        return { p: np, s: cAdded.s };
-    }
-
-    function probe(cProbed): TrieC {
-        const q = cProbed.s.shift();
-        const k: number = Object.keys(cProbed.p).length;
-        const np = cProbed.p[q];
-
-        t.push({ q: q, k: k, p: cProbed.p });
-        if (!np) {
-            t = [];
-            return { p: [], s: [] };
-        }
-        return { p: np, s: cProbed.s };
-    }
-
-    function insert() {
-        c.p._data = data;
-        return x ? 'added' : 'updated';
-    }
-
-    function remove() {
-        if (t.length) {
-            t = t.reverse();
-            while (t.length) {
-                const d = t.shift();
-                delete d.p[d.q];
-                if (d.k > 1) {
-                    t = [];
-                }
-            }
-            return 'removed';
-        }
-        return 'absent';
-    }
-
-    while (c.s.length) {
-        c = f1(c);
-    }
-    return f2();
-}
-
-// add word to trie (word will be converted to uppercase)
-// data associated with the word
-// returns 'added' or 'updated'
-function addToTrie(trie, word, data) {
-    return _trieOp('+', trie, word, data);
-}
-
-// remove word from trie (word will be converted to uppercase)
-// returns 'removed' or 'absent'
-// Angular>=2 added in quotes for data. error TS2554: Expected 4 arguments, but got 3.
-function removeFromTrie(trie, word) {
-    return _trieOp('-', trie, word, '');
-}
-
-// lookup word (converted to uppercase) in trie
-// returns:
-//    undefined if the word is not in the trie
-//    -1 for a partial match (word is a prefix to an existing word)
-//    data for the word for an exact match
-function trieLookup(trie, word) {
-    const s = word.toUpperCase().split('');
-    let p = trie;
-    let n;
-
-    while (s.length) {
-        n = s.shift();
-        p = p[n];
-        if (!p) {
-            return undefined;
-        }
-    }
-    if (p._data) {
-        return p._data;
-    }
-    return -1;
-}
-
-
 /**
  * ONOS GUI -- Util -- General Purpose Functions
  */
@@ -561,4 +456,46 @@
         return html;
     }
 
+    /**
+     * add word to trie (word will be converted to uppercase)
+     * data associated with the word
+     * returns 'added' or 'updated'
+     */
+    addToTrie(trie, word, data) {
+        return new Trie(TrieOp.PLUS, trie, word, data);
+    }
+
+    /**
+     * remove word from trie (word will be converted to uppercase)
+     * returns 'removed' or 'absent'
+     */
+    removeFromTrie(trie, word) {
+        return new Trie(TrieOp.MINUS, trie, word);
+    }
+
+    /**
+     * lookup word (converted to uppercase) in trie
+     * returns:
+     *    undefined if the word is not in the trie
+     *    -1 for a partial match (word is a prefix to an existing word)
+     *    data for the word for an exact match
+     */
+    trieLookup(trie, word) {
+        const s = word.toUpperCase().split('');
+        let p = trie;
+        let n;
+
+        while (s.length) {
+            n = s.shift();
+            p = p[n];
+            if (!p) {
+                return undefined;
+            }
+        }
+        if (p._data) {
+            return p._data;
+        }
+        return -1;
+    }
+
 }
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.spec.ts
new file mode 100644
index 0000000..5f4b349
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.spec.ts
@@ -0,0 +1,316 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import {ActivatedRoute, Params} from '@angular/router';
+
+import { KeysService, KeysToken } from './keys.service';
+import { FnService } from './fn.service';
+import { LogService } from '../log.service';
+import { NavService } from '../nav/nav.service';
+
+import {of} from 'rxjs';
+import * as d3 from 'd3';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockNavService {}
+
+/*
+ ONOS GUI -- Key Handler Service - Unit Tests
+ */
+describe('KeysService', () => {
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let ks: KeysService;
+    let mockWindow: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const qhs: any = {};
+    let d3Elem: any;
+    let elem: any;
+    let last: any;
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            },
+            location: <any>{
+                hostname: 'foo',
+                host: 'foo',
+                port: '80',
+                protocol: 'http',
+                search: { debug: 'true' },
+                href: 'ws://foo:123/onos/ui/websock/path',
+                absUrl: 'ws://foo:123/onos/ui/websock/path'
+            }
+        };
+        fs = new FnService(ar, logSpy, mockWindow);
+
+        d3Elem = d3.select('body').append('p').attr('id', 'ptest');
+        elem = d3Elem.node();
+        last = {
+            view: null,
+            key: null,
+            code: null,
+            ev: null
+        };
+
+        TestBed.configureTestingModule({
+            providers: [KeysService,
+                { provide: FnService, useValue: fs},
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: NavService, useClass: MockNavService},
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+        ks = TestBed.get(KeysService);
+        ks.installOn(d3Elem);
+        ks.bindQhs(qhs);
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    afterEach(() => {
+        d3.select('#ptest').remove();
+    });
+
+    it('should be created', () => {
+        expect(ks).toBeTruthy();
+    });
+
+    it('should define api functions', () => {
+        expect(fs.areFunctions(ks, [
+            'bindQhs', 'installOn', 'keyBindings', 'unbindKeys', 'dialogKeys',
+            'addSeq', 'remSeq', 'gestureNotes', 'enableKeys', 'enableGlobalKeys',
+            'checkNotGlobal', 'getKeyBindings',
+            'matchSeq', 'whatKey', 'textFieldInput', 'keyIn', 'qhlion', 'qhlionShowHide',
+            'qhlionHintEsc', 'qhlionHintT', 'setupGlobalKeys', 'quickHelp',
+            'escapeKey', 'toggleTheme', 'filterMaskedKeys', 'unexParam',
+            'setKeyBindings', 'bindDialogKeys', 'unbindDialogKeys'
+        ])).toBeTruthy();
+    });
+
+    function jsKeyDown(element, code: string, keyName: string) {
+        const ev = new KeyboardEvent('keydown',
+            { code: code, key: keyName });
+
+        // Chromium Hack
+        // if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
+        //     Object.defineProperty(ev, 'keyCode', {
+        //         get: () => { return this.keyCodeVal; }
+        //     });
+        //     Object.defineProperty(ev, 'which', {
+        //         get: () => { return this.keyCodeVal; }
+        //     });
+        // }
+
+        if (ev.code !== code.toString()) {
+            console.warn('keyCode mismatch ' + ev.code +
+                '(' + ev.which + ') -> ' + code);
+        }
+        element.dispatchEvent(ev);
+    }
+
+    // === Key binding related tests
+    it('should start with default key bindings', () => {
+        const state = ks.getKeyBindings();
+        const gk = state.globalKeys;
+        const mk = state.maskedKeys;
+        const vk = state.viewKeys;
+        const vf = state.viewFunction;
+
+        expect(gk.length).toEqual(4);
+        ['backSlash', 'slash', 'esc', 'T'].forEach((k) => {
+            expect(fs.contains(gk, k)).toBeTruthy();
+        });
+
+        expect(mk.length).toEqual(3);
+        ['backSlash', 'slash', 'T'].forEach((k) => {
+            expect(fs.contains(mk, k)).toBeTruthy();
+        });
+
+        expect(vk.length).toEqual(0);
+        expect(vf).toBeFalsy();
+    });
+
+    function bindTestKeys(withDescs?) {
+        const keys = ['A', '1', 'F5', 'equals'];
+        const kb = {};
+
+        function cb(view, key, code, ev) {
+            last.view = view;
+            last.key = key;
+            last.code = code;
+            last.ev = ev;
+        }
+
+        function bind(k) {
+            return withDescs ?
+                [(view, key, code, ev) => {cb(view, key, code, ev); }, 'desc for key ' + k] :
+                (view, key, code, ev) => {cb(view, key, code, ev); };
+        }
+
+        keys.forEach((k) => {
+            kb[k] = bind(k);
+        });
+
+        ks.keyBindings(kb);
+    }
+
+    function verifyCall(key, code) {
+        // TODO: update expectation, when view tokens are implemented
+        expect(last.view).toEqual(KeysToken.KEYEV);
+        last.view = null;
+
+        expect(last.key).toEqual(key);
+        last.key = null;
+
+        expect(last.code).toEqual(code);
+        last.code = null;
+
+        expect(last.ev).toBeTruthy();
+        last.ev = null;
+    }
+
+    function verifyNoCall() {
+        expect(last.view).toBeNull();
+        expect(last.key).toBeNull();
+        expect(last.code).toBeNull();
+        expect(last.ev).toBeNull();
+    }
+
+    function verifyTestKeys() {
+        jsKeyDown(elem, '65', 'A'); // 'A'
+        verifyCall('A', '65');
+        jsKeyDown(elem, '66', 'B'); // 'B'
+        verifyNoCall();
+
+        jsKeyDown(elem, '49', '1'); // '1'
+        verifyCall('1', '49');
+        jsKeyDown(elem, '50', '2'); // '2'
+        verifyNoCall();
+
+        jsKeyDown(elem, '116', 'F5'); // 'F5'
+        verifyCall('F5', '116');
+        jsKeyDown(elem, '117', 'F6'); // 'F6'
+        verifyNoCall();
+
+        jsKeyDown(elem, '187', '='); // 'equals'
+        verifyCall('equals', '187');
+        jsKeyDown(elem, '189', '-'); // 'dash'
+        verifyNoCall();
+
+        const vk = ks.getKeyBindings().viewKeys;
+
+        expect(vk.length).toEqual(4);
+        ['A', '1', 'F5', 'equals'].forEach((k) => {
+            expect(fs.contains(vk, k)).toBeTruthy();
+        });
+
+        expect(ks.getKeyBindings().viewFunction).toBeFalsy();
+    }
+
+    it('should allow specific key bindings', () => {
+        bindTestKeys();
+        verifyTestKeys();
+    });
+
+    it('should allow specific key bindings with descriptions', () => {
+        bindTestKeys(true);
+        verifyTestKeys();
+    });
+
+    it('should warn about masked keys', () => {
+        const k = {
+            'space': (token, key, code, ev) => cb(token, key, code, ev),
+            'T': (token, key, code, ev) => cb(token, key, code, ev)
+        };
+        let count = 0;
+
+        function cb(token, key, code, ev) {
+            count++;
+            // console.debug('count = ' + count, token, key, code);
+        }
+
+        ks.keyBindings(k);
+
+        expect(logServiceSpy.warn).toHaveBeenCalledWith('setKeyBindings()\n: Key "T" is reserved');
+
+        // the 'T' key should NOT invoke our callback
+        expect(count).toEqual(0);
+        jsKeyDown(elem, '84', 'T'); // 'T'
+        expect(count).toEqual(0);
+
+        // but the 'space' key SHOULD invoke our callback
+        jsKeyDown(elem, '32', ' '); // 'space'
+        expect(count).toEqual(1);
+    });
+
+    it('should block keys when disabled', () => {
+        let cbCount = 0;
+
+        function cb() { cbCount++; }
+
+        function pressA() { jsKeyDown(elem, '65', 'A'); }  // 65 == 'A' keycode
+
+        ks.keyBindings({ A: () => cb() });
+
+        expect(cbCount).toBe(0);
+
+        pressA();
+        expect(cbCount).toBe(1);
+
+        ks.enableKeys(false);
+        pressA();
+        expect(cbCount).toBe(1);
+
+        ks.enableKeys(true);
+        pressA();
+        expect(cbCount).toBe(2);
+    });
+
+    // === Gesture notes related tests
+    it('should start with no notes', () => {
+        expect(ks.gestureNotes()).toEqual([]);
+    });
+
+    it('should allow us to add nodes', () => {
+        const notes = [
+            ['one', 'something about one'],
+            ['two', 'description of two']
+        ];
+        ks.gestureNotes(notes);
+
+        expect(ks.gestureNotes()).toEqual(notes);
+    });
+
+    it('should ignore non-arrays', () => {
+        ks.gestureNotes({foo: 4});
+        expect(ks.gestureNotes()).toEqual([]);
+    });
+
+    // Consider adding test to ensure array contains 2-tuples of strings
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.ts
new file mode 100644
index 0000000..ac68c1a
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.ts
@@ -0,0 +1,407 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import * as d3 from 'd3';
+import { LogService } from '../log.service';
+import { FnService } from '../util/fn.service';
+import { LionService } from './lion.service';
+import { NavService } from '../nav/nav.service';
+
+export interface KeyHandler {
+    globalKeys: Object;
+    maskedKeys: Object;
+    dialogKeys: Object;
+    viewKeys: any;
+    viewFn: any;
+    viewGestures: string[][];
+}
+
+export enum KeysToken {
+    KEYEV = 'keyev'
+}
+
+/**
+ * ONOS GUI -- Keys Service Module.
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class KeysService {
+    enabled: boolean = true;
+    globalEnabled: boolean = true;
+    keyHandler: KeyHandler = <KeyHandler>{
+        globalKeys: {},
+        maskedKeys: {},
+        dialogKeys: {},
+        viewKeys: {},
+        viewFn: null,
+        viewGestures: [],
+    };
+
+    seq: any = {};
+    matching: boolean = false;
+    matched: string = '';
+    lookup: any;
+    textFieldDoesNotBlock: any = {
+        enter: 1,
+        esc: 1,
+    };
+    qhs: any; // Quick Help Service ??
+
+    constructor(
+        protected log: LogService,
+        protected fs: FnService,
+        protected ls: LionService,
+        protected ns: NavService
+    ) {
+        this.log.debug('KeyService constructed');
+    }
+
+    bindQhs(_qhs_) {
+        this.qhs = _qhs_;
+    }
+
+    installOn(elem) {
+        this.log.debug('Installing keys handler');
+        elem.on('keydown', () => { this.keyIn(); });
+        this.setupGlobalKeys();
+    }
+
+    keyBindings(x) {
+        if (x === undefined) {
+            return this.getKeyBindings();
+        } else {
+            this.setKeyBindings(x);
+        }
+    }
+
+    unbindKeys() {
+        this.keyHandler.viewKeys = {};
+        this.keyHandler.viewFn = null;
+        this.keyHandler.viewGestures = [];
+    }
+
+    dialogKeys(x) {
+        if (x === undefined) {
+            this.unbindDialogKeys();
+        } else {
+            this.bindDialogKeys(x);
+        }
+    }
+
+    addSeq(word, data) {
+        this.fs.addToTrie(this.seq, word, data);
+    }
+
+    remSeq(word) {
+        this.fs.removeFromTrie(this.seq, word);
+    }
+
+    gestureNotes(g?) {
+        if (g === undefined) {
+            return this.keyHandler.viewGestures;
+        } else {
+            this.keyHandler.viewGestures = this.fs.isA(g) || [];
+        }
+    }
+
+    enableKeys(b) {
+        this.enabled = b;
+    }
+
+    enableGlobalKeys(b) {
+        this.globalEnabled = b;
+    }
+
+    checkNotGlobal(o) {
+        const oops = [];
+        if (this.fs.isO(o)) {
+            o.forEach((val, key) => {
+                if (this.keyHandler.globalKeys[key]) {
+                    oops.push(key);
+                }
+            });
+            if (oops.length) {
+                this.log.warn('Ignoring reserved global key(s):', oops.join(','));
+                oops.forEach((key) => {
+                    delete o[key];
+                });
+            }
+        }
+    }
+
+    protected matchSeq(key) {
+        if (!this.matching && key === 'shift-shift') {
+            this.matching = true;
+            return true;
+        }
+        if (this.matching) {
+            this.matched += key;
+            this.lookup = this.fs.trieLookup(this.seq, this.matched);
+            if (this.lookup === -1) {
+                return true;
+            }
+            this.matching = false;
+            this.matched = '';
+            if (!this.lookup) {
+                return;
+            }
+            // ee.cluck(lookup);
+            return true;
+        }
+    }
+
+    protected whatKey(code: number): string {
+        switch (code) {
+            case 8: return 'delete';
+            case 9: return 'tab';
+            case 13: return 'enter';
+            case 16: return 'shift';
+            case 27: return 'esc';
+            case 32: return 'space';
+            case 37: return 'leftArrow';
+            case 38: return 'upArrow';
+            case 39: return 'rightArrow';
+            case 40: return 'downArrow';
+            case 186: return 'semicolon';
+            case 187: return 'equals';
+            case 188: return 'comma';
+            case 189: return 'dash';
+            case 190: return 'dot';
+            case 191: return 'slash';
+            case 192: return 'backQuote';
+            case 219: return 'openBracket';
+            case 220: return 'backSlash';
+            case 221: return 'closeBracket';
+            case 222: return 'quote';
+            default:
+                if ((code >= 48 && code <= 57) ||
+                    (code >= 65 && code <= 90)) {
+                    return String.fromCharCode(code);
+                } else if (code >= 112 && code <= 123) {
+                    return 'F' + (code - 111);
+                }
+                return null;
+        }
+    }
+
+    protected textFieldInput() {
+        const t = d3.event.target.tagName.toLowerCase();
+        return t === 'input' || t === 'textarea';
+    }
+
+    protected keyIn() {
+        const event = d3.event;
+        // d3.events can set the keyCode, but unit tests based on KeyboardEvent
+        // cannot set keyCode since the attribute has been deprecated
+        const code = event.keyCode ? event.keyCode : event.code;
+        let key = this.whatKey(Number.parseInt(code));
+        this.log.debug('Key detected', event, key, event.code, event.keyCode);
+        const textBlockable = !this.textFieldDoesNotBlock[key];
+        const modifiers = [];
+
+        if (event.metaKey) {
+            modifiers.push('cmd');
+        }
+        if (event.altKey) {
+            modifiers.push('alt');
+        }
+        if (event.shiftKey) {
+            modifiers.push('shift');
+        }
+
+        if (!key) {
+            return;
+        }
+
+        modifiers.push(key);
+        key = modifiers.join('-');
+
+        if (textBlockable && this.textFieldInput()) {
+            return;
+        }
+
+        const kh: KeyHandler = this.keyHandler;
+        const gk = kh.globalKeys[key];
+        const gcb = this.fs.isF(gk) || (this.fs.isA(gk) && this.fs.isF(gk[0]));
+        const dk = kh.dialogKeys[key];
+        const dcb = this.fs.isF(dk);
+        const vk = kh.viewKeys[key];
+        const kl = this.fs.isF(kh.viewKeys._keyListener);
+        const vcb = this.fs.isF(vk) || (this.fs.isA(vk) && this.fs.isF(vk[0])) || this.fs.isF(kh.viewFn);
+        const token: KeysToken = KeysToken.KEYEV; // indicate this was a key-pressed event
+
+        event.stopPropagation();
+
+        if (this.enabled) {
+            if (this.matchSeq(key)) {
+                return;
+            }
+
+            // global callback?
+            if (gcb && gcb(token, key, code, event)) {
+                // if the event was 'handled', we are done
+                return;
+            }
+            // dialog callback?
+            if (dcb) {
+                dcb(token, key, code, event);
+                // assume dialog handled the event
+                return;
+            }
+            // otherwise, let the view callback have a shot
+            if (vcb) {
+                this.log.debug('Letting view callback have a shot', vcb, token, key, code, event );
+                vcb(token, key, code, event);
+            }
+            if (kl) {
+                kl(key);
+            }
+        }
+    }
+
+    // functions to obtain localized strings deferred from the setup of the
+    //  global key data structures.
+    protected qhlion() {
+        return this.ls.bundle('core.fw.QuickHelp');
+    }
+    protected qhlionShowHide() {
+        return this.qhlion()('qh_hint_show_hide_qh');
+    }
+
+    protected qhlionHintEsc() {
+        return this.qhlion()('qh_hint_esc');
+    }
+
+    protected qhlionHintT() {
+        return this.qhlion()('qh_hint_t');
+    }
+
+    protected setupGlobalKeys() {
+        Object.assign(this.keyHandler, {
+            globalKeys: {
+                backSlash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
+                slash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
+                esc: [(view, key, code, ev) => this.escapeKey(view, key, code, ev), this.qhlionHintEsc],
+                T: [(view, key, code, ev) => this.toggleTheme(view, key, code, ev), this.qhlionHintT],
+            },
+            globalFormat: ['backSlash', 'slash', 'esc', 'T'],
+
+            // Masked keys are global key handlers that always return true.
+            // That is, the view will never see the event for that key.
+            maskedKeys: {
+                slash: 1,
+                backSlash: 1,
+                T: 1,
+            },
+        });
+    }
+
+    protected quickHelp(view, key, code, ev) {
+        if (!this.globalEnabled) {
+            return false;
+        }
+        this.qhs.showQuickHelp(this.keyHandler);
+        return true;
+    }
+
+    // returns true if we 'consumed' the ESC keypress, false otherwise
+    protected escapeKey(view, key, code, ev) {
+        return this.ns.hideNav() || this.qhs.hideQuickHelp();
+    }
+
+    protected toggleTheme(view, key, code, ev) {
+        if (!this.globalEnabled) {
+            return false;
+        }
+        // ts.toggleTheme();
+        return true;
+    }
+
+    protected filterMaskedKeys(map: any, caller: any, remove: boolean): any[] {
+        const masked = [];
+        const msgs = [];
+
+        d3.map(map).keys().forEach((key) => {
+            if (this.keyHandler.maskedKeys[key]) {
+                masked.push(key);
+                msgs.push(caller, ': Key "' + key + '" is reserved');
+            }
+        });
+
+        if (msgs.length) {
+            this.log.warn(msgs.join('\n'));
+        }
+
+        if (remove) {
+            masked.forEach((k) => {
+                delete map[k];
+            });
+        }
+        return masked;
+    }
+
+    protected unexParam(fname, x) {
+        this.log.warn(fname, ': unexpected parameter-- ', x);
+    }
+
+    protected setKeyBindings(keyArg) {
+        const fname = 'setKeyBindings()';
+        const kFunc = this.fs.isF(keyArg);
+        const kMap = this.fs.isO(keyArg);
+
+        if (kFunc) {
+            // set general key handler callback
+            this.keyHandler.viewFn = kFunc;
+        } else if (kMap) {
+            this.filterMaskedKeys(kMap, fname, true);
+            this.keyHandler.viewKeys = kMap;
+        } else {
+            this.unexParam(fname, keyArg);
+        }
+    }
+
+    getKeyBindings() {
+        const gkeys = d3.map(this.keyHandler.globalKeys).keys();
+        const masked = d3.map(this.keyHandler.maskedKeys).keys();
+        const vkeys = d3.map(this.keyHandler.viewKeys).keys();
+        const vfn = !!this.fs.isF(this.keyHandler.viewFn);
+
+        return {
+            globalKeys: gkeys,
+            maskedKeys: masked,
+            viewKeys: vkeys,
+            viewFunction: vfn,
+        };
+    }
+
+    protected bindDialogKeys(map) {
+        const fname = 'bindDialogKeys()';
+        const kMap = this.fs.isO(map);
+
+        if (kMap) {
+            this.filterMaskedKeys(map, fname, true);
+            this.keyHandler.dialogKeys = kMap;
+        } else {
+            this.unexParam(fname, map);
+        }
+    }
+
+    protected unbindDialogKeys() {
+        this.keyHandler.dialogKeys = {};
+    }
+
+}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/lion.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/lion.service.spec.ts
new file mode 100644
index 0000000..b0c252c
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/lion.service.spec.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import { of } from 'rxjs';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { FnService } from '../util/fn.service';
+import { GlyphService } from '../svg/glyph.service';
+import { LionService } from './lion.service';
+import { UrlFnService } from '../remote/urlfn.service';
+import { WSock } from '../remote/wsock.service';
+import { WebSocketService, WsOptions } from '../remote/websocket.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockWSock {}
+
+class MockGlyphService {}
+
+class MockUrlFnService {}
+
+/**
+ * ONOS GUI -- Lion -- Localization Utilities - Unit Tests
+ */
+describe('LionService', () => {
+    let log: LogService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        windowMock = <any>{
+            location: <any> {
+                hostname: '',
+                host: '',
+                port: '',
+                protocol: '',
+                search: { debug: 'true'},
+                href: ''
+            }
+        };
+        fs = new FnService(ar, log, windowMock);
+
+        TestBed.configureTestingModule({
+            providers: [LionService,
+                { provide: FnService, useValue: fs },
+                { provide: GlyphService, useClass: MockGlyphService },
+                { provide: LogService, useValue: log },
+                { provide: UrlFnService, useClass: MockUrlFnService },
+                { provide: WSock, useClass: MockWSock },
+                { provide: WebSocketService, useClass: WebSocketService },
+                { provide: 'Window', useFactory: (() => windowMock ) },
+            ]
+        });
+    });
+
+    it('should be created', inject([LionService], (service: LionService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/prefs.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/prefs.service.spec.ts
new file mode 100644
index 0000000..d925ecf
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/prefs.service.spec.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { PrefsService } from '../util/prefs.service';
+import { FnService } from '../util/fn.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+class MockFnService {}
+
+class MockWebSocketService {
+    createWebSocket() {}
+    isConnected() { return false; }
+    unbindHandlers() {}
+    bindHandlers() {}
+}
+
+/**
+ * ONOS GUI -- Util -- User Preference Service - Unit Tests
+ */
+describe('PrefsService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [PrefsService,
+                { provide: LogService, useValue: log },
+                { provide: FnService, useClass: MockFnService },
+                { provide: WebSocketService, useClass: MockWebSocketService },
+            ]
+        });
+    });
+
+    it('should be created', inject([PrefsService], (service: PrefsService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/theme.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/theme.service.spec.ts
new file mode 100644
index 0000000..16d38e3
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/theme.service.spec.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { ThemeService } from './theme.service';
+
+/**
+ * ONOS GUI -- Util -- Theme Service - Unit Tests
+ */
+describe('ThemeService', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            providers: [ThemeService,
+                { provide: LogService, useValue: log },
+            ]
+        });
+    });
+
+    it('should be created', inject([ThemeService], (service: ThemeService) => {
+        expect(service).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/trie.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/trie.ts
new file mode 100644
index 0000000..5e08061
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/trie.ts
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+export interface TrieC {
+    p: any;
+    s: string[];
+}
+
+export interface TrieT {
+    k: any;
+    p: any;
+    q: any;
+}
+
+export enum TrieRemoved {
+    REMOVED = 'removed',
+    ABSENT = 'absent'
+}
+
+export enum TrieInsert {
+    ADDED = 'added',
+    UPDATED = 'updated'
+}
+
+/**
+ * Combine TrieRemoved and TrieInsert in to a union type
+ */
+export type TrieActions = TrieRemoved | TrieInsert;
+
+export enum TrieOp {
+    PLUS = '+',
+    MINUS = '-'
+}
+
+
+export class Trie {
+    p: any;
+    w: string;
+    s: string[];
+    c: TrieC;
+    t: TrieT[];
+    x: number;
+    f1: (TrieC) => TrieC;
+    f2: () => TrieActions;
+    data: any;
+
+
+    constructor(
+        op: TrieOp,
+        trie: any,
+        word: string,
+        data?: any
+    ) {
+        this.p = trie;
+        this.w = word.toUpperCase();
+        this.s = this.w.split('');
+        this.c = { p: this.p, s: this.s },
+        this.t = [];
+        this.x = 0;
+        this.f1 = op === TrieOp.PLUS ? this.add : this.probe;
+        this.f2 = op === TrieOp.PLUS ? this.insert : this.remove;
+        this.data = data;
+        while (this.c.s.length) {
+            this.c = this.f1(this.c);
+        }
+    }
+
+    add(cAdded: TrieC): TrieC {
+        const q = cAdded.s.shift();
+        let np = cAdded.p[q];
+
+        if (!np) {
+            cAdded.p[q] = {};
+            np = cAdded.p[q];
+            this.x = 1;
+        }
+        return { p: np, s: cAdded.s };
+    }
+
+    probe(cProbed: TrieC): TrieC {
+        const q = cProbed.s.shift();
+        const k: number = Object.keys(cProbed.p).length;
+        const np = cProbed.p[q];
+
+        this.t.push({ q: q, k: k, p: cProbed.p });
+        if (!np) {
+            this.t = [];
+            return { p: [], s: [] };
+        }
+        return { p: np, s: cProbed.s };
+    }
+
+    insert(): TrieInsert {
+        this.c.p._data = this.data;
+        return this.x ? TrieInsert.ADDED : TrieInsert.UPDATED;
+    }
+
+    remove(): TrieRemoved {
+        if (this.t.length) {
+            this.t = this.t.reverse();
+            while (this.t.length) {
+                const d = this.t.shift();
+                delete d.p[d.q];
+                if (d.k > 1) {
+                    this.t = [];
+                }
+            }
+            return TrieRemoved.REMOVED;
+        }
+        return TrieRemoved.ABSENT;
+    }
+}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/detailspanel.base.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/detailspanel.base.ts
index 11dc8c9..03c681b 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/detailspanel.base.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/detailspanel.base.ts
@@ -55,7 +55,7 @@
         protected wss: WebSocketService,
         protected tag: string,
     ) {
-        super(fs, ls, log, wss, {});
+        super(fs, ls, log);
         this.root = tag + 's';
         this.req = tag + 'DetailsRequest';
         this.resp = tag + 'DetailsResponse';
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts
index 90cdfd5..0377c47 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/panel.base.ts
@@ -16,41 +16,6 @@
 import { FnService } from '../util/fn.service';
 import { LoadingService } from '../layer/loading.service';
 import { LogService } from '../log.service';
-import { WebSocketService } from '../remote/websocket.service';
-
-
-const noop = (): any => undefined;
-
-/**
- ********* Static functions *********
- */
-function margin(p) {
-    return p.settings.margin;
-}
-
-function hideMargin(p) {
-    return p.settings.hideMargin;
-}
-
-function noPx(p, what) {
-    return Number(p.el.style(what).replace(/px$/, ''));
-}
-
-function widthVal(p) {
-    return noPx(p, 'width');
-}
-
-function heightVal(p) {
-    return noPx(p, 'height');
-}
-
-function pxShow(p) {
-    return margin(p) + 'px';
-}
-
-function pxHide(p) {
-    return (-hideMargin(p) - widthVal(p) - (noPx(p, 'padding') * 2)) + 'px';
-}
 
 
 /**
@@ -60,14 +25,7 @@
     showPanel(cb: any): void;
     hidePanel(cb: any): void;
     togglePanel(cb: any): void;
-    emptyPanel(): void;
-    appendPanel(what: any): void;
-    panelWidth(w: number): number;
-    panelHeight(h: number): number;
-    panelBBox(): string;
     panelIsVisible(): boolean;
-    classed(cls: any, bool: boolean): boolean;
-    panelEl(): any;
 }
 
 /**
@@ -77,36 +35,22 @@
  */
 export abstract class PanelBaseImpl implements PanelBase {
 
-    protected on: boolean;
-    protected el: any;
+    on: boolean;
 
     constructor(
         protected fs: FnService,
         protected ls: LoadingService,
         protected log: LogService,
-        protected wss: WebSocketService,
-        protected settings: any
     ) {
 //        this.log.debug('Panel base class constructed');
     }
 
     showPanel(cb) {
-        const endCb = this.fs.isF(cb) || noop;
         this.on = true;
-        this.el.transition().duration(this.settings.xtnTime)
-            .each('end', endCb)
-            .style(this.settings.edge, pxShow(this))
-            .style('opacity', 1);
     }
 
     hidePanel(cb) {
-        const endCb = this.fs.isF(cb) || noop;
-        const endOpacity = this.settings.fade ? 0 : 1;
         this.on = false;
-        this.el.transition().duration(this.settings.xtnTime)
-            .each('end', endCb)
-            .style(this.settings.edge, pxHide(this))
-            .style('opacity', endOpacity);
     }
 
     togglePanel(cb): boolean {
@@ -118,45 +62,10 @@
         return this.on;
     }
 
-    emptyPanel(): string {
-        return this.el.text('');
-    }
-
-    appendPanel(what) {
-        return this.el.append(what);
-    }
-
-    panelWidth(w: number): number {
-        if (w === undefined) {
-            return widthVal(this);
-        }
-        this.el.style('width', w + 'px');
-    }
-
-    panelHeight(h: number): number {
-        if (h === undefined) {
-            return heightVal(this);
-        }
-        this.el.style('height', h + 'px');
-    }
-
-    panelBBox(): string {
-        return this.el.node().getBoundingClientRect();
-    }
-
     panelIsVisible(): boolean {
         return this.on;
     }
 
-    classed(cls, bool): boolean {
-        return this.el.classed(cls, bool);
-    }
-
-    panelEl() {
-        return this.el;
-    }
-
-
     /**
      * A dummy implementation of the lionFn until the response is received and the LION
      * bundle is received from the WebSocket
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/tableresize.directive.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/tableresize.directive.spec.ts
new file mode 100644
index 0000000..b1e87e6
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/tableresize.directive.spec.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 { TestBed, inject } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { TableResizeDirective } from './tableresize.directive';
+import { LogService } from '..//log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { MastService } from '../mast/mast.service';
+import { FnService } from '../util/fn.service';
+
+class MockMastService {}
+
+class MockFnService extends FnService {
+    constructor(ar: ActivatedRoute, log: LogService, w: Window) {
+        super(ar, log, w);
+    }
+}
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Widget -- Table Resize Directive - Unit Tests
+ */
+describe('TableResizeDirective', () => {
+    let log: LogService;
+    let mockWindow: Window;
+    let ar: ActivatedRoute;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute(['debug', 'DetectBrowserDirective']);
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            }
+        };
+        TestBed.configureTestingModule({
+            providers: [ TableResizeDirective,
+                { provide: FnService, useValue: new MockFnService(ar, log, mockWindow) },
+                { provide: LogService, useValue: log },
+                { provide: MastService, useClass: MockMastService },
+                { provide: 'Window', useFactory: (() => mockWindow ) },
+            ]
+        });
+    });
+
+    afterEach(() => {
+        log = null;
+    });
+
+    it('should create an instance', inject([TableResizeDirective], (directive: TableResizeDirective) => {
+        expect(directive).toBeTruthy();
+    }));
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/tableresize.directive.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/tableresize.directive.ts
index 0678583..5d3a9eb 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/tableresize.directive.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/widget/tableresize.directive.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { AfterContentChecked, Directive, ElementRef, Inject } from '@angular/core';
+import { AfterContentChecked, Directive, Inject } from '@angular/core';
 import { FnService } from '../util/fn.service';
 import { LogService } from '../log.service';
 import { MastService } from '../mast/mast.service';
@@ -34,7 +34,6 @@
     constructor(protected fs: FnService,
         protected log: LogService,
         protected mast: MastService,
-        protected el: ElementRef,
         @Inject('Window') private w: any) {
 
         log.info('TableResizeDirective constructed');
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts
index 51cf201..5f162d5 100644
--- a/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/public_api.ts
@@ -36,10 +36,14 @@
 export * from './lib/svg/svgutil.service';
 export * from './lib/svg/glyphdata.service';
 export * from './lib/svg/glyph.service';
+export * from './lib/svg/zoom.service';
+
 export * from './lib/util/prefs.service';
 export * from './lib/util/fn.service';
 export * from './lib/util/lion.service';
 export * from './lib/util/theme.service';
+export * from './lib/util/keys.service';
+export * from './lib/util/trie';
 
 export * from './lib/mast/mast/mast.component';
 export * from './lib/layer/veil/veil.component';