diff --git a/src/composables/countIntersections.ts b/src/composables/CalculateOverlaps.ts similarity index 51% rename from src/composables/countIntersections.ts rename to src/composables/CalculateOverlaps.ts index f833b1a2cbbb22888eeeb9fe157d3f3b81dba9ec..1e38ed06d62905f0b96f3b83404e37a2ef3a0fff 100644 --- a/src/composables/countIntersections.ts +++ b/src/composables/CalculateOverlaps.ts @@ -1,28 +1,343 @@ +// Type import import { Node } from '@metabohub/viz-core/src/types/Node'; import { Network } from "@metabohub/viz-core/src/types/Network"; -import { checkIntersection } from 'line-intersect'; import { Link } from '@metabohub/viz-core/src/types/Link'; -import { c } from 'vite/dist/node/types.d-aGj9QkWt'; import { GraphStyleProperties } from '@metabohub/viz-core/src/types/GraphStyleProperties'; -import { getSizeNodePixel } from './CalculateSize'; -import { link } from 'fs'; import { Coordinate, Size } from '../types/CoordinatesSize'; -// CHNAGE THE CODE : SEE METRICSNETWORK (for end of internship : keep this one) +// Composable imports +import { getSizeNodePixel } from './CalculateSize'; + +// General imports +import { checkIntersection } from 'line-intersect'; /** - * Check if the coordinates (x, y) are the same as the node's coordinates - * @param node the node - * @param x coordinate - * @param y coordinate - * @returns a boolean + * This file contains the functions to count the number of intersections in a network and the number of overlapping nodes and edges. + * + * ********************************* + * + * 0. Edge intersection + * + * -> isIntersectionGraph : + * check if there is any intersection between edges in the network + * + * -> edgesIntersection : + * check if there is any intersection between edges + * + * -> commonEndBetween2Edges : + * check if there is a common end between two edges + * + * -> sameCoordinates : + * check if two nodes have the same coordinates + * + * ********************************* + * + * 1. Nodes overlap + * + * -> isOverlapNodes : + * check if there is any overlap between nodes in the network + * + * -> nodeOverlap : + * check if two nodes overlap + * + * ********************************* + * + * 2. Node Edge overlap + * + * -> isOverlapNodesEdges : + * check if there is any overlap between nodes and edges in the network + * + * -> nodeEdgeOverlap : + * check if a node overlaps with an edge + * + * -> isPointInsideRect : + * check if a point is inside a rectangle + * + */ + + + + + +/*******************************************************************************************************************************************************/ +//_________________________________________________________0. Edge intersection ________________________________________________________________________ + + + +/** + * Determines if a given graph has any intersecting edges. + * + * @param nodes - An object where keys are node identifiers and values are coordinates of the center of the nodes. + * @param links - An array of link objects, each containing a source and target node identifier. + * @returns A boolean indicating whether any edges in the graph intersect. */ -function isNodeCoord(node: {x:number,y:number}, x: number, y: number): boolean { - return (node.x == x && node.y == y); +export function isIntersectionGraph(nodes: {[key:string]:Coordinate},links:{source:string,target:string}[]): boolean { + for (let i=0 ; i<links.length ; i++) { + for (let j=i+1 ; j<links.length ; j++) { + const link1=links[i]; + const link2=links[j]; + // check if intersection + let node1Link1=nodes[link1.source]; + let node2Link1=nodes[link1.target]; + let node1Link2=nodes[link2.source]; + let node2Link2=nodes[link2.target]; + if (edgesIntersection(node1Link1,node2Link1,node1Link2,node2Link2)){ + return true; + } + } + } + return false; } +/** + * Determines if two edges intersect. If the edges share a common end, they are not considered to intersect. + * + * @param node1Link1 - The coordinates of the first point of the first edge. + * @param node2Link1 - The coordinates of the second point of the first edge. + * @param node1Link2 - The coordinates of the first point of the second edge. + * @param node2Link2 - The coordinates of the second point of the second edge. + * @returns `true` if the edges intersect, otherwise `false`. + */ +function edgesIntersection(node1Link1:{x:number,y:number},node2Link1:{x:number,y:number},node1Link2:{x:number,y:number},node2Link2:{x:number,y:number}): boolean { + // case node in common + if (commonEndBetween2Edges(node1Link1,node2Link1,node1Link2,node2Link2)) { + return false; + } + const result = checkIntersection(node1Link1.x, node1Link1.y, node2Link1.x, node2Link1.y, node1Link2.x, node1Link2.y, node2Link2.x, node2Link2.y); + if (result.type == "intersecting") { + return true; + }else{ + return false; + } +} + +/** + * Determines if there is a common end (same end coordinates) between two edges. + * + * @param node1Link1 - The coordinates of the first node of the first edge. + * @param node2Link1 - The coordinates of the second node of the first edge. + * @param node1Link2 - The coordinates of the first node of the second edge. + * @param node2Link2 - The coordinates of the second node of the second edge. + * @returns `true` if there is a common node between the two edges, otherwise `false`. + */ +function commonEndBetween2Edges(node1Link1:{x:number,y:number},node2Link1:{x:number,y:number},node1Link2:{x:number,y:number},node2Link2:{x:number,y:number}): boolean { + if (sameCoordinates(node1Link1,node1Link2) || sameCoordinates(node1Link1,node2Link2) || sameCoordinates(node2Link1,node1Link2) || sameCoordinates(node2Link1,node2Link2)) { + return true; + }else { + return false; + } +} + +/** + * Determines if two nodes have the same coordinates. + * + * @param node1 - The first node with x and y coordinates. + * @param node2 - The second node with x and y coordinates. + * @returns `true` if both nodes have the same x and y coordinates, otherwise `false`. + */ +function sameCoordinates(node1: {x:number,y:number},node2: {x:number,y:number}): boolean { + if (!node1 || !node2 || node1.x==null || node1.y==null || node2.x==null || node2.y==null) { + return false; + } + return node1.x===node2.x && node1.y===node2.y; +} + + +/*******************************************************************************************************************************************************/ +//____________________________________________________________1. Nodes overlap __________________________________________________________________________ + + + +/** + * Checks if any nodes in the given network overlap based on their positions and sizes. + * + * @param nodesPosition - An object where keys are node IDs and values are their coordinates. + * @param network - The network containing nodes and their properties. + * @param networkStyle - The style properties of the graph, used to determine node sizes. + * @returns `true` if any nodes overlap, otherwise `false`. + */ +export function isOverlapNodes(nodesPosition: {[key:string]:Coordinate},network:Network,networkStyle:GraphStyleProperties):boolean{ + const nodesID=Object.keys(nodesPosition); + for (let i=0 ; i<nodesID.length ; i++) { + for (let j=i+1 ; j<nodesID.length ; j++) { + // info about node1 + const node1=network.nodes[nodesID[i]]; + const posNode1=nodesPosition[nodesID[i]]; + const sizeNode1=getSizeNodePixel(node1,networkStyle); + // info about node2 + const node2=network.nodes[nodesID[j]]; + const posNode2=nodesPosition[nodesID[j]]; + const sizeNode2=getSizeNodePixel(node2,networkStyle); + + if (nodeOverlap(posNode1,sizeNode1,posNode2,sizeNode2)){ + return true; + } + + } + } + return false; +} + +/** + * Determines if two nodes overlap based on their coordinates and sizes. + * + * @param coord1 - The coordinates of the first node. + * @param size1 - The size (width and height) of the first node. + * @param coord2 - The coordinates of the second node. + * @param size2 - The size (width and height) of the second node. + * @returns `true` if the nodes overlap, `false` otherwise. + * + * @remarks + * This function checks if the bounding rectangles of two nodes overlap. + * It handles cases where any of the input parameters are null or undefined by returning `false`. + * + */ +function nodeOverlap(coord1: Coordinate, size1: Size, coord2: Coordinate, size2: Size): boolean { + if ( !coord1 || !size1 || !coord2 || !size2 || size1.width == null || size1.height == null || size2.width == null + || size2.height == null || coord1.x == null || coord1.y == null || coord2.x == null || coord2.y == null || + size1.width == undefined || size1.height == undefined || size2.width == undefined || size2.height == undefined || + coord1.x == undefined || coord1.y == undefined || coord2.x == undefined || coord2.y == undefined) { + // Handle null or undefined inputs appropriately + return false; + } + + // rectangle 1 + const left1 = coord1.x - size1.width / 2; + const right1 = coord1.x + size1.width / 2; + const top1 = coord1.y - size1.height / 2; + const bottom1 = coord1.y + size1.height / 2; + + // rectangle 2 + const left2 = coord2.x - size2.width / 2; + const right2 = coord2.x + size2.width / 2; + const top2 = coord2.y - size2.height / 2; + const bottom2 = coord2.y + size2.height / 2; + + // overlap? + const overlapX = left1 < right2 && right1 > left2; + const overlapY = top1 < bottom2 && bottom1 > top2; + + return overlapX && overlapY; +} + + +/*******************************************************************************************************************************************************/ +//________________________________________________________2. Node Edge overlap ________________________________________________________________________ + + + +/** + * Checks if any node overlaps with any edge in the network. + * + * @param nodesPosition - An object where keys are node IDs and values are objects containing x and y coordinates of the nodes. + * @param links - An array of link objects, each containing a source and target node ID. + * @param network - The network object containing nodes and their properties. + * @param networkStyle - The style properties of the graph. + * @returns A boolean indicating whether any node overlaps with any edge. + */ +export function isOverlapNodesEdges(nodesPosition: {[key:string]:{x:number,y:number}},links:{source:string,target:string}[],network:Network,networkStyle:GraphStyleProperties):boolean{ + const nodesID=Object.keys(nodesPosition); + for (let i=0 ; i<nodesID.length ; i++) { + // info about node + const node=network.nodes[nodesID[i]]; + const posNode=nodesPosition[nodesID[i]]; + const sizeNode=getSizeNodePixel(node,networkStyle); + + for (let j=0 ; j<links.length ; j++) { + // info about link + const link=links[j]; + // if node is linked to the edge : continue + if(link.source==nodesID[i] || link.target==nodesID[i]){ + continue; + }else{ + let posLink1=nodesPosition[link.source]; + let posLink2=nodesPosition[link.target]; + if (nodeEdgeOverlap(posNode,sizeNode,posLink1,posLink2)){ + return true; + } + } + + } + } + return false; +} + + + +/** + * Determines if a node, represented as a rectangle, overlaps with an edge defined by two points. + * + * @param centerCoordNode - The center coordinates of the node. + * @param sizeNode - The size of the node, including width and height. + * @param posLink1 - The first endpoint of the edge. + * @param posLink2 - The second endpoint of the edge. + * @returns `true` if the node overlaps with the edge, `false` otherwise. + */ +function nodeEdgeOverlap(centerCoordNode: Coordinate, sizeNode:Size, posLink1: Coordinate, posLink2: Coordinate): boolean { + + // Treat the node as a rectangle + const rect = { + left: centerCoordNode.x - sizeNode.width / 2, + right: centerCoordNode.x + sizeNode.width / 2, + top: centerCoordNode.y - sizeNode.height / 2, + bottom: centerCoordNode.y + sizeNode.height / 2 + }; + + // Check if any of the edge's endpoints is inside the rectangle + if (isPointInsideRect(posLink1,rect) || isPointInsideRect(posLink2,rect)) { + return true; // One of the endpoints is inside the rectangle + } + + // Check for overlap between the edge and the sides of the rectangle + // Convert the sides of the rectangle into line segments + const edges = [ + { start: { x: rect.left, y: rect.top }, end: { x: rect.right, y: rect.top } }, // Top + { start: { x: rect.right, y: rect.top }, end: { x: rect.right, y: rect.bottom } }, // Right + { start: { x: rect.left, y: rect.bottom }, end: { x: rect.right, y: rect.bottom } }, // Bottom + { start: { x: rect.left, y: rect.top }, end: { x: rect.left, y: rect.bottom } } // Left + ]; + + // Use checkIntersection function to check if two line segments intersect + for (const edge of edges) { + const result = checkIntersection(edge.start.x,edge.start.y, edge.end.x,edge.end.y, posLink1.x, posLink1.y,posLink2.x,posLink2.y); + if (result.type == "intersecting") { + return true; // There is an overlap + } + } + + return false; // No overlap detected +} + + +/** + * Determines if a given point is inside a specified rectangle. + * + * @param point - The coordinate of the point to check. + * @param rectangle - The boundaries of the rectangle, defined by its left, right, top, and bottom edges. + * @returns `true` if the point is inside the rectangle, `false` otherwise. + */ +function isPointInsideRect(point: Coordinate, rectangle:{left:number,right:number,top:number,bottom:number}):boolean{ + return point.x >= rectangle.left && point.x <= rectangle.right && point.y >= rectangle.top && point.y <= rectangle.bottom; +} + + + +// CHNAGE THE CODE : SEE METRICSNETWORK (for end of internship : keep this one) + +// /** +// * Check if the coordinates (x, y) are the same as the node's coordinates +// * @param node the node +// * @param x coordinate +// * @param y coordinate +// * @returns a boolean +// */ +// function isNodeCoord(node: {x:number,y:number}, x: number, y: number): boolean { +// return (node.x == x && node.y == y); +// } + + //______________________Intersection in the network______________________ /** @@ -134,47 +449,22 @@ function isNodeCoord(node: {x:number,y:number}, x: number, y: number): boolean { ////CLEAN CODE : CHANGE FORMAT TO NETWORK AND USE THE OTHER FUNCTIONS FOR CYCLE -function AdjustCoordNodeToCenter2(node:Node,nodeCoord:{x:number,y:number},style:GraphStyleProperties):{x:number,y:number}{ - const size = getSizeNodePixel(node,style); - return {x:nodeCoord.x-size.width/2,y:nodeCoord.y-size.height/2} -} +// function AdjustCoordNodeToCenter2(node:Node,nodeCoord:{x:number,y:number},style:GraphStyleProperties):{x:number,y:number}{ +// const size = getSizeNodePixel(node,style); +// return {x:nodeCoord.x-size.width/2,y:nodeCoord.y-size.height/2} +// } -function edgesIntersection(node1Link1:{x:number,y:number},node2Link1:{x:number,y:number},node1Link2:{x:number,y:number},node2Link2:{x:number,y:number}): boolean { - // case node in common - if (commonNodeBetween2EdgesCoord(node1Link1,node2Link1,node1Link2,node2Link2)) { - return false;//intersection2ConnectedLinks(node1Link1,node2Link1,node1Link2,node2Link2); - } - const result = checkIntersection(node1Link1.x, node1Link1.y, node2Link1.x, node2Link1.y, node1Link2.x, node1Link2.y, node2Link2.x, node2Link2.y); - if (result.type == "intersecting") { - return true; - }else{ - return false; - } -} -function commonNodeBetween2EdgesID(link1: {source:string,target:string},link2: {source:string,target:string}): boolean { - if (link1.source==link2.source || link1.source==link2.target || link1.target==link2.source || link1.target==link2.target) { - return true; - }else { - return false; - } -} +// function commonNodeBetween2EdgesID(link1: {source:string,target:string},link2: {source:string,target:string}): boolean { +// if (link1.source==link2.source || link1.source==link2.target || link1.target==link2.source || link1.target==link2.target) { +// return true; +// }else { +// return false; +// } +// } -function commonNodeBetween2EdgesCoord(node1Link1:{x:number,y:number},node2Link1:{x:number,y:number},node1Link2:{x:number,y:number},node2Link2:{x:number,y:number}): boolean { - if (sameNode(node1Link1,node1Link2) || sameNode(node1Link1,node2Link2) || sameNode(node2Link1,node1Link2) || sameNode(node2Link1,node2Link2)) { - return true; - }else { - return false; - } -} -function sameNode(node1: {x:number,y:number},node2: {x:number,y:number}): boolean { - if (!node1 || !node2 || node1.x==null || node1.y==null || node2.x==null || node2.y==null) { - return false; - } - return node1.x===node2.x && node1.y===node2.y; -} // function intersection2ConnectedLinks(node1Link1:{x:number,y:number},node2Link1:{x:number,y:number},node1Link2:{x:number,y:number},node2Link2:{x:number,y:number}): boolean { @@ -208,222 +498,82 @@ function sameNode(node1: {x:number,y:number},node2: {x:number,y:number}): boolea // } -export function countIntersectionGraph(nodes: {[key:string]:{x:number,y:number}},links:{source:string,target:string}[],network:Network,style:GraphStyleProperties): number { - let nb: number = 0; - for (let i=0 ; i<links.length ; i++) { - for (let j=i+1 ; j<links.length ; j++) { - const link1=links[i]; - const link2=links[j]; - // check if intersection - let node1Link1=nodes[link1.source]; - //node1Link1=AdjustCoordNodeToCenter2(network.nodes[link1.source],node1Link1,style); - let node2Link1=nodes[link1.target]; - //node2Link1=AdjustCoordNodeToCenter2(network.nodes[link1.target],node2Link1,style); - let node1Link2=nodes[link2.source]; - //node1Link2=AdjustCoordNodeToCenter2(network.nodes[link2.source],node1Link2,style); - let node2Link2=nodes[link2.target]; - //node2Link2=AdjustCoordNodeToCenter2(network.nodes[link2.target],node2Link2,style); - if (edgesIntersection(node1Link1,node2Link1,node1Link2,node2Link2)){ - nb++; - } - } - } - return nb; -} - -export function isIntersectionGraph(nodes: {[key:string]:{x:number,y:number}},links:{source:string,target:string}[],network:Network,style:GraphStyleProperties): boolean { - for (let i=0 ; i<links.length ; i++) { - for (let j=i+1 ; j<links.length ; j++) { - const link1=links[i]; - const link2=links[j]; - // check if intersection - let node1Link1=nodes[link1.source]; - //node1Link1=AdjustCoordNodeToCenter2(network.nodes[link1.source],node1Link1,style); - let node2Link1=nodes[link1.target]; - //node2Link1=AdjustCoordNodeToCenter2(network.nodes[link1.target],node2Link1,style); - let node1Link2=nodes[link2.source]; - //node1Link2=AdjustCoordNodeToCenter2(network.nodes[link2.source],node1Link2,style); - let node2Link2=nodes[link2.target]; - //node2Link2=AdjustCoordNodeToCenter2(network.nodes[link2.target],node2Link2,style); - if (edgesIntersection(node1Link1,node2Link1,node1Link2,node2Link2)){ - return true; - } - } - } - return false; -} +// export function countIntersectionGraph(nodes: {[key:string]:{x:number,y:number}},links:{source:string,target:string}[],network:Network,style:GraphStyleProperties): number { +// let nb: number = 0; +// for (let i=0 ; i<links.length ; i++) { +// for (let j=i+1 ; j<links.length ; j++) { +// const link1=links[i]; +// const link2=links[j]; +// // check if intersection +// let node1Link1=nodes[link1.source]; +// //node1Link1=AdjustCoordNodeToCenter2(network.nodes[link1.source],node1Link1,style); +// let node2Link1=nodes[link1.target]; +// //node2Link1=AdjustCoordNodeToCenter2(network.nodes[link1.target],node2Link1,style); +// let node1Link2=nodes[link2.source]; +// //node1Link2=AdjustCoordNodeToCenter2(network.nodes[link2.source],node1Link2,style); +// let node2Link2=nodes[link2.target]; +// //node2Link2=AdjustCoordNodeToCenter2(network.nodes[link2.target],node2Link2,style); +// if (edgesIntersection(node1Link1,node2Link1,node1Link2,node2Link2)){ +// nb++; +// } +// } +// } +// return nb; +// } //______________________Nodes overlap for graph______________________ -export function countOverlapNodes(nodesPosition: {[key:string]:Coordinate},network:Network,networkStyle:GraphStyleProperties):number{ - let nb=0; - const nodesID=Object.keys(nodesPosition); - for (let i=0 ; i<nodesID.length ; i++) { - for (let j=i+1 ; j<nodesID.length ; j++) { - // info about node1 - const node1=network.nodes[nodesID[i]]; - const posNode1=nodesPosition[nodesID[i]]; - const sizeNode1=getSizeNodePixel(node1,networkStyle); - // info about node2 - const node2=network.nodes[nodesID[j]]; - const posNode2=nodesPosition[nodesID[j]]; - const sizeNode2=getSizeNodePixel(node2,networkStyle); - - if (nodeOverlap(posNode1,sizeNode1,posNode2,sizeNode2)){ - nb+=1; - } - - } - } - return nb; -} - -export function isOverlapNodes(nodesPosition: {[key:string]:Coordinate},network:Network,networkStyle:GraphStyleProperties):boolean{ - const nodesID=Object.keys(nodesPosition); - for (let i=0 ; i<nodesID.length ; i++) { - for (let j=i+1 ; j<nodesID.length ; j++) { - // info about node1 - const node1=network.nodes[nodesID[i]]; - const posNode1=nodesPosition[nodesID[i]]; - const sizeNode1=getSizeNodePixel(node1,networkStyle); - // info about node2 - const node2=network.nodes[nodesID[j]]; - const posNode2=nodesPosition[nodesID[j]]; - const sizeNode2=getSizeNodePixel(node2,networkStyle); - - if (nodeOverlap(posNode1,sizeNode1,posNode2,sizeNode2)){ - return true; - } - - } - } - return false; -} - -function nodeOverlap(coord1: Coordinate, size1: Size, coord2: Coordinate, size2: Size): boolean { - if ( !coord1 || !size1 || !coord2 || !size2 || size1.width == null || size1.height == null || size2.width == null - || size2.height == null || coord1.x == null || coord1.y == null || coord2.x == null || coord2.y == null || - size1.width == undefined || size1.height == undefined || size2.width == undefined || size2.height == undefined || - coord1.x == undefined || coord1.y == undefined || coord2.x == undefined || coord2.y == undefined) { - // Handle null or undefined inputs appropriately - return false; - } - - // rectangle 1 - const left1 = coord1.x - size1.width / 2; - const right1 = coord1.x + size1.width / 2; - const top1 = coord1.y - size1.height / 2; - const bottom1 = coord1.y + size1.height / 2; - - // rectangle 2 - const left2 = coord2.x - size2.width / 2; - const right2 = coord2.x + size2.width / 2; - const top2 = coord2.y - size2.height / 2; - const bottom2 = coord2.y + size2.height / 2; +// export function countOverlapNodes(nodesPosition: {[key:string]:Coordinate},network:Network,networkStyle:GraphStyleProperties):number{ +// let nb=0; +// const nodesID=Object.keys(nodesPosition); +// for (let i=0 ; i<nodesID.length ; i++) { +// for (let j=i+1 ; j<nodesID.length ; j++) { +// // info about node1 +// const node1=network.nodes[nodesID[i]]; +// const posNode1=nodesPosition[nodesID[i]]; +// const sizeNode1=getSizeNodePixel(node1,networkStyle); +// // info about node2 +// const node2=network.nodes[nodesID[j]]; +// const posNode2=nodesPosition[nodesID[j]]; +// const sizeNode2=getSizeNodePixel(node2,networkStyle); + +// if (nodeOverlap(posNode1,sizeNode1,posNode2,sizeNode2)){ +// nb+=1; +// } - // overlap? - const overlapX = left1 < right2 && right1 > left2; - const overlapY = top1 < bottom2 && bottom1 > top2; +// } +// } +// return nb; +// } - return overlapX && overlapY; -} //______________________Nodes overlap with edges for graph______________________ -export function countOverlapNodesEdges(nodesPosition: {[key:string]:{x:number,y:number}},links:{source:string,target:string}[],network:Network,networkStyle:GraphStyleProperties):number{ - let nb=0; - const nodesID=Object.keys(nodesPosition); - for (let i=0 ; i<nodesID.length ; i++) { - // info about node - const node=network.nodes[nodesID[i]]; - const posNode=nodesPosition[nodesID[i]]; - const sizeNode=getSizeNodePixel(node,networkStyle); - - for (let j=0 ; j<links.length ; j++) { - // info about link - const link=links[j]; - // if node is linked to the edge : continue - if(link.source==nodesID[i] || link.target==nodesID[i]){ - continue; - }else{ - let posLink1=nodesPosition[link.source]; - let posLink2=nodesPosition[link.target]; - //posLink1=AdjustCoordNodeToCenter2(network.nodes[link.source],posLink1,networkStyle); - //posLink2=AdjustCoordNodeToCenter2(network.nodes[link.target],posLink2,networkStyle); - if (nodeEdgeOverlap(posNode,sizeNode,posLink1,posLink2)){ - nb+=1; - } - } - - } - } - return nb; -} - -export function isOverlapNodesEdges(nodesPosition: {[key:string]:{x:number,y:number}},links:{source:string,target:string}[],network:Network,networkStyle:GraphStyleProperties):boolean{ - const nodesID=Object.keys(nodesPosition); - for (let i=0 ; i<nodesID.length ; i++) { - // info about node - const node=network.nodes[nodesID[i]]; - const posNode=nodesPosition[nodesID[i]]; - const sizeNode=getSizeNodePixel(node,networkStyle); - - for (let j=0 ; j<links.length ; j++) { - // info about link - const link=links[j]; - // if node is linked to the edge : continue - if(link.source==nodesID[i] || link.target==nodesID[i]){ - continue; - }else{ - let posLink1=nodesPosition[link.source]; - let posLink2=nodesPosition[link.target]; - //posLink1=AdjustCoordNodeToCenter2(network.nodes[link.source],posLink1,networkStyle); - //posLink2=AdjustCoordNodeToCenter2(network.nodes[link.target],posLink2,networkStyle); - if (nodeEdgeOverlap(posNode,sizeNode,posLink1,posLink2)){ - return true; - } - } - - } - } - return false; -} - - - -function nodeEdgeOverlap(centerCoordNode: Coordinate, sizeNode:Size, posLink1: Coordinate, posLink2: Coordinate): boolean { - - // Treat the node as a rectangle - const rect = { - left: centerCoordNode.x - sizeNode.width / 2, - right: centerCoordNode.x + sizeNode.width / 2, - top: centerCoordNode.y - sizeNode.height / 2, - bottom: centerCoordNode.y + sizeNode.height / 2 - }; - - // Check if any of the edge's endpoints is inside the rectangle - const isPointInsideRect = (point: { x: number, y: number }) => - point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom; - - if (isPointInsideRect(posLink1) || isPointInsideRect(posLink2)) { - return true; // One of the endpoints is inside the rectangle - } - - // Check for overlap between the edge and the sides of the rectangle - // Convert the sides of the rectangle into line segments - const edges = [ - { start: { x: rect.left, y: rect.top }, end: { x: rect.right, y: rect.top } }, // Top - { start: { x: rect.right, y: rect.top }, end: { x: rect.right, y: rect.bottom } }, // Right - { start: { x: rect.left, y: rect.bottom }, end: { x: rect.right, y: rect.bottom } }, // Bottom - { start: { x: rect.left, y: rect.top }, end: { x: rect.left, y: rect.bottom } } // Left - ]; - - // Use checkIntersection function to check if two line segments intersect - for (const edge of edges) { - const result = checkIntersection(edge.start.x,edge.start.y, edge.end.x,edge.end.y, posLink1.x, posLink1.y,posLink2.x,posLink2.y); - if (result.type == "intersecting") { - return true; // There is an overlap - } - } +// export function countOverlapNodesEdges(nodesPosition: {[key:string]:{x:number,y:number}},links:{source:string,target:string}[],network:Network,networkStyle:GraphStyleProperties):number{ +// let nb=0; +// const nodesID=Object.keys(nodesPosition); +// for (let i=0 ; i<nodesID.length ; i++) { +// // info about node +// const node=network.nodes[nodesID[i]]; +// const posNode=nodesPosition[nodesID[i]]; +// const sizeNode=getSizeNodePixel(node,networkStyle); + +// for (let j=0 ; j<links.length ; j++) { +// // info about link +// const link=links[j]; +// // if node is linked to the edge : continue +// if(link.source==nodesID[i] || link.target==nodesID[i]){ +// continue; +// }else{ +// let posLink1=nodesPosition[link.source]; +// let posLink2=nodesPosition[link.target]; +// //posLink1=AdjustCoordNodeToCenter2(network.nodes[link.source],posLink1,networkStyle); +// //posLink2=AdjustCoordNodeToCenter2(network.nodes[link.target],posLink2,networkStyle); +// if (nodeEdgeOverlap(posNode,sizeNode,posLink1,posLink2)){ +// nb+=1; +// } +// } - return false; // No overlap detected -} \ No newline at end of file +// } +// } +// return nb; +// } diff --git a/src/composables/CalculateRelationCycle.ts b/src/composables/CalculateRelationCycle.ts index 9be5928a4c24a9af4bcba55dd9b6b96938e90f5a..f68a09a92928f0b6414b0d7840ae8c7f9c233241 100644 --- a/src/composables/CalculateRelationCycle.ts +++ b/src/composables/CalculateRelationCycle.ts @@ -29,7 +29,7 @@ import { inCycle } from "./GetSetAttributsNodes"; * -> getNodesIDPlacedInGroupCycle * retrieves the IDs of nodes placed (with x and y define) in a specific group cycle * - * -> getNodesPlacedInGroupCycle + * -> getNodesPlacedInGroupCycleAsArray * retrieves the nodes (id, x, y) placed in a specific group cycle. If position is fixed it's (id, fx, fy). If there is no posisiton, it's only the id * * -> getNodesPlacedInGroupCycleAsObject @@ -59,7 +59,7 @@ import { inCycle } from "./GetSetAttributsNodes"; * ********************************* * 2. Get graph * - * -> getListNodeLinksForCycleGroup + * -> getListNodeLinksForCycleGroupAsArray * retrieves the list of node and links for a specific cycle group. Position of nodes can be fixed * * -> getListNodeLinksForCycleGroupAsObject @@ -101,19 +101,21 @@ export function neighborsGroupCycle(subgraphNetwork:SubgraphNetwork,cycleGroupId // sort nodes of the group cycle by x if (xSort){ nodes.sort((nodeIdA, nodeIdB) => { - if(!(nodeIdA in positionNodesCycleGroup) || !(nodeIdB in positionNodesCycleGroup)) throw new Error("Node not plaed inside groupe cycle"); - const nodeA = positionNodesCycleGroup[nodeIdA]; - const nodeB = positionNodesCycleGroup[nodeIdB]; + if(!(nodeIdA in positionNodesCycleGroup) || !(nodeIdB in positionNodesCycleGroup)) throw new Error("Node not placed inside groupe cycle"); + const nodeA = positionNodesCycleGroup[nodeIdA] as Coordinate; + const nodeB = positionNodesCycleGroup[nodeIdB] as Coordinate; return nodeA.x - nodeB.x; }); } if (parentOrChild==="parent"){ // get parent nodes - const parentCycles = Array.from(new Set(parentNodeNotInCycle(subgraphNetwork, nodes,xSort).flat())); + const parentsDoubleArray = parentNodeNotInCycle(subgraphNetwork, nodes,xSort); + const parentCycles = Array.from(new Set(parentsDoubleArray.flat())); return parentCycles; } else { // get child nodes - const childCycles = Array.from(new Set(childNodeNotInCycle(subgraphNetwork, nodes,xSort).flat())); + const childrenDoubleArray = childNodeNotInCycle(subgraphNetwork, nodes,xSort); + const childCycles = Array.from(new Set(childrenDoubleArray.flat())); return childCycles; } }else{ @@ -215,7 +217,7 @@ export function getNodesIDPlacedInGroupCycle(subgraphNetwork:SubgraphNetwork,gro * @returns An array of objects representing the nodes placed in the group cycle. Each object contains the node ID and optionally the x and y coordinates if positionAsFixed is true. * If the group cycle ID is not found or the precalculated node positions are not available, null is returned. */ -export function getNodesPlacedInGroupCycle(subgraphNetwork:SubgraphNetwork,groupCycleID:string,positionAsFixed:boolean=false):{ id: string,x?:number, y?:number, fx?:number, fy?:number }[]{ +export function getNodesPlacedInGroupCycleAsArray(subgraphNetwork:SubgraphNetwork,groupCycleID:string,positionAsFixed:boolean=false):{ id: string,x?:number, y?:number, fx?:number, fy?:number }[]{ if (subgraphNetwork[TypeSubgraph.CYCLEGROUP] && groupCycleID in subgraphNetwork[TypeSubgraph.CYCLEGROUP]){ const groupCycle =subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleID]; if (groupCycle.precalculatedNodesPosition){ @@ -224,8 +226,8 @@ export function getNodesPlacedInGroupCycle(subgraphNetwork:SubgraphNetwork,group .filter(([_, item]) => { return item.x !== undefined && item.y !== undefined }) .map(([key, item]) => { if (item.x!==null || item.y!==null){ - if (positionAsFixed) return { id: key,fx:item.x, fy:item.y } - else return { id: key,x:item.x, y:item.y } + if (positionAsFixed) return { id: key,fx:item.x as number, fy:item.y as number} + else return { id: key,x:item.x as number, y:item.y as number } }else{ return { id: key } @@ -240,7 +242,7 @@ export function getNodesPlacedInGroupCycle(subgraphNetwork:SubgraphNetwork,group } /** - * Retrieves the nodes (id, x, y) placed in a specific group cycle. If position is fixed it's (id, fx, fy). + * Retrieves the nodes (id:{x, y}) placed in a specific group cycle. * If there is no posisiton, it's only the id * * @param subgraphNetwork - The subgraph network object. @@ -258,7 +260,7 @@ export function getNodesPlacedInGroupCycleAsObject(subgraphNetwork:SubgraphNetwo .filter(([_, item]) => { return item.x !== undefined && item.y !== undefined }) .reduce<{ [key:string]:Coordinate}>((acc, node) => { if (node[1].x!==null || node[1].y!==null){ - acc[node[0]]={ x:node[1].x, y:node[1].y } + acc[node[0]]={ x:node[1].x as number, y:node[1].y as number } } return acc; },{}); @@ -331,7 +333,7 @@ export function sortLinksWithAllGroupCycle(subgraphNetwork:SubgraphNetwork,order // change ordre with group cycle if (orderChange && subgraphNetwork[TypeSubgraph.CYCLEGROUP]){ // adding edge in right order for each group cycle - Object.keys(subgraphNetwork[TypeSubgraph.CYCLEGROUP]).forEach((groupCycle) => { + Object.keys(subgraphNetwork[TypeSubgraph.CYCLEGROUP]).forEach( (groupCycle) => { const resultSorting=sortLinksWithGroupCycle(subgraphNetwork,groupCycle); subgraphNetwork=resultSorting.subgraphNetwork; links=links.concat(resultSorting.linksOrdered); @@ -363,8 +365,8 @@ function sortLinksWithGroupCycle(subgraphNetwork:SubgraphNetwork,groupCycle:stri if( subgraphNetwork[TypeSubgraph.CYCLEGROUP] && groupCycle in subgraphNetwork[TypeSubgraph.CYCLEGROUP]){ // sort parent of cycle by x of the child in the cycle // (first : parent of the left node of group cycle) - const parents=neighborsGroupCycle(subgraphNetwork,groupCycle,"parent",true); - const children=neighborsGroupCycle(subgraphNetwork,groupCycle,"child",true); + const parents= neighborsGroupCycle(subgraphNetwork,groupCycle,"parent",true); + const children= neighborsGroupCycle(subgraphNetwork,groupCycle,"child",true); let nodeOrder:string[]=[]; let source:"node"|"groupCycle"; @@ -452,9 +454,9 @@ export function getLinksForListNodes(network: NetworkLayout, nodes: string[]): { * * @returns An object containing the list of nodes and links for the cycle group. */ -export function getListNodeLinksForCycleGroup(subgraphNetwork:SubgraphNetwork,groupCycleName:string,positionAsFixed:boolean=false) +export function getListNodeLinksForCycleGroupAsArray(subgraphNetwork:SubgraphNetwork,groupCycleName:string,positionAsFixed:boolean=false) :{nodes:{ id: string,fx?:number, fy?:number,x?:number,y?:number }[],links:{source:string,target:string}[]}{ - const nodesGroupCycle=getNodesPlacedInGroupCycle(subgraphNetwork,groupCycleName,positionAsFixed); + const nodesGroupCycle=getNodesPlacedInGroupCycleAsArray(subgraphNetwork,groupCycleName,positionAsFixed); const nodesGroupCycleName=Object.values(nodesGroupCycle).map(node=>node.id); const linksGroupCycle=getLinksForListNodes(subgraphNetwork.network,nodesGroupCycleName); return {nodes:nodesGroupCycle,links:linksGroupCycle}; @@ -469,7 +471,7 @@ export function getListNodeLinksForCycleGroup(subgraphNetwork:SubgraphNetwork,gr * @returns An object containing the list of nodes and links for the cycle group. */ export function getListNodeLinksForCycleGroupAsObject(subgraphNetwork:SubgraphNetwork,groupCycleName:string) -:{nodes:{[key:string]:{ x:number,y:number }},links:{source:string,target:string}[]}{ +:{nodes:{[key:string]:Coordinate},links:{source:string,target:string}[]}{ const nodesGroupCycle=getNodesPlacedInGroupCycleAsObject(subgraphNetwork,groupCycleName); const nodesGroupCycleName=Object.keys(nodesGroupCycle); const linksGroupCycle=getLinksForListNodes(subgraphNetwork.network,nodesGroupCycleName); diff --git a/src/composables/CalculateSize.ts b/src/composables/CalculateSize.ts index c564f7c5c9d59965487d614c786ef572becfc993..e4056b597a74e2cee8829cd31f7276580715095a 100644 --- a/src/composables/CalculateSize.ts +++ b/src/composables/CalculateSize.ts @@ -365,8 +365,8 @@ function getSizeGroupCycles(subgraphNetwork:SubgraphNetwork,groupCycle:Subgraph) if (groupCycle.precalculatedNodesPosition){ // get all nodes with x and y coordinates const listNodesMetadata = Object.entries(groupCycle.precalculatedNodesPosition) - .filter(([_,item]) => item.x !== undefined && item.y !== undefined); - const listCoordinates = listNodesMetadata.map(([_,item]) => {return {x:item.x,y:item.y}}); + .filter(([_,item]) => item.x !== undefined && item.y !== undefined && item.x!==null && item.y!==null); + const listCoordinates = listNodesMetadata.map(([_,item]) => {return {x:item.x as number,y:item.y as number}}); const listID = listNodesMetadata.map(([id,_]) => {return id}); // get the size of the rectangle const {width,height,center}=rectangleSize(listCoordinates,listID,subgraphNetwork); diff --git a/src/composables/LayoutDrawCycle.ts b/src/composables/LayoutDrawCycle.ts index 8ab2523e15475edd97c59dd83227f0911fe06336..94e8ac65d2e67077bc123faa471259f0f6ef6fee 100644 --- a/src/composables/LayoutDrawCycle.ts +++ b/src/composables/LayoutDrawCycle.ts @@ -5,32 +5,124 @@ import { Network } from "@metabohub/viz-core/src/types/Network"; import { GraphStyleProperties } from "@metabohub/viz-core/src/types/GraphStyleProperties"; import { Link } from "@metabohub/viz-core/src/types/Link"; import { Node } from "@metabohub/viz-core/src/types/Node"; +import { Coordinate, CoordinateNull } from "../types/CoordinatesSize"; +import { NodeLayout } from "../types/NetworkLayout"; + + // Composable imports import { getMeanNodesSizePixel, medianEdgeLength, rectangleSize } from "./CalculateSize"; -import { isIntersectionGraph, isOverlapNodes, isOverlapNodesEdges } from "./countIntersections"; -import { inCycle } from "./GetSetAttributsNodes"; +import { isIntersectionGraph, isOverlapNodes, isOverlapNodesEdges } from "./CalculateOverlaps"; +import { childNodeNotInCycle, getListNodeLinksForCycleGroupAsArray, getListNodeLinksForCycleGroupAsObject, getNodesPlacedInGroupCycleAsObject, parentNodeNotInCycle } from "./CalculateRelationCycle"; +import { updateNodeMetadataSubgraph } from "./SubgraphForSubgraphNetwork"; // General imports import { group } from "console"; import { start } from "repl"; -import { a } from "vitest/dist/suite-ghspeorC"; import * as d3 from 'd3'; import { link } from "fs"; import { emit } from "process"; -import { childNodeNotInCycle, getListNodeLinksForCycleGroup, getListNodeLinksForCycleGroupAsObject, parentNodeNotInCycle } from "./CalculateRelationCycle"; +/** + * This file contains functions to calculate the coordinates of nodes to draw them as circle. + * + * ********************************* + * + * 0. Calculate coordinates for nodes in cycles + * + * -> coordinateAllCycles : + * give precalculated coordinates for all cycles in the subgraphNetwork, there are not in the nodes network. + * + * -> coordinateCycle : + * give coordinates for a cycle in a group of cycles. A group of cycle is cycles with common nodes. All the group of cycle are independent (no common nodes) from each other. + * + * -> independentCycleCoordinates : + * Calculate position of a cycle as circle, it will be centered at the origin (0,0) and its nodes are positioned in a circular layout. + * + * -> tangentCycleCoordinates : + * Calculate position of a cycle as circle, it will be tangent to the circle of the fixed node. + * + * -> lineCycleCoordinates : + * Calculate position of a cycle as line(s). A line for each unfixed interval in the cycle. + * + * -> cycleNodesCoordinates : + * Calculate position of a cycle as circle, it will be centered at the origin (0,0) and its nodes are positioned in a circular layout. + * + * -> lineNodesCoordinates : + * Calculate position of a cycle as line(s). A line for each unfixed interval in the cycle. + * + * -> forceGroupCycle : + * Place node already placed in a cycle group with a force layout + * + * + * ********************************* + * + * 1. Find top node for cycle coordinate + * + * -> findTopCycleNode : + * Find the index of the top cycle node based on the given subgraph network and cycle nodes. + * + * -> getIndexNodesAssociatedMinY : + * Returns indices of lists containing minimum y-coordinate nodes. + * + * -> getIndexNodesAssociatedMaxY : + * Returns indices of lists containing minimum y-coordinate nodes. + * + * -> nodeMedianX : + * Get the name of the node with the median x-coordinate from a list of nodes. + * + * ********************************* + * + * 2. Utilitary functions for cycle coordinate + * + * -> sortingCycleForDrawing : + * Sorting function for knowing order of cycle drawing. + * + * -> cycleGroupName : + * Create a name for a group of cycle. + * + * -> addNewCycleGroup : + * Add a new group of cycle to the subgraph network. + * + * -> updateGroupCycles : + * Update the group of cycles based on the remaining cycles to draw. + * + * -> getRadiusSize : + * Calculate the radius size of a cycle based on the nodes in the cycle. + * * + * -> assignCoordinateToNode : + * Assigns coordinates to a node in the subgraph network. + * + * -> undoIfCreateOverlap : + * Undo the node placement if it creates an overlap with other nodes. + * + * -> isOverlapInCycleGroupDrawing : + * Check if there is an overlap in the drawing of the cycle group. + * + * * -> getUnfixedIntervals : + * Get the unfixed intervals in a cycle. + * + * ********************************* + * + * 3. Drawing cycle : shift position and placement in network + * + * -> drawAllCyclesGroup : + * Draw all the cycles in the group of cycles. + * + * -> drawCycleGroup : + * Draw a group of cycles. + * + */ -//------------------------------------------------------------------------------------------------------------ -//__________________________________Calculate coordinates for nodes in cycles______________________________________________________ -//------------------------------------------------------------------------------------------------------------ +/*******************************************************************************************************************************************************/ +//___________________________________________________0. Calculate coordinates for nodes in cycles__________________________________________________________________________ /** - * give coordinates for all cycles in the subgraph network. + * give precalculated coordinates for all cycles in the subgraphNetwork, there are not in the nodes network. * @param subgraphNetwork - The subgraph network containing cycles. * @param allowInterncircle - Whether to allow internal circles within cycles. Default is false. * @param radiusFactor - The factor to determine the radius of the cycles. Default is 15. @@ -39,25 +131,26 @@ import { childNodeNotInCycle, getListNodeLinksForCycleGroup, getListNodeLinksFor export async function coordinateAllCycles(subgraphNetwork:SubgraphNetwork,allowInterncircle:boolean=false):Promise<SubgraphNetwork> { const network = subgraphNetwork.network; - const cycles = subgraphNetwork.cycles? Object.values(subgraphNetwork.cycles):undefined; - let i=0 + const cycles = subgraphNetwork[TypeSubgraph.CYCLE]? Object.values(subgraphNetwork[TypeSubgraph.CYCLE]):undefined; + let i=0; let newGroup=true; if (cycles && cycles.length > 0) { // creation first cycle group let group=0; let groupName=cycleGroupName(String(group)); subgraphNetwork=addNewCycleGroup(subgraphNetwork,groupName); + if(!subgraphNetwork[TypeSubgraph.CYCLEGROUP] || !subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupName]) throw new Error(" cycle group not in subgraph network : error in addNewCycleGroup"); + const cycleGroups=subgraphNetwork[TypeSubgraph.CYCLEGROUP]; // Find the first cycle to draw : it shouldn't have a 'forgraph' of type cycle as it should be a parent ------------------- const parentCycles = cycles.filter(cycle => !cycle.parentSubgraph || cycle.parentSubgraph.type !== TypeSubgraph.CYCLE); - if (parentCycles.length === 0) { - console.error("No cycle found without a forSubgraph of type cycle"); - return; - } - parentCycles.sort((a, b) => sortingCycleForDrawing(subgraphNetwork,a,b,true)); + if (parentCycles.length === 0) throw new Error("No cycle found without a parent subgraph of type cycle : review implementation"); + + parentCycles.sort( (a, b) => sortingCycleForDrawing(subgraphNetwork,a,b,true)); const largestParentCycle = parentCycles[0]; // get largest cycle - subgraphNetwork.cyclesGroup[groupName].nodes.push(largestParentCycle.name); // add it to the current group of cycle - coordinateCycle(subgraphNetwork, largestParentCycle.name,groupName); // give coordinate for largest cycle + + cycleGroups[groupName].nodes.push(largestParentCycle.name); // add it to the current group of cycle + await coordinateCycle(subgraphNetwork, largestParentCycle.name,groupName); // give coordinate for largest cycle // Drawing the others : -------------------------------------------------------------------------------------------- @@ -67,6 +160,7 @@ export async function coordinateAllCycles(subgraphNetwork:SubgraphNetwork,allowI // If group of connected cycle is drawn : update group cycle const updateGroupCycle=await updateGroupCycles(remainingCycles,subgraphNetwork,group,groupName); subgraphNetwork=updateGroupCycle.subgraphNetwork; + // new cycle group ? if(updateGroupCycle.group!==group){ group=updateGroupCycle.group; groupName=cycleGroupName(String(group)); @@ -81,15 +175,15 @@ export async function coordinateAllCycles(subgraphNetwork:SubgraphNetwork,allowI // sort cycles by number of fixed node (and then by size) remainingCycles.sort((a, b) => sortingCycleForDrawing(subgraphNetwork,a,b,newGroup)); - const cycleToDraw = remainingCycles[0]; // the cycle with the most fixed nodes + const cycleToDraw = remainingCycles[0]; // the cycle with the most fixed nodes (or constraints) // if groupcycle do not exist : add one - if (!(groupName in subgraphNetwork.cyclesGroup)){ + if (!(groupName in cycleGroups)){ subgraphNetwork=addNewCycleGroup(subgraphNetwork,groupName); } // add the cycle to the current group of cycle - subgraphNetwork.cyclesGroup[groupName].nodes.push(cycleToDraw.name); + cycleGroups[groupName].nodes.push(cycleToDraw.name); // give coordinate to cycle node - coordinateCycle(subgraphNetwork, cycleToDraw.name,groupName,allowInterncircle); + await coordinateCycle(subgraphNetwork, cycleToDraw.name,groupName,allowInterncircle); // remove cycle from the list of cycle to process remainingCycles.shift(); @@ -109,71 +203,6 @@ export async function coordinateAllCycles(subgraphNetwork:SubgraphNetwork,allowI return subgraphNetwork; } -/** - * Returns the cycle group name by appending the given name with a prefix. - * - * @param name - The name to be appended with the prefix. - * @returns The cycle group name. - */ -function cycleGroupName(name:string):string{ - return "cycle_group_"+name; -} -/** - * Adds a new cycle group to the subgraph network. - * - * @param subgraphNetwork - The subgraph network to add the cycle group to. - * @param groupName - The name of the cycle group. - * @returns The updated subgraph network with the added cycle group. - */ -function addNewCycleGroup(subgraphNetwork:SubgraphNetwork, groupName:string):SubgraphNetwork{ - if(!subgraphNetwork.cyclesGroup){ - subgraphNetwork.cyclesGroup={}; - } - subgraphNetwork.cyclesGroup[groupName]={name:groupName,nodes:[],metadata:{}}; - return subgraphNetwork; -} - -/** - * Sorting function for knowing order of cycle drawing. - * First sort by number of circle fixed nodes (nodes fixed in a circle drawing), then by size, by number of parent nodes (of the cycle), and finally by number of child nodes (of the cycle) . - * @param subgraphNetwork - The subgraph network. - * @param a - The first cycle to compare. - * @param b - The second cycle to compare. - * @returns A number indicating the sorting order. - */ -function sortingCycleForDrawing(subgraphNetwork:SubgraphNetwork,a:Subgraph,b:Subgraph,fullConstraint:boolean=false):number{ - const network=subgraphNetwork.network; - - // first sort by number of fixed nodes - const fixedNodesA = a.nodes.filter(node => network.nodes[node].metadata && network.nodes[node].metadata.fixedCycle).length; - const fixedNodesB = b.nodes.filter(node => network.nodes[node].metadata && network.nodes[node].metadata.fixedCycle).length; - if (fixedNodesA !== fixedNodesB){ - return fixedNodesB - fixedNodesA; - }else{ - // sort by size - if ( !fullConstraint || b.nodes.length !== a.nodes.length ){ - return b.nodes.length - a.nodes.length; - }else{ - // then by number of parent nodes - const totalParentNodesA = parentNodeNotInCycle(subgraphNetwork, a.nodes) - .flat().length; - const totalParentNodesB = parentNodeNotInCycle(subgraphNetwork, b.nodes) - .flat().length; - if (totalParentNodesA !== totalParentNodesB){ - return totalParentNodesB - totalParentNodesA; - }else{ - // then by number of child nodes - const totalChildNodesA = childNodeNotInCycle(subgraphNetwork, a.nodes) - .flat().length; - const totalChildNodesB = childNodeNotInCycle(subgraphNetwork, b.nodes) - .flat().length; - - return totalChildNodesB - totalChildNodesA; - } - } - } -} - /** @@ -189,103 +218,133 @@ function sortingCycleForDrawing(subgraphNetwork:SubgraphNetwork,a:Subgraph,b:Sub */ async function coordinateCycle(subgraphNetwork:SubgraphNetwork, cycleToDrawID:string,groupCycleName:string,allowInterncircle:boolean=true):Promise<SubgraphNetwork>{ const network = subgraphNetwork.network; - - // Get nodes to place - let cycle:string[]=[]; - if (cycleToDrawID in subgraphNetwork.cycles){ - cycle=subgraphNetwork.cycles[cycleToDrawID].nodes; - subgraphNetwork.cycles[cycleToDrawID].metadata={}; - }else{ - console.log('cycle not in subgraph network'); - } + if(!subgraphNetwork[TypeSubgraph.CYCLE] || !subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID]) throw new Error("cycle not in subgraph network"); + const nodesCycle=subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID].nodes; + if (nodesCycle.length===0) throw new Error("no node in cycle"); // Check existence of all nodes - const cycleExist = cycle.every(node => node in network.nodes); + const cycleExist = nodesCycle.every(node => node in network.nodes); + if (!cycleExist) throw new Error("Some nodes in the cycle do not exist in the network"); - // Nodes with attribute 'fixedCycle' : if a node had been fixed in a line, his position can change, if fixed in a circle , no change - const nodesFixedCircle = cycle.filter(node => - network.nodes[node].metadata && network.nodes[node].metadata.fixedCycle + // Nodes with attribute 'fixedInCircle' : if a node had been fixed in a line, his position can change, if fixed in a circle , no change + const nodesFixedInCircle = nodesCycle.filter(node => + network.nodes[node].metadataLayout && network.nodes[node].metadataLayout.fixedInCircle ); - const nodesFixed = cycle.filter(node => - network.nodes[node].metadata && network.nodes[node].metadata.fixedInCycle + const nodesFixed = nodesCycle.filter(node => + network.nodes[node].metadataLayout && network.nodes[node].metadataLayout.isFixed ); // Update node metadata to place them in cycleGroup - cycle.forEach(node=>{ - network.nodes[node].metadata[TypeSubgraph.CYCLEGROUP]=groupCycleName; + nodesCycle.forEach(node=>{ + updateNodeMetadataSubgraph(network,node,groupCycleName,TypeSubgraph.CYCLEGROUP); }); - // If cycle exist: place his nodes - if (cycleExist && cycle.length>0){ - if (nodesFixedCircle.length===0){ // if independant cycle (first of a group cycle)---------------------------------------------------------------------------------- + // Nodes placement + + if (nodesFixedInCircle.length===0){ // if independant cycle (first of a group cycle)---------------------------------------------------------------------------------- - subgraphNetwork=await independentCycleCoordinates(subgraphNetwork,cycleToDrawID,groupCycleName); + subgraphNetwork=await independentCycleCoordinates(subgraphNetwork,cycleToDrawID,groupCycleName); - } else if (nodesFixedCircle.length===1){ // if cycle linked to another cycle by one node ---------------------------------------------------------------------------------- - - const tangentNode=network.nodes[nodesFixedCircle[0]]; - subgraphNetwork=await tangentCycleCoordinates(subgraphNetwork,cycleToDrawID,groupCycleName,tangentNode,nodesFixed,allowInterncircle); + } else if (nodesFixedInCircle.length===1){ // if cycle linked to another cycle by one node ---------------------------------------------------------------------------------- + + const tangentNode=network.nodes[nodesFixedInCircle[0]]; + subgraphNetwork=await tangentCycleCoordinates(subgraphNetwork,cycleToDrawID,groupCycleName,tangentNode,nodesFixed,allowInterncircle); + - - } else { // several node in common with other cycle(s) ---------------------------------------------------------------------------------- + } else { // several node in common with other cycle(s) ---------------------------------------------------------------------------------- - subgraphNetwork=lineCycleCoordinates(subgraphNetwork,cycleToDrawID,groupCycleName); - - } + subgraphNetwork= await lineCycleCoordinates(subgraphNetwork,cycleToDrawID,groupCycleName); } - return subgraphNetwork; } +/** + * Calculates and assigns coordinates for the nodes in a independant cycle within a subgraphNetwork. + * Independant means that the cycle do not countains nodes already placed, i.e with coordinates. + * The cycle is centered at the origin (0,0) and its nodes are positioned in a circular layout. + * + * @param subgraphNetwork - The subgraph network containing the cycle to be drawn. + * @param cycleToDrawID - The identifier of the cycle to be drawn. + * @param groupCycleName - The name of the group to which the cycle belongs. + * @returns A promise that resolves to the updated subgraph network with the cycle's nodes positioned. + */ async function independentCycleCoordinates(subgraphNetwork:SubgraphNetwork,cycleToDrawID:string,groupCycleName:string):Promise<SubgraphNetwork>{ const network = subgraphNetwork.network; - const cycle=subgraphNetwork.cycles[cycleToDrawID].nodes; + if (!subgraphNetwork[TypeSubgraph.CYCLE] || !subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID]) throw new Error("cycle not in subgraphNetwork"); + const cycle=subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID] + const cycleNodes=cycle.nodes; // radius and centroid - const radius = await getRadiusSize(cycle,network,subgraphNetwork.networkStyle); - // first cycle centered at 0,0 - const centroidX=0; - const centroidY=0; - subgraphNetwork.cycles[cycleToDrawID].metadata.radius=radius; - subgraphNetwork.cycles[cycleToDrawID].metadata.centroid={x:centroidX,y:centroidY}; + const radius = await getRadiusSize(cycleNodes,network,subgraphNetwork.networkStyle); + cycle.radiusCycle=radius; + cycle.centroidCycle={x:0,y:0}; // first cycle (of group cycle) centered at 0,0 // Shift cycle - const topIndex = findTopCycleNode(subgraphNetwork,cycle); // first node of list is the top - const cycleCopy= cycle.slice(); + const topIndex = await findTopCycleNode(subgraphNetwork,cycleNodes); // first node of list is the top + const cycleCopy= cycleNodes.slice(); const shiftedCycle = cycleCopy.splice(topIndex).concat(cycleCopy); // Give position to each node (no nedd to check if overlap with other cycle, it is the first of the group) - subgraphNetwork=cycleNodesCoordinates(cycleToDrawID,shiftedCycle,centroidX,centroidY,radius,subgraphNetwork,-Math.PI/2,groupCycleName).subgraphNetwork; + const coordinates=await cycleNodesCoordinates(cycleToDrawID,shiftedCycle, cycle.centroidCycle.x, cycle.centroidCycle.y, cycle.radiusCycle,subgraphNetwork,-Math.PI/2,groupCycleName) + subgraphNetwork=coordinates.subgraphNetwork; return subgraphNetwork; } -async function tangentCycleCoordinates(subgraphNetwork:SubgraphNetwork,cycleToDrawID:string,groupCycleName:string,nodeFixed:Node,nodesPlaced:string[],allowInterncircle:boolean=false):Promise<SubgraphNetwork>{ + + +/** + * Calculate position of a cycle as circle, it will be tangent to the circle of the fixed node. + * @param subgraphNetwork - The subgraph network containing the cycle to be drawn. + * @param cycleToDrawID - The identifier of the cycle to be drawn. + * @param groupCycleName - The name of the group to which the cycle belongs. + * @param nodeFixed - The node that is fixed in a circle, the common node with the other cycle. + * @param nodesPlaced - The nodes that are already placed in the cycle. + * @param allowInterncircle - A flag indicating whether to allow internal circles for fixed nodes (default: false). + * @returns A promise that resolves to the updated subgraph network with the cycle's nodes positioned. + */ +async function tangentCycleCoordinates(subgraphNetwork:SubgraphNetwork,cycleToDrawID:string,groupCycleName:string,nodeFixed:NodeLayout,nodesPlaced:string[],allowInterncircle:boolean=false):Promise<SubgraphNetwork>{ const network = subgraphNetwork.network; - const cycle=subgraphNetwork.cycles[cycleToDrawID].nodes; + if (!subgraphNetwork[TypeSubgraph.CYCLE] || !subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID]) throw new Error("cycle not in subgraphNetwork"); + const cycleNodes=subgraphNetwork.cycles[cycleToDrawID].nodes; // get fixed node coordinates (in the group cycle) - const groupCycleFixed=nodeFixed.metadata[TypeSubgraph.CYCLEGROUP] as string; - const coordNodeFixed=subgraphNetwork.cyclesGroup[groupCycleFixed].metadata[nodeFixed.id] as {x:number,y:number}; + if (!nodeFixed.metadataLayout || !nodeFixed.metadataLayout[TypeSubgraph.CYCLEGROUP]) throw new Error("node not a group cycle"); + const groupCycleFixed=nodeFixed.metadataLayout[TypeSubgraph.CYCLEGROUP]; + if (!subgraphNetwork[TypeSubgraph.CYCLEGROUP] || !subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleFixed] || + !subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleFixed].precalculatedNodesPosition || + !subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleFixed].precalculatedNodesPosition[nodeFixed.id]) throw new Error("fixed node not in group cycle, or no precalculated position"); + const getCoordNodeFixed=subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleFixed].precalculatedNodesPosition[nodeFixed.id]; + if (getCoordNodeFixed.x==null || getCoordNodeFixed.y==null) throw new Error("fixed node position not defined"); + const coordNodeFixed={x:getCoordNodeFixed.x,y:getCoordNodeFixed.y}; // first node of cycle is the one fixed : - const cycleCopy= cycle.slice(); - const firstIndex=cycle.indexOf(nodeFixed.id); + const cycleCopy= cycleNodes.slice(); + const firstIndex=cycleNodes.indexOf(nodeFixed.id); const shiftedCycle = cycleCopy.splice(firstIndex).concat(cycleCopy); // radius - const radius = await getRadiusSize(cycle,network,subgraphNetwork.networkStyle); - subgraphNetwork.cycles[cycleToDrawID].metadata.radius=radius; + const radius = await getRadiusSize(cycleNodes,network,subgraphNetwork.networkStyle); + subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID].radiusCycle=radius; //centroid depending on fixed cycle - const radiusFixedCycle=subgraphNetwork.cycles[nodeFixed.metadata.fixedCycle as string].metadata.radius as number; - const centroidFixedCycle=subgraphNetwork.cycles[nodeFixed.metadata.fixedCycle as string].metadata.centroid; - const fixedAngle = Math.atan2(coordNodeFixed.y - centroidFixedCycle["y"], coordNodeFixed.x - centroidFixedCycle["x"]); + if (!nodeFixed.metadataLayout.fixedInCircle) throw new Error("fixed node for tangent drawing not in a circle"); + const circleAlreadyDrawn=nodeFixed.metadataLayout.fixedInCircle as string; + if (!subgraphNetwork[TypeSubgraph.CYCLE] || !subgraphNetwork[TypeSubgraph.CYCLE][circleAlreadyDrawn]) throw new Error("fixed node for tangent drawing not in a circle"); + + const radiusFixedCycle=subgraphNetwork[TypeSubgraph.CYCLE][circleAlreadyDrawn].radiusCycle; + if (!radiusFixedCycle) throw new Error("no radius for circle of fixed node"); + + const centroidFixedCycle=subgraphNetwork[TypeSubgraph.CYCLE][circleAlreadyDrawn].centroidCycle; + if (!centroidFixedCycle) throw new Error("no centroid for circle of fixed node"); + + const fixedAngle = Math.atan2(coordNodeFixed.y - centroidFixedCycle.y, coordNodeFixed.x - centroidFixedCycle["x"]); let centroidX:number; let centroidY:number; let d:number; + // if has some node fixed in a line : if(allowInterncircle && nodesPlaced.length >1){ d = radiusFixedCycle - radius; // circle draw internally @@ -293,61 +352,74 @@ async function tangentCycleCoordinates(subgraphNetwork:SubgraphNetwork,cycleToDr }else{ // completely indep of fixed nodes but for the common node d = radius + radiusFixedCycle; // circle draw externally } - centroidX = centroidFixedCycle["x"] + d * Math.cos(fixedAngle); - centroidY = centroidFixedCycle["y"] + d * Math.sin(fixedAngle); - subgraphNetwork.cycles[cycleToDrawID].metadata.centroid={x:centroidX,y:centroidY}; + centroidX = centroidFixedCycle.x + d * Math.cos(fixedAngle); + centroidY = centroidFixedCycle.y + d * Math.sin(fixedAngle); + subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID].centroidCycle={x:centroidX,y:centroidY}; // shift of start angle (default:pi/2) : angle of fixed node in the new cycle (with centroid calculted before) - const positionFixedNode=subgraphNetwork.cyclesGroup[groupCycleName].metadata[nodeFixed.id] as {x:number,y:number}; - const shiftAngle = Math.atan2(positionFixedNode.y - centroidY, positionFixedNode.x - centroidX); + const shiftAngle = Math.atan2(coordNodeFixed.y - centroidY, coordNodeFixed.x - centroidX); // Give position to each node, and get position before drawing cycle - const cycleCoord=cycleNodesCoordinates(cycleToDrawID,shiftedCycle,centroidX,centroidY,radius,subgraphNetwork,shiftAngle,groupCycleName); + const cycleCoord=await cycleNodesCoordinates(cycleToDrawID,shiftedCycle,centroidX,centroidY,radius,subgraphNetwork,shiftAngle,groupCycleName); subgraphNetwork=cycleCoord.subgraphNetwork; const positionBefore=cycleCoord.positionBefore; - subgraphNetwork=undoIfOverlap(subgraphNetwork,groupCycleName,positionBefore); + subgraphNetwork=undoIfCreateOverlap(subgraphNetwork,groupCycleName,positionBefore); return subgraphNetwork; } -function lineCycleCoordinates(subgraphNetwork:SubgraphNetwork,cycleToDrawID:string,groupCycleName:string){ - const cycle=subgraphNetwork.cycles[cycleToDrawID].nodes; +/** + * Calculates the coordinates for drawing a cycle as line(s). A line for each unfixed interval in the cycle. + * An unfixed interval is a sequence of nodes in the cycle that are not already fixed in a circle. + * + * @param subgraphNetwork - The subgraph network containing the cycles and cycle groups. + * @param cycleToDrawID - The ID of the cycle to draw. + * @param groupCycleName - The name of the cycle group. + * @returns The updated subgraph network with the calculated coordinates. + * @throws Will throw an error if the cycle or cycle group is not found in the subgraph network. + * @throws Will throw an error if there are no precalculated node positions in the cycle group. + * @throws Will throw an error if the start or end node positions are not defined. + */ +async function lineCycleCoordinates(subgraphNetwork:SubgraphNetwork,cycleToDrawID:string,groupCycleName:string):Promise<SubgraphNetwork>{ + if (!subgraphNetwork[TypeSubgraph.CYCLE] || !subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID]) throw new Error("cycle not in subgraphNetwork"); + if (!subgraphNetwork[TypeSubgraph.CYCLEGROUP] || !subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]) throw new Error("group cycle not in subgraphNetwork"); + const cycleGroup=subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]; + if (!cycleGroup.precalculatedNodesPosition) throw new Error("no precalculatedNodesPosition in group cycle"); + const precalculatedNodesPosition=cycleGroup.precalculatedNodesPosition; + + // get cycle and the intervals to draw : the one not already fixed in a circle + const cycleNodes=subgraphNetwork[TypeSubgraph.CYCLE][cycleToDrawID].nodes; + const unfixedInterval= await getUnfixedIntervals(cycleNodes,subgraphNetwork); - const unfixedInterval=getUnfixedIntervals(cycle,subgraphNetwork); let cycleAsLine:string[]=[]; - unfixedInterval.forEach(interval=>{ - const startNode=cycle[(interval[0]-1+ cycle.length) % cycle.length]; - const endNode=cycle[(interval[1]+1+ cycle.length) % cycle.length]; - const startNodePosition=subgraphNetwork.cyclesGroup[groupCycleName].metadata[startNode] as {x:number,y:number}; - const endNodePosition=subgraphNetwork.cyclesGroup[groupCycleName].metadata[endNode] as {x:number,y:number}; + + unfixedInterval.forEach(async interval=>{ + const startNode=cycleNodes[(interval[0]-1+ cycleNodes.length) % cycleNodes.length]; + const endNode=cycleNodes[(interval[1]+1+ cycleNodes.length) % cycleNodes.length]; + + if (!precalculatedNodesPosition[startNode] || !precalculatedNodesPosition[endNode]) throw new Error("no precalculatedNodesPosition for start and end node of line"); + const startNodePosition=precalculatedNodesPosition[startNode]; + const endNodePosition=precalculatedNodesPosition[endNode]; + + if (startNodePosition.x==null || startNodePosition.y==null || endNodePosition.x==null || endNodePosition.y==null) throw new Error("start or end node position not defined"); + if (interval[0]>interval[1]){ // if begginning of the cycle at the end, and the end at the beginning - cycleAsLine=cycle.slice(interval[0],cycle.length).concat(cycle.slice(0,interval[1]+1)); + cycleAsLine=cycleNodes.slice(interval[0],cycleNodes.length).concat(cycleNodes.slice(0,interval[1]+1)); }else{ - cycleAsLine=cycle.slice(interval[0],interval[1]+1); + cycleAsLine=cycleNodes.slice(interval[0],interval[1]+1); } + // Give position to each node - const lineCoord=lineNodesCoordinates(startNodePosition,endNodePosition,cycleAsLine,subgraphNetwork,groupCycleName); + const lineCoord=await lineNodesCoordinates(startNodePosition as Coordinate,endNodePosition as Coordinate,cycleAsLine,subgraphNetwork,groupCycleName); subgraphNetwork=lineCoord.subgraphNetwork; const positionBefore=lineCoord.positionBefore; - subgraphNetwork=undoIfOverlap(subgraphNetwork,groupCycleName,positionBefore); + subgraphNetwork=undoIfCreateOverlap(subgraphNetwork,groupCycleName,positionBefore); }); return subgraphNetwork; } -/** - * Calculates the radius size based on the length of the cycle array and the radius factor. - * @param cycle - The array of strings representing the cycle. - * @param radiusFactor - The factor to multiply the length of the cycle by to calculate the radius size. Default value is 15. - * @returns The calculated radius size. - */ -async function getRadiusSize(cycle:string[],network:Network,styleNetwork:GraphStyleProperties):Promise<number>{ - const nodes=Object.values(network.nodes).filter(node=>cycle.includes(node.id)); - const meanSize=await getMeanNodesSizePixel(nodes,styleNetwork); - return cycle.length*(meanSize.height+meanSize.width)/2; -} - /** * Calculates and assigns the coordinates for nodes to draw it as a circle. @@ -359,52 +431,29 @@ async function getRadiusSize(cycle:string[],network:Network,styleNetwork:GraphSt * @param radius - The radius of the cycle. * @param subgraphNetwork - The subgraph network. * @param shiftAngle - The angle by which to shift the cycle from the first node (optional, default is -Math.PI/2 to put the first node at the top). - * @param groupcycle - The name of the group cycle (optional). - * @returns The updated subgraph network. + * @param groupCycleName - The name of the group cycle (optional). + * @returns The updated subgraph network and the position of nodes before the change. */ -function cycleNodesCoordinates(cycleName:string,cycle:string[],centroidX:number,centroidY:number,radius:number,subgraphNetwork:SubgraphNetwork, - shiftAngle:number=-Math.PI/2,groupcycle?:string,):{subgraphNetwork:SubgraphNetwork,positionBefore:{[key:string]:{x:number,y:number}}}{ +async function cycleNodesCoordinates(cycleName:string,cycle:string[],centroidX:number,centroidY:number,radius:number,subgraphNetwork:SubgraphNetwork, + shiftAngle:number=-Math.PI/2,groupCycleName?:string,):Promise<{subgraphNetwork:SubgraphNetwork,positionBefore:{[key:string]:CoordinateNull}}>{ const network=subgraphNetwork.network; - let positionBefore:{[key:string]:{x:number,y:number}}={}; + let positionBefore:{[key:string]:CoordinateNull}={}; cycle.forEach((node, i) => { const nodeNetwork=network.nodes[node]; + // coordinates of the node // positive shift angle rotate cycle to the right, negative to the left const x = centroidX + radius * Math.cos(2 * Math.PI * i / cycle.length + shiftAngle ); const y = centroidY + radius * Math.sin(2 * Math.PI * i / cycle.length + shiftAngle ); - // Give position if not fixed - if(network.nodes[node].metadata && !network.nodes[node].metadata.fixedCycle){ - if (groupcycle){ - if (groupcycle in subgraphNetwork.cyclesGroup){ - if (!subgraphNetwork.cyclesGroup[groupcycle].metadata[node]){ - subgraphNetwork.cyclesGroup[groupcycle].metadata[node] = {}; - // get position before drawing cycle - positionBefore[node]={x:null,y:null}; - }else{ - // get position before drawing cycle - const xBefore=subgraphNetwork.cyclesGroup[groupcycle].metadata[node]["x"]; - const yBefore=subgraphNetwork.cyclesGroup[groupcycle].metadata[node]["y"]; - positionBefore[node]={x:xBefore,y:yBefore}; - } - // assign new position - subgraphNetwork.cyclesGroup[groupcycle].metadata[node]["x"]=x; - subgraphNetwork.cyclesGroup[groupcycle].metadata[node]["y"]=y; - } else { - console.error("CycleGroup not in subgraphNetwork"); - } - } else if (node in subgraphNetwork.network.value.nodes) { - // get position before drawing cycle - const xBefore=subgraphNetwork.network.nodes[node].x; - const yBefore=subgraphNetwork.network.nodes[node].x; - positionBefore[node]={x:xBefore,y:yBefore}; - // assign new position - subgraphNetwork.network.nodes[node].x=x; - subgraphNetwork.network.nodes[node].y=y; - } else{ - console.error("Node not in network or groupcycle not provided") - } + // Give position if not fixed in circle + if(!nodeNetwork.metadataLayout || !nodeNetwork.metadataLayout.fixedInCircle){ + + // assign coordinates to the node, and get the position before + const assignedCoordinates= assignCoordinateToNode(subgraphNetwork,node,x,y,groupCycleName); + subgraphNetwork=assignedCoordinates.subgraphNetwork; + positionBefore[node]=assignedCoordinates.positionBefore; // Fix the nodes if (!nodeNetwork.metadata) nodeNetwork.metadata={}; @@ -418,85 +467,6 @@ function cycleNodesCoordinates(cycleName:string,cycle:string[],centroidX:number, } - -function undoIfOverlap(subgraphNetwork:SubgraphNetwork,groupCycleName:string,positionBefore:{[key:string]:{x:number,y:number}}):SubgraphNetwork{ - // is there overlap in the group cycle ? - const isOverlap=isOverlapCycles(subgraphNetwork,groupCycleName); - // if overlap, put back the position before drawing cycle - if(isOverlap){ - Object.entries(positionBefore).forEach(([nodeID,coord])=>{ - const node=subgraphNetwork.cyclesGroup[groupCycleName].metadata[nodeID] as {x:number,y:number}; - node.x = positionBefore[nodeID].x; - node.y = positionBefore[nodeID].y; - }); - } - return subgraphNetwork; -} - -function isOverlapCycles(subgraphNetwork:SubgraphNetwork,groupCycleName:string):boolean{ - const graph =getListNodeLinksForCycleGroupAsObject(subgraphNetwork,groupCycleName); - // intersection of edges : - const intersectionEdges=isIntersectionGraph(graph.nodes ,graph.links,subgraphNetwork.network,subgraphNetwork.networkStyle.value); - if (intersectionEdges){ - return true; - }else{ - // overlap of nodes - const nodesOverlap=isOverlapNodes(graph.nodes,subgraphNetwork.network,subgraphNetwork.networkStyle.value); - if (nodesOverlap){ - return true; - }else{ - // overlap of node with edges - const edgeNodeOverlap=isOverlapNodesEdges(graph.nodes,graph.links,subgraphNetwork.network.value,subgraphNetwork.networkStyle.value); - if (edgeNodeOverlap){ - return true; - } - } - } - return false; -} - - -/** - * Retrieves the unfixed intervals from a list of nodes. An unfixed interval is a continuous range of nodes that are not fixed in a cycle. - * An unfixed interval is a continuous range of nodes that are not fixed in the cycle. - * - * @param nodes - An array of node IDs. - * @param subgraphNetwork - The subgraph network containing the nodes. - * @returns An array of intervals representing the unfixed intervals. - */ -function getUnfixedIntervals(nodes:string[],subgraphNetwork:SubgraphNetwork) { - let intervals:number[][] = []; - let start = null; - const network=subgraphNetwork.network; - nodes.forEach((nodeID,i) => { - const node=network.nodes[nodeID]; - if (node.metadata && !node.metadata.fixedInCycle) { - if (start === null) { - start = i; - } - } else { - if (start !== null) { - intervals.push([start, i - 1]); - start = null; - } - } - }); - - // Handle case where the last node is fixed - if (start !== null) { - intervals.push([start, nodes.length - 1]); - } - - // case first interval and last are linked : combine interval as one long interval - if (intervals.length!==0 && intervals[0][0]===0 && intervals[intervals.length-1][1]===nodes.length-1){ - intervals[0][0]=intervals[intervals.length-1][0]; // change start of first interval - intervals.pop(); // remove last interval - } - - return intervals; -} - - /** * Calculates and assigns the coordinates for nodes along a line between two points. * @param start - The starting point coordinates (x, y). @@ -504,12 +474,12 @@ function getUnfixedIntervals(nodes:string[],subgraphNetwork:SubgraphNetwork) { * @param nodes - An array of node names to place. * @param subgraphNetwork - The subgraph network object. * @param groupCycleName - Optional. The name of the group cycle in witch the line is draw. - * @returns The updated subgraph network object. + * @returns The updated subgraph network object, and position of nodes before the change */ -function lineNodesCoordinates(start: {x: number, y: number}, end: {x: number, y: number}, nodes: string[], - subgraphNetwork:SubgraphNetwork,groupCycleName?:string):{subgraphNetwork:SubgraphNetwork,positionBefore:{[key:string]:{x:number,y:number}}} { +async function lineNodesCoordinates(start: {x: number, y: number}, end: {x: number, y: number}, nodes: string[], + subgraphNetwork:SubgraphNetwork,groupCycleName?:string):Promise<{subgraphNetwork:SubgraphNetwork,positionBefore:{[key:string]:CoordinateNull}}> { const network=subgraphNetwork.network; - let positionBefore:{[key:string]:{x:number,y:number}}={}; + let positionBefore:{[key:string]:CoordinateNull}={}; // Calculate direction vector let dx = end.x - start.x; @@ -527,104 +497,45 @@ function lineNodesCoordinates(start: {x: number, y: number}, end: {x: number, y: // Place nodes nodes.forEach((node, i) => { + + // coordinates of the node let d = nodeDistance * (i + 1); const x= start.x + ux * d; const y = start.y + uy * d; - // Give position - if (groupCycleName){ - if (groupCycleName in subgraphNetwork.cyclesGroup){ - if (!subgraphNetwork.cyclesGroup[groupCycleName].metadata[node]){ - subgraphNetwork.cyclesGroup[groupCycleName].metadata[node] = {}; - // get position before drawing cycle - positionBefore[node]={x:null,y:null}; - }else{ - // get position before drawing cycle - const xBefore=subgraphNetwork.cyclesGroup[groupCycleName].metadata[node]["x"]; - const yBefore=subgraphNetwork.cyclesGroup[groupCycleName].metadata[node]["y"]; - positionBefore[node]={x:xBefore,y:yBefore}; - } - // assign new position - subgraphNetwork.cyclesGroup[groupCycleName].metadata[node]["x"]=x; - subgraphNetwork.cyclesGroup[groupCycleName].metadata[node]["y"]=y; - } else { - console.error("CycleGroup not in subgraphNetwork"); - } - } else if (node in subgraphNetwork.network.nodes) { - // get position before drawing cycle - const xBefore=subgraphNetwork.network.nodes[node].x; - const yBefore=subgraphNetwork.network.nodes[node].x; - positionBefore[node]={x:xBefore,y:yBefore}; - // assign new position - subgraphNetwork.network.nodes[node].x=x; - subgraphNetwork.network.nodes[node].y=y; - } else{ - console.error("Node not in network or groupcycle not provided") - } + // assign coordinates to the node, and get the position before + const assignedCoordinates= assignCoordinateToNode(subgraphNetwork,node,x,y,groupCycleName); + subgraphNetwork=assignedCoordinates.subgraphNetwork; + positionBefore[node]=assignedCoordinates.positionBefore; // Fix the nodes const nodeNetwork=network.nodes[node]; - if (!nodeNetwork.metadata) nodeNetwork.metadata={}; - nodeNetwork.metadata.fixedInCycle= true; - nodeNetwork.metadata.fixedCycle= undefined; + if (!nodeNetwork.metadataLayout) nodeNetwork.metadataLayout={}; + nodeNetwork.metadataLayout.isFixed= true; + nodeNetwork.metadataLayout.fixedInCircle= undefined; }); return {subgraphNetwork:subgraphNetwork,positionBefore:positionBefore}; } - /** - * If the group cycle is drawn : - * - Updates the group cycles in the subgraph network : add its size and center of rectangle. - * - Change the group number. - * - * @param remainingCycles - The remaining cycles to process in the subgraph. - * @param subgraphNetwork - The subgraph network. - * @param group - The current cycle group number. - * @param groupCycleName - The name of the group cycle. - * @returns An object containing the updated subgraph network and group number. + * Place node already placed in a cycle group with a force layout + * @param subgraphNetwork - The subgraph network object. + * @param groupCycleName - The name of the group cycle where node need to be placed + * @param force - The force to apply to the nodes. Default is -500. + * @returns A promise that resolves to the updated subgraph network with the nodes placed in the group cycle. */ -async function updateGroupCycles(remainingCycles: Subgraph[], subgraphNetwork: SubgraphNetwork, group: number, groupCycleName: string): Promise<{subgraphNetwork: SubgraphNetwork, group: number}> { - const network = subgraphNetwork.network; - const groupCycleIsDraw = isRemainingCycleIndepOfDrawing(remainingCycles, subgraphNetwork); - - if (groupCycleIsDraw && subgraphNetwork.cyclesGroup[groupCycleName].metadata) { - - - // force algo for node that have null position - subgraphNetwork = await forceGroupCycle(subgraphNetwork, groupCycleName); - - // get size of group and update cycle group information - const listCoord = Object.values(subgraphNetwork.cyclesGroup[groupCycleName].metadata) - .filter(item => item["x"] !== undefined && item["y"] !== undefined); - const {width, height, center} = rectangleSize(listCoord as {x: number, y: number}[]); - subgraphNetwork.cyclesGroup[groupCycleName].width = width; - subgraphNetwork.cyclesGroup[groupCycleName].height = height; - subgraphNetwork.cyclesGroup[groupCycleName].originalPosition = center; - - // change group - group += 1; - } else if (groupCycleIsDraw) { - console.error('No coordinates for group cycle'); - // change group - group += 1; - } - - return {subgraphNetwork: subgraphNetwork, group: group}; -} - - async function forceGroupCycle(subgraphNetwork:SubgraphNetwork, groupCycleName:string,force:number=-500):Promise<SubgraphNetwork>{ - - if (!subgraphNetwork.cyclesGroup[groupCycleName] || !subgraphNetwork.cyclesGroup[groupCycleName].metadata){ - return subgraphNetwork; - } + if (!subgraphNetwork[TypeSubgraph.CYCLEGROUP] || !subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]) throw new Error("group cycle not in subgraphNetwork"); + const groupCycle=subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]; + if (!groupCycle.precalculatedNodesPosition) groupCycle.precalculatedNodesPosition={}; + const precalculatedNodesPosition=groupCycle.precalculatedNodesPosition; // get subgraph for groupCycle - const graph =getListNodeLinksForCycleGroup(subgraphNetwork,groupCycleName,true); + const graph =getListNodeLinksForCycleGroupAsArray(subgraphNetwork,groupCycleName,true); // need to apply force ? - const nullNode= graph.nodes.filter(node=>node["fx"]==null || node["fy"]==null); + const nullNode= graph.nodes.filter(node=>node.fx==undefined || node.fy==undefined); // node not fixed (?) if (nullNode.length==0){ return subgraphNetwork; } @@ -653,98 +564,84 @@ async function forceGroupCycle(subgraphNetwork:SubgraphNetwork, groupCycleName:s // get the new position of the nodes graph.nodes.forEach(node => { - subgraphNetwork.cyclesGroup[groupCycleName].metadata[node.id] = { x: node["x"], y: node["y"] }; + if(node.x!==undefined && isFinite(node.x) && node.y!==undefined && isFinite(node.y)){ + precalculatedNodesPosition[node.id] = { x: node.x, y: node.y }; + }else{ + throw new Error("error in node coordinates after force layout"); + } }); return subgraphNetwork; } -/** - * Checks if all nodes in the remaining cycles are unfixed in the subgraph network. - * - * @param remainingCycles - An array of remaining cycles in the subgraph. - * @param subgraphNetwork - The subgraph network containing the nodes. - * @returns A boolean indicating whether all nodes in the remaining cycles are unfixed. - */ -function isRemainingCycleIndepOfDrawing(remainingCycles:Subgraph[], subgraphNetwork:SubgraphNetwork):boolean{ - - const network = subgraphNetwork.network; - - if (remainingCycles.every(cycle => - cycle.nodes.every(node => - !network.nodes[node].metadata || !network.nodes[node].metadata.fixedInCycle - ) - )) { - // All nodes in all remaining cycles are unfixed - return true; - } else { - // At least one node in the remaining cycles fixed - return false; - } -} - - - - - - - - -//------------------------------------------------------------------------------------------------------------ -//__________________________________ Utility functions______________________________________________________ -//------------------------------------------------------------------------------------------------------------ +/*******************************************************************************************************************************************************/ +//___________________________________________________1. Find top node for cycle coordinate ________________________________________________________________ /** * Finds the index of the top cycle node based on the given subgraph network and cycle nodes. * The top cycle node is determined by the node with the highest parent (smaller y) if it exists. Or the opposite node in the cycle of the node with the lowest child (bigger y). - * If multiple nodes have the highest parent or lowest child, the one with the median x value is chosen. + * If multiple nodes have the highest parent or lowest child, the one with the median x value is chosen (or lowest of the two median if odd number). * If no parent or child nodes are found, the first node in the cycle is chosen. * * @param subgraphNetwork - The subgraph network object. * @param cycleNodes - An array of cycle node names. * @returns The index of the top cycle node. */ -function findTopCycleNode(subgraphNetwork: SubgraphNetwork, cycleNodes:string[]):number{ +async function findTopCycleNode(subgraphNetwork: SubgraphNetwork, cycleNodes:string[]):Promise<number>{ // find node with the highest parent (smaller y) // get parent nodes of the cycle - const cycleNodesParent = parentNodeNotInCycle(subgraphNetwork, cycleNodes); + const cycleNodesParent = parentNodeNotInCycle(subgraphNetwork, cycleNodes); // get the one with highest parent - const withHighestParent=getNodesAssociatedMinY(subgraphNetwork, cycleNodesParent) - .map(i=>cycleNodes[i]); + const indexAssociatedMinY= await getIndexNodesAssociatedMinY(subgraphNetwork, cycleNodesParent) + const withHighestParent= indexAssociatedMinY.map(i=>cycleNodes[i]); if (withHighestParent.length===1){ - return cycleNodes.indexOf(withHighestParent[0]); + const indexNode=cycleNodes.indexOf(withHighestParent[0]); + if(indexNode>=0){ + return indexNode ; + }else{ + throw new Error("indexOf return -1 : parentNodeNotInCycle or getNodesAssociatedMinY not working or used appropriately"); + } } else if (withHighestParent.length>1){ // if several : take the one with median x - const nodeMedianName=nodeMedianX(subgraphNetwork, withHighestParent); - return cycleNodes.indexOf(nodeMedianName); + const nodeMedianName=await nodeMedianX(subgraphNetwork, withHighestParent); + const indexNode=cycleNodes.indexOf(nodeMedianName); + if(indexNode>=0){ + return indexNode ; + }else{ + throw new Error("indexOf return -1 : parentNodeNotInCycle, nodeMedianX or getNodesAssociatedMinY not working or used appropriately"); + } }else{ // if no parent : opposite node of the node with lowest child (to put the one with with to the bottom) let bottomNode:number; // find node with the lowest child (bigger y) // get child nodes of the cycle - const cycleNodesChild = childNodeNotInCycle(subgraphNetwork, cycleNodes); + const cycleNodesChild = childNodeNotInCycle(subgraphNetwork, cycleNodes); // get the one with lowest child - const withLowestChild=getNodesAssociatedMaxY(subgraphNetwork, cycleNodesChild) - .map(i=>cycleNodes[i]); + const indexAssociatedMaxY=await getIndexNodesAssociatedMaxY(subgraphNetwork, cycleNodesChild) + const withLowestChild = indexAssociatedMaxY.map(i=>cycleNodes[i]); - if (withLowestChild.length>=1){ + if (withLowestChild.length>=1){ if(withLowestChild.length==1){ bottomNode=cycleNodes.indexOf(withLowestChild[0]); - }else if (withLowestChild.length>1){ + }else{ // if several : take the one with median x - const nodeMedianName=nodeMedianX(subgraphNetwork, withLowestChild); + const nodeMedianName=await nodeMedianX(subgraphNetwork, withLowestChild); bottomNode=cycleNodes.indexOf(nodeMedianName); } + if (bottomNode>=0){ // get the opposite node of the first (one as to be chosen) node in the - return (bottomNode+Math.floor(cycleNodes.length/2))%cycleNodes.length; + return (bottomNode+Math.floor(cycleNodes.length/2))%cycleNodes.length; + }else{ + throw new Error("indexOf return -1 : childNodeNotInCycle, nodeMedianX or getNodesAssociatedMaxY not working or used appropriately"); + } }else{ return 0; @@ -753,7 +650,6 @@ function findTopCycleNode(subgraphNetwork: SubgraphNetwork, cycleNodes:string[]) } - /** * Returns indices of lists containing minimum y-coordinate nodes. * @@ -761,19 +657,21 @@ function findTopCycleNode(subgraphNetwork: SubgraphNetwork, cycleNodes:string[]) * @param associatedListNodes - The list of list of nodes. * @returns An array of indices representing lists containing minimum y-coordinate nodes. */ -function getNodesAssociatedMinY(subgraphNetwork: SubgraphNetwork, associatedListNodes: string[][]): number[] { +async function getIndexNodesAssociatedMinY(subgraphNetwork: SubgraphNetwork, associatedListNodes: string[][]): Promise<number[]> { const network=subgraphNetwork.network; let minY=Infinity; let minNodes: number[] = []; associatedListNodes.forEach((listNodes,i) => { listNodes.forEach(node => { - if (network.nodes[node] && network.nodes[node].y!==undefined && network.nodes[node].y!==null) { + if (network.nodes[node] && isFinite(network.nodes[node].y)) { if (network.nodes[node].y < minY) { minY = network.nodes[node].y; minNodes = [i]; }else if (network.nodes[node].y == minY && !minNodes.includes(i)) { minNodes.push(i); } + }else{ + throw new Error("no node in network or node without y-coordinate"); } }); }); @@ -787,19 +685,21 @@ function getNodesAssociatedMinY(subgraphNetwork: SubgraphNetwork, associatedList * @param associatedListNodes - The list of list of nodes. * @returns An array of indices representing lists containing maximum y-coordinate nodes. */ -function getNodesAssociatedMaxY(subgraphNetwork: SubgraphNetwork, associatedListNodes: string[][]): number[] { +async function getIndexNodesAssociatedMaxY(subgraphNetwork: SubgraphNetwork, associatedListNodes: string[][]): Promise<number[]> { const network=subgraphNetwork.network; let maxY=-Infinity; let maxNodes:number[]=[]; associatedListNodes.forEach((listNodes,i) => { listNodes.forEach(node => { - if (network.nodes[node] && network.nodes[node].y!==undefined && network.nodes[node].y!==null) { + if (network.nodes[node] && isFinite(network.nodes[node].y)) { if (network.nodes[node].y > maxY) { maxY = network.nodes[node].y; maxNodes = [i]; }else if (network.nodes[node].y == maxY && !maxNodes.includes(i)) { maxNodes.push(i); } + }else{ + throw new Error("no node in network or node without y-coordinate"); } }); }); @@ -813,7 +713,7 @@ function getNodesAssociatedMaxY(subgraphNetwork: SubgraphNetwork, associatedList * @param listNodes - An array of node names. * @returns The name of the node with the median x-coordinate. */ -function nodeMedianX(subgraphNetwork: SubgraphNetwork, listNodes: string[]): string { +async function nodeMedianX(subgraphNetwork: SubgraphNetwork, listNodes: string[]): Promise<string> { const network=subgraphNetwork.network; let xValues = listNodes.map(node => [node,network.nodes[node].x]); xValues.sort((a, b) => Number(a[1]) - Number(b[1])); // sort by x @@ -830,27 +730,340 @@ function nodeMedianX(subgraphNetwork: SubgraphNetwork, listNodes: string[]): str } +/*******************************************************************************************************************************************************/ +//___________________________________________________2. Utilitary functions for cycle coordinate ________________________________________________________________ + +/** + * Sorting function for knowing order of cycle drawing. + * First sort by number of circle fixed nodes (nodes fixed in a circle drawing), then by size, + * then if full counstraint is true : by number of parent nodes (of the cycle), and finally by number of child nodes (of the cycle). + * @param subgraphNetwork - The subgraph network. + * @param a - The first cycle to compare. + * @param b - The second cycle to compare. + * @param fullConstraint - A flag indicating whether to consider the full constraint (default is false). + * @returns A number indicating the sorting order. + */ + function sortingCycleForDrawing(subgraphNetwork:SubgraphNetwork,a:Subgraph,b:Subgraph,fullConstraint:boolean=false):number{ + const network=subgraphNetwork.network; + + // first sort by number of fixed nodes + const fixedNodesA = a.nodes.filter(node => network.nodes[node].metadataLayout && network.nodes[node].metadataLayout.fixedInCircle).length; + const fixedNodesB = b.nodes.filter(node => network.nodes[node].metadataLayout && network.nodes[node].metadataLayout.fixedInCircle).length; + if (fixedNodesA !== fixedNodesB){ + return fixedNodesB - fixedNodesA; + }else{ + // sort by size + if ( !fullConstraint || b.nodes.length !== a.nodes.length ){ + return b.nodes.length - a.nodes.length; + }else{ + // then by number of parent nodes + const totalParentNodesA = parentNodeNotInCycle(subgraphNetwork, a.nodes) + .flat().length; + const totalParentNodesB = parentNodeNotInCycle(subgraphNetwork, b.nodes) + .flat().length; + if (totalParentNodesA !== totalParentNodesB){ + return totalParentNodesB - totalParentNodesA; + }else{ + // then by number of child nodes + const totalChildNodesA = childNodeNotInCycle(subgraphNetwork, a.nodes) + .flat().length; + const totalChildNodesB = childNodeNotInCycle(subgraphNetwork, b.nodes) + .flat().length; + + return totalChildNodesB - totalChildNodesA; + } + } + } +} + + +/** + * Returns the cycle group name by appending the given name with a prefix. + * + * @param name - The name to be appended with the prefix. + * @returns The cycle group name. + */ +function cycleGroupName(name:string):string{ + return "cycle_group_"+name; +} +/** + * Adds a new cycle group to the subgraphNetwork. + * + * @param subgraphNetwork - The subgraph network to add the cycle group to. + * @param groupName - The name of the cycle group. + * @returns The updated subgraph network with the added cycle group. + */ +function addNewCycleGroup(subgraphNetwork:SubgraphNetwork, groupName:string):SubgraphNetwork{ + if(!subgraphNetwork[TypeSubgraph.CYCLEGROUP]){ + subgraphNetwork[TypeSubgraph.CYCLEGROUP]={}; + } + subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupName]={name:groupName,nodes:[],type:TypeSubgraph.CYCLEGROUP}; + return subgraphNetwork; +} +/** + * If the group cycle is drawn : + * - Updates the group cycles in the subgraphNetwork : add its size and center of rectangle. + * - Change the group number. + * + * @param remainingCycles - The remaining cycles to process in the subgraph. + * @param subgraphNetwork - The subgraph network. + * @param group - The current cycle group number. + * @param groupCycleName - The name of the group cycle. + * @returns An object containing the updated subgraph network and group number. + */ +async function updateGroupCycles(remainingCycles: Subgraph[], subgraphNetwork: SubgraphNetwork, group: number, groupCycleName: string): Promise<{subgraphNetwork: SubgraphNetwork, group: number}> { + if (! subgraphNetwork[TypeSubgraph.CYCLEGROUP] || ! subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]) throw new Error("Group cycle not in subgraphNetwork"); + const cycleGroup=subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]; + // check if group cycle is drawn, that is, no fixed nodes for other cycles (other cycle independant) + const groupCycleIsDraw = isRemainingCycleIndepOfDrawing(remainingCycles, subgraphNetwork); + if (groupCycleIsDraw && cycleGroup.precalculatedNodesPosition) { + + // force algo for node that have null position + subgraphNetwork = await forceGroupCycle(subgraphNetwork, groupCycleName); -//------------------------------------------------------------------------------------------------------------ -//________________________________Drawing cycle : shift position and placement in network_______________________________________ -//------------------------------------------------------------------------------------------------------------ + // get size of group and update cycle group information + const nodesInCycleGroup =getNodesPlacedInGroupCycleAsObject(subgraphNetwork,groupCycleName); + const coordinatesNodesCycleGroup = Object.values(nodesInCycleGroup) + const {width, height, center} = rectangleSize(coordinatesNodesCycleGroup); + cycleGroup.width = width; + cycleGroup.height = height; + cycleGroup.originalPosition = center; + // change group + group += 1; + + } else if (groupCycleIsDraw) { + throw new Error('No coordinates for group cycle'); + } + + return {subgraphNetwork: subgraphNetwork, group: group}; +} /** - * Draws all cycle groups in the given subgraph network, that is put the precalculated coordinates inside the network + * Checks if all nodes in the remaining cycles are unfixed in the subgraphNetwork. + * + * @param remainingCycles - An array of remaining cycles in the subgraph. + * @param subgraphNetwork - The subgraph network containing the nodes. + * @returns A boolean indicating whether all nodes in the remaining cycles are unfixed. + */ +function isRemainingCycleIndepOfDrawing(remainingCycles:Subgraph[], subgraphNetwork:SubgraphNetwork):boolean{ + + const network = subgraphNetwork.network; + + if (remainingCycles.every(cycle => + cycle.nodes.every(node => + !network.nodes[node].metadataLayout || !network.nodes[node].metadataLayout.isFixed + ) + )) { + // All nodes in all remaining cycles are unfixed + return true; + } else { + // At least one node in the remaining cycles fixed + return false; + } +} + + +/** + * Calculates the radius size based on the length of the cycle array and the radius factor. + * @param cycle - The array of strings representing the cycle. + * @param radiusFactor - The factor to multiply the length of the cycle by to calculate the radius size. Default value is 15. + * @returns The calculated radius size. + */ +async function getRadiusSize(cycle:string[],network:Network,styleNetwork:GraphStyleProperties):Promise<number>{ + const nodes=Object.values(network.nodes).filter(node=>cycle.includes(node.id)); + const meanSize=await getMeanNodesSizePixel(nodes,styleNetwork); + return cycle.length*(meanSize.height+meanSize.width)/2; +} + + + +/** + * Assigns coordinates to a node within a subgraphNetwork, and get previous position of the node. If a group cycle name is provided, + * the coordinates are assigned in the group cycle as a precalculated position. Otherwise, + * the coordinates are assigned directly to the network. + * + * @param {SubgraphNetwork} subgraphNetwork - The subgraph network containing the node. + * @param {string} nodeID - The ID of the node to assign coordinates to. + * @param {number} x - The x-coordinate to assign to the node. + * @param {number} y - The y-coordinate to assign to the node. + * @param {string} [groupCycleName] - Optional name of the group cycle to assign the coordinates to. + * @returns {{subgraphNetwork: SubgraphNetwork, positionBefore: CoordinateNull}} An object containing the updated subgraph network and the node's position before the assignment. + * @throws {Error} If the group cycle name is provided but not found in the subgraph network, or if the node is not found in the network and no group cycle name is provided. + */ +function assignCoordinateToNode(subgraphNetwork:SubgraphNetwork,nodeID:string,x:number,y:number,groupCycleName?:string):{subgraphNetwork:SubgraphNetwork,positionBefore:CoordinateNull}{ + const network=subgraphNetwork.network; + let positionBefore:CoordinateNull; + + // if groupCycleName is provided, assign position to the groupCycle as precalculated position + if (groupCycleName){ + if (!subgraphNetwork[TypeSubgraph.CYCLEGROUP] || !subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]) throw new Error("group cycle not in subgraphNetwork"); + const groupCycle=subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]; + + if (!groupCycle.precalculatedNodesPosition) groupCycle.precalculatedNodesPosition={}; + const precalculatedNodesPosition=groupCycle.precalculatedNodesPosition; + + if (!precalculatedNodesPosition[nodeID]){ + // get position before drawing cycle, when node never been placed + positionBefore={x:null,y:null}; + // assign new position + precalculatedNodesPosition[nodeID]={x:x,y:y}; + }else{ + // get position before drawing cycle + const xBefore=precalculatedNodesPosition[nodeID].x; + const yBefore=precalculatedNodesPosition[nodeID].y; + positionBefore={x:xBefore,y:yBefore}; + // assign new position + precalculatedNodesPosition[nodeID].x=x; + precalculatedNodesPosition[nodeID].y=y; + } + // else assign position to the network + } else if (nodeID in network.nodes) { + // get position before drawing cycle (in the network) + const xBefore=network.nodes[nodeID].x; + const yBefore=network.nodes[nodeID].x; + positionBefore={x:xBefore,y:yBefore}; + // assign new position + network.nodes[nodeID].x=x; + network.nodes[nodeID].y=y; + } else{ + throw new Error("Node not in network or groupcycle not provided") + } + return {subgraphNetwork:subgraphNetwork,positionBefore:positionBefore}; +} + +/** + * Reverts the positions of nodes in a subgraph network to their previous positions if there is an overlap in the group cycle. + * + * @param subgraphNetwork - The subgraph network containing the group cycle. + * @param groupCycleName - The name of the group cycle to check for overlaps. + * @param positionBefore - An object mapping node IDs to their positions before the drawing cycle. + * @returns The updated subgraph network with node positions reverted if there was an overlap. + * @throws Will throw an error if the group cycle does not exist in the subgraph network. + */ +function undoIfCreateOverlap(subgraphNetwork:SubgraphNetwork,groupCycleName:string,positionBefore:{[key:string]:CoordinateNull}):SubgraphNetwork{ + if ( !subgraphNetwork[TypeSubgraph.CYCLEGROUP] || ! subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName] ) throw new Error("No group cycle in subgraphNetwork"); + const groupCycle=subgraphNetwork[TypeSubgraph.CYCLEGROUP][groupCycleName]; + + // is there overlap in the group cycle ? + const isOverlap=isOverlapInCycleGroupDrawing(subgraphNetwork,groupCycleName); + // if overlap, put back the position before drawing cycle + if(isOverlap){ + Object.entries(positionBefore).forEach(([nodeID,coord])=>{ + if (!groupCycle.precalculatedNodesPosition) groupCycle.precalculatedNodesPosition={}; + const xBefore=positionBefore[nodeID].x; + const yBefore=positionBefore[nodeID].y; + + if (!groupCycle.precalculatedNodesPosition[nodeID]){ + groupCycle.precalculatedNodesPosition[nodeID]={x:xBefore,y:yBefore}; + } else { + const node=groupCycle.precalculatedNodesPosition[nodeID]; + node.x = xBefore; + node.y = yBefore; + } + + }); + } + return subgraphNetwork; +} + +/** + * Determines if there are overlapping in cycle group. + * + * This function checks for intersections and overlaps among nodes and edges + * within a specified cycle group in the subgraph network. It performs the following checks: + * 1. Intersection of edges. + * 2. Overlap of nodes. + * 3. Overlap of nodes with edges. + * + * @param subgraphNetwork - The subgraph network containing the cycle groups and network details. + * @param groupCycleName - The name of the cycle group to check for overlaps. + * @returns `true` if there are overlapping cycles, otherwise `false`. + */ +function isOverlapInCycleGroupDrawing(subgraphNetwork:SubgraphNetwork,groupCycleName:string):boolean{ + const graph =getListNodeLinksForCycleGroupAsObject(subgraphNetwork,groupCycleName); + // intersection of edges : + const intersectionEdges=isIntersectionGraph(graph.nodes ,graph.links); + if (intersectionEdges){ + return true; + }else{ + // overlap of nodes + const nodesOverlap=isOverlapNodes(graph.nodes,subgraphNetwork.network,subgraphNetwork.networkStyle); + if (nodesOverlap){ + return true; + }else{ + // overlap of node with edges + const edgeNodeOverlap=isOverlapNodesEdges(graph.nodes,graph.links,subgraphNetwork.network,subgraphNetwork.networkStyle); + if (edgeNodeOverlap){ + return true; + } + } + } + return false; +} + + +/** + * Retrieves the unfixed intervals from a list of nodes. An unfixed interval is a continuous range of nodes that are not fixed in a cycle. + * An unfixed interval is a continuous range of nodes that are not fixed in the circle. + * + * @param nodes - An array of node IDs. + * @param subgraphNetwork - The subgraph network containing the nodes. + * @returns An array of intervals representing the unfixed intervals. + */ +async function getUnfixedIntervals(nodes:string[],subgraphNetwork:SubgraphNetwork):Promise<number[][]> { + let intervals:number[][] = []; + let start : number |null= null; + const network=subgraphNetwork.network; + nodes.forEach((nodeID,i) => { + const node=network.nodes[nodeID]; + if (node.metadataLayout && !node.metadataLayout.fixedInCircle) { + if (start === null) { + start = i; + } + } else { + if (start !== null) { + intervals.push([start, i - 1]); + start = null; + } + } + }); + + // Handle case where the last node is fixed + if (start !== null) { + intervals.push([start, nodes.length - 1]); + } + + // case first interval and last are linked : combine interval as one long interval + if (intervals.length!==0 && intervals[0][0]===0 && intervals[intervals.length-1][1]===nodes.length-1){ + intervals[0][0]=intervals[intervals.length-1][0]; // change start of first interval + intervals.pop(); // remove last interval + } + + return intervals; +} + + + +/*******************************************************************************************************************************************************/ +//____________________________________________3. Drawing cycle : shift position and placement in network________________________________________________ + + + +/** + * Draws all cycle groups in the given subgraphNetwork, that is, put the precalculated coordinates inside the network * * @param subgraphNetwork - The subgraph network containing the cycle groups. */ -export function drawAllCyclesGroup(subgraphNetwork:SubgraphNetwork) { +export function drawAllCyclesGroup(subgraphNetwork:SubgraphNetwork):void { - //console.log('drawing cycles group'); if (TypeSubgraph.CYCLEGROUP in subgraphNetwork){ - const cycleGroups = Object.keys(subgraphNetwork.cyclesGroup); + const cycleGroups = Object.keys(subgraphNetwork[TypeSubgraph.CYCLEGROUP] as {[key:string]:Subgraph}); cycleGroups.forEach(cycleGroup => { drawCycleGroup(cycleGroup,subgraphNetwork); }); @@ -865,17 +1078,25 @@ export function drawAllCyclesGroup(subgraphNetwork:SubgraphNetwork) { * @param subgraphNetwork - The subgraph network containing the cycle group and its nodes. */ function drawCycleGroup(cycleGroup:string,subgraphNetwork:SubgraphNetwork):void{ - const metanodePosition=subgraphNetwork.cyclesGroup[cycleGroup].position; - const currentCenterPosition=subgraphNetwork.cyclesGroup[cycleGroup].originalPosition; - const dx=metanodePosition.x-currentCenterPosition.x; - const dy=metanodePosition.y-currentCenterPosition.y; - const listNode = Object.entries(subgraphNetwork.cyclesGroup[cycleGroup].metadata) - .filter(([key,item]) => item["x"] !== undefined && item["y"] !== undefined); - listNode.forEach(([nodeID,coord])=>{ - const node=subgraphNetwork.network.value.nodes[nodeID]; - node.x = coord["x"] + dx; - node.y = coord["y"] + dy; - }); + if (subgraphNetwork[TypeSubgraph.CYCLEGROUP]){ + + if(!(cycleGroup in subgraphNetwork[TypeSubgraph.CYCLEGROUP])) throw new Error("Cycle group not in subgraph network"); + const cycleGroupSubgraph=subgraphNetwork[TypeSubgraph.CYCLEGROUP][cycleGroup]; + if ( !cycleGroupSubgraph.position || !cycleGroupSubgraph.originalPosition) throw new Error("Position and/or originalPosition not in cycle group"); + const metanodePosition=cycleGroupSubgraph.position; + const currentCenterPosition=cycleGroupSubgraph.originalPosition; + const dx=metanodePosition.x-currentCenterPosition.x; + const dy=metanodePosition.y-currentCenterPosition.y; + + const listNodeInCycleGroup=getNodesPlacedInGroupCycleAsObject(subgraphNetwork,cycleGroup); + Object.entries(listNodeInCycleGroup).forEach(([nodeID,coord])=>{ + if (!(nodeID in subgraphNetwork.network.nodes)) throw new Error("Node not in network"); + const node=subgraphNetwork.network.nodes[nodeID]; + node.x = coord.x + dx; + node.y = coord.y + dy; + }); + + } } diff --git a/src/composables/LayoutFindCycle.ts b/src/composables/LayoutFindCycle.ts index a0bba171ed1fe9a5c142f54a8cb788ed52fc1127..23e66ee39ce760132a813720f1c0cab83ac916dd 100644 --- a/src/composables/LayoutFindCycle.ts +++ b/src/composables/LayoutFindCycle.ts @@ -1,14 +1,52 @@ +// Type imports import { SubgraphNetwork } from "../types/SubgraphNetwork"; import { Network } from "@metabohub/viz-core/src/types/Network"; -import { addSubgraphToNetwork, createSubgraph, updateNodeMetadataSubgraph } from "./SubgraphForSubgraphNetwork"; -import { TypeSubgraph } from "../types/Subgraph"; +import { Subgraph, TypeSubgraph } from "../types/Subgraph"; + + +// Composable imports +import { addSubgraphToNetwork, createSubgraph, updateNodeMetadataSubgraph, updateParentSubgraphOf } from "./SubgraphForSubgraphNetwork"; import { keepFirstReversibleNode, renameAllIDNode } from "./LayoutReversibleReactions"; +/** + * This file contains functions to find (directed) cycles in a network, and add them in the subgraphNetwork. + * If there is reversible reactions, the function will choose the appropriate direction to get the biggest directed cycles, + * and then remove the other version of the reaction node (and rename it if necessary). Cycles having common nodes will be associated as parent-child. + * + * -> addDirectedCycleToSubgraphNetwork : + * adds directed cycles to a subgraphNetwork. + * + * -> getJohnsonCycles : + * finds cycles in a given subgraph network using Johnson's algorithm. + * + * -> graphForJohnson : + * constructs a graph representation suitable for Johnson's algorithm to find cycles. + * + * -> JohnsonAlgorithm : + * finds all elementary cycles in a directed graph using Johnson's algorithm. + * + * -> arePermutations : + * checks if two arrays are permutations of each other. + */ + + + +/** + * Adds directed cycles to a subgraph network. + * + * This function identifies cycles within the given subgraph network and adds them to the network. + * It ensures that reversible reactions are appropriately directed (in order to get a maximum of the biggest directed cycles) and updates the metadata of nodes + * to include cycle information. If cycles share common nodes, it establishes parent-child relationships + * between the cycles based on their sizes : the biggest is the parent of the smallest. + * + * @param {SubgraphNetwork} subgraphNetwork - The subgraph network to which cycles will be added. + * @param {number} [minsize=4] - The minimum size of cycles to be considered. + * @returns {SubgraphNetwork} - The updated subgraph network with added cycles. + */ export function addDirectedCycleToSubgraphNetwork(subgraphNetwork:SubgraphNetwork,minsize:number=4):SubgraphNetwork{ - //console.log('add cycle'); - if (!subgraphNetwork.cycles){ - subgraphNetwork.cycles={}; + if (!subgraphNetwork[TypeSubgraph.CYCLE]){ + subgraphNetwork[TypeSubgraph.CYCLE]={}; } let nodeToRename:{[key: string]: string}={}; // to keep track of reversible reaction that have been removed @@ -17,7 +55,7 @@ export function addDirectedCycleToSubgraphNetwork(subgraphNetwork:SubgraphNetwor // sort result depending on size : longest cycles first const sortedCycles = Object.entries(cycles).sort((a, b) => b[1].length - a[1].length); // [1] to get the value of object (not key) - // adding cycles to subgraph network, and choose appropriate direction for reversible reactions + // adding cycles to subgraph network, and choose appropriate direction for reversible reactions : sortedCycles.forEach(cycle=>{// cycle[0] is the name/id, cycle[1] is the list of nodes // check if all nodes of cycle still exists (needed because of removal of reversible nodes) let existingCycle=true; @@ -40,7 +78,7 @@ export function addDirectedCycleToSubgraphNetwork(subgraphNetwork:SubgraphNetwor // has common nodes with a cycle in subgraphNetwork ? - let networkCycles = Object.values(subgraphNetwork.cycles); + let networkCycles = Object.values(subgraphNetwork[TypeSubgraph.CYCLE] as {[key:string]:Subgraph}); let i = 0; let commonNodes = []; while (i < networkCycles.length) { @@ -54,23 +92,27 @@ export function addDirectedCycleToSubgraphNetwork(subgraphNetwork:SubgraphNetwor const subgraph=createSubgraph(cycle[0],cycle[1],[],TypeSubgraph.CYCLE); subgraphNetwork=addSubgraphToNetwork(subgraphNetwork,subgraph,TypeSubgraph.CYCLE); - // if combined cycle (with cycle i) : that is, if there are more than one common nodes - if (commonNodes.length > 1) { - const sizeCycle=cycle[1].length; - const sizeCommonCycle= networkCycles[i].nodes.length; - // add information of 'parent' (bigger cycle) and 'child' (smaller cycle) cycle - if (sizeCycle<=sizeCommonCycle){ // if sorting worked : it is always the case - subgraphNetwork.cycles[cycle[0]].parentSubgraph={name:networkCycles[i].name,type:TypeSubgraph.CYCLE}; - if(! subgraphNetwork.cycles[networkCycles[i].name].childrenSubgraphs){ - subgraphNetwork.cycles[networkCycles[i].name].childrenSubgraphs=[]; - } - subgraphNetwork.cycles[networkCycles[i].name].childrenSubgraphs.push({name:cycle[0],type:TypeSubgraph.CYCLE}); - }else{ - subgraphNetwork.cycles[networkCycles[i].name].parentSubgraph={name:cycle[0],type:TypeSubgraph.CYCLE}; - if(! subgraphNetwork.cycles[cycle[0]].childrenSubgraphs){ - subgraphNetwork.cycles[cycle[0]].childrenSubgraphs=[]; + if (subgraphNetwork[TypeSubgraph.CYCLE]){ + // if combined cycle (with cycle i) : that is, if there are more than one common nodes + if (commonNodes.length > 1) { + const sizeCycle=cycle[1].length; + const sizeCommonCycle= networkCycles[i].nodes.length; + // add information of 'parent' (bigger cycle) and 'child' (smaller cycle) cycle + if (sizeCycle<=sizeCommonCycle){ // if sorting worked : it is supposed to always be the case + subgraphNetwork[TypeSubgraph.CYCLE][cycle[0]].parentSubgraph={name:networkCycles[i].name,type:TypeSubgraph.CYCLE}; + updateParentSubgraphOf(subgraphNetwork,subgraphNetwork[TypeSubgraph.CYCLE][cycle[0]]); + // if(! subgraphNetwork[TypeSubgraph.CYCLE][networkCycles[i].name].childrenSubgraphs){ + // subgraphNetwork[TypeSubgraph.CYCLE][networkCycles[i].name].childrenSubgraphs=[]; + // } + // subgraphNetwork[TypeSubgraph.CYCLE][networkCycles[i].name].childrenSubgraphs.push({name:cycle[0],type:TypeSubgraph.CYCLE}); + }else{ + subgraphNetwork[TypeSubgraph.CYCLE][networkCycles[i].name].parentSubgraph={name:cycle[0],type:TypeSubgraph.CYCLE}; + updateParentSubgraphOf(subgraphNetwork,subgraphNetwork[TypeSubgraph.CYCLE][networkCycles[i].name]); + // if(! subgraphNetwork[TypeSubgraph.CYCLE] [cycle[0]].childrenSubgraphs){ + // subgraphNetwork[TypeSubgraph.CYCLE] [cycle[0]].childrenSubgraphs=[]; + // } + // subgraphNetwork[TypeSubgraph.CYCLE] [cycle[0]].childrenSubgraphs.push({name:networkCycles[i].name,type:TypeSubgraph.CYCLE}); } - subgraphNetwork.cycles[cycle[0]].childrenSubgraphs.push({name:networkCycles[i].name,type:TypeSubgraph.CYCLE}); } } @@ -83,6 +125,14 @@ export function addDirectedCycleToSubgraphNetwork(subgraphNetwork:SubgraphNetwor return subgraphNetwork; } +/** + * Finds cycles in a given subgraph network using Johnson's algorithm. + * + * @param subNetwork - The subgraph network to analyze. + * @param onlyDirectedCycle - A boolean indicating whether to find only directed cycles. Defaults to `true`. + * @param minsize - The minimum size of the cycles to be found. Defaults to `4`. + * @returns An object where keys are cycle identifiers and values are arrays of node identifiers representing the cycles. + */ function getJohnsonCycles(subNetwork:SubgraphNetwork,onlyDirectedCycle:boolean=true,minsize:number=4):{[key:string]:string[]} { // get graph structure for johnson algorithm const nodes=Object.keys(subNetwork.network.nodes).sort(); @@ -93,6 +143,14 @@ function getJohnsonCycles(subNetwork:SubgraphNetwork,onlyDirectedCycle:boolean=t +/** + * Constructs a graph representation suitable for Johnson's algorithm to find cycles. + * + * @param network - The network containing nodes and links. + * @param list_nodes - An array of node identifiers. + * @param onlyDirectedCycle - A boolean indicating whether to consider only directed cycles (default is true). + * @returns A 2D array representing the adjacency list of the graph. + */ function graphForJohnson(network:Network, list_nodes:string[], onlyDirectedCycle:boolean=true):number[][]{ let graph: number[][] = Array.from({ length: list_nodes.length }, () => []); network.links.forEach(link=>{ @@ -107,6 +165,20 @@ function graphForJohnson(network:Network, list_nodes:string[], onlyDirectedCycle } +/** + * Finds all elementary cycles in a directed graph using Johnson's algorithm. + * + * @param graph - The adjacency matrix of the graph where graph[i][j] is 1 if there is an edge from node i to node j, otherwise 0. + * @param list_nodes - An array of node identifiers corresponding to the indices in the graph. + * @param flag - Determines whether to find a single cycle or all cycles. Can be "Single" or "All". Default is "All". + * @param onlyDirectedCycle - If true, only directed cycles are considered. Default is true. + * @param minsize - The minimum size of the cycles to be considered. Default is 4. + * @param network - An optional parameter representing the network which contains additional metadata for nodes. + * + * @returns An object where the keys are cycle identifiers and the values are arrays of node identifiers representing the cycles. + * + * @throws Will throw an error if there is a null value in the stack, indicating a problem with stackTop. + */ export function JohnsonAlgorithm(graph: number[][], list_nodes:string[],flag: "Single" | "All"="All",onlyDirectedCycle:boolean=true,minsize:number=4,network?:Network): {[key:string]:string[]} { const nVertices: number = graph.length; let nbcycle:number=0; @@ -157,7 +229,7 @@ export function JohnsonAlgorithm(graph: number[][], list_nodes:string[],flag: "S // if network : node w not taken into account if reversible version in the stack (cycle with only one of the version) if(network){ const wNode=network.nodes[list_nodes[w]]; - if( "metadata" in wNode && "reversibleVersion" in wNode.metadata){ + if( wNode.metadata && "reversibleVersion" in wNode.metadata){ const stackCopy = stack.slice(0, stackTop); const reactionRev =wNode.metadata["reversibleVersion"] as string; const reversibleNumber=list_nodes.indexOf(reactionRev); @@ -169,14 +241,18 @@ export function JohnsonAlgorithm(graph: number[][], list_nodes:string[],flag: "S // If w is equal to start, a cycle is found (find only cycle that start from start node) if (w === start) { - let cycle: number[] = stack.slice(0, stackTop); + let cycle: number[] = stack.slice(0, stackTop) as number[]; // If the cycle length is more than 3, add it to the result if (cycle.length >= minsize) { const cycleID:string[]=[]; stack.slice(0, stackTop).forEach(nodeIndex=>{ - cycleID.push(list_nodes[nodeIndex]); + if (nodeIndex!==null){ + cycleID.push(list_nodes[nodeIndex]); + }else{ + throw new Error("null value in stack : problem with stackTop"); + } }); //adding check that not already found in the case of undirected cycle if (!onlyDirectedCycle && !Object.values(result).some(existingCycle => arePermutations(existingCycle, cycleID))) { @@ -212,7 +288,7 @@ export function JohnsonAlgorithm(graph: number[][], list_nodes:string[],flag: "S } } } - v = stackPop(); + v = stackPop() as number; return f; } @@ -252,7 +328,13 @@ export function JohnsonAlgorithm(graph: number[][], list_nodes:string[],flag: "S return result; } -//chatgtp function +/** + * Checks if two arrays are permutations of each other. + * + * @param arr1 - The first array. + * @param arr2 - The second array. + * @returns A boolean indicating whether the two arrays are permutations of each other. + */ function arePermutations(arr1: string[], arr2: string[]): boolean { if (arr1.length !== arr2.length) return false; diff --git a/src/composables/LayoutMain.ts b/src/composables/LayoutMain.ts index 05b65c4cf0e7ec9a9a5b8ac237ca050d9f34b844..2070008444a5343086614e9f3ffd42ab3282c9b2 100644 --- a/src/composables/LayoutMain.ts +++ b/src/composables/LayoutMain.ts @@ -156,15 +156,15 @@ export async function allSteps(subgraphNetwork: SubgraphNetwork,parameters:Param if (parameters.doMainChain){ if (printNameStep) console.log('Find main chain'); const sources=await getStartNodes(network,parameters.startNodeTypeMainChain); - addMainChainFromSources(subgraphNetwork, sources,parameters.getSubgraph, parameters.merge,parameters.pathType); + await addMainChainFromSources(subgraphNetwork, sources,parameters.getSubgraph, parameters.merge,parameters.pathType); } } ).then( - () => { + async () => { // add minibranch if(parameters.doMainChain && parameters.doMiniBranch){ if (printNameStep) console.log('Add minibranch'); - subgraphNetwork= addMiniBranchToMainChain(subgraphNetwork); + subgraphNetwork= await addMiniBranchToMainChain(subgraphNetwork); }else if ( !parameters.doMainChain && parameters.doMiniBranch){ console.warn('doMiniBranch is true but doMainChain is false : minibranch will not be added'); } diff --git a/src/composables/LayoutMainChain.ts b/src/composables/LayoutMainChain.ts index bd130f864244d5f8e7e57602aaf3d13fabaf1308..688ccaa303e2f3e1f17b5712d9a2e3614a87a0b9 100644 --- a/src/composables/LayoutMainChain.ts +++ b/src/composables/LayoutMainChain.ts @@ -1,54 +1,91 @@ +// Type imports import { PathType, StartNodesType } from "../types/EnumArgs"; +import { SubgraphNetwork } from "../types/SubgraphNetwork"; +import {TypeSubgraph, type Subgraph} from "../types/Subgraph"; +import { Network } from "@metabohub/viz-core/src/types/Network"; + +// Composable imports import { DFSWithSources, DFSsourceDAG } from "./AlgorithmDFS"; import { networkToGDSGraph } from "./ConvertFromNetwork"; -import { SubgraphNetwork } from "../types/SubgraphNetwork"; import { getStartNodes } from "./CalculateStartNodes"; -import { Network } from "@metabohub/viz-core/src/types/Network"; import { BFS } from "./AlgorithmBFS"; -import {TypeSubgraph, type Subgraph} from "../types/Subgraph"; import { addSubgraphToNetwork, createSubgraph, updateNodeMetadataSubgraph } from './SubgraphForSubgraphNetwork'; +/** + * This file contains functions find and add main chains and mini-branch (form main chain) in a subgraphNetwork. + * + * ********************************* + * + * 0. Main steps for main chains + * + * -> addMainChainFromSources : + * Adds main chains from sources to the subgraph network. + * + * -> addMiniBranchToMainChain : + * Adds mini branches to the main chain in the cluster network. + * + * + * ********************************* + * + * 1. Method for main chains + * + * -> getPathSourcesToTargetNode : + * Returns the longest paths from source nodes for the source DAG. + * + * -> DistanceFromSourceDAG : + * Dijkstra like algorithm to get longest distance from source node for each node. + * + * -> findMaxKeys : + * Find the keys associated with the maximum value in the object. + * + * -> mergeNewPath : + * Merge a new path in the object with all paths. + * + */ + +/*******************************************************************************************************************************************************/ +//___________________________________________________0. Main steps for main chains_______________________________________________________________________ + /** - * Add clusters (in ClusterNetwork object) taht are at least of a minimum size. A method is given in the parameters to get the clusters - * @param subgraphNetwork an object with the network and object for clusters - * @param sources array of node ID or a type of method to get sources automatically + * Adds main chains from sources to the subgraph network. + * + * @param subgraphNetwork - The subgraph network to which main chains will be added. + * @param sources - An array of source node IDs or a StartNodesType object. * RANK_ONLY : sources are nodes of rank 0 * SOURCE_ONLY : sources are topological sources of the network (nul indegree) * RANK_SOURCE : sources are node of rank 0, then source nodes * ALL : sources are all nodes * SOURCE_ALL : sources are topological sources, then all the others nodes * RANK_SOURCE_ALL : sources are node of rank 0, then topological sources, then all the other nodes - * For this function, the advised choice is either RANK_ONLY, SOURCE_ONLY or RANK_SOURCE. - * @param getMainChains function that return the clusters to add - * @param merge if true, merge the path with an existing one if a common node is found, else common nodes in several clusters - * => not taken by all methods of getCluster - * @param pathType the type of path to target node wanted : - * LONGEST : one of the longest path + * @param getMainChains - A function that returns main chains of paths from sources. Defaults to `getPathSourcesToTargetNode`. + * @param merge - A boolean indicating whether to merge paths. Defaults to `true`. + * @param pathType - The type of path to consider. Defaults to `PathType.ALL_LONGEST`. + * * LONGEST : one of the longest path * ALL_LONGEST : all the longest lenght * ALL : all path - * => not taken by all methods of getCluster - * @param minHeight minimum size of a cluster to be added - * @returns the clusterNetwork with more cluster + * @param minHeight - The minimum height of the main chain to be added. Defaults to `4`. + * @returns A promise of the updated subgraph network with the added main chains. */ -export function addMainChainFromSources(subgraphNetwork:SubgraphNetwork, sources:Array<string> | StartNodesType, - getMainChains:(network: Network, sources: Array<string>,merge?:boolean,pathType?:PathType) => {[key:string]:{nodes:Array<string>, height:number}}=getPathSourcesToTargetNode, +export async function addMainChainFromSources(subgraphNetwork:SubgraphNetwork, sources:Array<string> | StartNodesType, + getMainChains:(network: Network, sources: Array<string>,merge?:boolean,pathType?:PathType) => Promise<{[key:string]:{nodes:Array<string>, height:number}}>=getPathSourcesToTargetNode, merge:boolean=true, pathType:PathType=PathType.ALL_LONGEST, minHeight:number=4 -):SubgraphNetwork{ +):Promise<SubgraphNetwork>{ - //console.log('create main chain from longest path'); const network=subgraphNetwork.network; - subgraphNetwork[TypeSubgraph.MAIN_CHAIN]={}; + if(!subgraphNetwork[TypeSubgraph.MAIN_CHAIN]) subgraphNetwork[TypeSubgraph.MAIN_CHAIN]={}; + const mainChains=subgraphNetwork[TypeSubgraph.MAIN_CHAIN]; + // get sources if (!Array.isArray(sources)){ - sources=getStartNodes(network,sources); + sources=await getStartNodes(network,sources); } // get main chains of paths from sources - const newMainChains=getMainChains(network,sources as string[],merge,pathType); + const newMainChains=await getMainChains(network,sources as string[],merge,pathType); // add main chains if length > minsize, and update metadata for nodes Object.entries(newMainChains).forEach(([mainChainID,mainChain]:[string,{nodes:Array<string>, height:number}])=>{ @@ -56,10 +93,10 @@ export function addMainChainFromSources(subgraphNetwork:SubgraphNetwork, sources // create subgraph and add it const newMainChainId="mainChain__"+mainChainID; const newMainChain= createSubgraph(newMainChainId, mainChain.nodes,[],TypeSubgraph.MAIN_CHAIN); - subgraphNetwork.mainChains[newMainChainId]=newMainChain; + mainChains[newMainChainId]=newMainChain; // add metadata for node in cluster mainChain.nodes.forEach(nodeID=>{ - updateNodeMetadataSubgraph(network, nodeID, newMainChainId); + updateNodeMetadataSubgraph(network, nodeID, newMainChainId, TypeSubgraph.MAIN_CHAIN); }); } }); @@ -72,18 +109,19 @@ export function addMainChainFromSources(subgraphNetwork:SubgraphNetwork, sources * Adds mini branches to the main chain in the cluster network. * A mini branch is a child of a node in main chain cluster that has no children. * @param subgraphNetwork - The cluster network to modify. - * @returns The modified cluster network. + * @returns A promise of the modified cluster network. */ -export function addMiniBranchToMainChain(subgraphNetwork:SubgraphNetwork):SubgraphNetwork{ - //console.log('add mini branch to main chain'); +export async function addMiniBranchToMainChain(subgraphNetwork:SubgraphNetwork):Promise<SubgraphNetwork>{ + if(!subgraphNetwork[TypeSubgraph.MAIN_CHAIN]) return subgraphNetwork; + const mainChains=subgraphNetwork[TypeSubgraph.MAIN_CHAIN]; const network=subgraphNetwork.network; - const graph=networkToGDSGraph(network); + const graph=await networkToGDSGraph(network); // for each main chain : - Object.entries(subgraphNetwork.mainChains).forEach(([mainChainID,mainChain]:[string,Subgraph]) => { + Object.entries(mainChains).forEach(([mainChainID,mainChain]:[string,Subgraph]) => { const nodesToAdd:string[]=[]; // for each node of the main chain : mainChain.nodes.forEach(node=>{ - const children=graph.adjacent(node); + const children:string[]=graph.adjacent(node); children.forEach(child=>{ // if child is sink : if (graph.outdegree(child)===0){ @@ -106,129 +144,10 @@ export function addMiniBranchToMainChain(subgraphNetwork:SubgraphNetwork):Subgra -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// ---------------------------------------------- Method 1 of getCluster for main chains --------------------------------------------------------- -// ---------------------------------------------------------------------------------------------------------------------------------------------- - - -/** - * Get a long path from sources using a DFS. The path isn't the longest if there some undirected cycle. - * @param network - * @param sources to use for DFS - * @returns some node clusters with id - */ -export function getLongPathDFS(network:Network,sources:string[]):{[key:string]:{nodes:Array<string>, height:number}}{ - //console.log('DFS long path'); - // create graph for library from network - const graph=networkToGDSGraph(network); - // DFS - const dfs=DFSWithSources(network, sources); - // get new clusters : 'longest' paths from sources with DFS - return longestPathFromDFS(graph,dfs,sources as string[]); -} +/*******************************************************************************************************************************************************/ +//___________________________________________________1. Method for main chains_________________________________________________________________________ -/** - * The 'longest' (not the longest if undirected or directed cycle) path associated with each source with the DFS is found. BEWARE : the order of the id can change the result. - * @param graph object for graph-data-structure library - * @param dfs the return string (of nodes id) of a dfs (logically obtained with same sources as the sources for this functions) - * @param sources array of node ID or a type of method to get sources automatically - * RANK_ONLY : sources are nodes of rank 0 - * SOURCE_ONLY : sources are topological sources of the network (nul indegree) - * RANK_SOURCE : sources are node of rank 0, then source nodes - * ALL : sources are all nodes - * SOURCE_ALL : sources are topological sources, then all the others nodes - * RANK_SOURCE_ALL : sources are node of rank 0, then topological sources, then all the other nodes - * For this function, the advised choice is either RANK_ONLY, SOURCE_ONLY or RANK_SOURCE. - * - * @returns an object for the different path, the key is the source of the path - */ -function longestPathFromDFS(graph:{[key:string]:Function},DFSreversed:Array<string>,sources:Array<string>):{[key:string]:{nodes:Array<string>, height:number}}{ - let dfs = Array.from(DFSreversed).reverse(); // the code has been done whith a backward reading of dfs - - let longestPaths:{[key:string]:{nodes:Array<string>, height:number}}={}; - let path:Array<string>; - let source:string=undefined; - let i=dfs.length-1; // index of node in dfs array - let add=false; - - while( i !== -1 ){ // dfs nodes are read backwards - const visitedNode=dfs[i]; - const previousNode: string = (dfs.length - 1 >= i + 1) ? dfs[i + 1] : undefined; - - // if visited node is source - if( sources.includes(visitedNode) ){ - - if (source!==undefined){ - // end the path (of the previous source, if one) - longestPaths=endPath(source,longestPaths,path); - // suppress nodes after the current node (those already analysed in while loop, because backward reading) - dfs.splice(i + 1); - } - - // define new source and add to path - source = visitedNode; - longestPaths[source]={nodes:[source],height:1}; - add=true; - path=[source]; - - // if there is a previous node - } else if (previousNode){ - // if visited node is child of the previous visited node in dfs : add to path - if (nodeIsChildOf(graph,visitedNode,previousNode)){ - add=true; - path.push(visitedNode); - }else{ - // end the path if a node has been added in the last pass of the loop - if (add && source!==undefined){ - longestPaths=endPath(source,longestPaths,path); - } - // remove previous visited node if this node is not the parent of current node - dfs.splice(i+1,1); - path.splice(path.length-1); - i++; // next loop pass will be on the same visited node (because the parent of the visited node wasn't found) - add=false; // no node has been added - } - } - - i--; //backward reading - } - return longestPaths; -} - -/** - * Is the node a child of the parent node ? - * @param graph object that contains function to get children of a node - * @param node is this node a child of the parent? - * @param parentNode the parent node - * @returns boolean - */ -function nodeIsChildOf(graph:{[key:string]:Function},node:string, parentNode:string):boolean{ - return graph.adjacent(parentNode).includes(node); -} - -/** - * Check if the given path is longer than the one in the longest, if it is : update the path to keep the longest of the two - * @param source source of the path (first node) - * @param longestPaths - * @param path the path to check - * @returns the new longest paths - */ -function endPath(source:string, longestPaths:{[key:string]:{nodes:Array<string>, height:number}},path:Array<string>):{[key:string]:{nodes:Array<string>, height:number}}{ - if (source in longestPaths){ - if(longestPaths[source].height < path.length){ - longestPaths[source]={nodes:path.slice(),height:path.length}; - } - }else{ - console.error("source key not in object") - } - return longestPaths; -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// ---------------------------------------------- Method 2 of getCluster for main chains --------------------------------------------------------- -// ---------------------------------------------------------------------------------------------------------------------------------------------- - /** * Return the longest paths from source nodes for the source DAG. "Source DAG" doesn't mean the graph is a DAG, but the subraph containing all descendant of the source is a DAG. * Paths obtained are the longest for the created DAG, not the general graph (because it is NP-hard). @@ -242,34 +161,34 @@ function endPath(source:string, longestPaths:{[key:string]:{nodes:Array<string>, * ALL : all path * @returns some node clusters with id */ -export function getPathSourcesToTargetNode(network:Network, sources:string[],merge:boolean=true,pathType:PathType=PathType.ALL_LONGEST):{[key:string]:{nodes:Array<string>, height:number}}{ +export async function getPathSourcesToTargetNode(network:Network, sources:string[],merge:boolean=true,pathType:PathType=PathType.ALL_LONGEST):Promise<{[key:string]:{nodes:Array<string>, height:number}}>{ //console.log('DAG_Dijkstra'); let pathsFromSources:{[key:string]:{nodes:Array<string>, height:number}}={}; // for each source : do an independant dfs - sources.forEach(source=>{ + sources.forEach(async source=>{ // DFS to get a DAG from this source, and get topological sort - const {dfs,graph}=DFSsourceDAG(network,[source]); + const {dfs,graph}=await DFSsourceDAG(network,[source]); // get max distance from source node for all nodes, and by which parent nodes the node had been accessed - const {distances, parents}=DistanceFromSourceDAG(graph,dfs,pathType); + const {distances, parents}=await DistanceFromSourceDAG(graph,dfs,pathType); // get the farthest node from source (node with max distance) const targetNodes=findMaxKeys(distances); // for each target node : (if several path wanted) if (pathType==PathType.ALL_LONGEST || pathType==PathType.ALL){ - targetNodes.key.forEach(target => { + targetNodes.key.forEach(async target => { // get the parents that goes from source to target node const nodesBetweenSourceTarget=BFS(parents,target); // merge with an existing path if node in common // height is the max distance +1 - pathsFromSources=mergeNewPath(source,{nodes:nodesBetweenSourceTarget, height:targetNodes.max+1},pathsFromSources,merge); + pathsFromSources=await mergeNewPath(source,{nodes:nodesBetweenSourceTarget, height:targetNodes.max+1},pathsFromSources,merge); }); } else if(pathType==PathType.LONGEST){ // if only one path wanted : take the first // get the parents that goes from source to target node const nodesBetweenSourceTarget=BFS(parents,targetNodes.key[0]); // merge with an existing path if node in common - pathsFromSources=mergeNewPath(source,{nodes:nodesBetweenSourceTarget, height:targetNodes.max},pathsFromSources,merge); + pathsFromSources= await mergeNewPath(source,{nodes:nodesBetweenSourceTarget, height:targetNodes.max},pathsFromSources,merge); } }); @@ -286,7 +205,7 @@ export function getPathSourcesToTargetNode(network:Network, sources:string[],mer * ALL : add all parents * @returns maximal distance to the source and parents nodes for each nodes */ -function DistanceFromSourceDAG(graph:{[key:string]:Function}, topologicalOrderFromSource:string[],pathType:PathType=PathType.ALL_LONGEST):{distances:{[key:string]:number}, parents:{[key:string]:string[]}} { +async function DistanceFromSourceDAG(graph:{[key:string]:Function}, topologicalOrderFromSource:string[],pathType:PathType=PathType.ALL_LONGEST):Promise<{distances:{[key:string]:number}, parents:{[key:string]:string[]}}> { // the source is the first node in the topological order const source=topologicalOrderFromSource[0]; @@ -302,7 +221,7 @@ function DistanceFromSourceDAG(graph:{[key:string]:Function}, topologicalOrderFr // Process node in topological order topologicalOrderFromSource.forEach(parent=> { // For each children - graph.adjacent(parent).sort().forEach( child => { + graph.adjacent(parent).sort().forEach( (child:string) => { const childDistance= distanceFromSource[child]; const newDistance=distanceFromSource[parent] + graph.getEdgeWeight(parent,child); if ( newDistance > childDistance) { @@ -335,8 +254,8 @@ function DistanceFromSourceDAG(graph:{[key:string]:Function}, topologicalOrderFr * @param obj The object containing key-value pairs. * @returns The keys associated with the maximum value (or undefined if the object is empty) and the max value. */ -function findMaxKeys(obj: { [key: string]: number }): {key:string[]|undefined,max:number} { - let maxKeys: string[] | undefined; +function findMaxKeys(obj: { [key: string]: number }): {key:string[],max:number} { + let maxKeys: string[]=[]; let maxValue = -Infinity; Object.entries(obj).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) .forEach(([key, value]) => { @@ -361,7 +280,7 @@ function findMaxKeys(obj: { [key: string]: number }): {key:string[]|undefined,ma * @param [merge=true] if true, merge the path with an existing one if a common node is found * @returns all paths including the new one */ -function mergeNewPath(source:string,newPath:{nodes:Array<string>, height:number},pathsFromSources:{[key:string]:{nodes:Array<string>, height:number}}, merge:boolean=true):{[key:string]:{nodes:Array<string>, height:number}}{ +async function mergeNewPath(source:string,newPath:{nodes:Array<string>, height:number},pathsFromSources:{[key:string]:{nodes:Array<string>, height:number}}, merge:boolean=true):Promise<{[key:string]:{nodes:Array<string>, height:number}}>{ const keys=Object.keys(pathsFromSources).sort(); let hasmerged=false; if (merge) { @@ -388,4 +307,125 @@ function mergeNewPath(source:string,newPath:{nodes:Array<string>, height:number} pathsFromSources[source]=newPath; } return pathsFromSources; -} \ No newline at end of file +} + + + +// // ---------------------------------------------------------------------------------------------------------------------------------------------- +// // ---------------------------------------------- Method 1 of getCluster for main chains --------------------------------------------------------- +// // ---------------------------------------------------------------------------------------------------------------------------------------------- + + +// /** +// * Get a long path from sources using a DFS. The path isn't the longest if there some undirected cycle. +// * @param network +// * @param sources to use for DFS +// * @returns some node clusters with id +// */ +// export function getLongPathDFS(network:Network,sources:string[]):{[key:string]:{nodes:Array<string>, height:number}}{ +// //console.log('DFS long path'); +// // create graph for library from network +// const graph=networkToGDSGraph(network); +// // DFS +// const dfs=DFSWithSources(network, sources); +// // get new clusters : 'longest' paths from sources with DFS +// return longestPathFromDFS(graph,dfs,sources as string[]); +// } + + +// /** +// * The 'longest' (not the longest if undirected or directed cycle) path associated with each source with the DFS is found. BEWARE : the order of the id can change the result. +// * @param graph object for graph-data-structure library +// * @param dfs the return string (of nodes id) of a dfs (logically obtained with same sources as the sources for this functions) +// * @param sources array of node ID or a type of method to get sources automatically +// * RANK_ONLY : sources are nodes of rank 0 +// * SOURCE_ONLY : sources are topological sources of the network (nul indegree) +// * RANK_SOURCE : sources are node of rank 0, then source nodes +// * ALL : sources are all nodes +// * SOURCE_ALL : sources are topological sources, then all the others nodes +// * RANK_SOURCE_ALL : sources are node of rank 0, then topological sources, then all the other nodes +// * For this function, the advised choice is either RANK_ONLY, SOURCE_ONLY or RANK_SOURCE. +// * +// * @returns an object for the different path, the key is the source of the path +// */ +// function longestPathFromDFS(graph:{[key:string]:Function},DFSreversed:Array<string>,sources:Array<string>):{[key:string]:{nodes:Array<string>, height:number}}{ +// let dfs = Array.from(DFSreversed).reverse(); // the code has been done whith a backward reading of dfs + +// let longestPaths:{[key:string]:{nodes:Array<string>, height:number}}={}; +// let path:Array<string>; +// let source:string=undefined; +// let i=dfs.length-1; // index of node in dfs array +// let add=false; + +// while( i !== -1 ){ // dfs nodes are read backwards +// const visitedNode=dfs[i]; +// const previousNode: string = (dfs.length - 1 >= i + 1) ? dfs[i + 1] : undefined; + +// // if visited node is source +// if( sources.includes(visitedNode) ){ + +// if (source!==undefined){ +// // end the path (of the previous source, if one) +// longestPaths=endPath(source,longestPaths,path); +// // suppress nodes after the current node (those already analysed in while loop, because backward reading) +// dfs.splice(i + 1); +// } + +// // define new source and add to path +// source = visitedNode; +// longestPaths[source]={nodes:[source],height:1}; +// add=true; +// path=[source]; + +// // if there is a previous node +// } else if (previousNode){ +// // if visited node is child of the previous visited node in dfs : add to path +// if (nodeIsChildOf(graph,visitedNode,previousNode)){ +// add=true; +// path.push(visitedNode); +// }else{ +// // end the path if a node has been added in the last pass of the loop +// if (add && source!==undefined){ +// longestPaths=endPath(source,longestPaths,path); +// } +// // remove previous visited node if this node is not the parent of current node +// dfs.splice(i+1,1); +// path.splice(path.length-1); +// i++; // next loop pass will be on the same visited node (because the parent of the visited node wasn't found) +// add=false; // no node has been added +// } +// } + +// i--; //backward reading +// } +// return longestPaths; +// } + +// /** +// * Is the node a child of the parent node ? +// * @param graph object that contains function to get children of a node +// * @param node is this node a child of the parent? +// * @param parentNode the parent node +// * @returns boolean +// */ +// function nodeIsChildOf(graph:{[key:string]:Function},node:string, parentNode:string):boolean{ +// return graph.adjacent(parentNode).includes(node); +// } + +// /** +// * Check if the given path is longer than the one in the longest, if it is : update the path to keep the longest of the two +// * @param source source of the path (first node) +// * @param longestPaths +// * @param path the path to check +// * @returns the new longest paths +// */ +// function endPath(source:string, longestPaths:{[key:string]:{nodes:Array<string>, height:number}},path:Array<string>):{[key:string]:{nodes:Array<string>, height:number}}{ +// if (source in longestPaths){ +// if(longestPaths[source].height < path.length){ +// longestPaths[source]={nodes:path.slice(),height:path.length}; +// } +// }else{ +// console.error("source key not in object") +// } +// return longestPaths; +// } diff --git a/src/composables/SubgraphForSubgraphNetwork.ts b/src/composables/SubgraphForSubgraphNetwork.ts index 2222479e00dd56290029d880cce09be5fad3e38a..265a3e76e67150c89774b3247528f8acd4620c22 100644 --- a/src/composables/SubgraphForSubgraphNetwork.ts +++ b/src/composables/SubgraphForSubgraphNetwork.ts @@ -30,13 +30,13 @@ import { SubgraphNetwork } from "../types/SubgraphNetwork"; * @param classes The array of classes associated with the subgraph (defaults to an empty array). * @returns The newly created subgraph. */ -export function createSubgraph(name: string, nodes: Array<string>, classes: Array<string>, type: TypeSubgraph, forSubgraph?: {name:string,type:TypeSubgraph}, associatedSubgraphs?:Array<{name:string,type:TypeSubgraph}>): Subgraph { +export function createSubgraph(name: string, nodes: Array<string>, classes: Array<string>, type: TypeSubgraph, parentSubgraph?: {name:string,type:TypeSubgraph}, associatedSubgraphs?:Array<{name:string,type:TypeSubgraph}>): Subgraph { return { name, classes, nodes, type, - parentSubgraph: forSubgraph, + parentSubgraph: parentSubgraph, childrenSubgraphs: associatedSubgraphs }; } diff --git a/src/composables/__tests__/CalculateRelationCycle.test.ts b/src/composables/__tests__/CalculateRelationCycle.test.ts index 91418dd1e628820382baff7ead4d02847dd11a41..89b2fe2943ca8ca2c8f86d7e046697cfdebb2863 100644 --- a/src/composables/__tests__/CalculateRelationCycle.test.ts +++ b/src/composables/__tests__/CalculateRelationCycle.test.ts @@ -89,10 +89,12 @@ describe('CalculateRelationCycle', () => { expect(nodesID).toEqual(nodesIDExpected); }); + }); + describe('getNodesPlacedInGroupCycleAsArray', () => { it('should throw error because cycle group not defined in subgraphNetwork when trying to get node placed inside ', () => { // EXPECT - expect(()=>{CalculateRelationCycle.getNodesPlacedInGroupCycle(subgraphNetwork,"groupCycle")}).toThrow(); + expect(()=>{CalculateRelationCycle.getNodesPlacedInGroupCycleAsArray(subgraphNetwork,"groupCycle")}).toThrow(); }); @@ -122,8 +124,8 @@ describe('CalculateRelationCycle', () => { ]; // TEST - const nodes=CalculateRelationCycle.getNodesPlacedInGroupCycle(subgraphNetwork,"groupCycle"); - const nodesFixed=CalculateRelationCycle.getNodesPlacedInGroupCycle(subgraphNetwork,"groupCycle",true); + const nodes=CalculateRelationCycle.getNodesPlacedInGroupCycleAsArray(subgraphNetwork,"groupCycle"); + const nodesFixed=CalculateRelationCycle.getNodesPlacedInGroupCycleAsArray(subgraphNetwork,"groupCycle",true); // EXPECT expect(nodes).toEqual(nodesExpected); @@ -145,7 +147,7 @@ describe('CalculateRelationCycle', () => { const nodesExpected:{ id: string,x?:number, y?:number, fx?:number, fy?:number }[]=[]; // TEST - const nodes=CalculateRelationCycle.getNodesPlacedInGroupCycle(subgraphNetwork,"groupCycle"); + const nodes=CalculateRelationCycle.getNodesPlacedInGroupCycleAsArray(subgraphNetwork,"groupCycle"); // EXPECT expect(nodes).toEqual(nodesExpected); @@ -611,7 +613,7 @@ describe('CalculateRelationCycle', () => { // 2. Get graph ***************************************************************** describe('get Graph For Cycle Group', () => { - test('getListNodeLinksForCycleGroup', () => { + test('getListNodeLinksForCycleGroupAsArray', () => { // DATA nodes.node2.metadataLayout={ [TypeSubgraph.CYCLEGROUP]: "groupCycle" }; nodes.node3.metadataLayout={ [TypeSubgraph.CYCLEGROUP]: "groupCycle" }; @@ -648,8 +650,8 @@ describe('CalculateRelationCycle', () => { }; // TEST - const resultNotFixed=CalculateRelationCycle.getListNodeLinksForCycleGroup(subgraphNetwork,"groupCycle",false); - const resultFixed=CalculateRelationCycle.getListNodeLinksForCycleGroup(subgraphNetwork,"groupCycle",true); + const resultNotFixed=CalculateRelationCycle.getListNodeLinksForCycleGroupAsArray(subgraphNetwork,"groupCycle",false); + const resultFixed=CalculateRelationCycle.getListNodeLinksForCycleGroupAsArray(subgraphNetwork,"groupCycle",true); // EXPECT expect(resultNotFixed).toEqual(resultExpectedNotFixed); diff --git a/src/types/CoordinatesSize.ts b/src/types/CoordinatesSize.ts index b7f674be5d089dcb36dbab268319b510cfe75202..36e3f9cd98d672cea764ebbded3eb105945769cc 100644 --- a/src/types/CoordinatesSize.ts +++ b/src/types/CoordinatesSize.ts @@ -5,6 +5,12 @@ export interface Coordinate { } +export interface CoordinateNull { + x: number | null; + y: number | null; +} + + export interface Size { width: number; height: number; diff --git a/src/types/NetworkLayout.ts b/src/types/NetworkLayout.ts index ec8bae275e44710670dabaedec18b27ffe325383..3c64e2d03973f6aea3ce7289e0004ed81fa1f056 100644 --- a/src/types/NetworkLayout.ts +++ b/src/types/NetworkLayout.ts @@ -26,6 +26,8 @@ export interface NodeLayout extends Node { [TypeSubgraph.SECONDARY_CHAIN]?:string[], [TypeSubgraph.CYCLE]?:string[], [TypeSubgraph.CYCLEGROUP]?:string, + fixedInCircle?:string, + isFixed?:boolean, } } diff --git a/src/types/Parameters.ts b/src/types/Parameters.ts index ee4c1f3c4f853cea8c7407687e7db0ec55cea16f..b5d0e44d3987912bc77c281cd0f02d5a2c388b68 100644 --- a/src/types/Parameters.ts +++ b/src/types/Parameters.ts @@ -16,7 +16,7 @@ export interface Parameters { doReactionReversible: boolean; // do the step duplication and choice of reaction reversible ? doMainChain: boolean; // do the step main chain ? - getSubgraph : (network: Network, sources: Array<string>,merge?:boolean,pathType?:PathType) => {[key:string]:{nodes:Array<string>, height:number}}; // function to get subgraph (main chain) + getSubgraph : (network: Network, sources: Array<string>,merge?:boolean,pathType?:PathType) => Promise<{[key:string]:{nodes:Array<string>, height:number}}>; // function to get subgraph (main chain) startNodeTypeMainChain: StartNodesType; // for the main chain step : which are the start nodes? pathType: PathType; // main chain step : longest path , all longest paths or all paths merge: boolean; // merging main chain ? If not : nodes can be in several clusters diff --git a/src/types/Subgraph.ts b/src/types/Subgraph.ts index aa278eb684e58a6f4a81c78d63959884e804bb13..f6538d8a814d0f86a4859e2f21a66964242e25f3 100644 --- a/src/types/Subgraph.ts +++ b/src/types/Subgraph.ts @@ -1,4 +1,4 @@ -import { Coordinate } from "./CoordinatesSize"; +import { Coordinate, CoordinateNull } from "./CoordinatesSize"; import { Ordering } from "./EnumArgs"; /** @@ -32,10 +32,12 @@ export interface Subgraph { position?:Coordinate; originalPosition?:Coordinate; // if metanode : the metanode center not well positionned (precalulated position) - precalculatedNodesPosition?: {[key: string]: Coordinate}; // if metanode : the position of the nodes in the metanode - - metadata?: {[key: string]: string | number| boolean | {[key: string]: string | number} | Array<string>}; -} + precalculatedNodesPosition?: {[key: string]: CoordinateNull}; // if metanode : the position of the nodes in the metanode. Null indicates a need of placement by force layout + + radiusCycle?:number; // if cycle : the radius of the circle + centroidCycle?:Coordinate; // if cycle : the center of the circle + + }