GUI -- Implemented ZoomService, with unit tests.
- Added zoomer to topo.js; we are at least generating the events. - Added GlyphService.clear() Change-Id: I5400e52b58ee584866d8ffbb20d5bde70b336985
Showing
5 changed files
with
288 additions
and
16 deletions
| ... | @@ -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 | }); | ... | ... |
-
Please register or login to post a comment