blob: 6adf1f3229462b1275bc309479c0ea540439af29 [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Foundation
*
* 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 Model Module.
Auxiliary functions for the model of the topology; that is, our internal
representations of devices, hosts, links, etc.
*/
(function () {
'use strict';
// injected refs
var $log, fs, rnd;
// api to topoForce
var api;
/*
projection()
network {...}
restyleLinkElement( ldata )
removeLinkElement( ldata )
*/
// shorthand
var lu, rlk, nodes, links, linksByDevice;
var dim; // dimensions of layout [w,h]
// configuration 'constants'
var defaultLinkType = 'direct',
nearDist = 15;
function coordFromLngLat(loc) {
var p = api.projection();
return p ? p([loc.longOrX, loc.latOrY]) : [0, 0];
}
function lngLatFromCoord(coord) {
var p = api.projection();
return p ? p.invert(coord) : [0, 0];
}
function coordFromXY(loc) {
var bgWidth = 1000,
bgHeight = 1000;
var scale = 1000 / bgWidth,
yOffset = (1000 - (bgHeight * scale)) / 2;
// 1000 is a hardcoded HTML value of the SVG element (topo2.html)
var x = scale * loc.longOrX,
y = (scale * loc.latOrY) + yOffset;
return [x, y];
}
function positionNode(node, forUpdate) {
var meta = node.metaUi,
x = meta && meta.x,
y = meta && meta.y,
xy;
// if the device contains explicit LONG/LAT data, use that to position
if (setLongLat(node)) {
// indicate we want to update cached meta data...
return true;
}
// else if we have [x,y] cached in meta data, use that...
if (x !== undefined && y !== undefined) {
node.fixed = true;
node.px = node.x = x;
node.py = node.y = y;
return;
}
// if this is a node update (not a node add).. skip randomizer
if (forUpdate) {
return;
}
// Note: Placing incoming unpinned nodes at exactly the same point
// (center of the view) causes them to explode outwards when
// the force layout kicks in. So, we spread them out a bit
// initially, to provide a more serene layout convergence.
// Additionally, if the node is a host, we place it near
// the device it is connected to.
function rand() {
return {
x: rnd.randDim(dim[0]),
y: rnd.randDim(dim[1]),
};
}
function near(node) {
return {
x: node.x + nearDist + rnd.spread(nearDist),
y: node.y + nearDist + rnd.spread(nearDist),
};
}
function getDevice(cp) {
var d = lu[cp.device];
return d || rand();
}
xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
angular.extend(node, xy);
}
function setLongLat(node) {
var loc = node.location,
coord;
if (loc) {
coord = loc.locType === 'geo' ? coordFromLngLat(loc) : coordFromXY(loc);
node.fixed = true;
node.px = node.x = coord[0];
node.py = node.y = coord[1];
return true;
}
}
function resetAllLocations() {
nodes.forEach(function (d) {
setLongLat(d);
});
}
function mkSvgCls(dh, t, on) {
var ndh = 'node ' + dh,
ndht = t ? ndh + ' ' + t : ndh;
return on ? ndht + ' online' : ndht;
}
function createDeviceNode(device) {
var node = device;
// Augment as needed...
node.class = 'device';
node.svgClass = mkSvgCls('device', device.type, device.online);
positionNode(node);
return node;
}
function createHostNode(host) {
var node = host;
// Augment as needed...
node.class = 'host';
if (!node.type) {
node.type = 'endstation';
}
node.svgClass = mkSvgCls('host', node.type);
positionNode(node);
return node;
}
function createHostLink(hostId, devId, devPort) {
var linkKey = hostId + '/0-' + devId + '/' + devPort,
lnk = linkEndPoints(hostId, devId);
if (!lnk) {
return null;
}
// Synthesize link ...
angular.extend(lnk, {
key: linkKey,
class: 'link',
// NOTE: srcPort left undefined (host end of the link)
tgtPort: devPort,
type: function () { return 'hostLink'; },
expected: function () { return true; },
online: function () {
// hostlink target is edge switch
return lnk.target.online;
},
linkWidth: function () { return 1; },
});
return lnk;
}
function createLink(link) {
var lnk = linkEndPoints(link.src, link.dst);
if (!lnk) {
return null;
}
angular.extend(lnk, {
key: link.id,
class: 'link',
fromSource: link,
srcPort: link.srcPort,
tgtPort: link.dstPort,
position: {
x1: 0,
y1: 0,
x2: 0,
y2: 0,
},
// functions to aggregate dual link state
type: function () {
var s = lnk.fromSource,
t = lnk.fromTarget;
return (s && s.type) || (t && t.type) || defaultLinkType;
},
expected: function () {
var s = lnk.fromSource,
t = lnk.fromTarget;
return (s && s.expected) && (t && t.expected);
},
online: function () {
var s = lnk.fromSource,
t = lnk.fromTarget,
both = lnk.source.online && lnk.target.online;
return both && (s && s.online) && (t && t.online);
},
linkWidth: function () {
var s = lnk.fromSource,
t = lnk.fromTarget,
ws = (s && s.linkWidth) || 0,
wt = (t && t.linkWidth) || 0;
return lnk.position.multiLink ? 5 : Math.max(ws, wt);
},
extra: link.extra,
});
return lnk;
}
function linkEndPoints(srcId, dstId) {
var srcNode = lu[srcId],
dstNode = lu[dstId],
sMiss = !srcNode ? missMsg('src', srcId) : '',
dMiss = !dstNode ? missMsg('dst', dstId) : '';
if (sMiss || dMiss) {
$log.error('Node(s) not on map for link:' + sMiss + dMiss);
// logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
return null;
}
return {
source: srcNode,
target: dstNode,
};
}
function missMsg(what, id) {
return '\n[' + what + '] "' + id + '" missing';
}
function makeNodeKey(d, what) {
var port = what + 'Port';
return d[what] + '/' + d[port];
}
function makeLinkKey(d, flipped) {
var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
return one + '-' + two;
}
function findLinkById(id) {
// check to see if this is a reverse lookup, else default to given id
var key = rlk[id] || id;
return key && lu[key];
}
function findLink(linkData, op) {
var key = makeLinkKey(linkData),
keyrev = makeLinkKey(linkData, 1),
link = lu[key],
linkRev = lu[keyrev],
result = {},
ldata = link || linkRev,
rawLink;
if (op === 'add') {
if (link) {
// trying to add a link that we already know about
result.ldata = link;
result.badLogic = 'addLink: link already added';
} else if (linkRev) {
// we found the reverse of the link to be added
result.ldata = linkRev;
if (linkRev.fromTarget) {
result.badLogic = 'addLink: link already added';
}
}
} else if (op === 'update') {
if (!ldata) {
result.badLogic = 'updateLink: link not found';
} else {
rawLink = link ? ldata.fromSource : ldata.fromTarget;
result.updateWith = function (data) {
angular.extend(rawLink, data);
api.restyleLinkElement(ldata);
};
}
} else if (op === 'remove') {
if (!ldata) {
result.badLogic = 'removeLink: link not found';
} else {
rawLink = link ? ldata.fromSource : ldata.fromTarget;
if (!rawLink) {
result.badLogic = 'removeLink: link not found';
} else {
result.removeRawLink = function () {
// remove link out of aggregate linksByDevice list
var linksForDevPair = linksByDevice[ldata.devicePair],
rmvIdx = fs.find(ldata.key, linksForDevPair, 'key');
if (rmvIdx >= 0) {
linksForDevPair.splice(rmvIdx, 1);
}
ldata.position.multilink = linksForDevPair.length >= 5;
if (link) {
// remove fromSource
ldata.fromSource = null;
if (ldata.fromTarget) {
// promote target into source position
ldata.fromSource = ldata.fromTarget;
ldata.fromTarget = null;
ldata.key = keyrev;
delete lu[key];
lu[keyrev] = ldata;
delete rlk[keyrev];
}
} else {
// remove fromTarget
ldata.fromTarget = null;
delete rlk[keyrev];
}
if (ldata.fromSource) {
api.restyleLinkElement(ldata);
} else {
api.removeLinkElement(ldata);
}
};
}
}
}
return result;
}
function findDevices(offlineOnly) {
var a = [];
nodes.forEach(function (d) {
if (d.class === 'device' && !(offlineOnly && d.online)) {
a.push(d);
}
});
return a;
}
function findHosts() {
var hosts = [];
nodes.forEach(function (d) {
if (d.class === 'host') {
hosts.push(d);
}
});
return hosts;
}
function findAttachedHosts(devId) {
var hosts = [];
nodes.forEach(function (d) {
if (d.class === 'host' && d.cp.device === devId) {
hosts.push(d);
}
});
return hosts;
}
function findAttachedLinks(devId) {
var lnks = [];
links.forEach(function (d) {
if (d.source.id === devId || d.target.id === devId) {
lnks.push(d);
}
});
return lnks;
}
// returns one-way links or where the internal link types differ
function findBadLinks() {
var lnks = [],
src, tgt;
links.forEach(function (d) {
// NOTE: skip edge links, which are synthesized
if (d.type() !== 'hostLink') {
delete d.bad;
src = d.fromSource;
tgt = d.fromTarget;
if (src && !tgt) {
d.bad = 'missing link';
} else if (src.type !== tgt.type) {
d.bad = 'type mismatch';
}
if (d.bad) {
lnks.push(d);
}
}
});
return lnks;
}
// ==========================
// Module definition
angular.module('ovTopo')
.factory('TopoModelService',
['$log', 'FnService', 'RandomService',
function (_$log_, _fs_, _rnd_) {
$log = _$log_;
fs = _fs_;
rnd = _rnd_;
function initModel(_api_, _dim_) {
api = _api_;
dim = _dim_;
lu = api.network.lookup;
rlk = api.network.revLinkToKey;
nodes = api.network.nodes;
links = api.network.links;
linksByDevice = api.network.linksByDevice;
}
function newDim(_dim_) {
dim = _dim_;
}
function destroyModel() { }
return {
initModel: initModel,
newDim: newDim,
destroyModel: destroyModel,
positionNode: positionNode,
resetAllLocations: resetAllLocations,
createDeviceNode: createDeviceNode,
createHostNode: createHostNode,
createHostLink: createHostLink,
createLink: createLink,
coordFromLngLat: coordFromLngLat,
lngLatFromCoord: lngLatFromCoord,
findLink: findLink,
findLinkById: findLinkById,
findDevices: findDevices,
findHosts: findHosts,
findAttachedHosts: findAttachedHosts,
findAttachedLinks: findAttachedLinks,
findBadLinks: findBadLinks,
};
}]);
}());