diff --git a/client/src/canvas/draw_legend.ts b/client/src/canvas/draw_legend.ts index 944886c..c830683 100644 --- a/client/src/canvas/draw_legend.ts +++ b/client/src/canvas/draw_legend.ts @@ -55,98 +55,100 @@ function drawRoundedRect( // set the canvas properties const canvas = document.getElementById('myCanvas') as HTMLCanvasElement; -const width = window.innerWidth; -canvas.width = width; -const scalingFactor = width / 400; // adjust the legend size according to the screen width -canvas.height = 460 * scalingFactor; -const ctx: CanvasRenderingContext2D = canvas.getContext('2d'); +if (canvas != null) { + const width = window.innerWidth; + canvas.width = width; + const scalingFactor = width / 400; // adjust the legend size according to the screen width + canvas.height = 460 * scalingFactor; + const ctx: CanvasRenderingContext2D = canvas.getContext('2d'); -// style properties for the legend that are found to fit well -//// circles -const circleRadius = 15 * scalingFactor; -const circleX = 30 * scalingFactor; -const orderedCircleColorsAndLabels = [ - ['--component-started-color', 'Component started'], - ['--component-finished-color', 'Component finished'], - ['--component-done-color', 'Component done'], - ['--statement-passed-color', 'Statement passed'], - ['--statement-failed-color', 'Statement failed'], - ['--statement-entry-color', 'Loop / Condition entry'] -]; -//// boxes -const rectX = 17 * scalingFactor; -const rectWidth = 27 * scalingFactor; -const rectBorderRadius = 6 * scalingFactor; -const orderedBoxColorsAndLabels = [ - ['--task-box-color', 'Task'], - ['--service-box-color', 'Service'], - ['--condition-box-color', 'Condition'], - ['--loop-box-color', 'Loop'], - ['--parallel-box-color', 'Parallel'] -]; -//// descriptionText -const textX = 80 * scalingFactor; -//// entries -const lineWidth = 2 * scalingFactor; -const lineHeight = 40 * scalingFactor; -const objectsHeightDifferenceCircleAndBox = -10 * scalingFactor; // the boxes are bigger, so make the gap between the circle and box smaller -let objectsLineStartY = 30 * scalingFactor; -const fontSize = 27 * scalingFactor; -const textHeightDifferenceCircleAndBox = 4 * scalingFactor; // same as for the objects -let textLineStartY = 39 * scalingFactor; + // style properties for the legend that are found to fit well + //// circles + const circleRadius = 15 * scalingFactor; + const circleX = 30 * scalingFactor; + const orderedCircleColorsAndLabels = [ + ['--component-started-color', 'Component started'], + ['--component-finished-color', 'Component finished'], + ['--component-done-color', 'Component done'], + ['--statement-passed-color', 'Statement passed'], + ['--statement-failed-color', 'Statement failed'], + ['--statement-entry-color', 'Loop / Condition entry'] + ]; + //// boxes + const rectX = 17 * scalingFactor; + const rectWidth = 27 * scalingFactor; + const rectBorderRadius = 6 * scalingFactor; + const orderedBoxColorsAndLabels = [ + ['--task-box-color', 'Task'], + ['--service-box-color', 'Service'], + ['--condition-box-color', 'Condition'], + ['--loop-box-color', 'Loop'], + ['--parallel-box-color', 'Parallel'] + ]; + //// descriptionText + const textX = 80 * scalingFactor; + //// entries + const lineWidth = 2 * scalingFactor; + const lineHeight = 40 * scalingFactor; + const objectsHeightDifferenceCircleAndBox = -10 * scalingFactor; // the boxes are bigger, so make the gap between the circle and box smaller + let objectsLineStartY = 30 * scalingFactor; + const fontSize = 27 * scalingFactor; + const textHeightDifferenceCircleAndBox = 4 * scalingFactor; // same as for the objects + let textLineStartY = 39 * scalingFactor; -// draw the legend symbols as coloured circles and rectangles + // draw the legend symbols as coloured circles and rectangles -// draw the circles -for (const [circleColor, circleLabel] of orderedCircleColorsAndLabels) { - // draw the colored circle - ctx.beginPath(); - ctx.arc(circleX, objectsLineStartY, circleRadius, 0, 2 * Math.PI); - ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( - circleColor - ); - ctx.fill(); - ctx.lineWidth = lineWidth; - ctx.stroke(); + // draw the circles + for (const [circleColor, circleLabel] of orderedCircleColorsAndLabels) { + // draw the colored circle + ctx.beginPath(); + ctx.arc(circleX, objectsLineStartY, circleRadius, 0, 2 * Math.PI); + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( + circleColor + ); + ctx.fill(); + ctx.lineWidth = lineWidth; + ctx.stroke(); - // draw the label - ctx.font = String(fontSize) + 'px arial'; - ctx.fillStyle = '#000000'; - ctx.fillText(circleLabel, textX, textLineStartY); + // draw the label + ctx.font = String(fontSize) + 'px arial'; + ctx.fillStyle = '#000000'; + ctx.fillText(circleLabel, textX, textLineStartY); - // set the start height for the next line - objectsLineStartY += lineHeight; - textLineStartY += lineHeight; -} + // set the start height for the next line + objectsLineStartY += lineHeight; + textLineStartY += lineHeight; + } -// adjust the start height for the next line when switching from circle to box -objectsLineStartY += objectsHeightDifferenceCircleAndBox; -textLineStartY += textHeightDifferenceCircleAndBox; + // adjust the start height for the next line when switching from circle to box + objectsLineStartY += objectsHeightDifferenceCircleAndBox; + textLineStartY += textHeightDifferenceCircleAndBox; -// draw the boxes -for (const [boxColor, boxLabel] of orderedBoxColorsAndLabels) { - ctx.beginPath(); - drawRoundedRect( - ctx, - rectX, - objectsLineStartY, - rectWidth, - rectWidth, - rectBorderRadius - ); - ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( - boxColor - ); - ctx.fill(); - ctx.lineWidth = lineWidth; - ctx.stroke(); + // draw the boxes + for (const [boxColor, boxLabel] of orderedBoxColorsAndLabels) { + ctx.beginPath(); + drawRoundedRect( + ctx, + rectX, + objectsLineStartY, + rectWidth, + rectWidth, + rectBorderRadius + ); + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue( + boxColor + ); + ctx.fill(); + ctx.lineWidth = lineWidth; + ctx.stroke(); - // draw the label - ctx.font = String(fontSize) + 'px arial'; - ctx.fillStyle = '#000000'; - ctx.fillText(boxLabel, textX, textLineStartY); + // draw the label + ctx.font = String(fontSize) + 'px arial'; + ctx.fillStyle = '#000000'; + ctx.fillText(boxLabel, textX, textLineStartY); - // set the start height for the next line - objectsLineStartY += lineHeight; - textLineStartY += lineHeight; + // set the start height for the next line + objectsLineStartY += lineHeight; + textLineStartY += lineHeight; + } } diff --git a/client/src/code_visualization/element_creation.ts b/client/src/code_visualization/element_creation.ts index c20dfcb..c79b808 100644 --- a/client/src/code_visualization/element_creation.ts +++ b/client/src/code_visualization/element_creation.ts @@ -11,7 +11,7 @@ import { Point } from '../types/point'; * read out the stored HTML data to create nodes, edges and containers to visualize the PFDL program. * @returns graph elements as valid JSON strings that are accepted by cytoscape */ -export function getGraphElements() { +export function getGraphElements(parsedDotfile = null) { // retrieve graph data const dotfileContent = document.getElementById('graphElementsDiv').innerText; if (dotfileContent == null || dotfileContent.search('graph') == -1) { @@ -23,12 +23,15 @@ export function getGraphElements() { const containers = buildTreeStructure(treeStructure, dotfileString); - // 3rd party package - const parse = require('dotparser'); - const parsedContent = parse(dotfileString)[0]; + if (parsedDotfile == null) { + // parse the dotfile here + // 3rd party package + const parse = require('dotparser'); + parsedDotfile = parse(dotfileString)[0]; + } // convert into utf-8 string - const [nodes, edges] = parseElementsFromDotFileString(parsedContent); + const [nodes, edges] = parseElementsFromDotFileString(parsedDotfile); return getFinalGraphElements(nodes, edges, containers); } @@ -159,7 +162,7 @@ const loopTree = ( * @param fileString a JSON string containing the graph objects * @returns an array containing all existing nodes and edges in the dotfile */ -const parseElementsFromDotFileString = ( +export const parseElementsFromDotFileString = ( fileString: any, standaloneTesting = false ) => { diff --git a/client/src/code_visualization/graph_creation.ts b/client/src/code_visualization/graph_creation.ts index ccb682e..54d151f 100644 --- a/client/src/code_visualization/graph_creation.ts +++ b/client/src/code_visualization/graph_creation.ts @@ -13,9 +13,9 @@ import { createTooltips } from './event_handler'; */ const getGraphElements = require('./element_creation').getGraphElements; -export function createGraph(cy) { +export function createGraph(cy, parsedDotfile = null) { // add all graph nodes to cytoscape - const elements = getGraphElements(); + const elements = getGraphElements(parsedDotfile); if (elements == null) { return false; } diff --git a/client/src/code_visualization/main.ts b/client/src/code_visualization/main.ts index 2c1129f..e571bdb 100644 --- a/client/src/code_visualization/main.ts +++ b/client/src/code_visualization/main.ts @@ -6,32 +6,69 @@ import { setupCytoscapeInstance } from './setup_cytoscape'; import { setupEventHandlers } from './event_handler'; import { createGraph } from './graph_creation'; import { rotateNodes } from './context_menu'; +import { parseElementsFromDotFileString } from './element_creation'; -const cy = setupCytoscapeInstance(document.getElementById('cy')); -setupEventHandlers(cy); +const cytoscapeDiv = document.getElementById('cy'); +if (cytoscapeDiv) { + const cy = setupCytoscapeInstance(document.getElementById('cy')); + setupEventHandlers(cy); -// create the graph -createGraph(cy); + // create the graph + createGraph(cy, null); -// global function used to update the dashboard in the standalone browser version -(window as any).createCodeVisualization = function (refreshData = true) { - if (refreshData) { + // global function to enable the dashboard in the standalone browser version to access the cytoscape instance + (window as any).getCy = function () { + return cy; + }; + + // global function used to update the dashboard in the standalone browser version + (window as any).createCodeVisualization = function (parsedDotfile = null) { cy.elements().remove(); - const successfullyCreated = createGraph(cy); - if (!successfullyCreated) { - // no elements drawn - return; + createGraph(cy, parsedDotfile); + const imgElement = document.getElementById('codeVisuImg'); + if (imgElement) { + // graph should be displayed as an image, rotate it + rotateNodes(cy.nodes(), cy.nodes('.container'), false); } - } + }; + + // global function for the dashboard in the standalone browser version to enable the comparison of two dotfiles + (window as any).getElementsFromDotfile = function (dotfile) { + return parseElementsFromDotFileString(dotfile); + }; - // create and display image of the code visualization if the element 'codeVisuImg' exists - const imgElement = document.getElementById('codeVisuImg'); - if (imgElement) { - // only exists in standalone browser version (without vscode) - // create img of rotated graph - rotateNodes(cy.nodes(), cy.nodes('.container'), false); - const pngToDownload = cy.png({ full: true }); - rotateNodes(cy.nodes(), cy.nodes('.container'), true); - imgElement.setAttribute('src', pngToDownload); - } -}; + (window as any).flipTokensForNodeIds = function (nodeIds) { + nodeIds.forEach((id) => { + const node = cy.getElementById(id); + const tokenNode = cy.getElementById(id + '_token'); + if (tokenNode.length && tokenNode.inside()) { + // node had a token before, remove it + const newParentId = tokenNode.data('parent'); + node.data('label', ''); + node.move({ parent: newParentId }); + node.classes(['single_node', 'upper_round_node']); + tokenNode.remove(); + } else { + if (tokenNode.removed()) { + tokenNode.restore(); + } else { + // create a new token node for the object + const newTokenNode = { + group: 'nodes', + classes: ['upper_round_node', 'token_label'], + data: { + id: id + '_token', + label: '', + tippyContent: node.label, + parent: node.parentId + } + }; + cy.add(newTokenNode); + } + node.data('label', '•'); + node.move({ parent: node.id + '_token' }); + node.classes(['single_node', 'token_node']); + } + }); + }; +}