GUI -- Implemented ZoomService, with unit tests.
- Added zoomer to topo.js; we are at least generating the events.
- Added GlyphService.clear()
Change-Id: I5400e52b58ee584866d8ffbb20d5bde70b336985
diff --git a/web/gui/src/main/webapp/app/fw/svg/glyph.js b/web/gui/src/main/webapp/app/fw/svg/glyph.js
index 8129c16..621436c 100644
--- a/web/gui/src/main/webapp/app/fw/svg/glyph.js
+++ b/web/gui/src/main/webapp/app/fw/svg/glyph.js
@@ -124,9 +124,13 @@
.factory('GlyphService', ['$log', function (_$log_) {
$log = _$log_;
- function init() {
+ function clear() {
// start with a fresh map
glyphs = d3.map();
+ }
+
+ function init() {
+ clear();
register(birdViewBox, birdData);
register(glyphViewBox, glyphData);
register(badgeViewBox, badgeData);
@@ -175,6 +179,7 @@
}
return {
+ clear: clear,
init: init,
register: register,
ids: ids,
diff --git a/web/gui/src/main/webapp/app/fw/svg/zoom.js b/web/gui/src/main/webapp/app/fw/svg/zoom.js
index 2dc5236..09c8455 100644
--- a/web/gui/src/main/webapp/app/fw/svg/zoom.js
+++ b/web/gui/src/main/webapp/app/fw/svg/zoom.js
@@ -22,14 +22,112 @@
(function () {
'use strict';
+ // configuration
+ var defaultSettings = {
+ zoomMin: 0.25,
+ zoomMax: 10,
+ zoomEnabled: function (ev) { return true; },
+ zoomCallback: function () {}
+ };
+
+ // injected references to services
var $log;
angular.module('onosSvg')
- .factory('ZoomService', ['$log', function (_$log_) {
+ .factory('ZoomService', ['$log',
+
+ function (_$log_) {
$log = _$log_;
+/*
+ NOTE: opts is an object:
+ {
+ svg: svg, D3 selection of <svg> element
+ zoomLayer: zoomLayer, D3 selection of <g> element
+ zoomEnabled: function (ev) { ... },
+ zoomCallback: function () { ... }
+ }
+
+ where:
+ * svg and zoomLayer should be D3 selections of DOM elements.
+ * zoomLayer <g> is a child of <svg> element.
+ * zoomEnabled is an optional predicate based on D3 event.
+ * default is always enabled.
+ * zoomCallback is an optional callback invoked each time we pan/zoom.
+ * default is do nothing.
+
+ Optionally, zoomMin and zoomMax also can be defined.
+ These default to 0.25 and 10 respectively.
+*/
+ function createZoomer(opts) {
+ var cz = 'ZoomService.createZoomer(): ',
+ d3s = ' (D3 selection) property defined',
+ settings = $.extend({}, defaultSettings, opts),
+ zoom = d3.behavior.zoom()
+ .translate([0, 0])
+ .scale(1)
+ .scaleExtent([settings.zoomMin, settings.zoomMax])
+ .on('zoom', zoomed),
+ fail = false,
+ zoomer;
+
+ if (!settings.svg) {
+ $log.error(cz + 'No "svg" (svg tag)' + d3s);
+ fail = true;
+ }
+ if (!settings.zoomLayer) {
+ $log.error(cz + 'No "zoomLayer" (g tag)' + d3s);
+ fail = true;
+ }
+
+ if (fail) {
+ return null;
+ }
+
+ // zoom events from mouse gestures...
+ function zoomed() {
+ var ev = d3.event.sourceEvent;
+ if (settings.zoomEnabled(ev)) {
+ adjustZoomLayer(d3.event.translate, d3.event.scale);
+ }
+ }
+
+ function adjustZoomLayer(translate, scale) {
+ settings.zoomLayer.attr('transform',
+ 'translate(' + translate + ')scale(' + scale + ')');
+ settings.zoomCallback();
+ }
+
+ zoomer = {
+ panZoom: function (translate, scale) {
+ zoom.translate(translate).scale(scale);
+ adjustZoomLayer(translate, scale);
+ },
+
+ reset: function () {
+ zoomer.panZoom([0,0], 1);
+ },
+
+ translate: function () {
+ return zoom.translate();
+ },
+
+ scale: function () {
+ return zoom.scale();
+ },
+
+ scaleExtent: function () {
+ return zoom.scaleExtent();
+ }
+ };
+
+ // apply the zoom behavior to the SVG element
+ settings.svg && settings.svg.call(zoom);
+ return zoomer;
+ }
+
return {
- tbd: function () {}
+ createZoomer: createZoomer
};
}]);
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index adf7d2a..603a5a1 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -29,19 +29,22 @@
];
// references to injected services etc.
- var $log, ks, gs;
+ var $log, ks, zs, gs;
// DOM elements
- var defs;
+ var svg, defs;
// Internal state
- // ...
+ var zoomer;
// Note: "exported" state should be properties on 'self' variable
+ // --- Short Cut Keys ------------------------------------------------
+
var keyBindings = {
- W: [logWarning, 'log a warning'],
- E: [logError, 'log an error']
+ W: [logWarning, '(temp) log a warning'],
+ E: [logError, '(temp) log an error'],
+ R: [resetZoom, 'Reset pan / zoom']
};
// -----------------
@@ -54,32 +57,72 @@
}
// -----------------
+ function resetZoom() {
+ zoomer.reset();
+ }
+
function setUpKeys() {
ks.keyBindings(keyBindings);
}
+
+ // --- Glyphs, Icons, and the like -----------------------------------
+
function setUpDefs() {
- defs = d3.select('#ov-topo svg').append('defs');
+ defs = svg.append('defs');
gs.loadDefs(defs);
}
+ // --- Pan and Zoom --------------------------------------------------
+
+ // zoom enabled predicate. ev is a D3 source event.
+ function zoomEnabled(ev) {
+ return (ev.metaKey || ev.altKey);
+ }
+
+ function zoomCallback() {
+ var tr = zoomer.translate(),
+ sc = zoomer.scale();
+ $log.log('ZOOM: translate = ' + tr + ', scale = ' + sc);
+
+ // TODO: keep the map lines constant width while zooming
+ //bgImg.style('stroke-width', 2.0 / scale + 'px');
+ }
+
+ function setUpZoom() {
+ var zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer,
+ zoomEnabled: zoomEnabled,
+ zoomCallback: zoomCallback
+ });
+ }
+
+
+ // --- Controller Definition -----------------------------------------
+
angular.module('ovTopo', moduleDependencies)
.controller('OvTopoCtrl', [
- '$log', 'KeyService', 'GlyphService',
+ '$log', 'KeyService', 'ZoomService', 'GlyphService',
- function (_$log_, _ks_, _gs_) {
+ function (_$log_, _ks_, _zs_, _gs_) {
var self = this;
-
$log = _$log_;
ks = _ks_;
+ zs = _zs_;
gs = _gs_;
+ // exported state
self.message = 'Topo View Rocks!';
+ // svg layer and initialization of components
+ svg = d3.select('#ov-topo svg');
setUpKeys();
setUpDefs();
+ setUpZoom();
$log.log('OvTopoCtrl has been created');
}]);
diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js
index d743205..8ae2937 100644
--- a/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js
@@ -38,6 +38,7 @@
afterEach(function () {
d3.select('#myDefs').remove();
+ gs.clear();
});
it('should define GlyphService', function () {
@@ -59,6 +60,13 @@
expect(gs.ids().length).toEqual(numBaseGlyphs);
});
+ it('should remove glyphs on clear', function () {
+ gs.init();
+ expect(gs.ids().length).toEqual(numBaseGlyphs);
+ gs.clear();
+ expect(gs.ids().length).toEqual(0);
+ });
+
function verifyGlyphLoaded(id, vbox, prefix) {
var glyph = gs.glyph(id),
plen = prefix.length;
diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/zoom-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/zoom-spec.js
index 0346f19..467d866 100644
--- a/web/gui/src/main/webapp/tests/app/fw/svg/zoom-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/svg/zoom-spec.js
@@ -20,17 +20,135 @@
@author Simon Hunt
*/
describe('factory: fw/svg/zoom.js', function() {
- var zs;
+ var $log, fs, zs, svg, zoomLayer, zoomer;
- beforeEach(module('onosSvg'));
+ var cz = 'ZoomService.createZoomer(): ',
+ d3s = ' (D3 selection) property defined';
- beforeEach(inject(function (ZoomService) {
+ beforeEach(module('onosUtil', 'onosSvg'));
+
+ beforeEach(inject(function (_$log_, FnService, ZoomService) {
+ $log = _$log_;
+ fs = FnService;
zs = ZoomService;
+ svg = d3.select('body').append('svg').attr('id', 'mySvg');
+ zoomLayer = svg.append('g').attr('id', 'myZoomlayer');
}));
+ afterEach(function () {
+ d3.select('#mySvg').remove();
+ // Note: since zoomLayer is a child of svg, it should be removed also
+ });
+
it('should define ZoomService', function () {
expect(zs).toBeDefined();
});
- // TODO: unit tests for map functions
+ it('should define api functions', function () {
+ expect(fs.areFunctions(zs, ['createZoomer'])).toBeTruthy();
+ });
+
+ function verifyZoomerApi() {
+ expect(fs.areFunctions(zoomer, [
+ 'panZoom', 'reset', 'translate', 'scale'
+ ])).toBeTruthy();
+ }
+
+ it('should fail gracefully with no option object', function () {
+ spyOn($log, 'error');
+
+ zoomer = zs.createZoomer();
+ expect($log.error).toHaveBeenCalledWith(cz + 'No "svg" (svg tag)' + d3s);
+ expect($log.error).toHaveBeenCalledWith(cz + 'No "zoomLayer" (g tag)' + d3s);
+ expect(zoomer).toBeNull();
+ });
+
+ it('should complain if we miss required options', function () {
+ spyOn($log, 'error');
+
+ zoomer = zs.createZoomer({});
+ expect($log.error).toHaveBeenCalledWith(cz + 'No "svg" (svg tag)' + d3s);
+ expect($log.error).toHaveBeenCalledWith(cz + 'No "zoomLayer" (g tag)' + d3s);
+ expect(zoomer).toBeNull();
+ });
+
+ it('should work with minimal parameters', function () {
+ spyOn($log, 'error');
+
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer
+ });
+ expect($log.error).not.toHaveBeenCalled();
+ verifyZoomerApi();
+ });
+
+ it('should start at scale 1 and translate 0,0', function () {
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer
+ });
+ verifyZoomerApi();
+ expect(zoomer.translate()).toEqual([0,0]);
+ expect(zoomer.scale()).toEqual(1);
+ });
+
+ it('should allow programmatic pan/zoom', function () {
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer
+ });
+ verifyZoomerApi();
+ expect(zoomer.translate()).toEqual([0,0]);
+ expect(zoomer.scale()).toEqual(1);
+
+ zoomer.panZoom([20,30], 3);
+ expect(zoomer.translate()).toEqual([20,30]);
+ expect(zoomer.scale()).toEqual(3);
+ });
+
+ it('should provide default scale extent', function () {
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer
+ });
+ expect(zoomer.scaleExtent()).toEqual([0.25, 10]);
+ });
+
+ it('should allow us to override the minimum zoom', function () {
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer,
+ zoomMin: 1.23
+ });
+ expect(zoomer.scaleExtent()).toEqual([1.23, 10]);
+ });
+
+ it('should allow us to override the maximum zoom', function () {
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer,
+ zoomMax: 13
+ });
+ expect(zoomer.scaleExtent()).toEqual([0.25, 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 () {
+ var foo = { cb: function () {} };
+ spyOn(foo, 'cb');
+
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer,
+ zoomCallback: foo.cb
+ });
+
+ zoomer.panZoom([0,0], 2);
+ expect(foo.cb).toHaveBeenCalled();
+ });
+
});