text: /*
   * 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 D3 Module.
   Functions for manipulating the D3 visualizations of the Topology
   */
  
  (function () {
      'use strict';
  
      // injected refs
      var $log, fs, sus, is, ts, ps, ttbs;
  
      // api to topoForce
      var zoomer, api;
      /*
       node()                 // get ref to D3 selection of nodes
       link()                 // get ref to D3 selection of links
       linkLabel()            // get ref to D3 selection of link labels
       instVisible()          // true if instances panel is visible
       posNode()              // position node
       showHosts()            // true if hosts are to be shown
       restyleLinkElement()   // update link styles based on backing data
       updateLinkLabelModel() // update backing data for link labels
       */
  
      // configuration
      var devIconDim = 36,
          devColorDim = 32,
          labelPad = 4,
          hostRadius = 14,
          badgeConfig = {
              radius: 12,
              yoff: 5,
              gdelta: 10
          },
          halfDevIcon = devIconDim / 2,
          devBadgeOff = { dx: -halfDevIcon, dy: -halfDevIcon },
          hostBadgeOff = { dx: -hostRadius, dy: -hostRadius },
          portLabelDim = 30,
          status = {
              i: 'badgeInfo',
              w: 'badgeWarn',
              e: 'badgeError'
          };
  
      // NOTE: this type of hack should go away once we have implemented
      //       the server-side UiModel code.
      // {virtual -> cord} is for the E-CORD demo at ONS 2016
      var remappedDeviceTypes = {
          virtual: 'cord',
  
          // for now, map to the new glyphs via this lookup.
          // may have to find a better way to do this...
          'switch': 'm_switch',
          roadm: 'm_roadm',
          otn: 'm_otn',
          roadm_otn: 'm_roadm_otn',
          fiber_switch: 'm_fiberSwitch',
          microwave: 'm_microwave',
      };
  
      var remappedHostTypes = {
          router: 'm_router',
          endstation: 'm_endstation',
          bgpSpeaker: 'm_bgpSpeaker'
      };
  
      function mapDeviceTypeToGlyph(type) {
          return remappedDeviceTypes[type] || type || 'unknown';
      }
  
      function mapHostTypeToGlyph(type) {
          return remappedHostTypes[type] || type || 'unknown';
      }
  
      function badgeStatus(badge) {
          return status[badge.status] || status.i;
      }
  
      // internal state
      var deviceLabelIndex = 0,
          hostLabelIndex = 0;
  
      // note: these are the device icon colors without affinity (no master)
      var dColTheme = {
          light: {
              online: '#444444',
              offline: '#cccccc'
          },
          dark: {
              // TODO: theme
              online: '#444444',
              offline: '#cccccc'
          }
      };
  
      function devGlyphColor(d) {
          var o = d.online,
              id = d.master,
              otag = o ? 'online' : 'offline';
          return o ? sus.cat7().getColor(id, 0, ts.theme())
                   : dColTheme[ts.theme()][otag];
      }
  
      function setDeviceColor(d) {
          // want to color the square rectangle (no longer the 'use' glyph)
          d.el.selectAll('rect').filter(function (d, i) {return i === 1;})
              .style('fill', devGlyphColor(d));
      }
  
      function incDevLabIndex() {
          setDevLabIndex(deviceLabelIndex+1);
          switch(deviceLabelIndex) {
              case 0: return 'Hide device labels';
              case 1: return 'Show friendly device labels';
              case 2: return 'Show device ID labels';
          }
      }
  
      function setDevLabIndex(mode) {
          deviceLabelIndex = mode % 3;
          var p = ps.getPrefs('topo_prefs', ttbs.defaultPrefs);
          p.dlbls = deviceLabelIndex;
          ps.setPrefs('topo_prefs', p);
      }
  
      function hostLabel(d) {
          var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
          return d.labels[idx];
      }
  
      function deviceLabel(d) {
          var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
          return d.labels[idx];
      }
  
      function trimLabel(label) {
          return (label && label.trim()) || '';
      }
  
      function computeLabelWidth(n) {
          var text = n.select('text'),
              box = text.node().getBBox();
          return box.width + labelPad * 2;
      }
  
      function iconBox(dim, labelWidth) {
          return {
              x: -dim/2,
              y: -dim/2,
              width: dim + labelWidth,
              height: dim
          }
      }
  
      function updateDeviceRendering(d) {
          var node = d.el,
              bdg = d.badge,
              label = trimLabel(deviceLabel(d)),
              labelWidth;
  
          node.select('text').text(label);
          labelWidth = label ? computeLabelWidth(node) : 0;
  
          node.select('rect')
              .transition()
              .attr(iconBox(devIconDim, labelWidth));
  
          if (bdg) {
              renderBadge(node, bdg, devBadgeOff);
          }
      }
  
      function updateHostRendering(d) {
          var node = d.el,
              bdg = d.badge;
  
          updateHostLabel(d);
  
          if (bdg) {
              renderBadge(node, bdg, hostBadgeOff);
          }
      }
  
      function renderBadge(node, bdg, boff) {
          var bsel,
              bcr = badgeConfig.radius,
              bcgd = badgeConfig.gdelta;
  
          node.select('g.badge').remove();
  
          bsel = node.append('g')
              .classed('badge', true)
              .classed(badgeStatus(bdg), true)
              .attr('transform', sus.translate(boff.dx, boff.dy));
  
          bsel.append('circle')
              .attr('r', bcr);
  
          if (bdg.txt) {
              bsel.append('text')
                  .attr('dy', badgeConfig.yoff)
                  .attr('text-anchor', 'middle')
                  .text(bdg.txt);
          } else if (bdg.gid) {
              bsel.append('use')
                  .attr({
                      width: bcgd * 2,
                      height: bcgd * 2,
                      transform: sus.translate(-bcgd, -bcgd),
                      'xlink:href': '#' + bdg.gid
                  });
          }
      }
  
      function updateHostLabel(d) {
          var label = trimLabel(hostLabel(d));
          d.el.select('text').text(label);
      }
  
      function updateDeviceColors(d) {
          if (d) {
              setDeviceColor(d);
          } else {
              api.node().filter('.device').each(function (d) {
                  setDeviceColor(d);
              });
          }
      }
  
  
      // ==========================
      // updateNodes - subfunctions
  
      function deviceExisting(d) {
          var node = d.el;
          node.classed('online', d.online);
          updateDeviceRendering(d);
          api.posNode(d, true);
      }
  
      function hostExisting(d) {
          updateHostRendering(d);
          api.posNode(d, true);
      }
  
      function deviceEnter(d) {
          var node = d3.select(this),
              glyphId = mapDeviceTypeToGlyph(d.type),
              label = trimLabel(deviceLabel(d)),
              rect, crect, text, glyph, labelWidth;
  
          d.el = node;
  
          rect = node.append('rect');
          crect = node.append('rect');
  
          text = node.append('text').text(label)
              .attr('text-anchor', 'left')
              .attr('y', '0.3em')
              .attr('x', halfDevIcon + labelPad);
  
          glyph = is.addDeviceIcon(node, glyphId, devIconDim);
  
          labelWidth = label ? computeLabelWidth(node) : 0;
  
          rect.attr(iconBox(devIconDim, labelWidth));
          crect.attr(iconBox(devColorDim, 0));
          glyph.attr(iconBox(devIconDim, 0));
  
          node.attr('transform', sus.translate(-halfDevIcon, -halfDevIcon));
  
          d.el.selectAll('*')
              .style('transform', 'scale(' + api.deviceScale() + ')');
      }
  
      function hostEnter(d) {
          var node = d3.select(this),
              glyphId = mapHostTypeToGlyph(d.type),
              textDy = hostRadius + 10;
  
          d.el = node;
          sus.visible(node, api.showHosts());
  
          is.addHostIcon(node, hostRadius, glyphId);
  
          node.append('text')
              .text(hostLabel)
              .attr('dy', textDy)
              .attr('text-anchor', 'middle');
  
          d.el.selectAll('g').style('transform', 'scale(' + api.deviceScale() + ')');
          d.el.selectAll('text').style('transform', 'scale(' + api.deviceScale() + ')');
      }
  
      function hostExit(d) {
          var node = d.el;
          node.select('use')
              .style('opacity', 0.5)
              .transition()
              .duration(800)
              .style('opacity', 0);
  
          node.select('text')
              .style('opacity', 0.5)
              .transition()
              .duration(800)
              .style('opacity', 0);
  
          node.select('circle')
              .style('stroke-fill', '#555')
              .style('fill', '#888')
              .style('opacity', 0.5)
              .transition()
              .duration(1500)
              .attr('r', 0);
      }
  
      function deviceExit(d) {
          var node = d.el;
          node.select('use')
              .style('opacity', 0.5)
              .transition()
              .duration(800)
              .style('opacity', 0);
  
          node.selectAll('rect')
              .style('stroke-fill', '#555')
              .style('fill', '#888')
              .style('opacity', 0.5);
      }
  
  
      // ==========================
      // updateLinks - subfunctions
  
      function linkEntering(d) {
  
          var link = d3.select(this);
          d.el = link;
          d.el.style('stroke-width', api.linkWidthScale() + 'px');
          api.restyleLinkElement(d);
          if (d.type() === 'hostLink') {
              sus.visible(link, api.showHosts());
          }
      }
  
      var linkLabelOffset = '0.3em';
  
      function applyLinkLabels() {
          var entering;
  
          api.updateLinkLabelModel();
  
          // for elements already existing, we need to update the text
          // and adjust the rectangle size to fit
          api.linkLabel().each(function (d) {
              var el = d3.select(this),
                  rect = el.select('rect'),
                  text = el.select('text');
              text.text(d.label);
              rect.attr(rectAroundText(el));
          });
  
          entering = api.linkLabel().enter().append('g')
              .classed('linkLabel', true)
              .attr('id', function (d) { return d.id; });
  
          entering.each(function (d) {
              var el = d3.select(this),
                  rect,
                  text;
  
              if (d.ldata.type() === 'hostLink') {
                  el.classed('hostLinkLabel', true);
                  sus.visible(el, api.showHosts());
              }
  
              d.el = el;
              rect = el.append('rect');
              text = el.append('text').text(d.label);
              rect.attr(rectAroundText(el));
              text.attr('dy', linkLabelOffset);
  
              el.attr('transform', transformLabel(d.ldata.position, d.key));
          });
  
          // Remove any labels that are no longer required.
          api.linkLabel().exit().remove();
      }
  
      function rectAroundText(el) {
          var text = el.select('text'),
              box = text.node().getBBox();
  
          // translate the bbox so that it is centered on [x,y]
          box.x = -box.width / 2;
          box.y = -box.height / 2;
  
          // add padding
          box.x -= 1;
          box.width += 2;
          return box;
      }
  
      function generateLabelFunction() {
          var labels = [],
              xGap = 15,
              yGap = 17;
  
          return function (newId, newX, newY) {
              var idx = -1;
  
              labels.forEach(function (lab, i) {
                  var minX, maxX, minY, maxY;
  
                  if (lab.id === newId) {
                      idx = i;
                      return;
                  }
                  minX = lab.x - xGap;
                  maxX = lab.x + xGap;
                  minY = lab.y - yGap;
                  maxY = lab.y + yGap;
  
                  if (newX > minX && newX < maxX && newY > minY && newY < maxY) {
                      // labels are overlapped
                      newX = newX - xGap;
                      newY = newY - yGap;
                  }
              });
  
              if (idx === -1) {
                  labels.push({id: newId, x: newX, y: newY});
              } else {
                  labels[idx] = {id: newId, x: newX, y: newY};
              }
  
              return {x: newX, y: newY};
          }
      }
  
      var getLabelPos = generateLabelFunction();
  
      function transformLabel(p, id) {
          var dx = p.x2 - p.x1,
              dy = p.y2 - p.y1,
              xMid = dx/2 + p.x1,
              yMid = dy/2 + p.y1;
  
          if (id) {
              var pos = getLabelPos(id, xMid, yMid);
              return sus.translate(pos.x, pos.y);
          }
  
          return sus.translate(xMid, yMid);
      }
  
      function applyPortLabels(data, portLabelG) {
          var entering = portLabelG.selectAll('.portLabel')
              .data(data).enter().append('g')
              .classed('portLabel', true)
              .attr('id', function (d) { return d.id; });
  
          var labelScale = portLabelDim / (portLabelDim * zoomer.scale());
  
          entering.each(function (d) {
              var el = d3.select(this),
                  rect = el.append('rect'),
                  text = el.append('text').text(d.num);
  
              rect.attr(rectAroundText(el))
                  .style('transform', 'scale(' + labelScale + ')');
              text.attr('dy', linkLabelOffset)
                  .style('transform', 'scale(' + labelScale + ')');
  
              el.attr('transform', sus.translate(d.x, d.y));
          });
      }
  
      function labelPoint(linkPos) {
          var lengthUpLine = 1 / 3,
              dx = linkPos.x2 - linkPos.x1,
              dy = linkPos.y2 - linkPos.y1,
              movedX = dx * lengthUpLine,
              movedY = dy * lengthUpLine;
  
          return {
              x: movedX,
              y: movedY
          };
      }
  
      function calcGroupPos(linkPos) {
          var moved = labelPoint(linkPos);
          return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
      }
  
      // calculates where on the link that the hash line for 5+ label appears
      function hashAttrs(linkPos) {
          var hashLength = 25,
              halfLength = hashLength / 2,
              dx = linkPos.x2 - linkPos.x1,
              dy = linkPos.y2 - linkPos.y1,
              length = Math.sqrt((dx * dx) + (dy * dy)),
              moveAmtX = (dx / length) * halfLength,
              moveAmtY = (dy / length) * halfLength,
              mid = labelPoint(linkPos),
              angle = Math.atan(dy / dx) + 45;
  
          return {
              x1: mid.x - moveAmtX,
              y1: mid.y - moveAmtY,
              x2: mid.x + moveAmtX,
              y2: mid.y + moveAmtY,
              stroke: api.linkConfig()[ts.theme()].baseColor,
              transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
          };
      }
  
      function textLabelPos(linkPos) {
          var point = labelPoint(linkPos),
              dist = 20;
          return {
              x: point.x + dist,
              y: point.y + dist
          };
      }
  
      function applyNumLinkLabels(data, lblsG) {
          var labels = lblsG.selectAll('g.numLinkLabel')
                  .data(data, function (d) { return 'pair-' + d.id; }),
              entering;
  
          // update existing labels
          labels.each(function (d) {
              var el = d3.select(this);
  
              el.attr({
                  transform: function (d) { return calcGroupPos(d.linkCoords); }
              });
              el.select('line')
                  .attr(hashAttrs(d.linkCoords));
              el.select('text')
                  .attr(textLabelPos(d.linkCoords))
                  .text(d.num);
          });
  
          // add new labels
          entering = labels
              .enter()
              .append('g')
              .attr({
                  transform: function (d) { return calcGroupPos(d.linkCoords); },
                  id: function (d) { return 'pair-' + d.id; }
              })
              .classed('numLinkLabel', true);
  
          entering.each(function (d) {
              var el = d3.select(this);
  
              el.append('line')
                  .classed('numLinkHash', true)
                  .attr(hashAttrs(d.linkCoords));
              el.append('text')
                  .classed('numLinkText', true)
                  .attr(textLabelPos(d.linkCoords))
                  .text(d.num);
          });
  
          // remove old labels
          labels.exit().remove();
      }
  
      // ==========================
      // Module definition
  
      angular.module('ovTopo')
      .factory('TopoD3Service',
          ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
              'PrefsService', 'TopoToolbarService',
  
          function (_$log_, _fs_, _sus_, _is_, _ts_, _ps_, _ttbs_) {
              $log = _$log_;
              fs = _fs_;
              sus = _sus_;
              is = _is_;
              ts = _ts_;
              ps = _ps_;
              ttbs = _ttbs_;
  
              function initD3(_api_, _zoomer_) {
                  api = _api_;
                  zoomer = _zoomer_;
              }
  
              function destroyD3() { }
  
              return {
                  initD3: initD3,
                  destroyD3: destroyD3,
  
                  incDevLabIndex: incDevLabIndex,
                  setDevLabIndex: setDevLabIndex,
                  hostLabel: hostLabel,
                  deviceLabel: deviceLabel,
                  trimLabel: trimLabel,
  
                  updateDeviceLabel: updateDeviceRendering,
                  updateHostLabel: updateHostLabel,
                  updateDeviceColors: updateDeviceColors,
  
                  deviceExisting: deviceExisting,
                  hostExisting: hostExisting,
                  deviceEnter: deviceEnter,
                  hostEnter: hostEnter,
                  hostExit: hostExit,
                  deviceExit: deviceExit,
  
                  linkEntering: linkEntering,
                  applyLinkLabels: applyLinkLabels,
                  transformLabel: transformLabel,
                  applyPortLabels: applyPortLabels,
                  applyNumLinkLabels: applyNumLinkLabels
              };
          }]);
  }());
  