Skip to content

Commit

Permalink
Save the column positions (#2453)
Browse files Browse the repository at this point in the history
Signed-off-by: BOUTIER Charly <[email protected]>
  • Loading branch information
EstherDarkish authored Dec 31, 2024
1 parent 518f068 commit b293d22
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 189 deletions.
37 changes: 2 additions & 35 deletions src/components/graph/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,9 @@ function compressTreePlacements(nodes: CurrentTreeNode[], placements: PlacementG
// to the maximum column placement values (per row) of the nodes on the left.
// The resulting space we find represents how much we can shift the current column to the left.

const currentNodeFamilySize = 1 + countNodes(nodes, currentNodeId as UUID);
const currentSubTreeSize = 1 + countNodes(nodes, currentNodeId as UUID);
const indexOfCurrentNode = nodes.findIndex((n) => n.id === currentNodeId);
const nodesOfTheCurrentBranch = nodes.slice(indexOfCurrentNode, indexOfCurrentNode + currentNodeFamilySize);
const nodesOfTheCurrentBranch = nodes.slice(indexOfCurrentNode, indexOfCurrentNode + currentSubTreeSize);
const currentBranchMinimumColumnByRow = getMinimumColumnByRows(nodesOfTheCurrentBranch, placements);

// We have to compare with all the left nodes, not only the current branch's left neighbor, because in some
Expand Down Expand Up @@ -343,39 +343,6 @@ export function getFirstAncestorWithSibling(
return undefined;
}

/**
* Will find the sibling node whose X position is closer to xDestination in the X range provided.
*/
export function findClosestSiblingInRange(
nodes: CurrentTreeNode[],
node: CurrentTreeNode,
xOrigin: number,
xDestination: number
): CurrentTreeNode | null {
const minX = Math.min(xOrigin, xDestination);
const maxX = Math.max(xOrigin, xDestination);
const siblingNodes = findSiblings(nodes, node);
const nodesBetween = siblingNodes.filter((n) => n.position.x < maxX && n.position.x > minX);
if (nodesBetween.length > 0) {
const closestNode = nodesBetween.reduce(
(closest, current) =>
Math.abs(current.position.x - xDestination) < Math.abs(closest.position.x - xDestination)
? current
: closest,
nodesBetween[0]
);
return closestNode;
}
return null;
}

/**
* Will find the siblings of a provided node (all siblings have the same parent).
*/
function findSiblings(nodes: CurrentTreeNode[], node: CurrentTreeNode): CurrentTreeNode[] {
return nodes.filter((n) => n.parentId === node.parentId && n.id !== node.id);
}

/**
* Computes the absolute position of a node by calculating the sum of all the relative positions of
* the node's lineage.
Expand Down
156 changes: 43 additions & 113 deletions src/components/graph/network-modification-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BUILD_STATUS } from '../network/constants';
import { UUID } from 'crypto';
import { CurrentTreeNode, isReactFlowRootNodeData } from '../../redux/reducer';
import { Edge } from '@xyflow/react';
import { NetworkModificationNodeData, RootNodeData } from './tree-node.type';
import { AbstractNode, NetworkModificationNodeData, RootNodeData } from './tree-node.type';

// Function to count children nodes for a given parentId recursively in an array of nodes.
// TODO refactoring when changing NetworkModificationTreeModel as it becomes an object containing nodes
Expand All @@ -30,119 +30,52 @@ export default class NetworkModificationTreeModel {

isAnyNodeBuilding = false;

/**
* Will switch the order of two nodes in the tree.
* The nodeToMove will be moved, either to the left or right of the destinationNode, depending
* on their initial positions.
* Both nodes should have the same parent.
*/
switchSiblingsOrder(nodeToMove: CurrentTreeNode, destinationNode: CurrentTreeNode) {
if (!nodeToMove.parentId || nodeToMove.parentId !== destinationNode.parentId) {
console.error('Both nodes should have the same parent to switch their order');
return;
}
const nodeToMoveIndex = this.treeNodes.findIndex((node) => node.id === nodeToMove.id);
const destinationNodeIndex = this.treeNodes.findIndex((node) => node.id === destinationNode.id);

const numberOfNodesToMove: number = 1 + countNodes(this.treeNodes, nodeToMove.id);
const nodesToMove = this.treeNodes.splice(nodeToMoveIndex, numberOfNodesToMove);

if (nodeToMoveIndex > destinationNodeIndex) {
this.treeNodes.splice(destinationNodeIndex, 0, ...nodesToMove);
} else {
// When moving nodeToMove to the right, we have to take into account the splice function that changed the nodes' indexes.
// We also need to find the correct position of nodeToMove, to the right of the destination node, meaning we need to find
// how many children the destination node has and add all of them to the new index.
const destinationNodeIndexAfterSplice = this.treeNodes.findIndex((node) => node.id === destinationNode.id);
const destinationNodeFamilySize: number = 1 + countNodes(this.treeNodes, destinationNode.id);
this.treeNodes.splice(destinationNodeIndexAfterSplice + destinationNodeFamilySize, 0, ...nodesToMove);
// Will sort if columnPosition is defined, and not move the nodes if undefined
childrenNodeSorter(a: AbstractNode, b: AbstractNode) {
if (a.columnPosition !== undefined && b.columnPosition !== undefined) {
return a.columnPosition - b.columnPosition;
}

this.treeNodes = [...this.treeNodes];
return 0;
}

/**
* Finds the lowest common ancestor of two nodes in the tree.
*
* Example tree:
* A
* / \
* B D
* / / \
* C E F
*
* Examples:
* - getCommonAncestor(B, E) will return A
* - getCommonAncestor(E, F) will return D
*/
getCommonAncestor(nodeA: CurrentTreeNode, nodeB: CurrentTreeNode): CurrentTreeNode | null {
const getAncestors = (node: CurrentTreeNode) => {
const ancestors = [];
let current: CurrentTreeNode | undefined = node;
while (current && current.parentId) {
const parentId: string = current.parentId;
ancestors.push(parentId);
current = this.treeNodes.find((n) => n.id === parentId);
}
return ancestors;
};
// We get the entire ancestors of one of the nodes in an array, then iterate over the other node's ancestors
// until we find a node that is in the first array : this common node is an ancestor of both intial nodes.
const ancestorsA: string[] = getAncestors(nodeA);
let current: CurrentTreeNode | undefined = nodeB;
while (current && current.parentId) {
const parentId: string = current.parentId;
current = this.treeNodes.find((n) => n.id === parentId);
if (current && ancestorsA.includes(current.id)) {
return current;
}
}
console.warn('No common ancestor found !');
return null;
getChildren(parentNodeId: string): CurrentTreeNode[] {
return this.treeNodes.filter((n) => n.parentId === parentNodeId);
}

/**
* Finds the child of the ancestor node that is on the path to the descendant node.
*
* Example tree:
* A
* / \
* B D
* / / \
* C E F
*
* Examples:
* - getChildOfAncestorInLineage(A, E) will return D
* - getChildOfAncestorInLineage(D, F) will return F
*
* @param ancestor node, must be an ancestor of descendant node
* @param descendant node, must be a descendant of ancestor
* @returns The child of the ancestor node in the lineage or null if not found.
* @private
* Will reorganize treeNodes and put the children of parentNodeId in the order provided in nodeIds array.
* @param parentNodeId parent ID of the to be reordered children nodes
* @param orderedNodeIds array of children ID in the order we want
* @returns true if the order was changed
*/
getChildOfAncestorInLineage(ancestor: CurrentTreeNode, descendant: CurrentTreeNode): CurrentTreeNode | null {
let current: CurrentTreeNode | undefined = descendant;
while (current && current.parentId) {
const parentId: string = current.parentId;
if (parentId === ancestor.id) {
return current;
}
current = this.treeNodes.find((n) => n.id === parentId);
reorderChildrenNodes(parentNodeId: string, orderedNodeIds: string[]) {
// We check if the current position is already correct
const children = this.getChildren(parentNodeId);
if (orderedNodeIds.length !== children.length) {
console.warn('reorderNodes : synchronization error, reorder cancelled');
return false;
}
console.warn('The ancestor and descendant do not share the same branch !');
return null;
}

switchBranches(nodeToMove: CurrentTreeNode, destinationNode: CurrentTreeNode) {
// We find the nodes from the two branches that share the same parent
const commonAncestor = this.getCommonAncestor(nodeToMove, destinationNode);
if (commonAncestor) {
const siblingFromNodeToMoveBranch = this.getChildOfAncestorInLineage(commonAncestor, nodeToMove);
const siblingFromDestinationNodeBranch = this.getChildOfAncestorInLineage(commonAncestor, destinationNode);
if (siblingFromNodeToMoveBranch && siblingFromDestinationNodeBranch) {
this.switchSiblingsOrder(siblingFromNodeToMoveBranch, siblingFromDestinationNodeBranch);
}
if (children.map((child) => child.id).join(',') === orderedNodeIds.join(',')) {
// Already in the same order.
return false;
}
// Let's reorder the children :
// In orderedNodeIds order, we cut and paste the corresponding number of nodes in treeNodes.
const justAfterParentIndex = 1 + this.treeNodes.findIndex((n) => n.id === parentNodeId); // we add 1 here to set the index just after the parent node
let insertedNodes = 0;

orderedNodeIds.forEach((nodeId) => {
const nodesToMoveIndex = this.treeNodes.findIndex((n) => n.id === nodeId);
const subTreeSize = 1 + countNodes(this.treeNodes, nodeId as UUID); // We add 1 here to include the current node in its subtree size

// We remove from treeNodes the nodes that we want to move, (...)
const nodesToMove = this.treeNodes.splice(nodesToMoveIndex, subTreeSize);

// (...) and now we put them in their new position in the array
this.treeNodes.splice(justAfterParentIndex + insertedNodes, 0, ...nodesToMove);
insertedNodes += subTreeSize;
});
return true;
}

addChild(
Expand Down Expand Up @@ -224,7 +157,7 @@ export default class NetworkModificationTreeModel {
});

// overwrite old children nodes parentUuid when inserting new nodes
const nextNodes = this.treeNodes.map((node) => {
this.treeNodes = this.treeNodes.map((node) => {
if (newNode.childrenIds.includes(node.id)) {
return {
...node,
Expand All @@ -233,14 +166,13 @@ export default class NetworkModificationTreeModel {
}
return node;
});

this.treeNodes = nextNodes;
this.treeEdges = filteredEdges;
}

if (!skipChildren) {
// Add children of this node recursively
if (newNode.children) {
newNode.children.sort(this.childrenNodeSorter);
newNode.children.forEach((child) => {
this.addChild(child, newNode.id, undefined, undefined);
});
Expand Down Expand Up @@ -279,7 +211,7 @@ export default class NetworkModificationTreeModel {
if (!nodeToDelete) {
return;
}
const nextTreeNodes = filteredNodes.map((node) => {
this.treeNodes = filteredNodes.map((node) => {
if (node.parentId === nodeId) {
return {
...node,
Expand All @@ -288,8 +220,6 @@ export default class NetworkModificationTreeModel {
}
return node;
});

this.treeNodes = nextTreeNodes;
});
}

Expand Down Expand Up @@ -321,6 +251,7 @@ export default class NetworkModificationTreeModel {
// handle root node
this.treeNodes.push(convertNodetoReactFlowModelNode(elements));
// handle root children
elements.children.sort(this.childrenNodeSorter);
elements.children.forEach((child) => {
this.addChild(child, elements.id);
});
Expand All @@ -329,8 +260,7 @@ export default class NetworkModificationTreeModel {

newSharedForUpdate() {
/* shallow clone of the network https://stackoverflow.com/a/44782052 */
let newTreeModel = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
return newTreeModel;
return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
}

setBuildingStatus() {
Expand Down
1 change: 1 addition & 0 deletions src/components/graph/tree-node.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type AbstractNode = {
readOnly?: boolean;
reportUuid?: UUID;
type: NodeType;
columnPosition?: number;
};

export interface NodeBuildStatus {
Expand Down
9 changes: 9 additions & 0 deletions src/components/network-modification-tree-pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
networkModificationHandleSubtree,
setNodeSelectionForCopy,
resetLogsFilter,
reorderNetworkModificationTreeNodes,
} from '../redux/actions';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -214,6 +215,14 @@ export const NetworkModificationTreePane = ({ studyUuid, studyMapTreeDisplay })
);
}
);
} else if (studyUpdatedForce.eventData.headers['updateType'] === 'nodesColumnPositionsChanged') {
const orderedChildrenNodeIds = JSON.parse(studyUpdatedForce.eventData.payload);
dispatch(
reorderNetworkModificationTreeNodes(
studyUpdatedForce.eventData.headers['parentNode'],
orderedChildrenNodeIds
)
);
} else if (studyUpdatedForce.eventData.headers['updateType'] === 'nodeMoved') {
fetchNetworkModificationTreeNode(studyUuid, studyUpdatedForce.eventData.headers['movedNode']).then(
(node) => {
Expand Down
Loading

0 comments on commit b293d22

Please sign in to comment.