diff --git a/src/composables/LayoutManageSideCompounds.ts b/src/composables/LayoutManageSideCompounds.ts index 34416e9897899500b434cf1dca9b89d95bc66f2d..4593c9ea45f4a25b0ff8248225c4bbedcafd95c3 100644 --- a/src/composables/LayoutManageSideCompounds.ts +++ b/src/composables/LayoutManageSideCompounds.ts @@ -8,7 +8,7 @@ import { Coordinate } from "../types/CoordinatesSize"; import { GraphStyleProperties } from "@metabohub/viz-core/src/types/GraphStyleProperties"; // Composable imports -import { removeAllSelectedNodes , duplicateAllNodesByAttribut} from "@metabohub/viz-core"; +import { removeAllSelectedNodes , duplicateAllNodesByAttribut} from "./VizCoreFunctions"; import { getMeanNodesSizePixel, inchesToPixels, minEdgeLength as minEdgeLength, pixelsToInches } from "./CalculateSize"; import { sideCompoundAttribute,isDuplicate, isReaction, isSideCompound, setAsSideCompound } from "./GetSetAttributsNodes"; @@ -142,74 +142,74 @@ import { error } from "console"; -/** - * Add attribute of side compound to all nodes in network from a list - * @param network - * @param pathListSideCompounds path to the list of id of side compounds - */ -export async function addSideCompoundAttributeFromList(subgraphNetwork:SubgraphNetwork, pathListSideCompounds:string):Promise<void>{ - const listIDSideCompounds = await getIDSideCompoundsInNetworkFromFile(subgraphNetwork,pathListSideCompounds); - listIDSideCompounds.forEach((sideCompoundID) => { - setAsSideCompound(subgraphNetwork.network,sideCompoundID); - }); -} - -/** - * Return the list of id of side compounds in the network - * @param subgraphNetwork - * @param pathListSideCompounds path to the list of id of side compounds - * @returns list of id of side compounds in the network - */ -async function getIDSideCompoundsInNetworkFromFile(subgraphNetwork:SubgraphNetwork,pathListSideCompounds:string):Promise<string[]>{ - let listIDSideCompounds:string[]; - const network = subgraphNetwork.network; - try { - listIDSideCompounds = await getIDSideCompoundsFromFile(pathListSideCompounds); - const sideCompoundInNetwork = Object.keys(network.nodes).filter(id => listIDSideCompounds.includes(id)); - return sideCompoundInNetwork; - } catch (error) { - throw error; - } -} - -/** - * Return the list of id of side compounds from a file - * @param pathListSideCompounds path to the list of id of side compounds - * @returns list of id of side compounds - */ -async function getIDSideCompoundsFromFile(pathListSideCompounds:string):Promise<string[]>{ - try { - const sideCompoundsFile=pathListSideCompounds; - const sideCompoundsString = await getContentFromURL(sideCompoundsFile); - const lines = sideCompoundsString.split('\n'); - const listId: Array<string> = []; - lines.forEach((line: string) => { - listId.push(line.split('\t')[0]); - }) - return listId; - }catch (error) { - throw error; - } -} - -/** - * Fetch url to return data - * @param url URL to fetch - * @returns Return response - */ -export async function getContentFromURL(url: string): Promise<string> { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error('La requête a échoué avec le statut ' + response.status); - } - const content = await response.text(); - return content; - } catch (error) { - console.error('Une erreur s\'est produite lors de la récupération du contenu du fichier :', error); - throw error; - } - } +// /** +// * Add attribute of side compound to all nodes in network from a list +// * @param network +// * @param pathListSideCompounds path to the list of id of side compounds +// */ +// export async function addSideCompoundAttributeFromList(subgraphNetwork:SubgraphNetwork, pathListSideCompounds:string):Promise<void>{ +// const listIDSideCompounds = await getIDSideCompoundsInNetworkFromFile(subgraphNetwork,pathListSideCompounds); +// listIDSideCompounds.forEach((sideCompoundID) => { +// setAsSideCompound(subgraphNetwork.network,sideCompoundID); +// }); +// } + +// /** +// * Return the list of id of side compounds in the network +// * @param subgraphNetwork +// * @param pathListSideCompounds path to the list of id of side compounds +// * @returns list of id of side compounds in the network +// */ +// async function getIDSideCompoundsInNetworkFromFile(subgraphNetwork:SubgraphNetwork,pathListSideCompounds:string):Promise<string[]>{ +// let listIDSideCompounds:string[]; +// const network = subgraphNetwork.network; +// try { +// listIDSideCompounds = await getIDSideCompoundsFromFile(pathListSideCompounds); +// const sideCompoundInNetwork = Object.keys(network.nodes).filter(id => listIDSideCompounds.includes(id)); +// return sideCompoundInNetwork; +// } catch (error) { +// throw error; +// } +// } + +// /** +// * Return the list of id of side compounds from a file +// * @param pathListSideCompounds path to the list of id of side compounds +// * @returns list of id of side compounds +// */ +// async function getIDSideCompoundsFromFile(pathListSideCompounds:string):Promise<string[]>{ +// try { +// const sideCompoundsFile=pathListSideCompounds; +// const sideCompoundsString = await getContentFromURL(sideCompoundsFile); +// const lines = sideCompoundsString.split('\n'); +// const listId: Array<string> = []; +// lines.forEach((line: string) => { +// listId.push(line.split('\t')[0]); +// }) +// return listId; +// }catch (error) { +// throw error; +// } +// } + +// /** +// * Fetch url to return data +// * @param url URL to fetch +// * @returns Return response +// */ +// export async function getContentFromURL(url: string): Promise<string> { +// try { +// const response = await fetch(url); +// if (!response.ok) { +// throw new Error('La requête a échoué avec le statut ' + response.status); +// } +// const content = await response.text(); +// return content; +// } catch (error) { +// console.error('Une erreur s\'est produite lors de la récupération du contenu du fichier :', error); +// throw error; +// } +// } @@ -227,16 +227,17 @@ export async function getContentFromURL(url: string): Promise<string> { * @param pathListSideCompounds path to the list of id of side compounds * @returns subgraphNetwork with updated network and sideCompounds */ -export async function putDuplicatedSideCompoundAside(subgraphNetwork:SubgraphNetwork, doDuplicateSideCompounds:boolean,doPutAsideSideCompounds:boolean, addSideCompoundAttribute:boolean=true, pathListSideCompounds:string):Promise<SubgraphNetwork>{ +export async function putDuplicatedSideCompoundAside(subgraphNetwork:SubgraphNetwork, doDuplicateSideCompounds:boolean,doPutAsideSideCompounds:boolean):Promise<SubgraphNetwork>{ try { // finding side compounds in network - if (addSideCompoundAttribute){ - await addSideCompoundAttributeFromList(subgraphNetwork,pathListSideCompounds); - } + // if (addSideCompoundAttribute){ + // await addSideCompoundAttributeFromList(subgraphNetwork,pathListSideCompounds); + // } // duplication of side compounds if (doDuplicateSideCompounds){ await duplicateSideCompound(subgraphNetwork); } + // remove side compounds from network, they are keeped aside in subgraphNetwork.sideCompounds if (doPutAsideSideCompounds){ return removeSideCompoundsFromNetwork(subgraphNetwork); @@ -255,7 +256,7 @@ export async function putDuplicatedSideCompoundAside(subgraphNetwork:SubgraphNet * @param subgraphNetwork - The subgraph network containing the side compounds to be duplicated. * @returns void */ -export async function duplicateSideCompound(subgraphNetwork:SubgraphNetwork):Promise<void>{ +async function duplicateSideCompound(subgraphNetwork:SubgraphNetwork):Promise<void>{ const network = subgraphNetwork.network; const networkStyle = subgraphNetwork.networkStyle; // duplication of side compounds @@ -332,7 +333,9 @@ function removeSideCompoundsFromNetwork(subgraphNetwork:SubgraphNetwork): Subgra * @returns subgraphNetwork with updated network and sideCompounds */ export async function reinsertionSideCompounds(subgraphNetwork:SubgraphNetwork,factorMinEdgeLength:number=1/2,doReactionReversible:boolean):Promise<SubgraphNetwork>{ - if(subgraphNetwork.sideCompounds){ + if(!subgraphNetwork.sideCompounds){ + return subgraphNetwork; + }else { const sideCompounds = subgraphNetwork.sideCompounds // get information for length of edge for side compounds : // get the min length of edge in the network (if not, use default value) @@ -352,12 +355,13 @@ export async function reinsertionSideCompounds(subgraphNetwork:SubgraphNetwork,f } // for each reaction, apply motif stamp - Object.keys(sideCompounds).forEach( async (reactionID)=>{ - subgraphNetwork= await motifStampSideCompound(subgraphNetwork,reactionID,factorMinEdgeLength); - }); - } - return subgraphNetwork; + for (const reactionID of Object.keys(sideCompounds)) { + subgraphNetwork = await motifStampSideCompound(subgraphNetwork, reactionID, factorMinEdgeLength); + } + return subgraphNetwork; + } + } /** @@ -391,7 +395,7 @@ async function updateSideCompoundsReversibleReaction(subgraphNetwork:SubgraphNet if (subgraphNetwork.sideCompounds){ let sideCompounds=subgraphNetwork.sideCompounds; Object.keys(sideCompounds).forEach((reactionID)=>{ - if (!(reactionID in network.nodes)) throw new Error("Reaction not in subgraphNetwork") + if (!(reactionID in network.nodes)) throw new Error("Reaction in side compounds but not in network") // if reaction has been reversed : exchange products and reactants if(network.nodes[reactionID].metadataLayout && network.nodes[reactionID].metadataLayout.isReversedVersion){ const products=sideCompounds[reactionID].products; @@ -414,7 +418,7 @@ async function updateSideCompoundsReversibleReaction(subgraphNetwork:SubgraphNet * @returns The subgraphNetwork with the motif stamp applied for the reaction. */ async function motifStampSideCompound(subgraphNetwork:SubgraphNetwork,reactionID:string,factorMinEdgeLength:number=1/2):Promise<SubgraphNetwork>{ - //try { + try { // initialize reaction stamp let reaction= await initializeReactionSideCompounds(subgraphNetwork,reactionID); // find intervals between reactants and products @@ -430,10 +434,10 @@ async function motifStampSideCompound(subgraphNetwork:SubgraphNetwork,reactionID subgraphNetwork= await giveCoordAllSideCompounds(subgraphNetwork,reaction,factorMinEdgeLength); // insert side compounds in network insertAllSideCompoundsInNetwork(subgraphNetwork,reaction); - // } catch (error) { - // throw new Error("Error in motifStampSideCompound, reaction : "+ reactionID+ "\n"+error); - // } - return subgraphNetwork; + return subgraphNetwork; + } catch (error) { + throw new Error("Error in motifStampSideCompound, reaction : "+ reactionID+ "\n"+error); + } } @@ -514,7 +518,6 @@ async function getMetaboliteFromReaction(subgraphNetwork: SubgraphNetwork, idRea */ function angleRadianSegment(x1:number,y1:number,x2:number,y2:number,clockwise:boolean=true):number{ if (!isFinite(x1) || !isFinite(y1) || !isFinite(x2) || !isFinite(y2)) { - //console.error("Invalid coordinates for angle : one or more coordinates are not finite numbers."); throw new Error("Invalid coordinates for angle : one or more coordinates are not finite numbers."); } @@ -548,6 +551,16 @@ function angleRadianSegment(x1:number,y1:number,x2:number,y2:number,clockwise:bo async function addSideCompoundsIntervals(reaction: Reaction):Promise<Reaction> { try { + // if no reactant or product + if (Object.keys(reaction.metabolitesAngles).length===0) { + reaction.intervalsAvailables =[{ + typeInterval: 0, + reactant: undefined, + product: undefined, + }]; + return reaction; + } + // Sort metabolites by angle const sortedMetabolites = Object.entries(reaction.metabolitesAngles) .map(([id, {angle, type}]) => ({id, angle, type})) @@ -582,7 +595,6 @@ async function addSideCompoundsIntervals(reaction: Reaction):Promise<Reaction> { product: undefined, }]; } - return reaction; } catch(error){ throw error; @@ -709,9 +721,10 @@ function sizeInterval(reaction:Reaction,intervalIndex:number):number{ async function findSpacingSideCompounds(reaction:Reaction,sizeInterval:number):Promise<{reactant:number|undefined,product:number|undefined}>{ const reactantNumber=reaction.sideCompoundsReactants.length; const productNumber=reaction.sideCompoundsProducts.length; + if (reactantNumber<0 && productNumber<0) throw new Error("Number of side compounds negative"); return { - reactant: reactantNumber === 0 ? undefined : sizeInterval / (2 * (reactantNumber+1)), - product: productNumber === 0 ? undefined : sizeInterval / (2 * (productNumber+1)) + reactant: reactantNumber === 0 ? undefined : Number((sizeInterval / (2 * (reactantNumber+1))).toFixed(3)), + product: productNumber === 0 ? undefined : Number((sizeInterval / (2 * (productNumber+1))).toFixed(3)) }; } @@ -780,15 +793,14 @@ async function placeSideCompounds(sideCompounds: Array<Node>, reaction: Reaction } const interval = reaction.intervalsAvailables[0]; const startSideCompound = placeReactants ? interval.reactant : interval.product; - if (!startSideCompound) { - console.error("No start side compound found"); - return; + let startAngle:number=0; + if (startSideCompound) { + startAngle = startSideCompound ? reaction.metabolitesAngles[startSideCompound].angle : 0; } - let startAngle = startSideCompound ? reaction.metabolitesAngles[startSideCompound].angle : 0; + const angleSpacing = placeReactants ? reaction.angleSpacingReactant : reaction.angleSpacingProduct; - if (! angleSpacing! || isFinite(angleSpacing)) { - console.error("No angle spacing found"); - return; + if (angleSpacing===undefined || isNaN(angleSpacing) || angleSpacing===null){ + throw new Error("No angle spacing found"); } sideCompounds.forEach((sideCompoundNode, i) => { @@ -826,8 +838,8 @@ function determineDirection(typeInterval: number, placeReactants: boolean): numb * @returns The side compound node with updated coordinates. */ function giveCoordSideCompound(sideCompound:Node,angle:number,center:{x:number,y:number},distance:number):Node{ - sideCompound.x = center.x + distance * Math.cos(angle); - sideCompound.y = center.y + distance * Math.sin(angle); + sideCompound.x = Number((center.x + distance * Math.cos(angle)).toFixed(3)); + sideCompound.y = Number((center.y + distance * Math.sin(angle)).toFixed(3)); return sideCompound; } diff --git a/src/composables/VizCoreFunctions.ts b/src/composables/VizCoreFunctions.ts new file mode 100644 index 0000000000000000000000000000000000000000..610a47b518deab012095453f90657c16cfdcd82e --- /dev/null +++ b/src/composables/VizCoreFunctions.ts @@ -0,0 +1,124 @@ +// Type imports +import { GraphStyleProperties } from "@metabohub/viz-core/src/types/GraphStyleProperties"; +import { Network } from "@metabohub/viz-core/src/types/Network"; +import { Node } from "@metabohub/viz-core/src/types/Node"; + + + +/*******************************************************************************************************************************************************/ +//___________________________________________________0. Node duplication _________________________________________________________________________ + + +/** + * Duplicate all nodes according to a specific metadata attribut + * @param network Network object that contains all nodes + * @param attribut Metadata attribut of node. Must be a boolean + */ +export function duplicateAllNodesByAttribut(network: Network, networkStyle: GraphStyleProperties, attribut: string): void { + Object.keys(network.nodes).forEach((nodeID: string) => { + const node = network.nodes[nodeID] as Node; + if (node.metadata && node.metadata[attribut]) { + const checkAtt = node.metadata[attribut] as boolean; + if (checkAtt) { + duplicateNode(nodeID, network, networkStyle); + } + } + }); + } + + + /** + * Duplicate specific node in network object + * @param nodeId Node id + * @param network Network object + * @param networkStyle Style object + */ +export function duplicateNode(nodeId: string, network: Network, networkStyle: GraphStyleProperties): void { + if (network.nodes[nodeId]) { + const linksIndex = [] as Array<number>; + const originalNode = network.nodes[nodeId]; + + if (networkStyle.nodeStyles) { + networkStyle.nodeStyles['duplicate'] = { + fill: '#FFFFFF', + height: 10, + width: 10, + shape: 'circle', + } + } else { + networkStyle['nodeStyles'] = { + duplicate: { + fill: '#FFFFFF', + height: 10, + width: 10, + shape: 'circle', + } + } + } + + network.links.forEach((link) => { + if (link.source.id === nodeId || link.target.id === nodeId) { + const index = network.links.indexOf(link); + linksIndex.push(index); + } + }); + + for (let i = 0; i < linksIndex.length; i++) { + + const newNode: Node = { + id: nodeId + i, + classes: ['duplicate'], + label: originalNode.label, + x: 0, + y: 0 + }; + + const index = linksIndex[i]; + + if (network.links[index].source.id === nodeId) { + newNode.x = originalNode.x - ((originalNode.x - network.links[index].target.x) / 2); + newNode.y = originalNode.y - ((originalNode.y - network.links[index].target.y) / 2); + network.links[index].source = newNode; + } + + if (network.links[index].target.id === nodeId) { + newNode.x = originalNode.x - ((originalNode.x - network.links[index].source.x) / 2); + newNode.y = originalNode.y - ((originalNode.y - network.links[index].source.y) / 2); + network.links[index].target = newNode + } + + network.nodes[nodeId+i] = newNode; + } + + delete network.nodes[nodeId]; + } + } + + + +/*******************************************************************************************************************************************************/ +//___________________________________________________1. Node suppresion _________________________________________________________________________ + + +/** + * Remove a list of nodes from network object + * @param listId Array of nodes id + * @param network Network object + */ +export function removeAllSelectedNodes(listId: Array<string>, network: Network): void { + listId.forEach((nodeId: string) => { + if (Object.keys(network.nodes).includes(nodeId)) { + delete network.nodes[nodeId]; + + const links = network.links.filter((link) => { + if (link.source.id !== nodeId && link.target.id !== nodeId) { + return link; + } + }); + + network.links = links; + } + }); +} + + \ No newline at end of file diff --git a/src/composables/__tests__/LayoutManageSideCompounds.test.ts b/src/composables/__tests__/LayoutManageSideCompounds.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fc6785b5c947684eb229085d365c871d1f1e37d --- /dev/null +++ b/src/composables/__tests__/LayoutManageSideCompounds.test.ts @@ -0,0 +1,765 @@ +// Type imports +import { SubgraphNetwork } from "../../types/SubgraphNetwork"; +import { Network } from "@metabohub/viz-core/src/types/Network"; +import { Node } from "@metabohub/viz-core/src/types/Node"; +import { Link } from "@metabohub/viz-core/src/types/Link"; +import { MetaboliteType, Reaction, ReactionInterval } from "../../types/Reaction"; +import { VizArgs } from "../../types/EnumArgs"; +import { Coordinate } from "../../types/CoordinatesSize"; +import { GraphStyleProperties } from "@metabohub/viz-core/src/types/GraphStyleProperties"; +import { NetworkLayout, NodeLayout } from "../../types/NetworkLayout"; + + +// Composable imports +import * as LayoutManageSideCompounds from "../LayoutManageSideCompounds"; +import {sideCompoundAttribute} from "../GetSetAttributsNodes"; +import * as VizCoreFunctions from "../VizCoreFunctions"; +import * as CalculateSize from "../CalculateSize"; +import * as GetSetAttributsNodes from "../GetSetAttributsNodes"; + + + +describe("LayoutManageSideCompounds", () => { + + describe('putDuplicatedSideCompoundAside', () => { + + let network: Network; + let subgraphNetwork: SubgraphNetwork; + let nodes: {[key: string]: Node}; + let links:Link[]; + + let duplicateAllNodesByAttributMock: jest.SpyInstance; + let removeAllSelectedNodesMock: jest.SpyInstance; + let isSideCompoundMock: jest.SpyInstance; + let setAsSideCompoundMock: jest.SpyInstance; + let isDuplicateMock: jest.SpyInstance; + + beforeEach(() => { + + nodes={ + nodeA:{id: 'nodeA',x: 0, y: 0}, + nodeC:{id: 'nodeC',x: 2, y: 2,metadata:{[sideCompoundAttribute]:true}}, + nodeD:{id: 'nodeD',x: 3, y: 3}, + nodeE:{id: 'nodeE',x: 4, y: 4,metadata:{[sideCompoundAttribute]:true}}, + nodeF:{id: 'nodeF',x: 5, y: 5}, + nodeG:{id: 'nodeG',x: 6, y: 6}, + }; + + links=[ + {id:"link", source: nodes.nodeA, target: nodes.nodeF}, + {id:"link", source: nodes.nodeC, target: nodes.nodeF}, + {id:"link", source: nodes.nodeF, target: nodes.nodeD}, + {id:"link", source: nodes.nodeF, target: nodes.nodeE}, + {id:"link", source: nodes.nodeC, target: nodes.nodeG}, + ] + + network={ + id: 'network', + nodes: nodes, + links: links, + }; + + subgraphNetwork={ + network: network, + networkStyle: {} + }; + + // MOCK + duplicateAllNodesByAttributMock = jest.spyOn(VizCoreFunctions, 'duplicateAllNodesByAttribut'); + duplicateAllNodesByAttributMock.mockImplementation(() => { + network.nodes['nodeC0'] = {id: 'nodeC0',x: 2, y: 2,classes: ['duplicate']}; + network.nodes['nodeC1'] = {id: 'nodeC1',x: 2, y: 2,classes: ['duplicate']}; + delete network.nodes['nodeC']; + network.nodes['nodeE0'] = {id: 'nodeE0',x: 4, y: 4,classes: ['duplicate']}; + delete network.nodes['nodeE']; + network.links[1].source = network.nodes['nodeC0']; + network.links[4].source = network.nodes['nodeC1']; + network.links[3].target = network.nodes['nodeE0']; + }); + + removeAllSelectedNodesMock = jest.spyOn(VizCoreFunctions, 'removeAllSelectedNodes'); + isSideCompoundMock = jest.spyOn(GetSetAttributsNodes, 'isSideCompound'); + isSideCompoundMock.mockImplementation((node: NodeLayout) => { + return node.id === 'nodeC' || node.id === 'nodeC0'|| node.id === 'nodeC1' || + node.id === 'nodeE' || node.id === 'nodeE0'; + }); + + setAsSideCompoundMock = jest.spyOn(GetSetAttributsNodes, 'setAsSideCompound'); + setAsSideCompoundMock.mockImplementation((network:Network,nodeID: string) => { + if (network.nodes[nodeID].metadata){ + network.nodes[nodeID].metadata[sideCompoundAttribute]=true; + }else{ + network.nodes[nodeID].metadata={[sideCompoundAttribute]:true}; + } + }); + + isDuplicateMock = jest.spyOn(GetSetAttributsNodes, 'isDuplicate'); + isDuplicateMock.mockImplementation((network:Network,nodeID: string) => { + return nodeID === 'nodeC0'|| nodeID === 'nodeC1' || nodeID=== 'nodeE0'; + }); + + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should only duplicate side compound', async () => { + + // DATA + const resultExpected = { + "network": { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 3, "y": 3}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5}, + "nodeG": {"id": "nodeG", "x": 6, "y": 6}, + "nodeC0": {"id": "nodeC0", "x": 2, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC1": {"id": "nodeC1", "x": 2, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeE0": {"id": "nodeE0", "x": 4, "y": 4, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 0, "y": 0}, "target": {"id": "nodeF", "x": 5, "y": 5}}, + {"id": "link", "source": {"id": "nodeC0", "x": 2, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5}, "target": {"id": "nodeD", "x": 3, "y": 3}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5}, "target": {"id": "nodeE0", "x": 4, "y": 4, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}}, + {"id": "link", "source": {"id": "nodeC1", "x": 2, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeG", "x": 6, "y": 6}} + ] + }, + "networkStyle": {} + } + + + // TEST + const result = await LayoutManageSideCompounds.putDuplicatedSideCompoundAside(subgraphNetwork,true,false); + + // EXPECT + expect(result).toEqual(resultExpected); + + }); + + it('should put side compound aside', async () => { + + // MOCK + removeAllSelectedNodesMock.mockImplementation(() => { + delete network.nodes.nodeC; + delete network.nodes.nodeE; + const links = network.links.filter((link) => { + if (link.source.id !== "nodeC" && link.target.id !== "nodeC" && + link.source.id !== "nodeE" && link.target.id !== "nodeE" + ) { + return link; + } + }); + network.links = links; + }); + + // DATA + const resultExpected :SubgraphNetwork= { + "network": { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 3, "y": 3}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5}, + "nodeG": {"id": "nodeG", "x": 6, "y": 6} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 0, "y": 0}, "target": {"id": "nodeF", "x": 5, "y": 5}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5}, "target": {"id": "nodeD", "x": 3, "y": 3}} + ] + }, + "networkStyle": {}, + "sideCompounds": { + "nodeF": { + "reactants": [ + {"id": "nodeC", "x": 2, "y": 2, "metadata": {"isSideCompound": true}} + ], + "products": [ + {"id": "nodeE", "x": 4, "y": 4, "metadata": {"isSideCompound": true}} + ] + }, + "nodeG": { + "reactants": [ + {"id": "nodeC", "x": 2, "y": 2, "metadata": {"isSideCompound": true}} + ], + "products": [] + } + } + }; + + + // TEST + const result = await LayoutManageSideCompounds.putDuplicatedSideCompoundAside(subgraphNetwork,false,true); + + // EXPECT + expect(result).toEqual(resultExpected); + + }); + + it('should duplicate side compound and put them aside', async() => { + // MOCK + removeAllSelectedNodesMock.mockImplementation(() => { + delete network.nodes.nodeC0; + delete network.nodes.nodeC1; + delete network.nodes.nodeE0; + const links = network.links.filter((link) => { + if (link.source.id !== "nodeC0" && link.target.id !== "nodeC0" && + link.source.id !== "nodeC1" && link.target.id !== "nodeC1" && + link.source.id !== "nodeE0" && link.target.id !== "nodeE0" + ) { + return link; + } + }); + network.links = links; + }); + + // DATA + const resultExpected :SubgraphNetwork= { + "network": { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 3, "y": 3}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5}, + "nodeG": {"id": "nodeG", "x": 6, "y": 6} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 0, "y": 0}, "target": {"id": "nodeF", "x": 5, "y": 5}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5}, "target": {"id": "nodeD", "x": 3, "y": 3}} + ] + }, + "networkStyle": {}, + "sideCompounds": { + "nodeF": { + "reactants": [ + {"id": "nodeC0", "x": 2, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + ], + "products": [ + {"id": "nodeE0", "x": 4, "y": 4, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + ] + }, + "nodeG": { + "reactants": [ + {"id": "nodeC1", "x": 2, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + ], + "products": [] + } + } + }; + + + // TEST + const result = await LayoutManageSideCompounds.putDuplicatedSideCompoundAside(subgraphNetwork,true,true); + + // EXPECT + expect(result).toEqual(resultExpected); + + }); + + }); + + + describe('reinsertionSideCompounds', () => { + + let subgraphNetworkDupliAside: SubgraphNetwork; + let minEdgeLengthMock: jest.SpyInstance; + let getMeanNodesSizePixelMock: jest.SpyInstance; + let inchesToPixelsMock: jest.SpyInstance; + + beforeEach(() => { + + const nodes:{[key:string]:NodeLayout}= { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 7, "y": 6}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, metadataLayout:{isReversedVersion:true}}, + }; + subgraphNetworkDupliAside = { + "network": { + "id": "network", + nodes:nodes, + "links": [ + {"id": "link",source: nodes.nodeA, "target": nodes.nodeF}, + {"id": "link", source: nodes.nodeF, "target": nodes.nodeD} + ] + }, + "networkStyle": {}, + "sideCompounds": { + "nodeF": { + "reactants": [ + {"id": "nodeC0", "x": 2, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + ], + "products": [ + {"id": "nodeE0", "x": 4, "y": 4, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + ] + } + } + }; + + // MOCK + minEdgeLengthMock = jest.spyOn(CalculateSize, 'minEdgeLength'); + minEdgeLengthMock.mockReturnValue(3); + + getMeanNodesSizePixelMock = jest.spyOn(CalculateSize, 'getMeanNodesSizePixel'); + getMeanNodesSizePixelMock.mockReturnValue(Promise.resolve({height:2,width:2})); + + inchesToPixelsMock = jest.spyOn(CalculateSize, 'inchesToPixels'); + inchesToPixelsMock.mockReturnValue(2); + + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + + it('should throw error because reaction node in sideCompound but not in netwotk for update when reversed version', async () => { + // DATA + delete subgraphNetworkDupliAside.network.nodes.nodeF; + + // TEST & EXPECT + await expect(LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true)).rejects.toThrow("Reaction in side compounds but not in network"); + + }); + + it('should throw error because reaction node in sideCompound but not in network for initializeReactionSideCompounds (no update rev)', async () => { + // DATA + delete subgraphNetworkDupliAside.network.nodes.nodeF; + + // TEST & EXPECT + await expect(LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,false)).rejects.toThrow(); + + }); + + + it('should t change network because no product or reactant in reaction in side compounds', async () => { + // DATA + if (subgraphNetworkDupliAside.sideCompounds && subgraphNetworkDupliAside.sideCompounds.nodeF){ + subgraphNetworkDupliAside.sideCompounds.nodeF.products=[]; + subgraphNetworkDupliAside.sideCompounds.nodeF.reactants=[]; + } + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result.network).toEqual(subgraphNetworkDupliAside.network); + + }); + + it('should t change because no side compounds', async () => { + // DATA + delete subgraphNetworkDupliAside.sideCompounds; + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result).toEqual(subgraphNetworkDupliAside); + + }); + + it('should t change because side compounds empty', async () => { + // DATA + subgraphNetworkDupliAside.sideCompounds={}; + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result).toEqual(subgraphNetworkDupliAside); + + }); + + it('should use min edge length by default, when no rank and node sep', async () => { + // MOCK + minEdgeLengthMock.mockReturnValue(NaN); + + // DATA + const networkExpected:NetworkLayout = { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 7, "y": 6}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": { + "id": "nodeE0", + "x": 3.006, + "y": 5.161, + "classes": ["duplicate"], + "metadata": {"isSideCompound": true} + }, + "nodeC0": { + "id": "nodeC0", + "x": 5.478, + "y": 6.942, + "classes": ["duplicate"], + "metadata": {"isSideCompound": true} + } + }, + "links": [ + { + "id": "link", + "source": {"id": "nodeA", "x": 0, "y": 0}, + "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}} + }, + { + "id": "link", + "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "target": {"id": "nodeD", "x": 7, "y": 6} + }, + { + "id": "nodeE0--nodeF", + "source": { + "id": "nodeE0", + "x": 3.006, + "y": 5.161, + "classes": ["duplicate"], + "metadata": {"isSideCompound": true} + }, + "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}} + }, + { + "id": "nodeF--nodeC0", + "source": { + "id": "nodeF", + "x": 5, + "y": 5, + "metadataLayout": {"isReversedVersion": true} + }, + "target": { + "id": "nodeC0", + "x": 5.478, + "y": 6.942, + "classes": ["duplicate"], + "metadata": {"isSideCompound": true} + } + } + ] + }; + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + // => type interval : 0 + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + it('should use min edge length by default, when rank and node sep', async () => { + // MOCK + minEdgeLengthMock.mockReturnValue(NaN); + + // DATA + subgraphNetworkDupliAside.attributs={}; + subgraphNetworkDupliAside.attributs[VizArgs.RANKSEP]=2; + subgraphNetworkDupliAside.attributs[VizArgs.NODESEP]=1; + + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 7, "y": 6}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 3.006, "y": 5.161, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 5.478, "y": 6.942, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 0, "y": 0}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeD", "x": 7, "y": 6}}, + {"id": "nodeE0--nodeF", "source": {"id": "nodeE0", "x": 3.006, "y": 5.161, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeC0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeC0", "x": 5.478, "y": 6.942, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + } + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + + + it('should reinsert side compounds when case of biggest interval includes angle 0 (test 1)', async () => { + // DATA + subgraphNetworkDupliAside.network.nodes.nodeD.x=6; + subgraphNetworkDupliAside.network.nodes.nodeD.y=7; + + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 6, "y": 7}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 5.242, "y": 2.01, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 7.913, "y": 5.716, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + { + "id": "link", + "source": {"id": "nodeA", "x": 0, "y": 0}, + "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}} + }, + { + "id": "link", + "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "target": {"id": "nodeD", "x": 6, "y": 7} + }, + { + "id": "nodeE0--nodeF", + "source": {"id": "nodeE0", "x": 5.242, "y": 2.01, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}} + }, + { + "id": "nodeF--nodeC0", + "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "target": {"id": "nodeC0", "x": 7.913, "y": 5.716, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + } + ] + }; + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + // => type interval : 2 + + // EXPECT + expect(result.network).toEqual(networkExpected); + + + }); + + it('should reinsert side compounds but position of reactant and product is the other way', async () => { + // DATA + subgraphNetworkDupliAside.network.nodes.nodeD.x=0; + subgraphNetworkDupliAside.network.nodes.nodeD.y=0; + subgraphNetworkDupliAside.network.nodes.nodeA.x=7; + subgraphNetworkDupliAside.network.nodes.nodeA.y=6; + + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 7, "y": 6}, + "nodeD": {"id": "nodeD", "x": 0, "y": 0}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 5.716, "y": 7.913, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 2.01, "y": 5.242, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 7, "y": 6}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeD", "x": 0, "y": 0}}, + {"id": "nodeE0--nodeF", "source": {"id": "nodeE0", "x": 5.716, "y": 7.913, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeC0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeC0", "x": 2.01, "y": 5.242, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + } + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + // => type interval : 1 + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + it('should reinsert side compounds when case of biggest interval includes angle 0 (test 2)', async () => { + + // DATA + subgraphNetworkDupliAside.network.nodes.nodeD.x=0; + subgraphNetworkDupliAside.network.nodes.nodeD.y=0; + subgraphNetworkDupliAside.network.nodes.nodeA.x=6; + subgraphNetworkDupliAside.network.nodes.nodeA.y=7; + + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 6, "y": 7}, + "nodeD": {"id": "nodeD", "x": 0, "y": 0}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 7.913, "y": 5.716, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 5.242, "y": 2.01, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 6, "y": 7}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeD", "x": 0, "y": 0}}, + {"id": "nodeE0--nodeF", "source": {"id": "nodeE0", "x": 7.913, "y": 5.716, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeC0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeC0", "x": 5.242, "y": 2.01, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + }; + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + // => type interval : 3 + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + + it('should reinsert side compounds when only product in reaction', async () => { + // DATA + delete subgraphNetworkDupliAside.network.nodes.nodeA; + const newLinks=subgraphNetworkDupliAside.network.links.filter(link => link.source.id !== "nodeA"); + subgraphNetworkDupliAside.network.links=newLinks; + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeD": {"id": "nodeD", "x": 7, "y": 6}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 3.658, "y": 7.683, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 6.341, "y": 2.316, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeD", "x": 7, "y": 6}}, + {"id": "nodeE0--nodeF", "source": {"id": "nodeE0", "x": 3.658, "y": 7.683, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeC0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeC0", "x": 6.341, "y": 2.316, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + }; + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + it('should reinsert side compounds when only reactant in reaction', async () => { + // DATA + delete subgraphNetworkDupliAside.network.nodes.nodeD; + const newLinks=subgraphNetworkDupliAside.network.links.filter(link => link.target.id !== "nodeD"); + subgraphNetworkDupliAside.network.links=newLinks; + + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 7.122, "y": 2.879, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 2.879, "y": 7.122, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 0, "y": 0}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeE0--nodeF", "source": {"id": "nodeE0", "x": 7.122, "y": 2.879, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeC0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeC0", "x": 2.879, "y": 7.122, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + }; + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + it('should reinsert side compounds when no reactant or product in reaction', async () => { + // DATA + delete subgraphNetworkDupliAside.network.nodes.nodeD; + delete subgraphNetworkDupliAside.network.nodes.nodeA; + subgraphNetworkDupliAside.network.links=[]; + + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 4.999, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 4.999, "y": 8, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "nodeE0--nodeF", "source": {"id": "nodeE0", "x": 4.999, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeC0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeC0", "x": 4.999, "y": 8, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + }; + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + + + it('should reinsert side compounds with several side compound of same type (reactant or product)', async () => { + + // DATA + const nodeB0:NodeLayout={"id": "nodeB0", "x": 1, "y": 2, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}; + if (subgraphNetworkDupliAside.sideCompounds){ + subgraphNetworkDupliAside.sideCompounds.nodeF.reactants.push(nodeB0); + } + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 7, "y": 6}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeE0": {"id": "nodeE0", "x": 2.01, "y": 5.242, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeC0": {"id": "nodeC0", "x": 6.517, "y": 7.588, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeB0": {"id": "nodeB0", "x": 4.859, "y": 7.997, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 0, "y": 0}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeD", "x": 7, "y": 6}}, + {"id": "nodeE0--nodeF", "source": {"id": "nodeE0", "x": 2.01, "y": 5.242, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeC0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeC0", "x": 6.517, "y": 7.588, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}}, + {"id": "nodeF--nodeB0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeB0", "x": 4.859, "y": 7.997, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + }; + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,true); + + // EXPECT + expect(result.network).toEqual(networkExpected); + + + }); + + it('should reinsert side compounds but no update of reaction reversible', async () => { + // DATA + const networkExpected:NetworkLayout= { + "id": "network", + "nodes": { + "nodeA": {"id": "nodeA", "x": 0, "y": 0}, + "nodeD": {"id": "nodeD", "x": 7, "y": 6}, + "nodeF": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, + "nodeC0": {"id": "nodeC0", "x": 2.01, "y": 5.242, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, + "nodeE0": {"id": "nodeE0", "x": 5.716, "y": 7.913, "classes": ["duplicate"], "metadata": {"isSideCompound": true}} + }, + "links": [ + {"id": "link", "source": {"id": "nodeA", "x": 0, "y": 0}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "link", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeD", "x": 7, "y": 6}}, + {"id": "nodeC0--nodeF", "source": {"id": "nodeC0", "x": 2.01, "y": 5.242, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}, "target": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}}, + {"id": "nodeF--nodeE0", "source": {"id": "nodeF", "x": 5, "y": 5, "metadataLayout": {"isReversedVersion": true}}, "target": {"id": "nodeE0", "x": 5.716, "y": 7.913, "classes": ["duplicate"], "metadata": {"isSideCompound": true}}} + ] + }; + + + // TEST + const result = await LayoutManageSideCompounds.reinsertionSideCompounds(subgraphNetworkDupliAside,1,false); + + // EXPECT + expect(result.network).toEqual(networkExpected); + + }); + + + + }); + +}); \ No newline at end of file