topoLink.js 9.37 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 Link Module.
 Functions for highlighting/selecting links
 */

(function () {
    'use strict';

    // injected refs
    var $log, fs, sus, ts, flash, tss, tps;

    // internal state
    var api,
        td3,
        network,
        showPorts = true,       // enable port highlighting by default
        enhancedLink = null,    // the link over which the mouse is hovering
        selectedLink = null;    // the link which is currently selected

    // SVG elements;
    var svg;


    // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ========

    function getLogicalMousePosition(container) {
        var m = d3.mouse(container),
            sc = api.zoomer.scale(),
            tr = api.zoomer.translate(),
            mx = (m[0] - tr[0]) / sc,
            my = (m[1] - tr[1]) / sc;
        return {x: mx, y: my};
    }


    function sq(x) { return x * x; }

    function mdist(p, m) {
        return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
    }

    function prox(dist) {
        return dist / api.zoomer.scale();
    }

    function computeNearestNode(mouse) {
        var proximity = prox(30),
            nearest = null,
            minDist;

        if (network.nodes.length) {
            minDist = proximity * 2;

            network.nodes.forEach(function (d) {
                var dist;

                if (!api.showHosts() && d.class === 'host') {
                    return; // skip hidden hosts
                }

                dist = mdist({x: d.x, y: d.y}, mouse);
                if (dist < minDist && dist < proximity) {
                    minDist = dist;
                    nearest = d;
                }
            });
        }
        return nearest;
    }


    function computeNearestLink(mouse) {
        var proximity = prox(30),
            nearest = null,
            minDist;

        function pdrop(line, mouse) {
            var x1 = line.x1,
                y1 = line.y1,
                x2 = line.x2,
                y2 = line.y2,
                x3 = mouse.x,
                y3 = mouse.y,
                k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) /
                    (sq(y2-y1) + sq(x2-x1)),
                x4 = x3 - k * (y2-y1),
                y4 = y3 + k * (x2-x1);
            return {x:x4, y:y4};
        }

        // FIXME: x and y position calculated here, use link.position
        function lineSeg(d) {
            return {
                x1: d.source.x,
                y1: d.source.y,
                x2: d.target.x,
                y2: d.target.y
            };
        }

        function lineHit(line, p, m) {
            if (p.x < line.x1 && p.x < line.x2) return false;
            if (p.x > line.x1 && p.x > line.x2) return false;
            if (p.y < line.y1 && p.y < line.y2) return false;
            if (p.y > line.y1 && p.y > line.y2) return false;
            // line intersects, but are we close enough?
            return mdist(p, m) <= proximity;
        }

        if (network.links.length) {
            minDist = proximity * 2;

            network.links.forEach(function (d) {
                if (!api.showHosts() && d.type() === 'hostLink') {
                    return; // skip hidden host links
                }

                var line = lineSeg(d),
                    point = pdrop(line, mouse),
                    hit = lineHit(line, point, mouse),
                    dist;

                if (hit) {
                    dist = mdist(point, mouse);
                    if (dist < minDist) {
                        minDist = dist;
                        nearest = d;
                    }
                }
            });
        }
        return nearest;
    }

    function enhanceLink(ldata) {
        // if the new link is same as old link, do nothing
        if (enhancedLink && ldata && enhancedLink.key === ldata.key) return;

        // first, unenhance the currently enhanced link
        if (enhancedLink) {
            unenhance(enhancedLink);
        }
        enhancedLink = ldata;
        if (enhancedLink) {
            enhance(enhancedLink);
        }
    }

    function unenhance(d) {
        // guard against link element not set
        if (d.el) {
            d.el.classed('enhanced', false);
        }
        api.portLabelG().selectAll('.portLabel').remove();
    }

    function enhance(d) {
        var data = [],
            point;

        // guard against link element not set
        if (!d.el) return;

        d.el.classed('enhanced', true);

        // Define port label data objects.
        // NOTE: src port is absent in the case of host-links.

        point = locatePortLabel(d);
        angular.extend(point, {
            id: 'topo-port-tgt',
            num: d.tgtPort
        });
        data.push(point);

        if (d.srcPort) {
            point = locatePortLabel(d, 1);
            angular.extend(point, {
                id: 'topo-port-src',
                num: d.srcPort
            });
            data.push(point);
        }

        td3.applyPortLabels(data, api.portLabelG());
    }

    // FIXME: x and y position calculated here somewhere
    function locatePortLabel(link, src) {
        var near = src ? 'source' : 'target',
            far = src ? 'target' : 'source',
            ln = link[near],
            lf = link[far],
            offset = 32;

        function dist(x, y) { return Math.sqrt(x*x + y*y); }

        var dx = lf.x - ln.x,
            dy = lf.y - ln.y,
            k = offset / dist(dx, dy);

        return {x: k * dx + ln.x, y: k * dy + ln.y};
    }


    function selectLink(ldata) {
        // if the new link is same as old link, do nothing
        if (selectedLink && ldata && selectedLink.key === ldata.key) return;

        // make sure no nodes are selected
        tss.deselectAll();

        // first, unenhance the currently enhanced link
        if (selectedLink) {
            unselLink(selectedLink);
        }
        selectedLink = ldata;
        if (selectedLink) {
            selLink(selectedLink);
        }
    }

    function unselLink(d) {
        // guard against link element not set
        if (d.el) {
            d.el.classed('selected', false);
        }
    }

    function selLink(d) {
        // guard against link element not set
        if (!d.el) return;

        d.el.classed('selected', true);

        tps.displayLink(d);
        tps.displaySomething();
    }

    // ====== MOUSE EVENT HANDLERS ======

    function mouseMoveHandler() {
        var mp = getLogicalMousePosition(this),
            link = computeNearestLink(mp);
        enhanceLink(link);
    }

    function mouseClickHandler() {
        var mp, link, node;

        if (!tss.clickConsumed()) {
            mp = getLogicalMousePosition(this);
            node = computeNearestNode(mp);
            if (node) {
                $log.debug('found nearest node:', node.labels[1]);
                tss.selectObject(node);
            } else {
                link = computeNearestLink(mp);
                selectLink(link);
            }
        }
    }


    // ======================

    function togglePorts(x) {
        var kev = (x === 'keyev'),
            on = kev ? !showPorts : !!x,
            what = on ? 'Enable' : 'Disable',
            handler = on ? mouseMoveHandler : null;

        showPorts = on;

        if (!on) {
            enhanceLink(null);
        }
        svg.on('mousemove', handler);
        flash.flash(what + ' port highlighting');
        return on;
    }

    function deselectLink() {
        if (selectedLink) {
            unselLink(selectedLink);
            selectedLink = null;
            return true;
        }
        return false;
    }

    // ==========================
    // Module definition

    angular.module('ovTopo')
        .factory('TopoLinkService',
        ['$log', 'FnService', 'SvgUtilService', 'ThemeService', 'FlashService',
            'TopoSelectService', 'TopoPanelService',

        function (_$log_, _fs_, _sus_, _ts_, _flash_, _tss_, _tps_) {
            $log = _$log_;
            fs = _fs_;
            sus = _sus_;
            ts = _ts_;
            flash = _flash_;
            tss = _tss_;
            tps = _tps_;

            function initLink(_api_, _td3_) {
                api = _api_;
                td3 = _td3_;
                svg = api.svg;
                network = api.network;
                if (showPorts && !fs.isMobile()) {
                    svg.on('mousemove', mouseMoveHandler);
                }
                svg.on('click', mouseClickHandler);
            }

            function destroyLink() {
                // unconditionally remove any event handlers
                svg.on('mousemove', null);
                svg.on('click', null);
            }

            return {
                initLink: initLink,
                destroyLink: destroyLink,
                togglePorts: togglePorts,
                deselectLink: deselectLink
            };
        }]);
}());