First pass at a requirement diagram.

This commit is contained in:
Josh Sharpe
2021-02-23 23:26:37 -05:00
parent 1f523ee843
commit 55251e1024
22 changed files with 2983 additions and 1305 deletions

View File

@@ -0,0 +1,374 @@
import { line, select } from 'd3';
import dagre from 'dagre';
import graphlib from 'graphlib';
import * as configApi from '../../config';
import { log } from '../../logger';
import { configureSvgSize } from '../../utils';
import common from '../common/common';
import { parser } from './parser/requirementDiagram';
import requirementDb from './requirementDb';
import { insertLineEndings, ReqMarkers } from './requirementMarkers';
const conf = {};
let relCnt = 0;
export const setConf = function(cnf) {
if (typeof cnf === 'undefined') {
return;
}
const keys = Object.keys(cnf);
for (let i = 0; i < keys.length; i++) {
conf[keys[i]] = cnf[keys[i]];
}
};
const newRectNode = (parentNode, id) => {
return parentNode
.insert('rect', '#' + id)
.attr('class', 'req reqBox')
.attr('fill', conf.rect_fill)
.attr('fill-opacity', '100%')
.attr('stroke', conf.rect_border_color)
.attr('stroke-size', conf.rect_border_size)
.attr('x', 0)
.attr('y', 0)
.attr('width', conf.rect_min_width + 'px')
.attr('height', conf.rect_min_height + 'px');
};
const newTitleNode = (parentNode, id, txts) => {
let x = conf.rect_min_width / 2;
let title = parentNode
.append('text')
.attr('class', 'req reqLabel reqTitle')
.attr('id', id)
.attr('x', x)
.attr('y', 0)
.attr('dominant-baseline', 'hanging')
.attr(
'style',
'font-family: ' + configApi.getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px'
);
let i = 0;
txts.forEach(textStr => {
if (i == 0) {
title
.append('tspan')
.attr('text-anchor', 'middle')
.attr('x', conf.rect_min_width / 2)
.attr('dy', conf.rect_padding)
.text(textStr);
} else {
title
.append('tspan')
.attr('text-anchor', 'middle')
.attr('x', conf.rect_min_width / 2)
.attr('dy', conf.line_height * 0.75)
.text(textStr);
}
i++;
});
let yPadding = 1.5 * conf.rect_padding;
let linePadding = i * conf.line_height * 0.75;
let totalY = yPadding + linePadding;
parentNode
.append('line')
.attr('x1', '0')
.attr('x2', conf.rect_min_width)
.attr('y1', totalY)
.attr('y2', totalY)
.attr('style', `stroke: ${conf.rect_border_color}; stroke-width: 1`);
return {
titleNode: title,
y: totalY
};
};
const newBodyNode = (parentNode, id, txts, yStart) => {
let body = parentNode
.append('text')
.attr('class', 'req reqLabel')
.attr('id', id)
.attr('x', conf.rect_padding)
.attr('y', yStart)
.attr('dominant-baseline', 'hanging')
.attr(
'style',
'font-family: ' + configApi.getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px'
);
let currentRow = 0;
const charLimit = 30;
let wrappedTxts = [];
txts.forEach(textStr => {
let currentTextLen = textStr.length;
while (currentTextLen > charLimit && currentRow < 3) {
let firstPart = textStr.substring(0, charLimit);
textStr = textStr.substring(charLimit, textStr.length);
currentTextLen = textStr.length;
wrappedTxts[wrappedTxts.length] = firstPart;
currentRow++;
}
if (currentRow == 3) {
let lastStr = wrappedTxts[wrappedTxts.length - 1];
wrappedTxts[wrappedTxts.length - 1] = lastStr.substring(0, lastStr.length - 4) + '...';
} else {
wrappedTxts[wrappedTxts.length] = textStr;
}
currentRow = 0;
});
wrappedTxts.forEach(textStr => {
body
.append('tspan')
.attr('x', conf.rect_padding)
.attr('dy', conf.line_height)
.text(textStr);
});
return body;
};
const addEdgeLabel = (parentNode, svgPath, conf, txt) => {
// Find the half-way point
const len = svgPath.node().getTotalLength();
const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
// Append a text node containing the label
const labelId = 'rel' + relCnt;
relCnt++;
const labelNode = parentNode
.append('text')
.attr('class', 'er relationshipLabel')
.attr('id', labelId)
.attr('x', labelPoint.x)
.attr('y', labelPoint.y)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('style', 'font-family: ' + conf.fontFamily + '; font-size: ' + conf.fontSize + 'px')
.text(txt);
// Figure out how big the opaque 'container' rectangle needs to be
const labelBBox = labelNode.node().getBBox();
// Insert the opaque rectangle before the text label
parentNode
.insert('rect', '#' + labelId)
.attr('class', 'req reqLabelBox')
.attr('x', labelPoint.x - labelBBox.width / 2)
.attr('y', labelPoint.y - labelBBox.height / 2)
.attr('width', labelBBox.width)
.attr('height', labelBBox.height)
.attr('fill', 'white')
.attr('fill-opacity', '85%');
};
const drawRelationshipFromLayout = function(svg, rel, g, insert) {
// Find the edge relating to this relationship
const edge = g.edge(rel.src, rel.dst);
// Get a function that will generate the line path
const lineFunction = line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
// Insert the line at the right place
const svgPath = svg
.insert('path', '#' + insert)
.attr('class', 'er relationshipLine')
.attr('d', lineFunction(edge.points))
.attr('stroke', conf.rect_border_color)
.attr('fill', 'none');
if (rel.type == requirementDb.Relationships.CONTAINS) {
svgPath.attr(
'marker-start',
'url(' + common.getUrl(conf.arrowMarkerAbsolute) + '#' + rel.type + '_line_ending' + ')'
);
} else {
svgPath.attr('stroke-dasharray', '10,7');
svgPath.attr(
'marker-end',
'url(' +
common.getUrl(conf.arrowMarkerAbsolute) +
'#' +
ReqMarkers.ARROW +
'_line_ending' +
')'
);
}
addEdgeLabel(svg, svgPath, conf, `<<${rel.type}>>`);
return;
};
export const drawReqs = (reqs, graph, svgNode) => {
Object.keys(reqs).forEach(reqName => {
let req = reqs[reqName];
const groupNode = svgNode.append('g').attr('id', reqName);
const textId = 'req-' + reqName;
const rectNode = newRectNode(groupNode, textId);
let nodes = [];
let titleNodeInfo = newTitleNode(groupNode, reqName + '_title', [
`<<${req.type}>>`,
`${req.name}`
]);
nodes.push(titleNodeInfo.titleNode);
let bodyNode = newBodyNode(
groupNode,
reqName + '_body',
[
`Id: ${req.id}`,
`Text: ${req.text}`,
`Risk: ${req.risk}`,
`Verification: ${req.verifyMethod}`
],
titleNodeInfo.y
);
nodes.push(bodyNode);
const rectBBox = rectNode.node().getBBox();
// Add the entity to the graph
graph.setNode(reqName, {
width: rectBBox.width,
height: rectBBox.height,
shape: 'rect',
id: reqName
});
});
};
export const drawElements = (els, graph, svgNode) => {
Object.keys(els).forEach(elName => {
let el = els[elName];
const id = elName.replace(/\./g, '_');
const groupNode = svgNode.append('g').attr('id', id);
const textId = 'element-' + id;
const rectNode = newRectNode(groupNode, textId);
let nodes = [];
let titleNodeInfo = newTitleNode(groupNode, textId + '_title', [`<<Element>>`, `${elName}`]);
nodes.push(titleNodeInfo.titleNode);
let bodyNode = newBodyNode(
groupNode,
textId + '_body',
[`Type: ${el.type || 'Not Specified'}`, `Doc Ref: ${el.docref || 'None'}`],
titleNodeInfo.y
);
nodes.push(bodyNode);
const rectBBox = rectNode.node().getBBox();
// Add the entity to the graph
graph.setNode(id, {
width: rectBBox.width,
height: rectBBox.height,
shape: 'rect',
id: id
});
});
};
const addRelationships = (relationships, g) => {
relationships.forEach(function(r) {
g.setEdge(r.src, r.dst, { relationship: r });
});
return relationships;
};
const adjustEntities = function(svgNode, graph) {
graph.nodes().forEach(function(v) {
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
svgNode.select('#' + v);
svgNode
.select('#' + v)
.attr(
'transform',
'translate(' +
(graph.node(v).x - graph.node(v).width / 2) +
',' +
(graph.node(v).y - graph.node(v).height / 2) +
' )'
);
}
});
return;
};
export const draw = (text, id) => {
log.info('Drawing requirements!');
parser.yy = requirementDb;
parser.parse(text);
const svg = select(`[id='${id}']`);
insertLineEndings(svg, conf);
const g = new graphlib.Graph({
multigraph: false,
compound: false,
directed: true
})
.setGraph({
rankdir: conf.layoutDirection,
marginx: 20,
marginy: 20,
nodesep: 100,
edgesep: 100,
ranksep: 100
})
.setDefaultEdgeLabel(function() {
return {};
});
let requirements = requirementDb.getRequirements();
let elements = requirementDb.getElements();
let relationships = requirementDb.getRelationships();
drawReqs(requirements, g, svg);
drawElements(elements, g, svg);
addRelationships(relationships, g);
dagre.layout(g);
adjustEntities(svg, g);
relationships.forEach(function(rel) {
drawRelationshipFromLayout(svg, rel, g, id);
});
// svg.attr('height', '500px');
const padding = conf.rect_padding;
const svgBounds = svg.node().getBBox();
const width = svgBounds.width + padding * 2;
const height = svgBounds.height + padding * 2;
configureSvgSize(svg, height, width, conf.useMaxWidth);
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
};
export default {
setConf,
draw
};