Skip to content

Commit

Permalink
Fix issue:iVis-at-Bilkent/software-artifact-analyzer #50 : png full m…
Browse files Browse the repository at this point in the history
…ethod overflow added to the canvas
  • Loading branch information
LaraMerdol committed Jan 12, 2024
1 parent e13d077 commit 3455552
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 63 deletions.
2 changes: 1 addition & 1 deletion demo/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ <h2 class="accordion-header" id="panelsStayOpen-headingOne">
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#" onclick="exportPng()">PNG</a></li>
<li><a class="dropdown-item" href="#" onclick="exportPngAll()">PNG (all)</a></li>
<li><a class="dropdown-item" href="#" onclick="exportPngAll()">PNG (hall)</a></li>
<li><a class="dropdown-item" href="#" onclick="exportJson()">JSON</a></li>
<li><a class="dropdown-item" href="#" onclick="exportGraphml()">GraphML</a></li>
<li><a class="dropdown-item" href="#" onclick="exportSif()">SIF</a></li>
Expand Down
159 changes: 97 additions & 62 deletions src/cytoscape-visual-cues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,98 +781,134 @@ async function loadImageAsync(img: HTMLImageElement, src: string): Promise<void>
});
}

/**
* Gets a PNG of the graph including the cues.
* @param {any} options - png options.
* @param {string[]} ignoreElementClasses - Array of classes to ignore.
* @returns combinedBase64 - Base64-encoded PNG of the graph.
*/
export async function pngFull(options: any, ignoreElementClasses: string[] = []) {
try {
// Create a canvas and load and draw the PNG image of the graph using the png() function from cytoscape.js core.
const cy = this;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

if (!ctx) {
throw new Error('Unable to obtain 2D context for canvas.');
}

const graphBoundingBox = cy.elements().renderedBoundingBox();
const container = document.getElementById('cy');

if (!container) {
throw new Error('Container element not found.');
}

const base64png: string = cy.png(options);
const image = await loadImage(base64png);

canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
const elements = cy.elements();

// Calculate the positions of cues on the container, determine the required positions on the canvas, and then add the cues.
const graphBoundingBox = cy.elements().renderedBoundingBox(); //For option full calculate from graphBoundingBox
const container = document.getElementById('cy'); //For option !full calculate from container
if (!container) {
throw new Error('Container element not found.');
}
let z1 = (cy.zoom() / 2);
let overflow = 0;
let cuePositions: { cueX: number; cueY: number }[] = [];
for (const element of elements) {
const cues = element.getCueData();
for (const cueDic of Object.values(cues) as any[]) {
for (const key in cueDic) {
if (cueDic.hasOwnProperty(key)) {
const cue = cueDic[key];
if (cue.htmlElem) {
const bb = element.renderedBoundingBox({
includeLabels: false,
includeOverlays: false,
});
// Calculate cue position within the bounding box with the zoom information
const isEdge = element.isEdge();
const w = cue.htmlElem.offsetWidth * z1;
const h = cue.htmlElem.offsetHeight * z1;
const pos = cue.position;
let cueX = (bb.x2 + bb.x1) / 2 - w / 2;
let cueY = (bb.y1 + bb.y2) / 2 - h / 2;
if (pos.startsWith('bottom')) {
cueY = bb.y2 - h / 2;
} else if (pos.startsWith('top')) {
cueY = bb.y1 - h / 2;
}
if (pos.endsWith('right')) {
cueX = bb.x2 - w / 2;
} else if (pos.endsWith('left')) {
cueX = bb.x1 - w / 2;
}
if (isEdge) {
const p = getCuePositionOnEdge(element, pos as EdgeCuePosition);
if (p) {
cueX = p.x - w / 2;
cueY = p.y - h / 2;
}
}
const margins = getMargins(cue, element);
cueX += (margins.x * z1);
cueY += (margins.y * z1);
cuePositions.push({
cueX: cueX,
cueY: cueY,
});
if (pos.startsWith('bottom')) {
overflow = Math.max(overflow, Math.abs(bb.y2 - cueY))
} else if (pos.startsWith('top')) {
overflow = Math.max(overflow, Math.abs(cueY - bb.y1))
}
if (pos.endsWith('right')) {
overflow = Math.max(overflow, Math.abs(cueX - bb.x2))
} else if (pos.endsWith('left')) {
overflow = Math.max(overflow, Math.abs(cueX - bb.x1))
}
}
}
}
}
}

if (options.full) {
overflow = overflow * Math.max(image.width / graphBoundingBox.w, image.height / graphBoundingBox.h)
} else {
overflow = 0; //Margin for the cues at the edges visibility is 0 if full is false
}
canvas.width = image.width + 2 * overflow;
canvas.height = image.height + 2 * overflow;
if (!ctx) {
throw new Error('Unable to obtain 2D context for canvas.');
}
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, overflow, overflow, image.width, image.height);
let index = 0;
for (const element of elements) {
const cues = element.getCueData();
for (const cueDic of Object.values(cues) as any[]) {
for (const key in cueDic) {
if (cueDic.hasOwnProperty(key)) {
const cue = cueDic[key];
if (cue.htmlElem) {
//Add cues to the image canvas in a manner analogous to adding them to the cytoscape canvas.
try {
const elementHTML = cue.htmlElem as HTMLElement;
let ratio = 1;
let z1 = (cy.zoom()/2) * ratio;
//Use html2canvas package let you convert cue html element to canvas image
const elementHTML = cue.htmlElem as HTMLElement;
const dataUrl = await html2canvas(elementHTML, {
scale: options.scale,
scale: options.scale * (options.full ? 1/z1 : 1),
ignoreElements: (element) => ignoreElementClasses.some((className) => element.classList.contains(className)),
backgroundColor: 'transparent',
});
// Create an image element
const cueImg = await loadImage(dataUrl.toDataURL());
const bb = element.renderedBoundingBox({
includeLabels: false,
includeOverlays: false,
});
const isEdge = element.isEdge();
const w = cue.htmlElem.offsetWidth *z1;
const h = cue.htmlElem.offsetHeight *z1;
const pos = cue.position;

// Calculate cue position within the bounding box
let cueX = (bb.x2 + bb.x1) / 2 - w / 2;
let cueY = (bb.y1 + bb.y2) / 2 - h / 2;
if (pos.startsWith('bottom')) {
cueY = bb.y2 - h / 2;
} else if (pos.startsWith('top')) {
cueY = bb.y1 - h / 2;
}

if (pos.endsWith('right')) {
cueX = bb.x2 - w / 2;
} else if (pos.endsWith('left')) {
cueX = bb.x1 - w / 2;
}

if (isEdge) {
const p = getCuePositionOnEdge(element, pos as EdgeCuePosition);
if (p) {
cueX = p.x - w / 2;
cueY = p.y- h / 2;
}
}

// Adjust cue position based on margins
const margins = getMargins(cue, element);
cueX +=(margins.x *z1) ;
cueY += (margins.y *z1) ;
const cueX = cuePositions[index].cueX;
const cueY = cuePositions[index].cueY;
let cueXSource;
let cueYSource;

if (options.full) {
cueXSource = ((cueX - ((graphBoundingBox.x1 + graphBoundingBox.x2) / 2)) * image.width) / graphBoundingBox.w;
cueYSource = ((cueY - ((graphBoundingBox.y1 + graphBoundingBox.y2) / 2)) * image.height) / graphBoundingBox.h;
ctx.drawImage(cueImg, cueXSource + canvas.width / 2, cueYSource + canvas.height / 2, (1/options.scale)*cueImg.width * image.width /graphBoundingBox.w ,(1/options.scale) *cueImg.height *image.height /graphBoundingBox.h);
ctx.drawImage(cueImg, cueXSource + image.width / 2 + overflow, cueYSource + image.height / 2 + overflow, (z1 / options.scale) * cueImg.width * image.width / graphBoundingBox.w, (z1 / options.scale) * cueImg.height * image.height / graphBoundingBox.h);
} else {
cueXSource = (cueX * image.width) / container.offsetWidth;
cueYSource = (cueY * image.height) / container.offsetHeight;
ctx.drawImage(cueImg, cueXSource, cueYSource, (1/options.scale)* cueImg.width * image.width / container.offsetWidth, (1/options.scale)* cueImg.height * image.height /container.offsetHeight );
console.log()
ctx.drawImage(cueImg, cueXSource, cueYSource, (1 / options.scale) * cueImg.width * image.width / container.offsetWidth, (1 / options.scale) * cueImg.height * image.height / container.offsetHeight);
}
index += 1;

} catch (error) {
console.error('Error processing image:', error);
Expand All @@ -882,7 +918,6 @@ export async function pngFull(options: any, ignoreElementClasses: string[] = [])
}
}
}

const combinedBase64 = canvas.toDataURL();
return combinedBase64;

Expand Down

0 comments on commit 3455552

Please sign in to comment.