export async function getAutoLayoutedInfo()

in packages/dmn-editor/src/autolayout/autoLayoutInfo.ts [81:367]


export async function getAutoLayoutedInfo({
  __readonly_snapGrid,
  __readonly_nodesById,
  __readonly_edgesById,
  __readonly_nodes,
  __readonly_drgEdges,
  __readonly_isAlternativeInputDataShape,
}: {
  __readonly_snapGrid: SnapGrid;
  __readonly_nodesById: Map<string, Node<DmnDiagramNodeData, string | undefined>>;
  __readonly_edgesById: Map<string, Edge<DmnDiagramEdgeData>>;
  __readonly_nodes: Node<DmnDiagramNodeData, string | undefined>[];
  __readonly_drgEdges: DrgEdge[];
  __readonly_isAlternativeInputDataShape: boolean;
}) {
  const parentNodesById = new Map<string, AutolayoutParentNode>();
  const nodeParentsById = new Map<string, Set<string>>();

  /**
    Used to tell ELK that dependencies of nodes' children should be considered the node's dependency too.
    This allows us to not rely on INCLUDE_STRATEGY hierarchy handling on ELK, keeping disjoint graph components separate, rendering side-by-side.
   */
  const fakeEdgesForElk = new Set<Elk.ElkExtendedEdge>();

  const adjMatrix = getAdjMatrix(__readonly_drgEdges);

  // 1. First we populate the `parentNodesById` map so that we know exactly what parent nodes we're dealing with. Decision Service nodes have two fake nodes to represent Output and Encapsulated sections.
  for (const node of __readonly_nodes) {
    const dependencies = new Set<string>();
    const dependents = new Set<string>();

    if (node.data?.dmnObject?.__$$element === "decisionService") {
      const { namespace } = parseXmlHref(node.id);
      const outputs = new Set([
        ...(node.data.dmnObject.outputDecision ?? []).map((s) => addNamespaceToHref({ href: s["@_href"], namespace })),
      ]);
      const encapsulated = new Set([
        ...(node.data.dmnObject.encapsulatedDecision ?? []).map((s) =>
          addNamespaceToHref({ href: s["@_href"], namespace })
        ),
      ]);

      const idOfFakeNodeForOutputSection = `${node.id}${FAKE_MARKER}dsOutput`;
      const idOfFakeNodeForEncapsulatedSection = `${node.id}${FAKE_MARKER}dsEncapsulated`;

      const dsSize = MIN_NODE_SIZES[NODE_TYPES.decisionService]({ snapGrid: __readonly_snapGrid });
      parentNodesById.set(node.id, {
        elkNode: {
          id: node.id,
          width: dsSize["@_width"],
          height: dsSize["@_height"],
          children: [
            {
              id: idOfFakeNodeForOutputSection,
              width: dsSize["@_width"],
              height: dsSize["@_height"] / 2,
              children: [],
              layoutOptions: {
                ...ELK_OPTIONS,
                ...PARENT_NODE_ELK_OPTIONS,
              },
            },
            {
              id: idOfFakeNodeForEncapsulatedSection,
              width: dsSize["@_width"],
              height: dsSize["@_height"] / 2,
              children: [],
              layoutOptions: {
                ...ELK_OPTIONS,
                ...PARENT_NODE_ELK_OPTIONS,
              },
            },
          ],
          layoutOptions: {
            "elk.algorithm": "layered",
            "elk.direction": "UP",
            "elk.aspectRatio": "9999999999",
            "elk.partitioning.activate": "true",
            "elk.spacing.nodeNode": "0",
            "elk.spacing.componentComponent": "0",
            "layered.spacing.edgeEdgeBetweenLayers": "0",
            "layered.spacing.edgeNodeBetweenLayers": "0",
            "layered.spacing.nodeNodeBetweenLayers": "0",
            "elk.padding": "[left=0, top=0, right=0, bottom=0]",
          },
        },
        decisionServiceSection: "output",
        dependencies,
        dependents,
        contained: outputs,
        contains: ({ id }) => ({
          isInside: outputs.has(id) || encapsulated.has(id),
          decisionServiceSection: outputs.has(id) ? "output" : encapsulated.has(id) ? "encapsulated" : "n/a",
        }),
        isDependencyOf: ({ id }) => dependents.has(id),
        hasDependencyTo: ({ id }) => dependencies.has(id),
      });

      fakeEdgesForElk.add({
        id: `${node.id}${FAKE_MARKER}fakeOutputEncapsulatedEdge`,
        sources: [idOfFakeNodeForEncapsulatedSection],
        targets: [idOfFakeNodeForOutputSection],
      });
    } else if (node.data?.dmnObject?.__$$element === "group") {
      const groupSize = DEFAULT_NODE_SIZES[NODE_TYPES.group]({ snapGrid: __readonly_snapGrid });
      const groupBounds = node.data.shape["dc:Bounds"];
      parentNodesById.set(node.id, {
        decisionServiceSection: "n/a",
        elkNode: {
          id: node.id,
          width: groupBounds?.["@_width"] ?? groupSize["@_width"],
          height: groupBounds?.["@_height"] ?? groupSize["@_height"],
          children: [],
          layoutOptions: {
            ...ELK_OPTIONS,
            ...PARENT_NODE_ELK_OPTIONS,
          },
        },
        dependencies,
        dependents,
        contained: new Set(),
        contains: ({ id, bounds }) => ({
          isInside: getContainmentRelationship({
            bounds: bounds!,
            container: groupBounds!,
            snapGrid: __readonly_snapGrid,
            isAlternativeInputDataShape: __readonly_isAlternativeInputDataShape,
            containerMinSizes: MIN_NODE_SIZES[NODE_TYPES.group],
            boundsMinSizes: MIN_NODE_SIZES[__readonly_nodesById.get(id)?.type as NodeType],
          }).isInside,
          decisionServiceSection: "n/a",
        }),
        isDependencyOf: ({ id }) => dependents.has(id),
        hasDependencyTo: ({ id }) => dependencies.has(id),
      });
    }
  }

  // 2. Then we map all the nodes to elkNodes, including the parents. We mutate parents on the fly when iterating over the nodes list.
  const elkNodes = __readonly_nodes.flatMap((node) => {
    const parent = parentNodesById.get(node.id);
    if (parent) {
      return [];
    }

    const defaultSize = DEFAULT_NODE_SIZES[node.type as NodeType]({
      snapGrid: __readonly_snapGrid,
      isAlternativeInputDataShape: __readonly_isAlternativeInputDataShape,
    });
    const elkNode: Elk.ElkNode = {
      id: node.id,
      width: node.data.shape["dc:Bounds"]?.["@_width"] ?? defaultSize["@_width"],
      height: node.data.shape["dc:Bounds"]?.["@_height"] ?? defaultSize["@_height"],
      children: [],
      layoutOptions: {
        "partitioning.partition":
          // Since textAnnotations and knowledgeSources are not related to the logic, we leave them at the bottom.
          (node.type as NodeType) === NODE_TYPES.textAnnotation ||
          (node.type as NodeType) === NODE_TYPES.knowledgeSource
            ? "0"
            : "1",
      },
    };

    // FIXME: Tiago --> Improve performance here as part of https://github.com/apache/incubator-kie-issues/issues/451.
    const parents = [...parentNodesById.values()].filter(
      (p) => p.contains({ id: elkNode.id, bounds: node.data.shape["dc:Bounds"] }).isInside
    );
    if (parents.length > 0) {
      const decisionServiceSection = parents[0].contains({
        id: elkNode.id,
        bounds: node.data.shape["dc:Bounds"],
      }).decisionServiceSection;

      // The only relationship that ELK will know about is the first matching container for this node.
      if (decisionServiceSection === "n/a") {
        parents[0].elkNode.children?.push(elkNode);
      } else if (decisionServiceSection === "output") {
        parents[0].elkNode.children?.[0].children?.push(elkNode);
      } else if (decisionServiceSection === "encapsulated") {
        parents[0].elkNode.children?.[1].children?.push(elkNode);
      } else {
        throw new Error(`Unknown decisionServiceSection ${decisionServiceSection}`);
      }

      for (const p of parents) {
        p.contained?.add(elkNode.id); // We need to keep track of nodes that are contained by multiple groups, but ELK will only know about one of those containment relationships.
        nodeParentsById.set(node.id, new Set([...(nodeParentsById.get(node.id) ?? []), p.elkNode.id]));
      }
      return [];
    }

    return [elkNode];
  });

  // 3. After we have all containment relationships defined, we can proceed to resolving the hierarchical relationships.
  for (const [_, parentNode] of parentNodesById) {
    traverse(adjMatrix, parentNode.contained, [...parentNode.contained], "down", (n) => {
      parentNode.dependencies.add(n);
    });
    traverse(adjMatrix, parentNode.contained, [...parentNode.contained], "up", (n) => {
      parentNode.dependents.add(n);
    });

    const p = __readonly_nodesById.get(parentNode.elkNode.id);
    if (p?.type === NODE_TYPES.group && parentNode.elkNode.children?.length === 0) {
      continue; // Ignore empty group nodes.
    } else {
      elkNodes.push(parentNode.elkNode);
    }
  }

  // 4. After we have all containment and hierarchical relationships defined, we can add the fake edges so that ELK creates the structure correctly.
  for (const node of __readonly_nodes) {
    const parentNodes = [...parentNodesById.values()];

    const dependents = parentNodes.filter((p) => p.hasDependencyTo({ id: node.id }));
    for (const dependent of dependents) {
      // Not all nodes are present in all DRD
      if (__readonly_nodesById.has(node.id) && __readonly_nodesById.has(dependent.elkNode.id)) {
        fakeEdgesForElk.add({
          id: `${generateUuid()}${FAKE_MARKER}__fake`,
          sources: [node.id],
          targets: [dependent.elkNode.id],
        });
      }

      for (const p of nodeParentsById.get(node.id) ?? []) {
        // Not all nodes are present in all DRD
        if (__readonly_nodesById.has(p) && __readonly_nodesById.has(dependent.elkNode.id)) {
          fakeEdgesForElk.add({
            id: `${generateUuid()}${FAKE_MARKER}__fake`,
            sources: [p],
            targets: [dependent.elkNode.id],
          });
        }
      }
    }

    const dependencies = parentNodes.filter((p) => p.isDependencyOf({ id: node.id }));
    for (const dependency of dependencies) {
      // Not all nodes are present in all DRD
      if (__readonly_nodesById.has(node.id) && __readonly_nodesById.has(dependency.elkNode.id)) {
        fakeEdgesForElk.add({
          id: `${generateUuid()}${FAKE_MARKER}__fake`,
          sources: [dependency.elkNode.id],
          targets: [node.id],
        });
      }

      for (const p of nodeParentsById.get(node.id) ?? []) {
        // Not all nodes are present in all DRD
        if (__readonly_nodesById.has(p) && __readonly_nodesById.has(dependency.elkNode.id)) {
          fakeEdgesForElk.add({
            id: `${generateUuid()}${FAKE_MARKER}__fake`,
            sources: [dependency.elkNode.id],
            targets: [p],
          });
        }
      }
    }
  }

  // 5. Concatenate real and fake edges to pass to ELK.
  const elkEdges = [
    ...fakeEdgesForElk,
    ...[...__readonly_edgesById.values()].flatMap((e) => {
      // Not all nodes are present in all DRD
      if (__readonly_nodesById.has(e.source) && __readonly_nodesById.has(e.target)) {
        return {
          id: e.id,
          sources: [e.source],
          targets: [e.target],
        };
      } else {
        return [];
      }
    }),
  ];

  // 6. Run ELK.
  const autoLayoutedInfo = await runElk(elkNodes, elkEdges, ELK_OPTIONS);
  return {
    __readonly_autoLayoutedInfo: autoLayoutedInfo,
    __readonly_parentNodesById: parentNodesById,
  };
}