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 @@
.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,
......
......@@ -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
};
}]);
......
......@@ -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');
}]);
......
......@@ -38,6 +38,7 @@ describe('factory: fw/svg/glyph.js', function() {
afterEach(function () {
d3.select('#myDefs').remove();
gs.clear();
});
it('should define GlyphService', function () {
......@@ -59,6 +60,13 @@ describe('factory: fw/svg/glyph.js', function() {
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;
......
......@@ -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();
});
});
......