topoModel.js 11.5 KB
/*
 * Copyright 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 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;

    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.lng, loc.lat]) : [0, 0];
    }

    function lngLatFromCoord(coord) {
        var p = api.projection();
        return p ? p.invert(coord) : [0, 0];
    }

    function positionNode(node, forUpdate) {
        var meta = node.metaUi,
            x = meta && meta.x,
            y = meta && meta.y,
            xy;

        // If we have [x,y] already, use that...
        if (x && y) {
            node.fixed = true;
            node.px = node.x = x;
            node.py = node.y = y;
            return;
        }

        var location = node.location,
            coord;

        if (location && location.type === 'latlng') {
            coord = coordFromLngLat(location);
            node.fixed = true;
            node.px = node.x = coord[0];
            node.py = node.y = coord[1];
            return true;
        }

        // 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 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(host) {
        var src = host.id,
            dst = host.cp.device,
            id = host.ingress,
            lnk = linkEndPoints(src, dst);

        if (!lnk) {
            return null;
        }

        // Synthesize link ...
        angular.extend(lnk, {
            key: id,
            class: 'link',

            type: function () { return 'hostLink'; },
            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,

            // functions to aggregate dual link state
            type: function () {
                var s = lnk.fromSource,
                    t = lnk.fromTarget;
                return (s && s.type) || (t && t.type) || defaultLinkType;
            },
            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 Math.max(ws, wt);
            }
        });
        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 () {
                        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 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 links = [];
        links.forEach(function (d) {
            if (d.source.id === devId || d.target.id === devId) {
                links.push(d);
            }
        });
        return links;
    }


    // ==========================
    // 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;
            }

            function newDim(_dim_) {
                dim = _dim_;
            }

            function destroyModel() { }

            return {
                initModel: initModel,
                newDim: newDim,
                destroyModel: destroyModel,

                positionNode: positionNode,
                createDeviceNode: createDeviceNode,
                createHostNode: createHostNode,
                createHostLink: createHostLink,
                createLink: createLink,
                coordFromLngLat: coordFromLngLat,
                lngLatFromCoord: lngLatFromCoord,
                findLink: findLink,
                findLinkById: findLinkById,
                findDevices: findDevices,
                findAttachedHosts: findAttachedHosts,
                findAttachedLinks: findAttachedLinks
            }
        }]);
}());