/* * Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved. */ /* * This is an extension and not part of the main GoJS library. * The source code for this is at extensionsJSM/LinkLabelRouter.ts. * Note that the API for this class may change with any version, even point releases. * If you intend to use an extension in production, you should copy the code to your own source directory. * Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders. * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. */ /** * A custom Router for reducing overlaps between label objects on links by moving them apart with a custom ForceDirectedLayout. * You can modify the properties of that Layout by setting {@link layoutProps} in the constructor. * * By default, this router considers a "link label" to be any GraphObject that is part of a {@link Link} which is not a path Shape * or an arrowhead. You can customize objects that the router operates on by overriding {@link LinkLabelRouter.isLabel}. * * This Router will override the {@link Spot.offsetX} and {@link Spot.offsetY} of the {@link GraphObject.alignmentFocus} value for all link labels. * * Typical setup: * ``` * myDiagram.routers.add(new LinkLabelRouter({ * layoutProps: { * defaultElectricalCharge: 100, * ... * } * })); * ``` * * If you want to experiment with this extension, try the LinkLabelRouter sample. * @category Router Extension */ class LinkLabelRouter extends go.Router { constructor(init) { super(); this.name = 'LinkLabelRouter'; this.isRealtime = false; this._margin = new go.Margin(); if (init) Object.assign(this, init); if (init === null || init === void 0 ? void 0 : init.layoutProps) { this._layoutProps = init.layoutProps; this.layout = new LabelLayout(init.layoutProps); } else { this._layoutProps = {}; this.layout = new LabelLayout(); } this.layout.router = this; } /** * Properties of the underlying custom {@link ForceDirectedLayout} for this router. */ get layoutProps() { return this._layoutProps; } set layoutProps(value) { if (value !== this._layoutProps) { this._layoutProps = value; this.layout = new LabelLayout(this._layoutProps); this.layout.router = this; this.invalidateRouter(); } } /** * Margin that will be applied to each link label when checking for overlaps. * The default value is 0 on all sides. */ get margin() { return this._margin; } set margin(value) { if (value !== this._margin) { this._margin = value; this.invalidateRouter(); } } /** * Determines which GraphObjects in {@link Panel.elements} list of each link should be treated as labels. * By default this consists of all objects that are not the "main path" of the link, and are not fromArrows or toArrows. * * @param { go.GraphObject } obj * @returns */ isLabel(obj) { if (!obj) return false; const link = obj.panel; if (obj.panel === null) return false; if (link instanceof go.Link) { if (obj instanceof go.Shape && (obj.isPanelMain || obj.panel.findMainElement() === obj || obj.fromArrow !== 'None' || obj.toArrow !== 'None')) { return false; } else { return true; } } return false; } /** * Determine if the LinkLabelRouter should run on a given collection. * By default only run once on the whole Diagram, never on Groups * * @param { go.Diagram | go.Group } container * @returns */ canRoute(container) { if (container instanceof go.Group) return false; return super.canRoute(container); } /** * Attempt to move link label objects to avoid overlaps, if necessary. * * @param {go.Set} links * @param {*} container A Diagram or a Group * @returns */ routeLinks(links, container) { if (this.layout === null) return; if (container instanceof go.Group) return; this.layout.activeSet = links; if (container instanceof go.Diagram) this.layout.diagram = container; this.layout.doLayout(container.links); if (this.layout.network === null) return; for (const vertex of this.layout.network.vertexes) { if (!(vertex instanceof LabelVertex)) continue; if (vertex.isFixed) continue; const object = vertex.object; if (!object) continue; const x = isNaN(object.alignmentFocus.x) ? 0.5 : object.alignmentFocus.x; const y = isNaN(object.alignmentFocus.y) ? 0.5 : object.alignmentFocus.y; const dx = vertex.centerX - vertex.objectBounds.centerX; const dy = vertex.centerY - vertex.objectBounds.centerY; // moving alignmentFocus.offsetX/Y by some amount moves the node in the opposite direction, thus -dx and -dy object.alignmentFocus = new go.Spot(x, y, -dx, -dy); } } } /** @hidden @internal */ class LabelVertex extends go.ForceDirectedVertex { constructor(network) { super(network); this.object = null; this.objectBounds = null; this.currentBounds = null; this.isDummy = false; } } /** @hidden @internal */ class LabelLayout extends go.ForceDirectedLayout { constructor(init) { super(); /** @hidden */ this.router = null; /** @hidden */ this.activeSet = null; if (init) Object.assign(this, init); } /** * we should not ever do a prelayout on this virtual, "fake" force-directed network */ needsPrelayout() { return false; } /** * Keep track of the current bounding box of the link label on each node when moving its associated LabelVertex. * * @param { LabelVertex } v */ moveVertex(v) { const result = super.moveVertex(v); v.currentBounds.offset(v.centerX - v.currentBounds.centerX, v.centerY - v.currentBounds.centerY); return result; } /** * Only allow interaction between two nodes if their associated GraphObjects are currently intersecting. * * @param { LabelVertex } v1 * @param { LabelVertex } v2 */ shouldInteract(v1, v2) { if (v1.isDummy || v2.isDummy) return false; const b1 = v1.currentBounds; const b2 = v2.currentBounds; return b1.intersectsRect(b2); } makeNetwork(coll) { var _a; const net = new go.ForceDirectedNetwork(this); let allparts; if (coll instanceof go.Diagram) { allparts = coll.links; } else if (coll instanceof go.Group) { allparts = coll.memberParts; } else { allparts = coll; } for (const part of allparts) { if (!(part instanceof go.Link)) continue; part.ensureBounds(); for (const label of part.elements) { if (!this.router.isLabel(label)) continue; const margin = this.router.margin; const documentBounds = label.getDocumentBounds() .offset(label.alignmentFocus.offsetX, label.alignmentFocus.offsetY); // add margin to "real" document bounds if (margin instanceof go.Margin) { documentBounds.addMargin(margin); } else { documentBounds.grow(margin, margin, margin, margin); } if ((_a = this.activeSet) === null || _a === void 0 ? void 0 : _a.has(part)) { // add vertex for label node const v1 = new LabelVertex(net); v1.centerX = documentBounds.centerX; v1.centerY = documentBounds.centerY; v1.width = documentBounds.width; v1.height = documentBounds.height; v1.object = label; v1.objectBounds = documentBounds.copy(); v1.currentBounds = documentBounds.copy(); net.addVertex(v1); // add vertex for fixed dummy node at the label's original position const v2 = new LabelVertex(net); v2.centerX = v1.centerX; v2.centerY = v1.centerY; v2.charge = 0; v2.isFixed = true; v2.isDummy = true; net.addVertex(v2); // add edge to incentivize the Label to stay near its original position const e = new go.ForceDirectedEdge(net); e.length = 0; e.fromVertex = v1; e.toVertex = v2; net.addEdge(e); } else { const v = new LabelVertex(net); v.centerX = documentBounds.centerX; v.centerY = documentBounds.centerY; v.width = documentBounds.width; v.height = documentBounds.height; v.object = label; v.objectBounds = documentBounds.copy(); v.currentBounds = documentBounds.copy(); v.isFixed = true; net.addVertex(v); } } } return net; } }