CoordinateAssignmentStep.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import AlgorithmStep from "../AlgorithmStep.js";
  2. import { SiguiyamaContext } from "../siguiyama/SiguiyamaContext.js";
  3. import EdgeRoutingStepError from "../../errors/optimizer/EdgeRoutingStepError.js";
  4. import Edge from "../../graph/edge/Edge.js";
  5. import Layering from "../../graph/layering/Layering.js";
  6. import Node from "../../graph/node/Node.js";
  7. /**
  8. * Шаг назначения координат вершинам
  9. * Размещает вершины в двумерной плоскости по колонкам (слоям) и строкам (позициям в слое),
  10. * учитывая размеры вершин, вертикальный отступ между строками и горизонтальный отступ между слоями.
  11. */
  12. export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
  13. /**
  14. * Отступ между вершинами по вертикали внутри одной колонки.
  15. */
  16. private readonly _layerGap: number;
  17. /**
  18. * Отступ между колонками (слоями) по горизонтали.
  19. */
  20. private readonly _padding: number;
  21. /**
  22. * @param layerGap Вертикальный отступ между вершинами в колонке.
  23. * @param padding Горизонтальный отступ между слоями.
  24. * @throws {EdgeRoutingStepError} Если передан отрицательный `layerGap` или `padding`.
  25. */
  26. public constructor(layerGap: number = 100, padding: number = 60) {
  27. if(layerGap < 0)
  28. throw new EdgeRoutingStepError("Layer Gap must be greater than 0");
  29. if(padding < 0)
  30. throw new EdgeRoutingStepError("Padding must be greater than 0");
  31. super(CoordinateAssignmentStep.name);
  32. this._layerGap = layerGap;
  33. this._padding = padding;
  34. }
  35. public run(context: SiguiyamaContext): void {
  36. const { layering } = context;
  37. if(!layering)
  38. throw new Error("Layering of graph was not found!");
  39. this.assignCoordinates(layering);
  40. }
  41. /**
  42. * Назначение координат всем вершинам графа.
  43. *
  44. * Алгоритм:
  45. * - слои обходятся справа налево (`toReversed()`),
  46. * - для каждого слоя вычисляется ширина колонки как максимальная ширина вершины в слое,
  47. * - для каждой позиции `i` внутри слоя вычисляется высота строки как максимум по всем слоям в этой позиции `i`,
  48. * - вершина размещается по центру своей ячейки.
  49. * @param layering Слоистая укладка графа.
  50. */
  51. private assignCoordinates(layering: Layering<Node, Edge<Node>>) : void {
  52. const layers = layering.getLayers().toReversed();
  53. let xOffset = 0.0;
  54. for(const layer of layers) {
  55. const nodes = layer.getNodes();
  56. const width = Math.max(...nodes.map(node => node.getWidth()));
  57. let yOffset = 0.0;
  58. for(let i = 0; i < nodes.length; i++) {
  59. const node = nodes[i]!;
  60. const height = Math.max(...layers.map((l) => l.getNodes().at(i)?.getHeight() ?? 0));
  61. node.setX(xOffset + width / 2 - node.getWidth() / 2).setY(yOffset + height / 2 - node.getHeight() / 2);
  62. yOffset += height + this._layerGap;
  63. }
  64. xOffset += width + this._padding;
  65. }
  66. }
  67. }