Simon Hunt

GUI -- Implemented ZoomService, with unit tests.

- Added zoomer to topo.js; we are at least generating the events.
- Added GlyphService.clear()

Change-Id: I5400e52b58ee584866d8ffbb20d5bde70b336985
...@@ -124,9 +124,13 @@ ...@@ -124,9 +124,13 @@
124 .factory('GlyphService', ['$log', function (_$log_) { 124 .factory('GlyphService', ['$log', function (_$log_) {
125 $log = _$log_; 125 $log = _$log_;
126 126
127 - function init() { 127 + function clear() {
128 // start with a fresh map 128 // start with a fresh map
129 glyphs = d3.map(); 129 glyphs = d3.map();
130 + }
131 +
132 + function init() {
133 + clear();
130 register(birdViewBox, birdData); 134 register(birdViewBox, birdData);
131 register(glyphViewBox, glyphData); 135 register(glyphViewBox, glyphData);
132 register(badgeViewBox, badgeData); 136 register(badgeViewBox, badgeData);
...@@ -175,6 +179,7 @@ ...@@ -175,6 +179,7 @@
175 } 179 }
176 180
177 return { 181 return {
182 + clear: clear,
178 init: init, 183 init: init,
179 register: register, 184 register: register,
180 ids: ids, 185 ids: ids,
......
...@@ -22,14 +22,112 @@ ...@@ -22,14 +22,112 @@
22 (function () { 22 (function () {
23 'use strict'; 23 'use strict';
24 24
25 + // configuration
26 + var defaultSettings = {
27 + zoomMin: 0.25,
28 + zoomMax: 10,
29 + zoomEnabled: function (ev) { return true; },
30 + zoomCallback: function () {}
31 + };
32 +
33 + // injected references to services
25 var $log; 34 var $log;
26 35
27 angular.module('onosSvg') 36 angular.module('onosSvg')
28 - .factory('ZoomService', ['$log', function (_$log_) { 37 + .factory('ZoomService', ['$log',
38 +
39 + function (_$log_) {
29 $log = _$log_; 40 $log = _$log_;
30 41
42 +/*
43 + NOTE: opts is an object:
44 + {
45 + svg: svg, D3 selection of <svg> element
46 + zoomLayer: zoomLayer, D3 selection of <g> element
47 + zoomEnabled: function (ev) { ... },
48 + zoomCallback: function () { ... }
49 + }
50 +
51 + where:
52 + * svg and zoomLayer should be D3 selections of DOM elements.
53 + * zoomLayer <g> is a child of <svg> element.
54 + * zoomEnabled is an optional predicate based on D3 event.
55 + * default is always enabled.
56 + * zoomCallback is an optional callback invoked each time we pan/zoom.
57 + * default is do nothing.
58 +
59 + Optionally, zoomMin and zoomMax also can be defined.
60 + These default to 0.25 and 10 respectively.
61 +*/
62 + function createZoomer(opts) {
63 + var cz = 'ZoomService.createZoomer(): ',
64 + d3s = ' (D3 selection) property defined',
65 + settings = $.extend({}, defaultSettings, opts),
66 + zoom = d3.behavior.zoom()
67 + .translate([0, 0])
68 + .scale(1)
69 + .scaleExtent([settings.zoomMin, settings.zoomMax])
70 + .on('zoom', zoomed),
71 + fail = false,
72 + zoomer;
73 +
74 + if (!settings.svg) {
75 + $log.error(cz + 'No "svg" (svg tag)' + d3s);
76 + fail = true;
77 + }
78 + if (!settings.zoomLayer) {
79 + $log.error(cz + 'No "zoomLayer" (g tag)' + d3s);
80 + fail = true;
81 + }
82 +
83 + if (fail) {
84 + return null;
85 + }
86 +
87 + // zoom events from mouse gestures...
88 + function zoomed() {
89 + var ev = d3.event.sourceEvent;
90 + if (settings.zoomEnabled(ev)) {
91 + adjustZoomLayer(d3.event.translate, d3.event.scale);
92 + }
93 + }
94 +
95 + function adjustZoomLayer(translate, scale) {
96 + settings.zoomLayer.attr('transform',
97 + 'translate(' + translate + ')scale(' + scale + ')');
98 + settings.zoomCallback();
99 + }
100 +
101 + zoomer = {
102 + panZoom: function (translate, scale) {
103 + zoom.translate(translate).scale(scale);
104 + adjustZoomLayer(translate, scale);
105 + },
106 +
107 + reset: function () {
108 + zoomer.panZoom([0,0], 1);
109 + },
110 +
111 + translate: function () {
112 + return zoom.translate();
113 + },
114 +
115 + scale: function () {
116 + return zoom.scale();
117 + },
118 +
119 + scaleExtent: function () {
120 + return zoom.scaleExtent();
121 + }
122 + };
123 +
124 + // apply the zoom behavior to the SVG element
125 + settings.svg && settings.svg.call(zoom);
126 + return zoomer;
127 + }
128 +
31 return { 129 return {
32 - tbd: function () {} 130 + createZoomer: createZoomer
33 }; 131 };
34 }]); 132 }]);
35 133
......
...@@ -29,19 +29,22 @@ ...@@ -29,19 +29,22 @@
29 ]; 29 ];
30 30
31 // references to injected services etc. 31 // references to injected services etc.
32 - var $log, ks, gs; 32 + var $log, ks, zs, gs;
33 33
34 // DOM elements 34 // DOM elements
35 - var defs; 35 + var svg, defs;
36 36
37 // Internal state 37 // Internal state
38 - // ... 38 + var zoomer;
39 39
40 // Note: "exported" state should be properties on 'self' variable 40 // Note: "exported" state should be properties on 'self' variable
41 41
42 + // --- Short Cut Keys ------------------------------------------------
43 +
42 var keyBindings = { 44 var keyBindings = {
43 - W: [logWarning, 'log a warning'], 45 + W: [logWarning, '(temp) log a warning'],
44 - E: [logError, 'log an error'] 46 + E: [logError, '(temp) log an error'],
47 + R: [resetZoom, 'Reset pan / zoom']
45 }; 48 };
46 49
47 // ----------------- 50 // -----------------
...@@ -54,32 +57,72 @@ ...@@ -54,32 +57,72 @@
54 } 57 }
55 // ----------------- 58 // -----------------
56 59
60 + function resetZoom() {
61 + zoomer.reset();
62 + }
63 +
57 function setUpKeys() { 64 function setUpKeys() {
58 ks.keyBindings(keyBindings); 65 ks.keyBindings(keyBindings);
59 } 66 }
60 67
68 +
69 + // --- Glyphs, Icons, and the like -----------------------------------
70 +
61 function setUpDefs() { 71 function setUpDefs() {
62 - defs = d3.select('#ov-topo svg').append('defs'); 72 + defs = svg.append('defs');
63 gs.loadDefs(defs); 73 gs.loadDefs(defs);
64 } 74 }
65 75
66 76
77 + // --- Pan and Zoom --------------------------------------------------
78 +
79 + // zoom enabled predicate. ev is a D3 source event.
80 + function zoomEnabled(ev) {
81 + return (ev.metaKey || ev.altKey);
82 + }
83 +
84 + function zoomCallback() {
85 + var tr = zoomer.translate(),
86 + sc = zoomer.scale();
87 + $log.log('ZOOM: translate = ' + tr + ', scale = ' + sc);
88 +
89 + // TODO: keep the map lines constant width while zooming
90 + //bgImg.style('stroke-width', 2.0 / scale + 'px');
91 + }
92 +
93 + function setUpZoom() {
94 + var zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
95 + zoomer = zs.createZoomer({
96 + svg: svg,
97 + zoomLayer: zoomLayer,
98 + zoomEnabled: zoomEnabled,
99 + zoomCallback: zoomCallback
100 + });
101 + }
102 +
103 +
104 + // --- Controller Definition -----------------------------------------
105 +
67 angular.module('ovTopo', moduleDependencies) 106 angular.module('ovTopo', moduleDependencies)
68 107
69 .controller('OvTopoCtrl', [ 108 .controller('OvTopoCtrl', [
70 - '$log', 'KeyService', 'GlyphService', 109 + '$log', 'KeyService', 'ZoomService', 'GlyphService',
71 110
72 - function (_$log_, _ks_, _gs_) { 111 + function (_$log_, _ks_, _zs_, _gs_) {
73 var self = this; 112 var self = this;
74 -
75 $log = _$log_; 113 $log = _$log_;
76 ks = _ks_; 114 ks = _ks_;
115 + zs = _zs_;
77 gs = _gs_; 116 gs = _gs_;
78 117
118 + // exported state
79 self.message = 'Topo View Rocks!'; 119 self.message = 'Topo View Rocks!';
80 120
121 + // svg layer and initialization of components
122 + svg = d3.select('#ov-topo svg');
81 setUpKeys(); 123 setUpKeys();
82 setUpDefs(); 124 setUpDefs();
125 + setUpZoom();
83 126
84 $log.log('OvTopoCtrl has been created'); 127 $log.log('OvTopoCtrl has been created');
85 }]); 128 }]);
......
...@@ -38,6 +38,7 @@ describe('factory: fw/svg/glyph.js', function() { ...@@ -38,6 +38,7 @@ describe('factory: fw/svg/glyph.js', function() {
38 38
39 afterEach(function () { 39 afterEach(function () {
40 d3.select('#myDefs').remove(); 40 d3.select('#myDefs').remove();
41 + gs.clear();
41 }); 42 });
42 43
43 it('should define GlyphService', function () { 44 it('should define GlyphService', function () {
...@@ -59,6 +60,13 @@ describe('factory: fw/svg/glyph.js', function() { ...@@ -59,6 +60,13 @@ describe('factory: fw/svg/glyph.js', function() {
59 expect(gs.ids().length).toEqual(numBaseGlyphs); 60 expect(gs.ids().length).toEqual(numBaseGlyphs);
60 }); 61 });
61 62
63 + it('should remove glyphs on clear', function () {
64 + gs.init();
65 + expect(gs.ids().length).toEqual(numBaseGlyphs);
66 + gs.clear();
67 + expect(gs.ids().length).toEqual(0);
68 + });
69 +
62 function verifyGlyphLoaded(id, vbox, prefix) { 70 function verifyGlyphLoaded(id, vbox, prefix) {
63 var glyph = gs.glyph(id), 71 var glyph = gs.glyph(id),
64 plen = prefix.length; 72 plen = prefix.length;
......
...@@ -20,17 +20,135 @@ ...@@ -20,17 +20,135 @@
20 @author Simon Hunt 20 @author Simon Hunt
21 */ 21 */
22 describe('factory: fw/svg/zoom.js', function() { 22 describe('factory: fw/svg/zoom.js', function() {
23 - var zs; 23 + var $log, fs, zs, svg, zoomLayer, zoomer;
24 24
25 - beforeEach(module('onosSvg')); 25 + var cz = 'ZoomService.createZoomer(): ',
26 + d3s = ' (D3 selection) property defined';
26 27
27 - beforeEach(inject(function (ZoomService) { 28 + beforeEach(module('onosUtil', 'onosSvg'));
29 +
30 + beforeEach(inject(function (_$log_, FnService, ZoomService) {
31 + $log = _$log_;
32 + fs = FnService;
28 zs = ZoomService; 33 zs = ZoomService;
34 + svg = d3.select('body').append('svg').attr('id', 'mySvg');
35 + zoomLayer = svg.append('g').attr('id', 'myZoomlayer');
29 })); 36 }));
30 37
38 + afterEach(function () {
39 + d3.select('#mySvg').remove();
40 + // Note: since zoomLayer is a child of svg, it should be removed also
41 + });
42 +
31 it('should define ZoomService', function () { 43 it('should define ZoomService', function () {
32 expect(zs).toBeDefined(); 44 expect(zs).toBeDefined();
33 }); 45 });
34 46
35 - // TODO: unit tests for map functions 47 + it('should define api functions', function () {
48 + expect(fs.areFunctions(zs, ['createZoomer'])).toBeTruthy();
49 + });
50 +
51 + function verifyZoomerApi() {
52 + expect(fs.areFunctions(zoomer, [
53 + 'panZoom', 'reset', 'translate', 'scale'
54 + ])).toBeTruthy();
55 + }
56 +
57 + it('should fail gracefully with no option object', function () {
58 + spyOn($log, 'error');
59 +
60 + zoomer = zs.createZoomer();
61 + expect($log.error).toHaveBeenCalledWith(cz + 'No "svg" (svg tag)' + d3s);
62 + expect($log.error).toHaveBeenCalledWith(cz + 'No "zoomLayer" (g tag)' + d3s);
63 + expect(zoomer).toBeNull();
64 + });
65 +
66 + it('should complain if we miss required options', function () {
67 + spyOn($log, 'error');
68 +
69 + zoomer = zs.createZoomer({});
70 + expect($log.error).toHaveBeenCalledWith(cz + 'No "svg" (svg tag)' + d3s);
71 + expect($log.error).toHaveBeenCalledWith(cz + 'No "zoomLayer" (g tag)' + d3s);
72 + expect(zoomer).toBeNull();
73 + });
74 +
75 + it('should work with minimal parameters', function () {
76 + spyOn($log, 'error');
77 +
78 + zoomer = zs.createZoomer({
79 + svg: svg,
80 + zoomLayer: zoomLayer
81 + });
82 + expect($log.error).not.toHaveBeenCalled();
83 + verifyZoomerApi();
84 + });
85 +
86 + it('should start at scale 1 and translate 0,0', function () {
87 + zoomer = zs.createZoomer({
88 + svg: svg,
89 + zoomLayer: zoomLayer
90 + });
91 + verifyZoomerApi();
92 + expect(zoomer.translate()).toEqual([0,0]);
93 + expect(zoomer.scale()).toEqual(1);
94 + });
95 +
96 + it('should allow programmatic pan/zoom', function () {
97 + zoomer = zs.createZoomer({
98 + svg: svg,
99 + zoomLayer: zoomLayer
100 + });
101 + verifyZoomerApi();
102 + expect(zoomer.translate()).toEqual([0,0]);
103 + expect(zoomer.scale()).toEqual(1);
104 +
105 + zoomer.panZoom([20,30], 3);
106 + expect(zoomer.translate()).toEqual([20,30]);
107 + expect(zoomer.scale()).toEqual(3);
108 + });
109 +
110 + it('should provide default scale extent', function () {
111 + zoomer = zs.createZoomer({
112 + svg: svg,
113 + zoomLayer: zoomLayer
114 + });
115 + expect(zoomer.scaleExtent()).toEqual([0.25, 10]);
116 + });
117 +
118 + it('should allow us to override the minimum zoom', function () {
119 + zoomer = zs.createZoomer({
120 + svg: svg,
121 + zoomLayer: zoomLayer,
122 + zoomMin: 1.23
123 + });
124 + expect(zoomer.scaleExtent()).toEqual([1.23, 10]);
125 + });
126 +
127 + it('should allow us to override the maximum zoom', function () {
128 + zoomer = zs.createZoomer({
129 + svg: svg,
130 + zoomLayer: zoomLayer,
131 + zoomMax: 13
132 + });
133 + expect(zoomer.scaleExtent()).toEqual([0.25, 13]);
134 + });
135 +
136 + // TODO: test zoomed() where we fake out the d3.event.sourceEvent etc...
137 + // need to check default enabled (true) and custom enabled predicate
138 + // need to check that the callback is invoked also
139 +
140 + it('should invoke the callback on programmatic pan/zoom', function () {
141 + var foo = { cb: function () {} };
142 + spyOn(foo, 'cb');
143 +
144 + zoomer = zs.createZoomer({
145 + svg: svg,
146 + zoomLayer: zoomLayer,
147 + zoomCallback: foo.cb
148 + });
149 +
150 + zoomer.panZoom([0,0], 2);
151 + expect(foo.cb).toHaveBeenCalled();
152 + });
153 +
36 }); 154 });
......