624 lines
22 KiB
JavaScript
624 lines
22 KiB
JavaScript
var workflow = {}
|
|
|
|
// fetch('./test.json')
|
|
// .then((response) => response.json())
|
|
// .then((data) => {
|
|
// for (var key in data)
|
|
// workflow[key] = data[key];
|
|
// });
|
|
|
|
// Counters for placeholder IDs of states/actions added via GUI
|
|
var stateIdCounter = workflow.states ? workflow.states.length : 0;
|
|
var actionIdCounter = workflow.states ? workflow.actions.length : 0;
|
|
|
|
//Parse workflow
|
|
|
|
workflow.states.forEach(state => {
|
|
var messages = [];
|
|
state.stateData.messages.forEach(msg => messages.push(new Message(msg)));
|
|
state.stateData.messages = messages;
|
|
var viewers = [];
|
|
state.stateData.viewers.forEach(v => viewers.push(new Role(v)));
|
|
state.stateData.viewers = viewers;
|
|
state.stateData.payload = new Payload(state.stateData.payload);
|
|
})
|
|
|
|
workflow.actions.forEach(action => {
|
|
var messages = [];
|
|
action.actionData.messages.forEach(msg => messages.push(new Message(msg)));
|
|
action.actionData.messages = messages;
|
|
var viewers = [];
|
|
action.actionData.viewers.forEach(v => viewers.push(new Role(v)));
|
|
action.actionData.viewers = viewers;
|
|
var actors = [];
|
|
action.actionData.actors.forEach(v => actors.push(new Role(v)));
|
|
action.actionData.actors = actors;
|
|
var viewActors = [];
|
|
action.actionData['actor Viewers'].forEach(v => viewActors.push(new Role(v)));
|
|
action.actionData['actor Viewers'] = viewActors;
|
|
})
|
|
|
|
//Actors of the workflow
|
|
var actors = [];
|
|
workflow.actions.forEach(act => act.actionData.actors.forEach(a => {
|
|
var includes = false;
|
|
actors.forEach(actor => includes = includes || equalRoles(a, actor));
|
|
(!includes) && actors.push(a);
|
|
(!act.actionData.actorNames) && (act.actionData.actorNames = []);
|
|
act.actionData.actorNames.push(getRoleName(a));
|
|
}));
|
|
// console.log(actors);
|
|
// workflow.actions.forEach(a => console.log(a.actionData.actorNames));
|
|
|
|
function getRoleName(role) {
|
|
if (typeof role == 'string') {
|
|
return role;
|
|
} else if (role instanceof Role) {
|
|
return role.name;
|
|
} else {
|
|
return JSON.stringify(role);
|
|
}
|
|
}
|
|
|
|
const NO_ACTOR = 'None';
|
|
|
|
//Prepare actor highlighting
|
|
const selectedActor = document.getElementById('actor');
|
|
var allActors = document.createElement('option');
|
|
allActors.text = NO_ACTOR;
|
|
selectedActor.add(allActors);
|
|
actors.forEach(actor => {
|
|
var option = document.createElement('option');
|
|
option.text = getRoleName(actor);
|
|
selectedActor.add(option);
|
|
});
|
|
|
|
|
|
//Viewers of the workflow
|
|
var viewers = [];
|
|
//Actions/States with no explicit viewers
|
|
var viewableByAll = []
|
|
//Possible initiators
|
|
var initiators = []
|
|
//Implicit state from which initial actions can be selected
|
|
var initState = null;
|
|
//Identify all viewers of every action
|
|
workflow.actions.forEach(act => {
|
|
if (act.actionData.viewers.length === 0) {
|
|
viewableByAll.push(act.actionData);
|
|
} else {
|
|
act.actionData.viewers.forEach(v => {
|
|
var includes = false;
|
|
viewers.forEach(viewer => includes = includes || equalRoles(v, viewer));
|
|
(!includes) && viewers.push(v);
|
|
(!act.actionData.viewerNames) && (act.actionData.viewerNames = []);
|
|
act.actionData.viewerNames.push(getRoleName(v));
|
|
})
|
|
}
|
|
if (act.actionData.mode === 'initial') {
|
|
act.actionData.actorNames.forEach(an => !initiators.includes(an) && initiators.push(an));
|
|
}
|
|
});
|
|
//Identify all viewers of every state
|
|
workflow.states.forEach(st => {
|
|
if (st.name === '@@INIT') {
|
|
initState = st;
|
|
} else if (st.stateData.viewers.length === 0) {
|
|
viewableByAll.push(st.stateData);
|
|
} else {
|
|
st.stateData.viewers.forEach(v => {
|
|
var includes = false;
|
|
viewers.forEach(viewer => includes = includes || equalRoles(v, viewer));
|
|
(!includes) && viewers.push(v);
|
|
(!st.stateData.viewerNames) && (st.stateData.viewerNames = []);
|
|
st.stateData.viewerNames.push(getRoleName(v));
|
|
})
|
|
}
|
|
});
|
|
|
|
initState.stateData.viewerNames = initiators;
|
|
|
|
const ALL_VIEW = "Not explicitly specified";
|
|
if (viewableByAll.length > 0) {
|
|
viewers.push(ALL_VIEW);
|
|
var viewerNames = []
|
|
viewers.forEach(viewer => viewerNames.push(getRoleName(viewer)));
|
|
viewableByAll.forEach(data => {
|
|
data.viewerNames = viewerNames;
|
|
});
|
|
}
|
|
|
|
|
|
const NO_VIEWER = NO_ACTOR;
|
|
|
|
//Prepare viewer highlighting
|
|
const selectedViewer = document.getElementById('viewer');
|
|
var allViewers = document.createElement('option');
|
|
allViewers.text = NO_VIEWER;
|
|
selectedViewer.add(allViewers);
|
|
viewers.forEach(viewer => {
|
|
var option = document.createElement('option');
|
|
option.text = getRoleName(viewer);
|
|
selectedViewer.add(option);
|
|
});
|
|
|
|
|
|
//source & target nodes of all currently highlighted actions
|
|
var highlightedSources = [];
|
|
var highlightedTargets = [];
|
|
|
|
function selectActor() {
|
|
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
|
|
rightSelection = null;
|
|
highlightedSources = [];
|
|
highlightedTargets = [];
|
|
selectedViewer.value = NO_VIEWER;
|
|
workflow.actions.forEach(act => {
|
|
if (act.actionData.mode != 'automatic' && act.actionData.actorNames.includes(selectedActor.value)) {
|
|
highlightedSources.push(act.source.id);
|
|
highlightedTargets.push(act.target.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
function selectViewer() {
|
|
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
|
|
rightSelection = null;
|
|
highlightedSources = [];
|
|
highlightedTargets = [];
|
|
selectedActor.value = NO_ACTOR;
|
|
workflow.states.forEach(st => {
|
|
if (st.stateData.viewerNames.includes(selectedViewer.value)) {
|
|
highlightedSources.push(st.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
var selfLoops = {}; // All edges whose targets equal their sources.
|
|
var overlappingEdges = {}; // All edges whose target and source are connected by further.
|
|
const selfLoopCurvMin = 0.5; // Minimum curvature of a self loop.
|
|
const curvatureMinMax = 0.2; // Minimum/maximum curvature (1 +/- x) of overlapping edges.
|
|
|
|
var selection = null; // The currently selected node/edge.
|
|
var rightSelection = null; // The currently right clicked node/edge.
|
|
const sidePanel = document.getElementById('sidepanel');
|
|
const sideContent = document.getElementById('sidecontent');
|
|
//Context menus
|
|
const contextMenuBg = document.getElementById('ctmenubg'); //Click on background
|
|
const contextMenuSt = document.getElementById('ctmenust'); //Click on state
|
|
const contextMenuEd = document.getElementById('ctmenued'); //Click on edge
|
|
|
|
const edgeColourDefault = '#999999ff';
|
|
const edgeColourSelected = '#000000ff';
|
|
const edgeColourHighlightDefault = 'magenta';
|
|
const edgeColourHighlightSelected = 'red';
|
|
const edgeColourSubtleDefault = '#99999955';
|
|
const edgeColourSubtleSelected = '#00000055';
|
|
|
|
|
|
/**
|
|
* Checks if two roles are equal.
|
|
* @param {*} role1
|
|
* @param {*} role2
|
|
* @returns
|
|
*/
|
|
function equalRoles(role1, role2) {
|
|
role1 instanceof Role && (role1 = role1.json);
|
|
role2 instanceof Role && (role2 = role2.json);
|
|
var equal = role1.tag === role2.tag;
|
|
if (role1.tag == 'payload-reference') {
|
|
equal = equal && (role1['payload-label'] === role2['payload-label']);
|
|
} else if (role1.tag == 'user') {
|
|
equal = equal && (role1.user === role2.user);
|
|
} else if (role1.tag == 'authorized') {
|
|
equal = equal && (role1.authorized['dnf-terms'][0][0].var === role2.authorized['dnf-terms'][0][0].var);
|
|
}
|
|
return equal;
|
|
}
|
|
|
|
|
|
/**
|
|
* Identifies and stores self loops as well as overlapping edges (i.e. multiple edges sharing the
|
|
* same source and target).
|
|
*/
|
|
function identifyOverlappingEdges() {
|
|
selfLoops = {};
|
|
overlappingEdges = {};
|
|
workflow.actions.forEach(edge => {
|
|
var source = typeof(edge.source) === 'string' ? edge.source : edge.source.id;
|
|
var target = typeof(edge.target) === 'string' ? edge.target : edge.target.id;
|
|
var pre = source <= target ? source : target;
|
|
var post = source <= target ? target : source;
|
|
edge.nodePairId = pre + '_' + post;
|
|
var category = edge.source === edge.target ? selfLoops : overlappingEdges;
|
|
if (!category[edge.nodePairId]) category[edge.nodePairId] = [];
|
|
category[edge.nodePairId].push(edge);
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Computes the curvature of the loops stored in `selfLoops` and overlapping edges
|
|
* stored in `overlappingEdges`.
|
|
*/
|
|
function computeCurvatures() {
|
|
// Self loops
|
|
Object.keys(selfLoops).forEach(id => {
|
|
var edges = selfLoops[id];
|
|
for (let i = 0; i < edges.length; i++)
|
|
edges[i].curvature = selfLoopCurvMin + i / 10;
|
|
});
|
|
// Overlapping edges
|
|
Object.keys(overlappingEdges)
|
|
.filter(nodePairId => overlappingEdges[nodePairId].length > 1)
|
|
.forEach(nodePairId => {
|
|
var edges = overlappingEdges[nodePairId];
|
|
var lastIndex = edges.length - 1;
|
|
var lastEdge = edges[lastIndex];
|
|
lastEdge.curvature = curvatureMinMax;
|
|
let delta = 2 * curvatureMinMax / lastIndex;
|
|
for (let i = 0; i < lastIndex; i++) {
|
|
edges[i].curvature = - curvatureMinMax + i * delta;
|
|
if (lastEdge.source !== edges[i].source) edges[i].curvature *= -1;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function generatePanelContent(selection) {
|
|
var children = [];
|
|
var data = selection.stateData || selection.actionData
|
|
for (var key in data) {
|
|
if (key === 'viewerNames' || key === 'actorNames') continue;
|
|
var h = document.createElement('h2');
|
|
var heading = document.createTextNode(key.substring(0,1).toUpperCase() + key.substring(1));
|
|
h.appendChild(heading);
|
|
children.push(h);
|
|
var content = data[key];
|
|
if (content instanceof Array && content.length > 0 && content[0] instanceof Message) {
|
|
content.forEach(msg => msg.format().forEach(child => children.push(child)));
|
|
} else if (content instanceof Payload) {
|
|
content.format().forEach(child => children.push(child));
|
|
} else if (content instanceof Array && content.length > 0 && content[0] instanceof Role) {
|
|
var viewerList = document.createElement('ul');
|
|
content.forEach(viewer => {
|
|
var v = document.createElement('li');
|
|
v.appendChild(document.createTextNode(viewer.name));
|
|
viewerList.appendChild(v);
|
|
});
|
|
children.push(viewerList);
|
|
} else {
|
|
var p = document.createElement('p');
|
|
var text = document.createTextNode(JSON.stringify(data[key]));
|
|
p.appendChild(text);
|
|
children.push(p);
|
|
}
|
|
|
|
}
|
|
return children;
|
|
}
|
|
|
|
|
|
/**
|
|
* Marks the given item as selected.
|
|
* @param {*} item The node or edge to select.
|
|
*/
|
|
function select(item) {
|
|
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
|
|
rightSelection = null;
|
|
selection = selection === item ? null : item;
|
|
if (selection === item) {
|
|
while (sideContent.firstChild)
|
|
sideContent.removeChild(sideContent.lastChild);
|
|
sidePanel.style.display = 'block'
|
|
document.getElementById('sideheading').innerHTML = item.name;
|
|
var data = document.createElement('div');
|
|
var content = generatePanelContent(selection);
|
|
content.forEach(c => data.appendChild(c));
|
|
sideContent.appendChild(data);
|
|
} else {
|
|
sidePanel.style.display = 'none';
|
|
}
|
|
console.log(item);
|
|
}
|
|
|
|
|
|
function rightSelect() {
|
|
select(rightSelection);
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the nodes and edges of the workflow graph.
|
|
*/
|
|
function updateGraph() {
|
|
identifyOverlappingEdges()
|
|
computeCurvatures()
|
|
Graph.graphData({nodes: workflow.states, links: workflow.actions});
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a new action between two states.
|
|
* @param {*} source The source state.
|
|
* @param {*} target The target state.
|
|
*/
|
|
function connect(source, target) {
|
|
let linkId = actionIdCounter ++;
|
|
action = {id: linkId, source: source, target: target, name: 'action_' + linkId};
|
|
workflow.actions.push(action);
|
|
updateGraph();
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a new state to the workflow and auto-selects it.
|
|
*/
|
|
function addState() {
|
|
var nodeId = stateIdCounter ++;
|
|
var x = newStateCoords.x;
|
|
var y = newStateCoords.y;
|
|
state = {id: nodeId, x: x, y: y, name: 'state_' + nodeId, fx: x, fy: y, val: 5};
|
|
workflow.states.push(state);
|
|
updateGraph();
|
|
select(state);
|
|
}
|
|
|
|
function addEdge() {
|
|
//TODO
|
|
}
|
|
|
|
function removeRightSelection() {
|
|
if (rightSelection) {
|
|
if (rightSelection.actionData) removeAction(rightSelection);
|
|
else removeState(rightSelection);
|
|
if (selection === rightSelection) {
|
|
selection = null;
|
|
sidePanel.style.display = 'none';
|
|
}
|
|
rightSelection = null;
|
|
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Removes an edge from the workflow.
|
|
* @param {*} action The action to remove.
|
|
*/
|
|
function removeAction(action) {
|
|
workflow.actions.splice(workflow.actions.indexOf(action), 1);
|
|
}
|
|
|
|
|
|
/**
|
|
* Removes a state from the workflow.
|
|
* @param {*} state The state to remove.
|
|
*/
|
|
function removeState(state) {
|
|
workflow.actions
|
|
.filter(edge => edge.source === state || edge.target === state)
|
|
.forEach(edge => removeAction(edge));
|
|
workflow.states.splice(workflow.states.indexOf(state), 1);
|
|
var abbreviation = state.stateData && state.stateData.abbreviation;
|
|
abbreviation && stateAbbreviations.splice(stateAbbreviations.indexOf(abbreviation), 1);
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {*} node
|
|
* @returns The colour the given node should have.
|
|
*/
|
|
function getNodeColour(node) {
|
|
var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER)
|
|
|| highlightedSources.includes(node.id) || highlightedTargets.includes(node.id)
|
|
var alpha = standard ? 'ff' : '55';
|
|
var isSelected = selection === node || rightSelection === node;
|
|
if (node.stateData && node.stateData.final !== 'False' && node.stateData.final !== '') {
|
|
if (node.stateData.final === 'True' || node.stateData.final === 'ok') {
|
|
return (isSelected ? '#a4eb34' : '#7fad36') + alpha;
|
|
} else if (node.stateData.final === 'not-ok') {
|
|
return (isSelected ? '#f77474' : '#f25050') + alpha;
|
|
} else {
|
|
//console.log(node.stateData.final);
|
|
}
|
|
} else if (node.name === '@@INIT') {
|
|
return (isSelected ? '#e8cd84' : '#d1ad4b') + alpha;
|
|
} else {
|
|
return (isSelected ? '#5fbad9' : '#4496b3') + alpha;
|
|
}
|
|
}
|
|
|
|
function isHighlightedEdge(edge) {
|
|
var data = edge.actionData
|
|
var isViewer = data.viewerNames.includes(selectedViewer.value)
|
|
var isActor = data.mode != 'automatic' && data.actorNames.includes(selectedActor.value)
|
|
var isActorAuto = data.mode == 'automatic' && highlightedTargets.includes(edge.source.id)
|
|
return isViewer || isActor || isActorAuto;
|
|
}
|
|
|
|
function getEdgeColour(edge) {
|
|
var isSelected = selection === edge || rightSelection === edge;
|
|
if (isHighlightedEdge(edge)) {
|
|
return isSelected ? edgeColourHighlightSelected : edgeColourHighlightDefault;
|
|
} else if (selectedActor.value !== NO_ACTOR) {
|
|
return isSelected ? edgeColourSubtleSelected : edgeColourSubtleDefault;
|
|
} else {
|
|
return isSelected ? edgeColourSelected : edgeColourDefault;
|
|
}
|
|
}
|
|
|
|
//Compute abbreviations of the names of all states
|
|
var stateAbbreviations = [];
|
|
workflow.states.forEach(state => {
|
|
// var label = node.name.substring(0, 5);
|
|
var label = state.name.split(' '); // [node.name.substring(0, 6), node.name.substring(6, 12), node.name.substring(12, 18)];
|
|
for (var i = 0; i < label.length; i++) {
|
|
if (label[i] === '(') continue; // if the state name contains whitespace after the brace
|
|
var isBrace = label[i][0] === '(';
|
|
label[i] = label[i].substring(isBrace ? 1 : 0, isBrace ? 2 : 1);
|
|
}
|
|
labelString = label.join('').substring(0,6);
|
|
var counter = 1;
|
|
var len = labelString.length;
|
|
while (stateAbbreviations.includes(labelString)) {
|
|
labelString = labelString.substring(0,len) + "'" + counter++;
|
|
}
|
|
stateAbbreviations.push(labelString);
|
|
state.stateData.abbreviation = labelString;
|
|
});
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {*} event
|
|
* @param {HTMLElement} menu
|
|
*/
|
|
function openContextMenu(x, y, menu) {
|
|
menu.style.top = y - 25;
|
|
menu.style.left = x + 20;
|
|
menu.style.display = 'block';
|
|
}
|
|
|
|
var newStateCoords = {'x': 0, 'y': 0}; //Initial coordinates of the next new state
|
|
|
|
const Graph = ForceGraph()
|
|
(document.getElementById('graph'))
|
|
.linkDirectionalArrowLength(6)
|
|
.linkDirectionalArrowRelPos(1)
|
|
.linkColor(getEdgeColour)
|
|
.linkCurvature('curvature')
|
|
.linkCanvasObjectMode(() => 'after')
|
|
.linkCanvasObject((edge, context) => {
|
|
const MAX_FONT_SIZE = 4;
|
|
const LABEL_NODE_MARGIN = Graph.nodeRelSize() * edge.source.val * 1.5;
|
|
|
|
const source = edge.source;
|
|
const target = edge.target;
|
|
const curvature = edge.curvature || 0;
|
|
|
|
var textPos = (source === target) ? {x: source.x, y: source.y} : Object.assign(...['x', 'y'].map(c => ({
|
|
[c]: source[c] + (target[c] - source[c]) / 2
|
|
})));
|
|
|
|
const edgeVector = {x: target.x - source.x, y: target.y - source.y};
|
|
if (source !== target) {
|
|
var evLength = Math.sqrt(Math.pow(edgeVector.x, 2) + Math.pow(edgeVector.y, 2));
|
|
var perpendicular = {x: edgeVector.x, y: (-Math.pow(edgeVector.x, 2) / edgeVector.y)};
|
|
var pLength = Math.sqrt(Math.pow(perpendicular.x, 2) + Math.pow(perpendicular.y, 2));
|
|
perpendicular.x = perpendicular.x / pLength;
|
|
perpendicular.y = perpendicular.y / pLength;
|
|
var fromSource = {x: source.x + perpendicular.x, y: source.y + perpendicular.y};
|
|
// If source would cycle around target in clockwise direction, would fromSource point into this direction?
|
|
// If not, the perpendicular vector must be flipped in order to ensure that the label is displayed on the
|
|
// intended curved edge.
|
|
var isClockwise = (source.x < target.x && fromSource.y > source.y) ||
|
|
(source.x > target.x && fromSource.y < source.y) ||
|
|
(source.x === target.x && ((source.y < target.y && fromSource.x < source.x) ||
|
|
source.y > target.y && fromSource.x > source.x));
|
|
var offset = 0.5 * evLength * (isClockwise ? -curvature : curvature);
|
|
textPos = {x: textPos.x + perpendicular.x * offset, y: textPos.y + perpendicular.y * offset};
|
|
} else if (edge.__controlPoints) { // Position label relative to the Bezier control points of the self loop
|
|
edgeVector.x = edge.__controlPoints[2] - edge.__controlPoints[0];
|
|
edgeVector.y = edge.__controlPoints[3] - edge.__controlPoints[1];
|
|
var ctrlCenter = {x: edge.__controlPoints[0] + (edge.__controlPoints[2] - edge.__controlPoints[0]) / 2,
|
|
y: edge.__controlPoints[1] + (edge.__controlPoints[3] - edge.__controlPoints[1]) / 2};
|
|
var fromSource = {x: ctrlCenter.x - source.x, y: ctrlCenter.y - source.y};
|
|
var fromSrcLen = Math.sqrt(Math.pow(fromSource.x, 2) + Math.pow(fromSource.y, 2));
|
|
fromSource.x /= fromSrcLen;
|
|
fromSource.y /= fromSrcLen;
|
|
// The distance of the control point is 70 * curvature. Slightly more than half of it is appropriate here:
|
|
textPos = {x: source.x + fromSource.x * 37 * curvature, y: source.y + fromSource.y * 37 * curvature};
|
|
}
|
|
|
|
const maxTextLength = (source !== target) ? Math.sqrt(Math.pow(edgeVector.x, 2) + Math.pow(edgeVector.y, 2)) - LABEL_NODE_MARGIN
|
|
: 1.5 * Math.sqrt(4 * source.val + 100 * curvature);
|
|
|
|
var textAngle = Math.atan2(edgeVector.y, edgeVector.x);
|
|
// maintain label vertical orientation for legibility
|
|
if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle);
|
|
if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle);
|
|
|
|
var label = edge.name;
|
|
|
|
// estimate fontSize to fit in link length
|
|
//context.font = '1px Sans-Serif';
|
|
const fontSize = MAX_FONT_SIZE;// Math.min(MAX_FONT_SIZE, maxTextLength / context.measureText(label).width);
|
|
context.font = `${fontSize}px Sans-Serif`;
|
|
|
|
var textLen = context.measureText(label).width;
|
|
if (textLen > maxTextLength) {
|
|
var allowedLen = maxTextLength * (label.length / textLen);
|
|
label = label.substring(0, allowedLen);
|
|
if (label !== edge.name) label += '...';
|
|
textLen = context.measureText(label).width;
|
|
}
|
|
|
|
const bckgDimensions = [textLen, fontSize];
|
|
|
|
// draw text label (with background rect)
|
|
context.save();
|
|
context.translate(textPos.x, textPos.y);
|
|
context.rotate(textAngle);
|
|
|
|
context.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
context.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, ...bckgDimensions);
|
|
|
|
context.textAlign = 'center';
|
|
context.textBaseline = 'middle';
|
|
context.fillStyle = getEdgeColour(edge);
|
|
context.fillText(label, 0, 0);
|
|
context.restore();
|
|
})
|
|
.linkLineDash(edge => edge.actionData.mode == 'automatic' && [2, 3]) //[dash, gap]
|
|
.linkWidth(edge => (isHighlightedEdge(edge)) ? 3 : 1)
|
|
.linkDirectionalParticles(2)
|
|
.linkDirectionalParticleColor(() => '#00000055')
|
|
.linkDirectionalParticleWidth(edge => (isHighlightedEdge(edge)) ? 3 : 0)
|
|
.nodeCanvasObject((node, ctx) => {
|
|
ctx.fillStyle = getNodeColour(node);
|
|
ctx.beginPath();
|
|
ctx.arc(node.x, node.y, 2*node.val, 0, 2 * Math.PI, false);
|
|
ctx.fill();
|
|
|
|
if (! (node.stateData && node.stateData.abbreviation)) return;
|
|
|
|
ctx.fillStyle = 'white';
|
|
ctx.font = '4px Sans-Serif';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(node.stateData.abbreviation, node.x, node.y);
|
|
})
|
|
.onNodeDragEnd(node => {
|
|
node.fx = node.x;
|
|
node.fy = node.y;
|
|
})
|
|
.onNodeClick((node, _) => select(node))
|
|
.onNodeRightClick((node, event) => {
|
|
openContextMenu(event.layerX, event.layerY, contextMenuSt);
|
|
contextMenuBg.style.display = contextMenuEd.style.display = 'none';
|
|
rightSelection = node;
|
|
})
|
|
.onLinkClick((edge, _) => select(edge))
|
|
.onLinkRightClick((edge, event) => {
|
|
openContextMenu(event.layerX, event.layerY, contextMenuEd);
|
|
contextMenuBg.style.display = contextMenuSt.style.display = 'none';
|
|
rightSelection = edge;
|
|
})
|
|
.onBackgroundClick(_ => {
|
|
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
|
|
sidePanel.style.display = 'none';
|
|
rightSelection = null;
|
|
selection = null;
|
|
})
|
|
.onBackgroundRightClick(event => {
|
|
newStateCoords = Graph.screen2GraphCoords(event.layerX, event.layerY);
|
|
openContextMenu(event.layerX, event.layerY, contextMenuBg);
|
|
contextMenuEd.style.display = contextMenuSt.style.display = 'none';
|
|
rightSelection = null;
|
|
})
|
|
.autoPauseRedraw(false);
|
|
|
|
updateGraph(); |