/* * Copyright 2014,2015 Open Networking Laboratory * * 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. */ /* ONOS GUI -- Topology View Module */ (function () { 'use strict'; var moduleDependencies = [ 'ngCookies', 'onosUtil', 'onosSvg', 'onosRemote' ]; // references to injected services etc. var $scope, $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps, tds, tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, ttip, tov; // DOM elements var ovtopo, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer; // Internal state var zoomer, actionMap; // --- Short Cut Keys ------------------------------------------------ function setUpKeys(overlayKeys) { // key bindings need to be made after the services have been injected // thus, deferred to here... actionMap = { I: [toggleInstances, 'Toggle ONOS instances panel'], O: [toggleSummary, 'Toggle ONOS summary panel'], D: [toggleUseDetailsFlag, 'Disable / enable details panel'], H: [toggleHosts, 'Toggle host visibility'], M: [toggleOffline, 'Toggle offline visibility'], P: [togglePorts, 'Toggle Port Highlighting'], dash: [tfs.showBadLinks, 'Show bad links'], B: [toggleMap, 'Toggle background map'], S: [toggleSprites, 'Toggle sprite layer'], //X: [toggleNodeLock, 'Lock / unlock node positions'], Z: [tos.toggleOblique, 'Toggle oblique view (Experimental)'], N: [fltr.clickAction, 'Cycle node layers'], L: [tfs.cycleDeviceLabels, 'Cycle device labels'], U: [tfs.unpin, 'Unpin node (hover mouse over)'], R: [resetZoom, 'Reset pan / zoom'], dot: [ttbs.toggleToolbar, 'Toggle Toolbar'], E: [equalizeMasters, 'Equalize mastership roles'], esc: handleEscape, _keyListener: ttbs.keyListener, _helpFormat: [ ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B', 'S' ], ['X', 'Z', 'N', 'L', 'U', 'R', '-', 'E', '-', 'dot'], [] // this column reserved for overlay actions ] }; if (fs.isO(overlayKeys)) { mergeKeys(overlayKeys); } ks.keyBindings(actionMap); ks.gestureNotes([ ['click', 'Select the item and show details'], ['shift-click', 'Toggle selection state'], ['drag', 'Reposition (and pin) device / host'], ['cmd-scroll', 'Zoom in / out'], ['cmd-drag', 'Pan'] ]); } // when a topology overlay is activated, we need to bind their keystrokes // and include them in the quick-help panel function mergeKeys(extra) { var _hf = actionMap._helpFormat[2]; ks.checkNotGlobal(extra); extra._keyOrder.forEach(function (k) { var d = extra[k], cb = d && d.cb, tt = d && d.tt; // NOTE: ignore keys that are already defined if (d && !actionMap[k]) { actionMap[k] = [cb, tt]; _hf.push(k); } }); } // --- Keystroke functions ------------------------------------------- function toggleInstances(x) { updatePrefsState('insts', tis.toggle(x)); tfs.updateDeviceColors(); } function toggleSummary(x) { updatePrefsState('summary', tps.toggleSummary(x)); } function toggleUseDetailsFlag(x) { updatePrefsState('detail', tps.toggleUseDetailsFlag(x)); } function toggleHosts(x) { updatePrefsState('hosts', tfs.toggleHosts(x)); } function toggleOffline(x) { updatePrefsState('offdev', tfs.toggleOffline(x)); } function togglePorts(x) { updatePrefsState('porthl', tfs.togglePorts(x)); } function _togSvgLayer(x, G, tag, what) { var on = (x === 'keyev') ? !sus.visible(G) : !!x, verb = on ? 'Show' : 'Hide'; sus.visible(G, on); updatePrefsState(tag, on); flash.flash(verb + ' ' + what); } function toggleMap(x) { _togSvgLayer(x, mapG, 'bg', 'background map'); } function toggleSprites(x) { _togSvgLayer(x, spriteG, 'spr', 'sprite layer'); } function resetZoom() { zoomer.reset(); flash.flash('Pan and zoom reset'); } function equalizeMasters() { wss.sendEvent('equalizeMasters'); flash.flash('Equalizing master roles'); } function handleEscape() { if (tis.showMaster()) { // if an instance is selected, cancel the affinity mapping tis.cancelAffinity() } else if (tov.hooks.escape()) { // else if the overlay consumed the ESC event... // (work already done) } else if (tss.deselectAll()) { // else if we have node selections, deselect them all // (work already done) } else if (tls.deselectLink()) { // else if we have a link selected, deselect it // (work already done) } else if (tis.isVisible()) { // else if the Instance Panel is visible, hide it tis.hide(); tfs.updateDeviceColors(); } else if (tps.summaryVisible()) { // else if the Summary Panel is visible, hide it tps.hideSummaryPanel(); } } // --- Toolbar Functions --------------------------------------------- function notValid(what) { $log.warn('topo.js getActionEntry(): Not a valid ' + what); } function getActionEntry(key) { var entry; if (!key) { notValid('key'); return null; } entry = actionMap[key]; if (!entry) { notValid('actionMap entry'); return null; } return fs.isA(entry) || [entry, '']; } function setUpToolbar() { ttbs.init({ getActionEntry: getActionEntry, setUpKeys: setUpKeys }); ttbs.createToolbar(); } // --- Glyphs, Icons, and the like ----------------------------------- function setUpDefs() { defs = svg.append('defs'); gs.loadDefs(defs); sus.loadGlowDefs(defs); } // --- Pan and Zoom -------------------------------------------------- // zoom enabled predicate. ev is a D3 source event. function zoomEnabled(ev) { return fs.isMobile() || (ev.metaKey || ev.altKey); } function zoomCallback() { var sc = zoomer.scale(), tr = zoomer.translate(); ps.setPrefs('topo_zoom', {tx:tr[0], ty:tr[1], sc:sc}); // keep the map lines constant width while zooming mapG.style('stroke-width', (2.0 / sc) + 'px'); } function setUpZoom() { zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer'); zoomer = zs.createZoomer({ svg: svg, zoomLayer: zoomLayer, zoomEnabled: zoomEnabled, zoomCallback: zoomCallback }); } // callback invoked when the SVG view has been resized.. function svgResized(s) { tfs.newDim([s.width, s.height]); } // --- Background Map ------------------------------------------------ function setUpNoDevs() { var g, box; noDevsLayer = svg.append('g').attr({ id: 'topo-noDevsLayer', transform: sus.translate(500,500) }); // Note, SVG viewbox is '0 0 1000 1000', defined in topo.html. // We are translating this layer to have its origin at the center g = noDevsLayer.append('g'); gs.addGlyph(g, 'bird', 100).attr('class', 'noDevsBird'); g.append('text').text('No devices are connected') .attr({ x: 120, y: 80}); box = g.node().getBBox(); box.x -= box.width/2; box.y -= box.height/2; g.attr('transform', sus.translate(box.x, box.y)); showNoDevs(true); } function showNoDevs(b) { sus.visible(noDevsLayer, b); } var countryFilters = { world: function (c) { return c.properties.continent !== 'Antarctica'; }, // NOTE: for "usa" we are using our hand-crafted topojson file s_america: function (c) { return c.properties.continent === 'South America'; }, ns_america: function (c) { return c.properties.custom === 'US-cont' || c.properties.subregion === 'Central America' || c.properties.continent === 'South America'; }, japan: function (c) { return c.properties.geounit === 'Japan'; }, europe: function (c) { return c.properties.continent === 'Europe'; }, italy: function (c) { return c.properties.geounit === 'Italy'; }, uk: function (c) { // technically, Ireland is not part of the United Kingdom, // but the map looks weird without it showing. return c.properties.adm0_a3 === 'GBR' || c.properties.adm0_a3 === 'IRL'; }, s_korea: function (c) { return c.properties.adm0_a3 === 'KOR'; }, australia: function (c) { return c.properties.adm0_a3 === 'AUS'; } }; function setUpMap($loc) { var s1 = $loc.search().mapid, s2 = ps.getPrefs('topo_mapid'), mapId = s1 || (s2 && s2.id) || 'usa', promise, cfilter, opts; mapG = zoomLayer.append('g').attr('id', 'topo-map'); if (mapId === 'usa') { promise = ms.loadMapInto(mapG, '*continental_us'); } else { ps.setPrefs('topo_mapid', {id:mapId}); cfilter = countryFilters[mapId] || countryFilters.world; opts = { countryFilter: cfilter }; promise = ms.loadMapRegionInto(mapG, opts); } return promise; } function opacifyMap(b) { mapG.transition() .duration(1000) .attr('opacity', b ? 1 : 0); } function setUpSprites($loc, tspr) { var s1 = $loc.search().sprites, s2 = ps.getPrefs('topo_sprites'), sprId = s1 || (s2 && s2.id); spriteG = zoomLayer.append ('g').attr('id', 'topo-sprites'); if (sprId) { ps.setPrefs('topo_sprites', {id:sprId}); tspr.loadSprites(spriteG, defs, sprId); } } // --- User Preferemces ---------------------------------------------- var prefsState = {}; function updatePrefsState(what, b) { prefsState[what] = b ? 1 : 0; ps.setPrefs('topo_prefs', prefsState); } function restoreConfigFromPrefs() { // NOTE: toolbar will have set this for us.. prefsState = ps.asNumbers(ps.getPrefs('topo_prefs')); $log.debug('TOPO- Prefs State:', prefsState); flash.enable(false); toggleInstances(prefsState.insts); toggleSummary(prefsState.summary); toggleUseDetailsFlag(prefsState.detail); toggleHosts(prefsState.hosts); toggleOffline(prefsState.offdev); togglePorts(prefsState.porthl); toggleMap(prefsState.bg); toggleSprites(prefsState.spr); flash.enable(true); } // somewhat hackish, because summary update cannot happen until we // have opened the websocket to the server; hence this extra function // invoked after tes.start() function restoreSummaryFromPrefs() { prefsState = ps.asNumbers(ps.getPrefs('topo_prefs')); $log.debug('TOPO- Prefs SUMMARY State:', prefsState.summary); flash.enable(false); toggleSummary(prefsState.summary); flash.enable(true); } function topoStartDone() { var d = $scope.intentData; if (d) { tts.selectIntent(d); } } // --- Controller Definition ----------------------------------------- angular.module('ovTopo', moduleDependencies) .controller('OvTopoCtrl', ['$scope', '$log', '$location', '$timeout', '$cookies', 'FnService', 'MastService', 'KeyService', 'ZoomService', 'GlyphService', 'MapService', 'SvgUtilService', 'FlashService', 'WebSocketService', 'PrefsService', 'TopoDialogService', 'TopoEventService', 'TopoForceService', 'TopoPanelService', 'TopoInstService', 'TopoSelectService', 'TopoLinkService', 'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService', 'TopoToolbarService', 'TopoSpriteService', 'TooltipService', 'TopoOverlayService', function (_$scope_, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_, _zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _tds_, _tes_, _tfs_, _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_, _ttbs_, tspr, _ttip_, _tov_) { var params = $loc.search(), projection, dim, uplink = { // provides function calls back into this space showNoDevs: showNoDevs, projection: function () { return projection; }, zoomLayer: function () { return zoomLayer; }, zoomer: function () { return zoomer; }, opacifyMap: opacifyMap, topoStartDone: topoStartDone }; $scope = _$scope_; $log = _$log_; $cookies = _$cookies_; fs = _fs_; ks = _ks_; zs = _zs_; gs = _gs_; ms = _ms_; sus = _sus_; flash = _flash_; wss = _wss_; ps = _ps_; tds = _tds_; tes = _tes_; tfs = _tfs_; // TODO: consider funnelling actions through TopoForceService... // rather than injecting references to these 'sub-modules', // just so we can invoke functions on them. tps = _tps_; tis = _tis_; tss = _tss_; tls = _tls_; tts = _tts_; tos = _tos_; fltr = _fltr_; ttbs = _ttbs_; ttip = _ttip_; tov = _tov_; if (params.intentKey && params.intentAppId && params.intentAppName) { $scope.intentData = { key: params.intentKey, appId: params.intentAppId, appName: params.intentAppName }; } $scope.notifyResize = function () { svgResized(fs.windowSize(mast.mastHeight())); }; // Cleanup on destroyed scope.. $scope.$on('$destroy', function () { $log.log('OvTopoCtrl is saying Buh-Bye!'); tes.stop(); ks.unbindKeys(); tps.destroyPanels(); tds.closeDialog(); tis.destroyInst(); tfs.destroyForce(); ttbs.destroyToolbar(); }); // svg layer and initialization of components ovtopo = d3.select('#ov-topo'); svg = ovtopo.select('svg'); // set the svg size to match that of the window, less the masthead svg.attr(fs.windowSize(mast.mastHeight())); dim = [svg.attr('width'), svg.attr('height')]; setUpKeys(); setUpToolbar(); setUpDefs(); setUpZoom(); setUpNoDevs(); setUpMap($loc).then( function (proj) { var z = ps.getPrefs('topo_zoom') || {tx:0, ty:0, sc:1}; zoomer.panZoom([z.tx, z.ty], z.sc); $log.debug('** Zoom restored:', z); projection = proj; $log.debug('** We installed the projection:', proj); flash.enable(false); toggleMap(prefsState.bg); flash.enable(true); // now we have the map projection, we are ready for // the server to send us device/host data... tes.start(); // need to do the following so we immediately get // the summary panel data back from the server restoreSummaryFromPrefs(); } ); setUpSprites($loc, tspr); forceG = zoomLayer.append('g').attr('id', 'topo-force'); tfs.initForce(svg, forceG, uplink, dim); tis.initInst({ showMastership: tfs.showMastership }); tps.initPanels(); // temporary solution for persisting user settings restoreConfigFromPrefs(); ttbs.setDefaultOverlay(); $log.debug('registered overlays...', tov.list()); $log.log('OvTopoCtrl has been created'); }]); }());