App-Netdisco

 view release on metacpan or  search on metacpan

share/public/javascripts/netdisco-netmap.js  view on Meta::CPAN

            hit = n;
          }
        });
        return hit;
      },
      positions: function () {
        return fg.graphData().nodes.map(function (n) {
          return {
            ID: n.ID,
            x: Math.round(n.x),
            y: Math.round(n.y),
            fixed: n.fx !== undefined && n.fx !== null ? 1 : 0
          };
        });
      },
      links: function () {
        return fg.graphData().links.map(function (l) {
          return { source: endpointId(l, 'source'), target: endpointId(l, 'target') };
        });
      },
      screenXY: function (id) {
        const n = graph.nodeDataById(id);
        if (!n) {
          return null;
        }
        const p = fg.graph2ScreenCoords(n.x, n.y);
        const canvas = container.querySelector('canvas');
        if (!canvas) {
          return null;
        }
        const r = canvas.getBoundingClientRect();
        return { x: r.left + p.x, y: r.top + p.y };
      }
    };
    ndWindow.graph = graph;

    // force-graph exposes no simulation find() and no dblclick callback; its
    // own hit detection delivers the node to onNodeClick, so a double click is
    // two clicks on the same node inside the double-click window
    let lastClick = { id: null, at: 0 };
    fg.onNodeClick(function (n) {
      const now = Date.now();
      // 500 ms matches the platform double-click default the old renderer's
      // dblclick event inherited
      if (n.ID === lastClick.id && now - lastClick.at < 500) {
        window.location.assign(n.LINK);
        return;
      }
      lastClick = { id: n.ID, at: now };
    });

    fg.linkCurvature(function (l) {
      const s = endpointId(l, 'source'),
        t = endpointId(l, 'target');
      return s === t ? 0.6 : 0;
    });

    // the old template zoomed to the center node 1.5 s after start when
    // mapshow=neighbors (a legacy value still reachable from bookmarks)
    if (map.dataset.ndMapshow === 'neighbors') {
      setTimeout(function () {
        const n = graph.nodeDataById(graph.centernode);
        if (n) {
          fg.centerAt(n.x, n.y, 600);
          fg.zoom(4, 600);
        }
      }, 1500);
    }

    // box select: shift-drag replaces the old freehand lasso by ruling.
    // capture-phase listener so force-graph's own pan never sees the drag.
    /** @type {{active: boolean, x0: number, y0: number, el: HTMLElement|null}} */
    const box = { active: false, x0: 0, y0: 0, el: null };
    container.addEventListener(
      'pointerdown',
      function (ev) {
        if (!ev.shiftKey) {
          return;
        }
        ev.stopPropagation();
        ev.preventDefault();
        fg.enablePanInteraction(false).enableZoomInteraction(false);
        box.active = true;
        box.x0 = ev.clientX;
        box.y0 = ev.clientY;
        box.el = document.createElement('div');
        box.el.id = 'nd2_netmap-boxselect';
        // document.body sits outside the fullscreen element, so a box drawn
        // there would be invisible while fullscreen; append into whichever is
        // actually showing
        (document.fullscreenElement || document.body).appendChild(box.el);
      },
      true
    );
    /**
     * Resizes and repositions the box-select rectangle to track the pointer during a
     * shift-drag; does nothing when no box-select is active.
     * @param {PointerEvent} ev the pointermove event
     * @returns {void}
     */
    function onBoxPointerMove(ev) {
      if (!box.active || !box.el) {
        return;
      }
      const x = Math.min(box.x0, ev.clientX),
        y = Math.min(box.y0, ev.clientY);
      box.el.style.left = x + 'px';
      box.el.style.top = y + 'px';
      box.el.style.width = Math.abs(ev.clientX - box.x0) + 'px';
      box.el.style.height = Math.abs(ev.clientY - box.y0) + 'px';
    }
    /**
     * Finishes a box-select drag: removes the selection rectangle, restores pan and
     * zoom, and marks every node inside the box as selected. Does nothing when no
     * box-select is active.
     * @param {PointerEvent} ev the pointerup event
     * @returns {void}
     */
    function onBoxPointerUp(ev) {
      if (!box.active || !box.el || !container) {
        return;

share/public/javascripts/netdisco-netmap.js  view on Meta::CPAN

    ndWindow.__ndNetmapPointerHandlers = { move: onBoxPointerMove, up: onBoxPointerUp };
    window.addEventListener('pointermove', onBoxPointerMove);
    window.addEventListener('pointerup', onBoxPointerUp);

    fg.onNodeDrag(function (n, translate) {
      if (!n.selected) {
        return;
      }
      if (!dragSnap) {
        // hold the node objects themselves, not their IDs: an ID-keyed lookup
        // means a linear nodeDataById() scan per node per tick, and re-keying
        // by ID risks the numeric-vs-string coercion Object.keys() does
        dragSnap = [];
        fg.graphData().nodes.forEach(function (o) {
          if (o.selected && o.ID !== n.ID) {
            dragSnap.push({ node: o, x: o.x, y: o.y });
          }
        });
      }
      // translate is the per-tick incremental delta, not cumulative from drag
      // start, so the snapshot itself has to accumulate it tick by tick
      dragSnap.forEach(function (entry) {
        entry.x += translate.x;
        entry.y += translate.y;
        entry.node.fx = entry.node.x = entry.x;
        entry.node.fy = entry.node.y = entry.y;
      });
    });

    // fullscreen: same API dance the old template used, on the pane so the
    // sidebar stays outside it
    const fullscreenButton = document.getElementById('nd2_netmap-fullscreen');
    if (fullscreenButton) {
      fullscreenButton.addEventListener('click', function () {
        const netmapPane = document.getElementById('netmap_pane');
        if (netmapPane) {
          requestFullScreen(netmapPane);
        }
      });
    }
    const signal = beginRenderListeners();
    ['webkitfullscreenchange', 'mozfullscreenchange', 'fullscreenchange'].forEach(function (name) {
      document.addEventListener(
        name,
        function () {
          resizeGraphContainer();
          document.querySelectorAll('#nd2_netmap-fullscreen i').forEach(function (el) {
            el.className = isFullScreen() ? 'fas fa-compress fa-lg' : 'fas fa-expand fa-lg';
          });
        },
        { signal: signal }
      );
    });

    /**
     * Resizes the graph canvas to the pane's current width after a short delay,
     * letting the sidebar toggle or fullscreen transition finish first.
     * @returns {void}
     */
    function resizeGraphContainer() {
      setTimeout(function () {
        const resizePaneEl = document.getElementById('netmap_pane');
        const resizePaneParent = resizePaneEl && resizePaneEl.parentElement;
        fg.width(parseInt(resizePaneParent ? getComputedStyle(resizePaneParent).width : '0')).height(
          window.innerHeight - 100
        );
      }, 500);
    }
    const sidebarToggleIn = document.getElementById('nd_sidebar-toggle-img-in');
    if (sidebarToggleIn) sidebarToggleIn.addEventListener('click', resizeGraphContainer, { signal: signal });
    const sidebarToggleOut = document.getElementById('nd_sidebar-toggle-img-out');
    if (sidebarToggleOut) sidebarToggleOut.addEventListener('click', resizeGraphContainer, { signal: signal });
    window.addEventListener('resize', resizeGraphContainer, { signal: signal });

    // onEngineTick is a setter, not a subscription, so the tick count lives in
    // this handler rather than a second one that would replace it
    fg.onEngineTick(function () {
      ticksSinceStop++;
      setSpinnerState('nd_netmap-running');
    });

    // labels draw above this zoom
    const LABEL_ZOOM = +(map.dataset.ndLabelZoom || 0.9);
    const LABEL_SIZE = +(map.dataset.ndLabelSize || 8);
    // read once, not once per node per frame
    const showips = document.getElementById('nd_showips');

    fg.nodeCanvasObjectMode(function () {
      return 'after';
    }).nodeCanvasObject(function (n, ctx, scale) {
      if (n.selected) {
        ctx.beginPath();
        ctx.arc(n.x, n.y, n.radius + 2, 0, 2 * Math.PI);
        ctx.strokeStyle = '#0d6efd';
        ctx.lineWidth = 1.5 / scale;
        ctx.stroke();
      }
      if (scale < LABEL_ZOOM) {
        return;
      }
      ctx.textAlign = 'center';
      ctx.textBaseline = 'top';
      ctx.fillStyle = '#333';

      // Drawn from the two fields rather than splitting LABEL: a device name
      // may contain spaces, and a two-word split drops the rest of it.
      const gap = LABEL_SIZE * 0.5; // graph units, so it holds as the map zooms
      ctx.font = 'bold ' + LABEL_SIZE + 'px sans-serif';
      ctx.fillText(n.ORIG_LABEL, n.x, n.y + n.radius + gap);

      if (showips instanceof HTMLInputElement && showips.checked && n.ORIG_LABEL !== n.ID) {
        ctx.font = LABEL_SIZE + 'px sans-serif';
        ctx.fillText(n.ID, n.x, n.y + n.radius + gap + LABEL_SIZE + 1);
      }
    });

    // read once, not once per link per frame
    const showspeed = document.getElementById('nd_showspeed');
    fg.linkCanvasObjectMode(function () {
      return 'after';
    }).linkCanvasObject(function (l, ctx) {



( run in 1.040 second using v1.01-cache-2.11-cpan-85d3896f969 )