@sapui5/sap.suite.ui.commons 1.136.12 → 1.136.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/sap/suite/ui/commons/.library +1 -1
  3. package/src/sap/suite/ui/commons/AriaProperties.js +1 -1
  4. package/src/sap/suite/ui/commons/CalculationBuilder.js +1 -1
  5. package/src/sap/suite/ui/commons/CalculationBuilderExpression.js +1 -1
  6. package/src/sap/suite/ui/commons/CalculationBuilderFunction.js +1 -1
  7. package/src/sap/suite/ui/commons/CalculationBuilderGroup.js +1 -1
  8. package/src/sap/suite/ui/commons/CalculationBuilderItem.js +1 -1
  9. package/src/sap/suite/ui/commons/CalculationBuilderValidationResult.js +1 -1
  10. package/src/sap/suite/ui/commons/CalculationBuilderVariable.js +1 -1
  11. package/src/sap/suite/ui/commons/CloudFilePicker.js +1 -1
  12. package/src/sap/suite/ui/commons/MicroProcessFlow.js +1 -1
  13. package/src/sap/suite/ui/commons/MicroProcessFlowItem.js +1 -1
  14. package/src/sap/suite/ui/commons/TimelineRenderManager.js +8 -4
  15. package/src/sap/suite/ui/commons/flexibility/changeHandler/PropertyChangeMapper.js +1 -1
  16. package/src/sap/suite/ui/commons/imageeditor/CropCustomShapeHistoryItem.js +1 -1
  17. package/src/sap/suite/ui/commons/imageeditor/CropEllipseHistoryItem.js +1 -1
  18. package/src/sap/suite/ui/commons/imageeditor/CropRectangleHistoryItem.js +1 -1
  19. package/src/sap/suite/ui/commons/imageeditor/CustomSizeItem.js +1 -1
  20. package/src/sap/suite/ui/commons/imageeditor/FilterHistoryItem.js +1 -1
  21. package/src/sap/suite/ui/commons/imageeditor/FlipHistoryItem.js +1 -1
  22. package/src/sap/suite/ui/commons/imageeditor/HistoryItem.js +1 -1
  23. package/src/sap/suite/ui/commons/imageeditor/ImageEditor.js +1 -1
  24. package/src/sap/suite/ui/commons/imageeditor/ImageEditorContainer.js +1 -1
  25. package/src/sap/suite/ui/commons/imageeditor/ImageEditorResponsiveContainer.js +1 -1
  26. package/src/sap/suite/ui/commons/imageeditor/ResizeHistoryItem.js +1 -1
  27. package/src/sap/suite/ui/commons/imageeditor/RotateHistoryItem.js +1 -1
  28. package/src/sap/suite/ui/commons/library.js +100 -3
  29. package/src/sap/suite/ui/commons/messagebundle.properties +73 -12
  30. package/src/sap/suite/ui/commons/messagebundle_en_US_saprigi.properties +39 -5
  31. package/src/sap/suite/ui/commons/messagebundle_fr_CA.properties +1 -1
  32. package/src/sap/suite/ui/commons/networkgraph/ElementBase.js +19 -1
  33. package/src/sap/suite/ui/commons/networkgraph/Graph.js +554 -45
  34. package/src/sap/suite/ui/commons/networkgraph/GraphMap.js +25 -3
  35. package/src/sap/suite/ui/commons/networkgraph/GraphRenderer.js +19 -8
  36. package/src/sap/suite/ui/commons/networkgraph/KeyboardNavigator.js +367 -12
  37. package/src/sap/suite/ui/commons/networkgraph/Line.js +814 -22
  38. package/src/sap/suite/ui/commons/networkgraph/Node.js +573 -79
  39. package/src/sap/suite/ui/commons/networkgraph/Tooltip.js +4 -0
  40. package/src/sap/suite/ui/commons/networkgraph/Utils.js +249 -10
  41. package/src/sap/suite/ui/commons/networkgraph/layout/NoopLayout.js +77 -7
  42. package/src/sap/suite/ui/commons/networkgraph/util/ConnectionPathUtils.js +1174 -0
  43. package/src/sap/suite/ui/commons/networkgraph/util/CreateConnectionPopover.js +374 -0
  44. package/src/sap/suite/ui/commons/networkgraph/util/DependencyLayoutHelper.js +1017 -0
  45. package/src/sap/suite/ui/commons/networkgraph/util/DragDropManager.js +721 -0
  46. package/src/sap/suite/ui/commons/networkgraph/util/PortManager.js +582 -0
  47. package/src/sap/suite/ui/commons/statusindicator/Circle.js +1 -1
  48. package/src/sap/suite/ui/commons/statusindicator/CustomShape.js +1 -1
  49. package/src/sap/suite/ui/commons/statusindicator/DiscreteThreshold.js +1 -1
  50. package/src/sap/suite/ui/commons/statusindicator/FillingOption.js +1 -1
  51. package/src/sap/suite/ui/commons/statusindicator/LibraryShape.js +1 -1
  52. package/src/sap/suite/ui/commons/statusindicator/Path.js +1 -1
  53. package/src/sap/suite/ui/commons/statusindicator/PropertyThreshold.js +1 -1
  54. package/src/sap/suite/ui/commons/statusindicator/Rectangle.js +1 -1
  55. package/src/sap/suite/ui/commons/statusindicator/Shape.js +1 -1
  56. package/src/sap/suite/ui/commons/statusindicator/ShapeGroup.js +1 -1
  57. package/src/sap/suite/ui/commons/statusindicator/SimpleShape.js +1 -1
  58. package/src/sap/suite/ui/commons/statusindicator/StatusIndicator.js +1 -1
  59. package/src/sap/suite/ui/commons/taccount/TAccount.js +1 -1
  60. package/src/sap/suite/ui/commons/taccount/TAccountGroup.js +1 -1
  61. package/src/sap/suite/ui/commons/taccount/TAccountItem.js +1 -1
  62. package/src/sap/suite/ui/commons/taccount/TAccountItemProperty.js +1 -1
  63. package/src/sap/suite/ui/commons/taccount/TAccountPanel.js +1 -1
  64. package/src/sap/suite/ui/commons/themes/base/NetworkGraph.less +26 -13
  65. package/src/sap/suite/ui/commons/themes/base/NetworkGroup.less +4 -0
  66. package/src/sap/suite/ui/commons/themes/base/NetworkLine.less +58 -13
  67. package/src/sap/suite/ui/commons/themes/base/NetworkNode.less +251 -47
  68. package/src/sap/suite/ui/commons/themes/base/SemanticColorMixins.less +55 -0
@@ -21,12 +21,17 @@ sap.ui.define([
21
21
  ArrowOrientation = library.networkgraph.LineArrowOrientation,
22
22
  Shape = library.networkgraph.NodeShape,
23
23
  Orientation = library.networkgraph.Orientation,
24
- SemanticColorType = library.SemanticColorType;
24
+ SemanticColorType = library.SemanticColorType,
25
+ ConnectionType = library.networkgraph.ConnectionType;
25
26
 
26
27
  var BEND_RADIUS = 6, // Bezier 'radius' of smooth bends
27
28
  FOCUS_LANE_WIDTH = 5, // Distance of focus shadow line from the main line
28
29
  RELATIVE_ARROW_POSITION = 0.45,
29
30
  FIXED_ARROW_POSITION = 15,
31
+ MULTIPLE_ARROW_DISTANCE = 44,
32
+ LABEL_BOX_WIDTH = 30,
33
+ LABEL_BOX_HEIGHT = 22,
34
+ ARROW_LENGTH = 11, // Length of arrow from apex to tail
30
35
  ZERO_ANGLE_ARROW_POINTS = {
31
36
  Apex: {x: 5.5, y: 0},
32
37
  Second: {x: -5.5, y: -7.5},
@@ -114,6 +119,34 @@ sap.ui.define([
114
119
  */
115
120
  stretchToCenter: {
116
121
  type: "boolean", group: "Misc", defaultValue: false
122
+ },
123
+ /**
124
+ * Defines the connection type between the source and target nodes.
125
+ * Can be set to RightToLeft, LeftToRight, TopToBottom, or BottomToTop using {@link sap.suite.ui.commons.networkgraph.ConnectionType ConnectionType} enumeration.
126
+ * This property is used to determine how the line connects the source and target nodes. For example, if set to RightToLeft, the line starts from the right side of the source node and end at the left side of the target node.
127
+ * This property is valid only with the NoopLayout and drag and drop enabled network graph.
128
+ * @public
129
+ * @since 1.136.19
130
+ *
131
+ */
132
+ connectionType: {
133
+ type: "sap.suite.ui.commons.networkgraph.ConnectionType", defaultValue: ConnectionType.RightToLeft
134
+ },
135
+ /**
136
+ * Defines the text label to be displayed on the line.
137
+ *
138
+ * When set, displays the label text in a rounded rectangle box positioned near the target arrow.
139
+ * The label is rendered only when all of the following conditions are met:
140
+ * - This property has a non-empty value
141
+ * - The graph is using NoopLayout
142
+ * - The line's arrowPosition is set to "Both"
143
+ * - The line is long enough to accommodate the label
144
+ * If this property is empty or not set, no label is displayed on the line
145
+ * @public
146
+ * @since 1.136.19
147
+ */
148
+ labelName: {
149
+ type: "string", group: "Behavior", defaultValue: ""
117
150
  }
118
151
  },
119
152
  aggregations: {
@@ -297,13 +330,57 @@ sap.ui.define([
297
330
  "data-sap-ui": sId
298
331
  }, false, oRm);
299
332
 
333
+ // Add title for tooltip showing connection type (always shown)
334
+ if (this.getParent()._isNoopLayout() &&
335
+ this.getArrowPosition() === ArrowPosition.Both) {
336
+
337
+ var sConnectionType = this.getConnectionType();
338
+ var oMapping = this.getParent().getConnectionTypeMapping();
339
+ var sTooltipText = "";
340
+
341
+ if (sConnectionType === ConnectionType.LeftToRight) {
342
+ sTooltipText = oMapping.LeftToRight || ConnectionType.LeftToRight;
343
+ } else if (sConnectionType === ConnectionType.RightToLeft) {
344
+ sTooltipText = oMapping.RightToLeft || ConnectionType.RightToLeft;
345
+ } else if (sConnectionType === ConnectionType.LeftToLeft) {
346
+ sTooltipText = oMapping.LeftToLeft || ConnectionType.LeftToLeft;
347
+ } else if (sConnectionType === ConnectionType.RightToRight) {
348
+ sTooltipText = oMapping.RightToRight || ConnectionType.RightToRight;
349
+ } else {
350
+ sTooltipText = oMapping.RightToLeft || ConnectionType.RightToLeft;
351
+ }
352
+
353
+ if (sTooltipText) {
354
+ oRm.openStart("title");
355
+ oRm.openEnd();
356
+ oRm.text(sTooltipText);
357
+ oRm.close("title");
358
+ }
359
+ }
360
+
300
361
  // invisible wrapper for better event handling
301
362
  fnRenderPath("sapSuiteUiCommonsNetworkLineInvisibleWrapper", "invisibleWrapper", true);
302
363
 
303
364
  // path itself
304
- fnRenderPath("", "path");
365
+ fnRenderPath("", "path");
366
+
367
+ if (this.getArrowPosition() === ArrowPosition.Both && this._hasMultipleDirectedArrows() && this.getCoordinates().length >= 2 ) {
368
+ // Calculate total line length to determine arrow placement strategy
369
+ var fTotalLineLength = this._calculateTotalLineLength();
370
+ var bCanFitTwoArrows = this._canLineFitTwoArrows(fTotalLineLength);
305
371
 
306
- if (this.getArrowOrientation() !== ArrowOrientation.None && this.getCoordinates().length >= 2) {
372
+ if (bCanFitTwoArrows) {
373
+ // Line is long enough for both arrows - use smart source arrow logic
374
+ if (this._shouldRenderSourceArrow()) {
375
+ fnRenderArrow(ArrowOrientation.ParentOf, "multipleSource", "arrow-source");
376
+ }
377
+ // Always render target arrow (each incoming line gets its own target arrow)
378
+ fnRenderArrow(ArrowOrientation.ParentOf, "multipleTarget", "arrow-target");
379
+ } else {
380
+ // Line is too short for two arrows - only render target arrow
381
+ fnRenderArrow(ArrowOrientation.ParentOf, "multipleTarget", "arrow-target");
382
+ }
383
+ } else if (this.getArrowOrientation() !== ArrowOrientation.None && this.getCoordinates().length >= 2) {
307
384
  if (this.getArrowOrientation() === ArrowOrientation.Both) {
308
385
  if (this.getArrowPosition() === ArrowPosition.Middle) {
309
386
  // middle arrow is rendered "at once" using two groups of points
@@ -336,6 +413,169 @@ sap.ui.define([
336
413
  oRm.close("g");
337
414
  };
338
415
 
416
+ /**
417
+ * Renders the text label for this line. Used by batch text rendering.
418
+ * @param {sap.ui.core.RenderManager} oRm The RenderManager to use for rendering
419
+ * @param {string} [sIdSuffix] Optional suffix for element ID
420
+ * @private
421
+ */
422
+ Line.prototype._renderLineText = function (oRm, sIdSuffix) {
423
+ // Early return if any condition is not met:
424
+ // - labelName has a non-empty value
425
+ // - Graph must be in NoopLayout
426
+ // - arrowPosition must be Both (labels only shown when both arrows are present)
427
+ if (!this.getLabelName() ||
428
+ !this.getParent()._isNoopLayout() ||
429
+ this.getArrowPosition() !== ArrowPosition.Both) {
430
+ return;
431
+ }
432
+
433
+ // Always use the base line ID without any suffix to ensure consistency
434
+ // with _hideShowLineText which can't know what suffix was used during rendering
435
+ var sId = this.getId();
436
+ var {style: sStyle, class: sStatusClass} = this._getStatusStyle({
437
+ "stroke": ElementBase.ColorType.Border,
438
+ "stroke-width": ElementBase.ColorType.BorderWidth,
439
+ "stroke-dasharray": ElementBase.ColorType.BorderStyle
440
+ });
441
+
442
+ // Get the connection type and convert to display text
443
+ var sConnectionType = this.getConnectionType();
444
+ var oMapping = this.getParent().getConnectionTypeMapping();
445
+ var sFullText = ""; // Store the full text for tooltip
446
+
447
+ // Check if custom mapping exists for this connection type
448
+ switch (sConnectionType) {
449
+ case ConnectionType.LeftToRight:
450
+ sFullText = oMapping.LeftToRight || ConnectionType.LeftToRight;
451
+ break;
452
+ case ConnectionType.RightToLeft:
453
+ sFullText = oMapping.RightToLeft || ConnectionType.RightToLeft;
454
+ break;
455
+ case ConnectionType.LeftToLeft:
456
+ sFullText = oMapping.LeftToLeft || ConnectionType.LeftToLeft;
457
+ break;
458
+ case ConnectionType.RightToRight:
459
+ sFullText = oMapping.RightToRight || ConnectionType.RightToRight;
460
+ break;
461
+ default:
462
+ sFullText = oMapping.RightToLeft || ConnectionType.RightToLeft;
463
+ break;
464
+ }
465
+
466
+ // Calculate position near the ingoing arrow (end of the line)
467
+ // When ArrowPosition.Both, the target arrow is positioned at MULTIPLE_ARROW_DISTANCE (44px) from the end
468
+ var oCoords = this.getCoordinates();
469
+ if (oCoords && oCoords.length >= 2) {
470
+ var aFragmentLengthSum = [], fTotalDistance = 0;
471
+ var iLastIndex = oCoords.length - 1;
472
+
473
+ // Calculate cumulative distances along the path
474
+ for (var i = 0; i < iLastIndex; i++) {
475
+ if (oCoords[i].getX() === oCoords[i + 1].getX()) {
476
+ // Vertical line - only Y changes
477
+ fTotalDistance += Math.abs(oCoords[i + 1].getY() - oCoords[i].getY());
478
+ } else if (oCoords[i].getY() === oCoords[i + 1].getY()) {
479
+ // Horizontal line - only X changes
480
+ fTotalDistance += Math.abs(oCoords[i + 1].getX() - oCoords[i].getX());
481
+ } else {
482
+ // Diagonal line - use Pythagorean theorem If Lines are plotted Diagonally
483
+ fTotalDistance += Math.sqrt(
484
+ Math.pow(oCoords[i + 1].getX() - oCoords[i].getX(), 2) +
485
+ Math.pow(oCoords[i + 1].getY() - oCoords[i].getY(), 2)
486
+ );
487
+ }
488
+ // Store cumulative distance
489
+ aFragmentLengthSum.push(fTotalDistance);
490
+ }
491
+
492
+ // Check if there's enough space for the label box with 8px padding from target arrow
493
+ // When line is long enough for both arrows, check gap between them
494
+ // When line is short, source arrow is hidden and target arrow position is adjusted
495
+ var bCanFitTwoArrows = this._canLineFitTwoArrows(fTotalDistance);
496
+ var fTargetArrowDistance;
497
+ var fAvailableSpace;
498
+
499
+ if (bCanFitTwoArrows) {
500
+ // Both arrows present at fixed positions
501
+ fTargetArrowDistance = MULTIPLE_ARROW_DISTANCE;
502
+ // Available space = total - (source position + source arrow + target position + target arrow + 8px padding)
503
+ fAvailableSpace = fTotalDistance - (MULTIPLE_ARROW_DISTANCE + ARROW_LENGTH + MULTIPLE_ARROW_DISTANCE + ARROW_LENGTH + 8);
504
+ } else {
505
+ // Only target arrow present
506
+ fTargetArrowDistance = Math.min(MULTIPLE_ARROW_DISTANCE, fTotalDistance * 0.3);
507
+ // Available space = total - (target position + target arrow + 8px padding)
508
+ fAvailableSpace = fTotalDistance - (fTargetArrowDistance + ARROW_LENGTH + 8);
509
+ }
510
+
511
+ var fRequiredSpace = LABEL_BOX_WIDTH; // 30px box width
512
+ if (fAvailableSpace < fRequiredSpace) {
513
+ return;
514
+ }
515
+
516
+ // Position label so its end is 8px away from the target arrow
517
+ var fTargetDistance = fTotalDistance - fTargetArrowDistance - 8 - LABEL_BOX_WIDTH; // arrow position + 8px offset + full box width
518
+ var iTargetIndex = 0;
519
+ for (var j = 0; j < iLastIndex; j++) {
520
+ if (aFragmentLengthSum[j] >= fTargetDistance) {
521
+ iTargetIndex = j;
522
+ break;
523
+ }
524
+ }
525
+
526
+ // Calculate the exact position on the segment
527
+ var fSegmentStart = iTargetIndex > 0 ? aFragmentLengthSum[iTargetIndex - 1] : 0;
528
+ var fSegmentLength = aFragmentLengthSum[iTargetIndex] - fSegmentStart;
529
+ var fPositionInSegment = (fTargetDistance - fSegmentStart) / fSegmentLength;
530
+ var fLabelX = oCoords[iTargetIndex].getX() +
531
+ (oCoords[iTargetIndex + 1].getX() - oCoords[iTargetIndex].getX()) * fPositionInSegment;
532
+ var fLabelY = oCoords[iTargetIndex].getY() +
533
+ (oCoords[iTargetIndex + 1].getY() - oCoords[iTargetIndex].getY()) * fPositionInSegment;
534
+
535
+ // Calculate box position (centered on label coordinates)
536
+ var fBoxX = fLabelX - LABEL_BOX_WIDTH / 2;
537
+ var fBoxY = fLabelY - LABEL_BOX_HEIGHT / 2;
538
+
539
+ // Create a group to contain both rectangle and text
540
+ oRm.openStart("g");
541
+ oRm.class("sapSuiteUiCommonsNetworkLineTextBox");
542
+ oRm.attr("id", sId + "-textbox-middle");
543
+ oRm.attr("data-line-id", this._getLineId());
544
+ oRm.openEnd();
545
+
546
+ // Add title for tooltip on the label box group
547
+ oRm.openStart("title");
548
+ oRm.openEnd();
549
+ oRm.text(sFullText);
550
+ oRm.close("title");
551
+
552
+ // Render the rectangle background
553
+ oRm.openStart("rect");
554
+ oRm.class("sapSuiteUiCommonsNetworkLineTextBoxBackground " + sStatusClass);
555
+ //this.applyStyles(oRm, this.getStyleObject(sStyle)); //ToDo: to be applied if Box should also adhere to status styles
556
+ oRm.attr("x", fBoxX);
557
+ oRm.attr("y", fBoxY);
558
+ oRm.attr("width", LABEL_BOX_WIDTH);
559
+ oRm.attr("height", LABEL_BOX_HEIGHT);
560
+ oRm.attr("rx", 8); // Rounded corners
561
+ oRm.attr("ry", 8);
562
+ oRm.openEnd();
563
+ oRm.close("rect");
564
+
565
+ // Render the text inside the rectangle
566
+ oRm.openStart("text");
567
+ oRm.class("sapSuiteUiCommonsNetworkLineText");
568
+ oRm.attr("x", fLabelX);
569
+ oRm.attr("y", fLabelY); // Center vertically in the box
570
+ oRm.openEnd();
571
+ oRm.text(this.getLabelName());
572
+ oRm.close("text");
573
+
574
+ // Close the group
575
+ oRm.close("g");
576
+ }
577
+ };
578
+
339
579
  Line.prototype._renderFocusWrapper = function () {
340
580
  var fnAppendFocusLine = function (iShift) {
341
581
  var oPath = this._createElement("path", {
@@ -343,7 +583,19 @@ sap.ui.define([
343
583
  "class": "sapSuiteUiCommonsNetworkLineFocus"
344
584
  });
345
585
 
346
- this.$()[0].appendChild(oPath);
586
+ // Find the first text box group to insert before it
587
+ var oContainer = this.getDomRef();
588
+ var oFirstTextBox = oContainer ? oContainer.querySelector(".sapSuiteUiCommonsNetworkLineTextBox") : null;
589
+
590
+ if (oFirstTextBox) {
591
+ // Insert the focus line before the first text box group
592
+ oContainer.insertBefore(oPath, oFirstTextBox);
593
+ } else {
594
+ // Fallback: append to end if no text boxes found
595
+ if (oContainer) {
596
+ oContainer.appendChild(oPath);
597
+ }
598
+ }
347
599
  }.bind(this);
348
600
 
349
601
  if (!this._bFocusRendered) {
@@ -430,6 +682,33 @@ sap.ui.define([
430
682
 
431
683
  sPosition = sPosition || this.getArrowPosition();
432
684
 
685
+ // Handle multiple directed arrows positioning
686
+ if (sPosition === "multipleSource" || sPosition === "multipleTarget") {
687
+ // Check if line is long enough for the requested arrow position
688
+ var fTotalLineLength = this._calculateTotalLineLength();
689
+ var bCanFitTwoArrows = this._canLineFitTwoArrows(fTotalLineLength);
690
+
691
+ // If line is too short and we're trying to render source arrow, skip it
692
+ if (!bCanFitTwoArrows && sPosition === "multipleSource") {
693
+ // Return default fallback to avoid errors
694
+ return {
695
+ center: {x: oCoords[0].getX(), y: oCoords[0].getY()},
696
+ apex: {x: oCoords[1].getX(), y: oCoords[1].getY()}
697
+ };
698
+ }
699
+ // Find optimal segment by traveling 44px along the line path
700
+ var sDirection = sPosition === "multipleSource" ? "forward" : "backward";
701
+ var fDistance = bCanFitTwoArrows ? MULTIPLE_ARROW_DISTANCE : Math.min(MULTIPLE_ARROW_DISTANCE, fTotalLineLength * 0.3);
702
+ var oSegmentInfo = this._findOptimalArrowSegment(sDirection, fDistance);
703
+
704
+ return {
705
+ center: oSegmentInfo.center,
706
+ apex: oSegmentInfo.apex,
707
+ segmentRatio: oSegmentInfo.ratio,
708
+ isMultipleDirected: true
709
+ };
710
+ }
711
+
433
712
  if (this.getBends().length === 0) {
434
713
  iHolyIndex = 0;
435
714
  } else if (sPosition === ArrowPosition.Start) {
@@ -526,8 +805,14 @@ sap.ui.define([
526
805
 
527
806
  var fnCalcArrowPoint = function (oArrowVertex) {
528
807
  var fFixedPosition = FIXED_ARROW_POSITION;
529
- // First calculate where the center of the arrow is
530
- if (sPosition === ArrowPosition.Middle) {
808
+ // Handle multiple directed arrows with precise positioning along segments
809
+ if (sPosition === "multipleSource" || sPosition === "multipleTarget") {
810
+ // Use the exact position calculated to be precisely 44px away
811
+ oArrowCenter = oFragVector.exactPosition || {
812
+ x: oFragVector.center.x + (oFragVector.apex.x - oFragVector.center.x) * (oFragVector.segmentRatio || 0.5),
813
+ y: oFragVector.center.y + (oFragVector.apex.y - oFragVector.center.y) * (oFragVector.segmentRatio || 0.5)
814
+ };
815
+ } else if (sPosition === ArrowPosition.Middle) { // First calculate where the center of the arrow is
531
816
  oArrowCenter = {
532
817
  x: (oFragVector.apex.x - oFragVector.center.x) * RELATIVE_ARROW_POSITION + oFragVector.center.x,
533
818
  y: (oFragVector.apex.y - oFragVector.center.y) * RELATIVE_ARROW_POSITION + oFragVector.center.y
@@ -581,19 +866,47 @@ sap.ui.define([
581
866
  /**
582
867
  * @private
583
868
  */
584
- Line.prototype._getAccessibilityLabel = function () {
585
- var sFromNodeTitle = this.getFromNode().getTitle();
586
- var sFromNodeText = sFromNodeTitle ? sFromNodeTitle : this.getFromNode().getAltText();
869
+ Line.prototype._getAccessibilityLabel = function (oGraph) {
870
+ const aSentenceParts = [];
871
+ const sConnectionType = this.getConnectionType();
872
+ const oMapping = oGraph.getConnectionTypeMapping();
873
+ // In RTL mode, left and right sides of nodes are semantically swapped
874
+ const bRTL = this.getParent()._bIsRtl;
875
+ const sResolvedType = bRTL ? (Utils.mRTLSwap[sConnectionType] || sConnectionType) : sConnectionType;
876
+ const sConnectionTypeText = oMapping[sResolvedType];
877
+ const sFromNodeTitle = this.getFromNode().getTitle();
878
+ const sFromNodeText = sFromNodeTitle ? sFromNodeTitle : this.getFromNode().getAltText();
879
+ const sToNodeTitle = this.getToNode().getTitle();
880
+ const sToNodeText = sToNodeTitle ? sToNodeTitle : this.getToNode().getAltText();
881
+ let sConnectionText = oResourceBundle.getText('NETWORK_GRAPH_LINE_ACCESSIBILITY_CONNECTION_FROM_TO', [sFromNodeText, sToNodeText]);
882
+ if (sConnectionTypeText) {
883
+ sConnectionText = `${sConnectionTypeText} ${sConnectionText.charAt(0).toLowerCase()}${sConnectionText.slice(1)}`;
884
+ }
885
+ aSentenceParts.push(sConnectionText);
587
886
 
588
- var sToNodeTitle = this.getToNode().getTitle();
589
- var sToNodeText = sToNodeTitle ? sToNodeTitle : this.getToNode().getAltText();
590
- var sLineState = this.getSelected() ? oResourceBundle.getText("NETWORK_GRAPH_SELECTED_NODE") : "";
591
- var sAccLabel = oResourceBundle.getText("NETWORK_GRAPH_ACCESSIBILITY_LINE_LABEL", [sFromNodeText, sToNodeText]) + " " + this.getTitle();
592
- sAccLabel = sAccLabel.trim();
593
- if(sLineState) {
594
- sAccLabel += " " + sLineState;
887
+ const sStatusText = this._getStatusText(oGraph._oStatuses);
888
+
889
+ if (sStatusText) {
890
+ aSentenceParts.push(
891
+ `${oResourceBundle.getText('NETWORK_GRAPH_LINE_ACCESSIBILITY_STATUS')} ${sStatusText}`
892
+ );
893
+ }
894
+
895
+ if (this.getSelected()) {
896
+ aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_SELECTED_NODE'));
897
+ }
898
+
899
+ aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_ACCESSIBILITY_TOGGLE_STATE'));
900
+
901
+ const iIncomingConnectors = this.getFromNode().getChildLines().length;
902
+ if (iIncomingConnectors > 1) {
903
+ aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_LINE_NAVIGATION_ARROW_KEYS', [sFromNodeText]));
595
904
  }
596
- return sAccLabel + "." + oResourceBundle.getText("NETWORK_GRAPH_ACCESSIBILITY_TOGGLE_STATE");
905
+
906
+ aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_LINE_NAVIGATION_TAB_TO_TARGET', [sToNodeText]));
907
+ aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_LINE_NAVIGATION_SHIFT_TAB_TO_SOURCE', [sFromNodeText]));
908
+
909
+ return aSentenceParts.join('. ');
597
910
  };
598
911
 
599
912
  /* =========================================================== */
@@ -791,10 +1104,39 @@ sap.ui.define([
791
1104
  $line.on("mouseout", function (oEvent) {
792
1105
  this._mouseOut();
793
1106
  }.bind(this));
1107
+
1108
+ // Setup events for text box
1109
+ var oParent = this.getParent();
1110
+ if (oParent) {
1111
+ var sLineId = this._getLineId();
1112
+ var $textBox = oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]');
1113
+
1114
+ $textBox.on("click", function (oEvent) {
1115
+ this._click({
1116
+ ctrlKey: oEvent.ctrlKey,
1117
+ clientX: oEvent.clientX,
1118
+ clientY: oEvent.clientY
1119
+ });
1120
+ }.bind(this));
1121
+
1122
+ $textBox.on("mouseover", function (oEvent) {
1123
+ this._mouseOver();
1124
+ }.bind(this));
1125
+
1126
+ $textBox.on("mouseout", function (oEvent) {
1127
+ this._mouseOut();
1128
+ }.bind(this));
1129
+ }
794
1130
  };
795
1131
 
796
1132
  Line.prototype._mouseOut = function () {
797
1133
  this.$().removeClass(this.HIGHLIGHT_CLASS);
1134
+ // Remove highlight class from text box using data-line-id attribute
1135
+ var oParent = this.getParent();
1136
+ if (oParent) {
1137
+ var sLineId = this._getLineId();
1138
+ oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]').removeClass(this.HIGHLIGHT_CLASS);
1139
+ }
798
1140
  if (!this.getSelected()) {
799
1141
  this._setStatusColors("");
800
1142
  }
@@ -806,6 +1148,12 @@ sap.ui.define([
806
1148
  if (!this.getSelected() && bExecuteDefault) {
807
1149
  this._setStatusColors("Hover");
808
1150
  this.$().addClass(this.HIGHLIGHT_CLASS);
1151
+ // Add highlight class to text box using data-line-id attribute
1152
+ var oParent = this.getParent();
1153
+ if (oParent) {
1154
+ var sLineId = this._getLineId();
1155
+ oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]').addClass(this.HIGHLIGHT_CLASS);
1156
+ }
809
1157
  }
810
1158
  };
811
1159
 
@@ -907,10 +1255,37 @@ sap.ui.define([
907
1255
 
908
1256
  oParent._aShadedNodes = [];
909
1257
 
910
- var sFromNodeTitle = oFrom.getTitle();
1258
+ var sFromNodeTitle = oFrom.getTitle();
911
1259
  var sFromNodeText = sFromNodeTitle ? sFromNodeTitle : oFrom.getAltText();
912
1260
 
913
- var sTitle = "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipText\">" + sFromNodeText + "</span>";
1261
+ // Add connection type information (always shown in tooltip)
1262
+ var sConnectionTypeText = "";
1263
+ var sTitle = "";
1264
+
1265
+ if (this.getParent()._isNoopLayout() &&
1266
+ this.getArrowPosition() === ArrowPosition.Both) {
1267
+
1268
+ var sConnectionType = this.getConnectionType();
1269
+ var oMapping = this.getParent().getConnectionTypeMapping();
1270
+
1271
+ if (sConnectionType === ConnectionType.LeftToRight) {
1272
+ sConnectionTypeText = oMapping.LeftToRight || ConnectionType.LeftToRight;
1273
+ } else if (sConnectionType === ConnectionType.RightToLeft) {
1274
+ sConnectionTypeText = oMapping.RightToLeft || ConnectionType.RightToLeft;
1275
+ } else if (sConnectionType === ConnectionType.LeftToLeft) {
1276
+ sConnectionTypeText = oMapping.LeftToLeft || ConnectionType.LeftToLeft;
1277
+ } else if (sConnectionType === ConnectionType.RightToRight) {
1278
+ sConnectionTypeText = oMapping.RightToRight || ConnectionType.RightToRight;
1279
+ } else {
1280
+ sConnectionTypeText = oMapping.RightToLeft || ConnectionType.RightToLeft;
1281
+ }
1282
+
1283
+ if (sConnectionTypeText) {
1284
+ sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipText\">" + sConnectionTypeText + "</span><br/>";
1285
+ }
1286
+ }
1287
+
1288
+ sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipText\">" + sFromNodeText + "</span>";
914
1289
 
915
1290
  if (this._isBothArrow()) {
916
1291
  sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipArrow sapSuiteUiCommonsNetworkGraphLineTooltipDualArrow\"></span>"
@@ -923,7 +1298,7 @@ sap.ui.define([
923
1298
  sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipArrow\"></span>" + "</br>";
924
1299
  }
925
1300
 
926
- var sToNodeTitle = oTo.getTitle();
1301
+ var sToNodeTitle = oTo.getTitle();
927
1302
  var sToNodeText = sToNodeTitle ? sToNodeTitle : oTo.getAltText();
928
1303
 
929
1304
  sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipText\">" + sToNodeText + "</span>";
@@ -1092,6 +1467,18 @@ sap.ui.define([
1092
1467
  if (bFocus) {
1093
1468
  this._renderFocusWrapper();
1094
1469
  }
1470
+
1471
+ // Apply focus class to text box for blue border
1472
+ var oParent = this.getParent();
1473
+ if (oParent) {
1474
+ var sLineId = this._getLineId();
1475
+ var $textBox = oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]');
1476
+ if (bFocus) {
1477
+ $textBox.addClass(this.FOCUS_CLASS);
1478
+ } else {
1479
+ $textBox.removeClass(this.FOCUS_CLASS);
1480
+ }
1481
+ }
1095
1482
  };
1096
1483
 
1097
1484
  Line.prototype._isEndPosition = function () {
@@ -1108,9 +1495,29 @@ sap.ui.define([
1108
1495
  if (bCollapse) {
1109
1496
  this.$().hide();
1110
1497
  this._bIsHidden = true;
1498
+ // Also hide the line text label if it exists
1499
+ this._hideShowLineText(true);
1111
1500
  } else if (!this.getToNode()._bIsHidden && !this.getFromNode()._bIsHidden) {
1112
1501
  this.$().show();
1113
1502
  this._bIsHidden = false;
1503
+ // Also show the line text label if it exists
1504
+ this._hideShowLineText(false);
1505
+ }
1506
+ };
1507
+
1508
+ /**
1509
+ * Hides or shows the line text label element
1510
+ * @param {boolean} bHide True to hide, false to show
1511
+ * @private
1512
+ */
1513
+ Line.prototype._hideShowLineText = function (bHide) {
1514
+ // Use the same ID pattern that was set during _renderLineText
1515
+ // The text box ID is: this.getId() + "-textbox-middle"
1516
+ var sTextBoxId = this.getId() + "-textbox-middle";
1517
+ var oTextBox = document.getElementById(sTextBoxId);
1518
+
1519
+ if (oTextBox) {
1520
+ oTextBox.style.display = bHide ? "none" : "";
1114
1521
  }
1115
1522
  };
1116
1523
 
@@ -1220,10 +1627,10 @@ sap.ui.define([
1220
1627
  var LINE_TITLE_LENGTH = 25;
1221
1628
  var sTitle = this.getTitle() ? (this.getTitle() + " ") : "";
1222
1629
 
1223
- var sFromNodeTitle = this.getFromNode().getTitle();
1630
+ var sFromNodeTitle = this.getFromNode().getTitle();
1224
1631
  var sFromNodeText = sFromNodeTitle ? sFromNodeTitle : this.getFromNode().getAltText();
1225
1632
 
1226
- var sToNodeTitle = this.getToNode().getTitle();
1633
+ var sToNodeTitle = this.getToNode().getTitle();
1227
1634
  var sToNodeText = sToNodeTitle ? sToNodeTitle : this.getToNode().getAltText();
1228
1635
 
1229
1636
  return sTitle + "(" + Utils.trimText(sFromNodeText, LINE_TITLE_LENGTH) + " -> "
@@ -1258,5 +1665,390 @@ sap.ui.define([
1258
1665
  return false;
1259
1666
  };
1260
1667
 
1668
+ /**
1669
+ * Calculates the total length of the line by summing up all the segment distances.
1670
+ * @returns {number} Total line length in pixels
1671
+ * @private
1672
+ */
1673
+ Line.prototype._calculateTotalLineLength = function () {
1674
+ var oCoords = this.getCoordinates();
1675
+ var fTotalLength = 0;
1676
+
1677
+ if (!oCoords || oCoords.length < 2) {
1678
+ return 0;
1679
+ }
1680
+
1681
+ // Sum the distance of each line segment
1682
+ for (var i = 0; i < oCoords.length - 1; i++) {
1683
+ var fSegmentLength = Math.sqrt(
1684
+ Math.pow(oCoords[i + 1].getX() - oCoords[i].getX(), 2) +
1685
+ Math.pow(oCoords[i + 1].getY() - oCoords[i].getY(), 2)
1686
+ );
1687
+ fTotalLength += fSegmentLength;
1688
+ }
1689
+
1690
+ return fTotalLength;
1691
+ };
1692
+
1693
+ /**
1694
+ * Determines if a line is long enough to accommodate two arrows at 44px from each anchor
1695
+ * @param {number} fLineLength - Total length of the line
1696
+ * @returns {boolean} True if line can fit two arrows with proper spacing
1697
+ * @private
1698
+ */
1699
+ Line.prototype._canLineFitTwoArrows = function (fLineLength) {
1700
+ // Minimum line length calculation:
1701
+ // - 44px from source anchor
1702
+ // - 44px from target anchor
1703
+ // - 20px minimum spacing between arrows
1704
+ // Total: 108px minimum
1705
+ var fMinLineLength = (MULTIPLE_ARROW_DISTANCE * 2) + 20;
1706
+
1707
+ // Add some buffer for curved/bent lines where the actual path might be longer
1708
+ // but the arrow positions could still overlap
1709
+ var fSafeLineLength = fMinLineLength + 10; // 118px total
1710
+
1711
+ return fLineLength >= fSafeLineLength;
1712
+ };
1713
+
1714
+ Line.prototype._shouldRenderSourceArrow = function () {
1715
+ if (!this._hasMultipleDirectedArrows()) {
1716
+ return false;
1717
+ }
1718
+
1719
+ var oSourceNode = this.getFromNode();
1720
+ if (!oSourceNode) {
1721
+ return true; // Fallback: show arrow if source node not found
1722
+ }
1723
+
1724
+ var aOutgoingLines = oSourceNode.getChildLines();
1725
+
1726
+ // If source node has only one outgoing line, show the arrow
1727
+ if (!aOutgoingLines || aOutgoingLines.length <= 1) {
1728
+ return true;
1729
+ }
1730
+
1731
+ // Filter valid outgoing lines first
1732
+ var aValidOutgoingLines = aOutgoingLines.filter(function(oLine) {
1733
+ return oLine && oLine.getVisible() && !oLine._isIgnored();
1734
+ });
1735
+
1736
+ if (aValidOutgoingLines.length <= 1) {
1737
+ return true;
1738
+ }
1739
+
1740
+ // Group outgoing lines by their direction/side from the source node
1741
+ var oLinesByDirection = this._groupLinesByDirection(aValidOutgoingLines, oSourceNode, "outgoing");
1742
+
1743
+ // Get the direction of this line
1744
+ var sThisLineDirection = this._getLineDirection(this, oSourceNode, "outgoing");
1745
+
1746
+ // Get lines going in the same direction as this line
1747
+ var aLinesInSameDirection = oLinesByDirection[sThisLineDirection] || [];
1748
+
1749
+ if (aLinesInSameDirection.length <= 1) {
1750
+ return true;
1751
+ }
1752
+
1753
+ // Sort lines by their ID to ensure consistent ordering
1754
+ aLinesInSameDirection.sort(function(a, b) {
1755
+ return a._getLineId().localeCompare(b._getLineId());
1756
+ });
1757
+
1758
+ // Show arrow only on the first line in the sorted list for this direction
1759
+ return aLinesInSameDirection[0] === this;
1760
+ };
1761
+
1762
+ /**
1763
+ * Checks if multiple directed arrows feature is enabled at the graph level
1764
+ * @returns {boolean} True if the feature is enabled
1765
+ * @private
1766
+ */
1767
+ Line.prototype._hasMultipleDirectedArrows = function () {
1768
+ var oParent = this.getParent();
1769
+ return oParent && oParent._enableMultipleDirectedArrows === true;
1770
+ };
1771
+
1772
+ /**
1773
+ * Groups lines by their direction relative to a node
1774
+ * @param {array} aLines - Array of lines to group
1775
+ * @param {object} oNode - The reference node
1776
+ * @param {string} sType - "outgoing" or "incoming"
1777
+ * @returns {object} Object with direction as key and array of lines as value
1778
+ * @private
1779
+ */
1780
+ Line.prototype._groupLinesByDirection = function (aLines, oNode, sType) {
1781
+ var oGroupedLines = {
1782
+ "right": [],
1783
+ "left": [],
1784
+ "up": [],
1785
+ "down": []
1786
+ };
1787
+
1788
+ aLines.forEach(function(oLine) {
1789
+ var sDirection = this._getLineDirection(oLine, oNode, sType);
1790
+ if (oGroupedLines[sDirection]) {
1791
+ oGroupedLines[sDirection].push(oLine);
1792
+ }
1793
+ }.bind(this));
1794
+
1795
+ return oGroupedLines;
1796
+ };
1797
+
1798
+ /**
1799
+ * Determines the direction of a line relative to a node based on connection anchors
1800
+ * @param {object} oLine - The line object
1801
+ * @param {object} oNode - The reference node
1802
+ * @param {string} sType - "outgoing" or "incoming"
1803
+ * @returns {string} Direction: "right", "left", "up", or "down"
1804
+ * @private
1805
+ */
1806
+ Line.prototype._getLineDirection = function (oLine, oNode, sType) {
1807
+ // Validate inputs
1808
+ if (!oLine || !oNode) {
1809
+ return "right"; // Default fallback
1810
+ }
1811
+
1812
+ var oOtherNode = sType === "outgoing" ? oLine.getToNode() : oLine.getFromNode();
1813
+ if (!oOtherNode) {
1814
+ return "right"; // Default fallback
1815
+ }
1816
+
1817
+ // Use the actual line coordinates (source/target anchors) instead of node centers
1818
+ var oSourceCoords = oLine.getSource();
1819
+ var oTargetCoords = oLine.getTarget();
1820
+ var oNodeCenter = oNode.getCenterPosition();
1821
+
1822
+ // Validate coordinates
1823
+ if (!oSourceCoords || !oTargetCoords || !oNodeCenter ||
1824
+ typeof oSourceCoords.getX !== 'function' || typeof oSourceCoords.getY !== 'function' ||
1825
+ typeof oTargetCoords.getX !== 'function' || typeof oTargetCoords.getY !== 'function' ||
1826
+ typeof oNodeCenter.x !== 'number' || typeof oNodeCenter.y !== 'number') {
1827
+ return "right"; // Default fallback
1828
+ }
1829
+
1830
+ // Get the anchor point coordinates
1831
+ var fSourceX = oSourceCoords.getX();
1832
+ var fSourceY = oSourceCoords.getY();
1833
+ var fTargetX = oTargetCoords.getX();
1834
+ var fTargetY = oTargetCoords.getY();
1835
+
1836
+ // Determine which anchor point belongs to our reference node
1837
+ var fAnchorX, fAnchorY;
1838
+ if (sType === "outgoing") {
1839
+ // For outgoing lines, use the source anchor (where line starts from our node)
1840
+ fAnchorX = fSourceX;
1841
+ fAnchorY = fSourceY;
1842
+ } else {
1843
+ // For incoming lines, use the target anchor (where line ends at our node)
1844
+ fAnchorX = fTargetX;
1845
+ fAnchorY = fTargetY;
1846
+ }
1847
+
1848
+ // Calculate the direction based on anchor position relative to node center
1849
+ var fDeltaX = fAnchorX - oNodeCenter.x;
1850
+ var fDeltaY = fAnchorY - oNodeCenter.y;
1851
+
1852
+ // Use threshold to handle edge cases
1853
+ var fThreshold = 5.0; // 5px threshold for anchor-based detection
1854
+ var fAbsDeltaX = Math.abs(fDeltaX);
1855
+ var fAbsDeltaY = Math.abs(fDeltaY);
1856
+
1857
+ // If anchor is very close to center, fall back to target direction
1858
+ if (fAbsDeltaX < fThreshold && fAbsDeltaY < fThreshold) {
1859
+ // Use direction to other node as fallback
1860
+ var oOtherCenter = oOtherNode.getCenterPosition();
1861
+ if (oOtherCenter) {
1862
+ fDeltaX = oOtherCenter.x - oNodeCenter.x;
1863
+ fDeltaY = oOtherCenter.y - oNodeCenter.y;
1864
+ fAbsDeltaX = Math.abs(fDeltaX);
1865
+ fAbsDeltaY = Math.abs(fDeltaY);
1866
+ } else {
1867
+ return "right"; // Final fallback
1868
+ }
1869
+ }
1870
+
1871
+ // Determine direction based on larger delta
1872
+ if (fAbsDeltaX > fAbsDeltaY + fThreshold) {
1873
+ return fDeltaX > 0 ? "right" : "left";
1874
+ } else if (fAbsDeltaY > fAbsDeltaX + fThreshold) {
1875
+ return fDeltaY > 0 ? "down" : "up";
1876
+ } else {
1877
+ // When deltas are nearly equal, prefer horizontal direction for consistency
1878
+ return fDeltaX >= 0 ? "right" : "left";
1879
+ }
1880
+ };
1881
+
1882
+ /**
1883
+ * Finds the optimal segment for placing arrows on bent lines by traveling along the path
1884
+ * @param {string} sDirection - "forward" from source or "backward" from target
1885
+ * @param {number} fDistance - Distance to travel along the line (e.g., 44px)
1886
+ * @returns {object} Segment information with exact position 44px away from anchor
1887
+ * @private
1888
+ */
1889
+ Line.prototype._findOptimalArrowSegment = function (sDirection, fDistance) {
1890
+ var oCoords = this.getCoordinates(),
1891
+ iLastIndex = oCoords.length - 1,
1892
+ fTotalDistance = 0,
1893
+ fSegmentDistance,
1894
+ iSegmentIndex,
1895
+ fRemainingDistance,
1896
+ i;
1897
+
1898
+ // Threshold to detect if arrow is too close to a vertex/bend (10px)
1899
+ var fVertexThreshold = 10;
1900
+
1901
+ if (sDirection === "forward") {
1902
+ // Travel from source towards target
1903
+ for (i = 0; i < iLastIndex; i++) {
1904
+ fSegmentDistance = Math.sqrt(
1905
+ Math.pow(oCoords[i + 1].getX() - oCoords[i].getX(), 2) +
1906
+ Math.pow(oCoords[i + 1].getY() - oCoords[i].getY(), 2)
1907
+ );
1908
+
1909
+ if (fTotalDistance + fSegmentDistance >= fDistance) {
1910
+ // Found the segment where 44px distance falls
1911
+ fRemainingDistance = fDistance - fTotalDistance;
1912
+ iSegmentIndex = i;
1913
+ break;
1914
+ }
1915
+ fTotalDistance += fSegmentDistance;
1916
+ }
1917
+ } else {
1918
+ // Travel from target towards source
1919
+ for (i = iLastIndex; i > 0; i--) {
1920
+ fSegmentDistance = Math.sqrt(
1921
+ Math.pow(oCoords[i].getX() - oCoords[i - 1].getX(), 2) +
1922
+ Math.pow(oCoords[i].getY() - oCoords[i - 1].getY(), 2)
1923
+ );
1924
+
1925
+ if (fTotalDistance + fSegmentDistance >= fDistance) {
1926
+ // Found the segment where 44px distance falls
1927
+ fRemainingDistance = fDistance - fTotalDistance;
1928
+ iSegmentIndex = i - 1; // Use the segment index format (i to i+1)
1929
+ break;
1930
+ }
1931
+ fTotalDistance += fSegmentDistance;
1932
+ }
1933
+ }
1934
+
1935
+ // Fallback to first/last segment if distance is too large
1936
+ if (typeof iSegmentIndex === "undefined") {
1937
+ iSegmentIndex = sDirection === "forward" ? 0 : Math.max(0, iLastIndex - 1);
1938
+ fRemainingDistance = sDirection === "forward" ? fDistance : fDistance;
1939
+ }
1940
+
1941
+ // Calculate exact position along the segment
1942
+ var fSegmentLength = Math.sqrt(
1943
+ Math.pow(oCoords[iSegmentIndex + 1].getX() - oCoords[iSegmentIndex].getX(), 2) +
1944
+ Math.pow(oCoords[iSegmentIndex + 1].getY() - oCoords[iSegmentIndex].getY(), 2)
1945
+ );
1946
+
1947
+ var fRatio = fSegmentLength > 0 ? (fRemainingDistance / fSegmentLength) : 0;
1948
+ if (sDirection === "backward") {
1949
+ fRatio = 1 - fRatio; // Reverse the ratio for backward direction
1950
+ }
1951
+
1952
+ // Check if arrow position is too close to a vertex (bend point)
1953
+ var fDistanceToStart = fRatio * fSegmentLength;
1954
+ var fDistanceToEnd = (1 - fRatio) * fSegmentLength;
1955
+ var fAdjustedSegmentLength;
1956
+
1957
+ // Strategy: When arrow is too close to a bend, prefer moving AWAY from the anchor
1958
+ // to avoid overlap with node/port. This means:
1959
+ // - For "forward" direction (from source): prefer moving to next segment (away from source)
1960
+ // - For "backward" direction (from target): prefer moving to previous segment (away from target)
1961
+
1962
+ if (sDirection === "forward") {
1963
+ // Arrow traveling from source: prefer moving forward (away from source) when near bend
1964
+ if (fDistanceToEnd < fVertexThreshold && iSegmentIndex + 1 < iLastIndex) {
1965
+ // Too close to end of segment - move to next segment (preferred)
1966
+ fAdjustedSegmentLength = Math.sqrt(
1967
+ Math.pow(oCoords[iSegmentIndex + 2].getX() - oCoords[iSegmentIndex + 1].getX(), 2) +
1968
+ Math.pow(oCoords[iSegmentIndex + 2].getY() - oCoords[iSegmentIndex + 1].getY(), 2)
1969
+ );
1970
+
1971
+ if (fAdjustedSegmentLength > fVertexThreshold) {
1972
+ // Move arrow to start of next segment plus threshold
1973
+ iSegmentIndex = iSegmentIndex + 1;
1974
+ fRatio = fVertexThreshold / fAdjustedSegmentLength;
1975
+ fSegmentLength = fAdjustedSegmentLength;
1976
+ } else {
1977
+ // Next segment too short, stay on current segment away from vertex
1978
+ fRatio = Math.max(0, (fSegmentLength - fVertexThreshold) / fSegmentLength);
1979
+ }
1980
+ } else if (fDistanceToStart < fVertexThreshold) {
1981
+ // Too close to start of segment - only move back if critically close to source anchor
1982
+ // This is less preferred as it moves closer to the source node/port
1983
+ if (fDistanceToStart < 5 && iSegmentIndex > 0) {
1984
+ // Only if extremely close (< 5px) and previous segment exists
1985
+ fAdjustedSegmentLength = Math.sqrt(
1986
+ Math.pow(oCoords[iSegmentIndex].getX() - oCoords[iSegmentIndex - 1].getX(), 2) +
1987
+ Math.pow(oCoords[iSegmentIndex].getY() - oCoords[iSegmentIndex - 1].getY(), 2)
1988
+ );
1989
+
1990
+ if (fAdjustedSegmentLength > fVertexThreshold) {
1991
+ iSegmentIndex = iSegmentIndex - 1;
1992
+ fRatio = (fAdjustedSegmentLength - fVertexThreshold) / fAdjustedSegmentLength;
1993
+ fSegmentLength = fAdjustedSegmentLength;
1994
+ }
1995
+ } else {
1996
+ // Stay on current segment, move away from start vertex
1997
+ fRatio = Math.min(1, fVertexThreshold / fSegmentLength);
1998
+ }
1999
+ }
2000
+ } else if (sDirection === "backward") {
2001
+ // Arrow traveling from target: prefer moving backward (away from target) when near bend
2002
+ if (fDistanceToStart < fVertexThreshold && iSegmentIndex > 0) {
2003
+ // Too close to start of segment - move to previous segment (preferred)
2004
+ fAdjustedSegmentLength = Math.sqrt(
2005
+ Math.pow(oCoords[iSegmentIndex].getX() - oCoords[iSegmentIndex - 1].getX(), 2) +
2006
+ Math.pow(oCoords[iSegmentIndex].getY() - oCoords[iSegmentIndex - 1].getY(), 2)
2007
+ );
2008
+
2009
+ if (fAdjustedSegmentLength > fVertexThreshold) {
2010
+ // Move arrow to end of previous segment minus threshold
2011
+ iSegmentIndex = iSegmentIndex - 1;
2012
+ fRatio = (fAdjustedSegmentLength - fVertexThreshold) / fAdjustedSegmentLength;
2013
+ fSegmentLength = fAdjustedSegmentLength;
2014
+ } else {
2015
+ // Previous segment too short, stay on current segment away from vertex
2016
+ fRatio = Math.min(1, fVertexThreshold / fSegmentLength);
2017
+ }
2018
+ } else if (fDistanceToEnd < fVertexThreshold) {
2019
+ // Too close to end of segment - only move forward if critically close to target anchor
2020
+ // This is less preferred as it moves closer to the target node/port
2021
+ if (fDistanceToEnd < 5 && iSegmentIndex + 1 < iLastIndex) {
2022
+ // Only if extremely close (< 5px) and next segment exists
2023
+ fAdjustedSegmentLength = Math.sqrt(
2024
+ Math.pow(oCoords[iSegmentIndex + 2].getX() - oCoords[iSegmentIndex + 1].getX(), 2) +
2025
+ Math.pow(oCoords[iSegmentIndex + 2].getY() - oCoords[iSegmentIndex + 1].getY(), 2)
2026
+ );
2027
+
2028
+ if (fAdjustedSegmentLength > fVertexThreshold) {
2029
+ iSegmentIndex = iSegmentIndex + 1;
2030
+ fRatio = fVertexThreshold / fAdjustedSegmentLength;
2031
+ fSegmentLength = fAdjustedSegmentLength;
2032
+ }
2033
+ } else {
2034
+ // Stay on current segment, move away from end vertex
2035
+ fRatio = Math.max(0, (fSegmentLength - fVertexThreshold) / fSegmentLength);
2036
+ }
2037
+ }
2038
+ }
2039
+
2040
+ // Calculate the exact coordinates with adjusted position
2041
+ var fExactX = oCoords[iSegmentIndex].getX() + (oCoords[iSegmentIndex + 1].getX() - oCoords[iSegmentIndex].getX()) * fRatio;
2042
+ var fExactY = oCoords[iSegmentIndex].getY() + (oCoords[iSegmentIndex + 1].getY() - oCoords[iSegmentIndex].getY()) * fRatio;
2043
+
2044
+ return {
2045
+ segmentIndex: iSegmentIndex,
2046
+ ratio: fRatio,
2047
+ center: {x: oCoords[iSegmentIndex].getX(), y: oCoords[iSegmentIndex].getY()},
2048
+ apex: {x: oCoords[iSegmentIndex + 1].getX(), y: oCoords[iSegmentIndex + 1].getY()},
2049
+ exactPosition: {x: fExactX, y: fExactY}
2050
+ };
2051
+ };
2052
+
1261
2053
  return Line;
1262
2054
  });