feat(import): import bpmn shapes

We are able to import shapes based on BPMNDI.

Related to #1
This commit is contained in:
Nico Rehwaldt 2014-03-13 16:06:30 +01:00
parent 284657490e
commit 5a4d0b566a
12 changed files with 561 additions and 4 deletions

View File

@ -57,7 +57,8 @@ module.exports = function(grunt) {
options: {
alias: [
'<%= config.sources %>/main.js:bpmn',
'<%= config.sources %>/Model.js:bpmn/Model'
'<%= config.sources %>/Model.js:bpmn/Model',
'<%= config.sources %>/Diagram.js:bpmn/Diagram'
]
},
sources: {

View File

@ -1,18 +1,20 @@
<html>
<head>
<title>bpmn-js demo</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="drop-zone">Drop files here</div>
<output id="list"></output>
<div class="canvas"></div>
<div id="canvas"></div>
<script src="../bpmn.js"></script>
<script>
var bpmn = require('bpmn');
var Model = require('bpmn/Model');
var Diagram = require('bpmn/Diagram');
</script>
@ -36,6 +38,11 @@
if (err) {
console.error(err);
} else {
var diagram = new Diagram(document.getElementById('canvas'));
diagram.importDefinitions(definitions, function() {
console.log("import done!!");
});
console.log(definitions);
}
});

160
example/style.css Normal file
View File

@ -0,0 +1,160 @@
/**
* outline styles
*/
.djs-outline {
fill: none;
visibility: hidden;
}
.djs-group.hover .djs-outline,
.djs-group.selected .djs-outline {
visibility: visible;
stroke-dasharray: 2,4;
}
.djs-group.selected .djs-outline {
stroke: blue;
stroke-width: 1px;
}
.djs-group.hover .djs-outline {
stroke: red;
stroke-width: 1px;
}
.djs-group.drop-ok .djs-visual {
fill: green;
}
.djs-group.drop-not-ok .djs-visual {
fill: red;
}
/**
* drag styles
*/
.djs-dragger {
fill: white;
opacity: 0.3;
stroke: #333;
stroke-dasharray: 2,1;
}
.djs-shape.djs-dragging {
visibility: hidden;
}
/**
* hit shape styles
*/
.djs-hit {
stroke-opacity: 0.0;
stroke-width: 10px;
stroke: fuchsia;
fill: none;
}
/**
* shape / connection basic styles
*/
.djs-shape .djs-visual {
stroke: black;
stroke-width: 2px;
fill: #F3F3F3;
}
.djs-connection .djs-visual {
stroke-width: 2px;
stroke: #333;
fill: none;
}
.djs-connection .djs-bendpoint {
visibility: hidden;
}
.djs-connection:hover .djs-bendpoint {
visibility: visible;
}
.djs-connection .djs-bendpoint:hover {
stroke: #CCC;
stroke-width: 1px;
fill: yellow;
}
.djs-connection-marker {
fill: black;
stroke: none;
}
.djs-menu-vertical, .djs-menu-horizontal {
height: 35px;
background-color: #52B415;
padding-left: 10px;
}
.djs-menu-vertical > button {
display:inline-block;
margin-bottom:0;
font-weight:400;
text-align:center;
vertical-align:middle;
cursor:pointer;
background: #52B415 none;
color: white;
border: 0px #ffffff;
border-right-width: 1px;
border-bottom-style: solid;
border-left-width: 1px;
white-space:nowrap;
padding:5px 12px;
font-size:15px;
line-height:1.428571429;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
}
.djs-menu-vertical > .button:focus{
outline:thin dotted #333;
outline:5px auto -webkit-focus-ring-color;
outline-offset:-2px;
}
.djs-menu-vertical > .btn:hover,.btn:focus {
color:#707070;text-decoration:none;
}
.djs-menu-brand {
background: url("logo.png") scroll no-repeat transparent;
background-size: 35px 35px;
width: 35px;
height: 35px;
float: right;
}
/* Button icons*/
.djs-undo-button > i:before {
font-family: awesomesymbols;
content: "\f0e2";
font-style: normal;
}
.djs-redo-button > i:before {
font-family: awesomesymbols;
content: "\f01e";
font-style: normal;
}
.djs-rect-button > i {
width: 10px;
height: 10px;
border: 1px solid;
display: block;
}
/* Fonts */
@font-face {
font-family: "awesomesymbols";
src: url("../fonts/fontawesome-webfont.woff") format('woff');
font-weight: normal;
}

157
lib/BpmnTreeWalker.js Normal file
View File

@ -0,0 +1,157 @@
var _ = require('lodash');
function BpmnTraverser(visitor) {
var elementDiMap = {};
var elementGfxMap = {};
///// Helpers /////////////////////////////////
function contextual(fn, ctx) {
return function(e) {
fn(e, ctx);
};
}
function is(element, type) {
return element.__isInstanceOf(type);
}
function visit(element, di, ctx) {
// call visitor
var gfx = visitor(element, di, ctx);
// and log returned result
elementGfxMap[element.id] = gfx;
return gfx;
}
////// DI handling ////////////////////////////
function buildDiMap(definitions) {
_.forEach(definitions.diagrams, handleDiagram);
}
function registerDi(element) {
var bpmnElement = element.bpmnElement;
elementDiMap[bpmnElement.id] = element;
}
function getDi(bpmnElement) {
var id = bpmnElement.id;
return id ? elementDiMap[id] : null;
}
function handleDiagram(diagram) {
handlePlane(diagram.plane);
}
function handlePlane(plane) {
registerDi(plane);
_.forEach(plane.planeElement, handlePlaneElement);
}
function handlePlaneElement(planeElement) {
registerDi(planeElement);
}
////// Semantic handling //////////////////////
function handleDefinitions(definitions, diagram) {
buildDiMap(definitions);
// make sure we walk the correct bpmnElement
var diagrams = definitions.diagrams;
if (diagram && diagrams.indexOf(diagram) === -1) {
throw new Error('diagram not part of bpmn:Definitions');
}
if (!diagram) {
diagram = diagrams[0];
}
var rootElement = diagram.plane.bpmnElement;
if (is(rootElement, 'bpmn:Process')) {
handleProcess(rootElement);
} else
if (is(rootElement, 'bpmn:Collaboration')) {
handleCollaboration(rootElement);
} else {
throw new Error('unsupported root element for bpmndi:Diagram: ' + type.name);
}
}
function handleFlowNode(flowNode, context) {
var di = getDi(flowNode);
var childCtx = visit(flowNode, di, context);
if (is(flowNode, 'bpmn:FlowElementsContainer')) {
handleFlowElementsContainer(flowNode, childCtx);
}
}
function handleSequenceFlow(sequenceFlow, context) {
var di = getDi(sequenceFlow);
visit(sequenceFlow, di, context);
}
function handleFlowElementsContainer(container, context) {
var sequenceFlows = [];
// handle FlowNode(s)
_.forEach(container.flowElements, function(e) {
if (is(e, 'bpmn:SequenceFlow')) {
sequenceFlows.push(e);
} else
if (is(e, 'bpmn:FlowNode')) {
handleFlowNode(e, context);
} else {
throw new Error('unrecognized element: ' + e);
}
});
// handle SequenceFlows
_.forEach(sequenceFlows, contextual(handleSequenceFlow, context));
}
function handleParticipant(participant, context) {
var di = getDi(participant);
var newCtx = visit(participant, di, context);
var process = participant.processRef;
if (process) {
handleProcess(process, newCtx);
}
}
function handleProcess(process, context) {
handleFlowElementsContainer(process, context);
}
function handleCollaboration(collaboration) {
_.forEach(collaboration.participants, contextual(handleParticipant));
// TODO: handle message flows
}
///// API ////////////////////////////////
return {
handleDefinitions: handleDefinitions
};
}
module.exports = BpmnTraverser;

15
lib/Diagram.js Normal file
View File

@ -0,0 +1,15 @@
var DiagramJS = require('diagram-js');
var Importer = require('./Importer');
function Diagram(container) {
this.container = container;
}
Diagram.prototype.importDefinitions = function(definitions, done) {
this.diagram = new DiagramJS({ canvas: { container: this.container }});
Importer.importBpmnDiagram(this.diagram, definitions, done);
};
module.exports = Diagram;

23
lib/Importer.js Normal file
View File

@ -0,0 +1,23 @@
var BpmnTreeWalker = require('./BpmnTreeWalker');
function importBpmnDiagram(diagram, definitions, done) {
var canvas = diagram.resolve('canvas');
var visitor = function(element, di, parent) {
if (di.$type === 'bpmndi:BPMNShape') {
var bounds = di.bounds;
canvas.addShape({ id: element.id, type: element.$type, x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height });
}
};
var walker = new BpmnTreeWalker(visitor);
walker.handleDefinitions(definitions);
done();
}
module.exports.importBpmnDiagram = importBpmnDiagram;

View File

@ -0,0 +1,6 @@
var Diagram = require('./Diagram'),
Model = require('./Model');
module.exports.Diagram = Diagram;
module.exports.Model = Model;

View File

@ -52,7 +52,8 @@
"xsd-schema-validator": "0.0.3",
"sax": "~0.6.0",
"lodash": "~2.4.0",
"eve": "0.4.1"
"eve": "0.4.1",
"brfs": "~1.0.0"
},
"dependencies": {
"moddle": "~0.0.1",

View File

@ -23,7 +23,8 @@ module.exports = function(karma) {
// browserify configuration
browserify: {
debug: true,
watch: true
watch: true,
transform: [ 'debowerify', 'brfs' ]
}
});
};

51
test/fixtures/bpmn/collaboration.bpmn vendored Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="_pHDz0KojEeOJhIBv1RySdg" targetNamespace="http://activiti.org/bpmn">
<bpmn2:collaboration id="_Collaboration_2">
<bpmn2:participant id="_Participant_2" name="Pool" processRef="Process_1"/>
<bpmn2:participant id="Participant_1" name="Pool" processRef="Process_2"/>
</bpmn2:collaboration>
<bpmn2:process id="Process_1" isExecutable="false">
<bpmn2:laneSet id="LaneSet_1" name="Lane Set 1">
<bpmn2:lane id="Lane_1" name="Lane 1">
<bpmn2:childLaneSet xsi:type="bpmn2:tLaneSet" id="LaneSet_2">
<bpmn2:lane id="Lane_2" name="Lane 2"/>
<bpmn2:lane id="Lane_3" name="Lane 3">
<bpmn2:flowNodeRef>Task_1</bpmn2:flowNodeRef>
</bpmn2:lane>
</bpmn2:childLaneSet>
</bpmn2:lane>
</bpmn2:laneSet>
<bpmn2:task id="Task_1"/>
</bpmn2:process>
<bpmn2:process id="Process_2" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="_Collaboration_2">
<bpmndi:BPMNShape id="_BPMNShape_Participant_2" bpmnElement="_Participant_2" isHorizontal="true">
<dc:Bounds height="356.0" width="540.0" x="222.0" y="0.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Participant_3" bpmnElement="Participant_1" isHorizontal="true">
<dc:Bounds height="100.0" width="600.0" x="222.0" y="415.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Lane_2" bpmnElement="Lane_1" isHorizontal="true">
<dc:Bounds height="356.0" width="510.0" x="252.0" y="0.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Lane_3" bpmnElement="Lane_2" isHorizontal="true">
<dc:Bounds height="215.0" width="480.0" x="282.0" y="0.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Lane_4" bpmnElement="Lane_3" isHorizontal="true">
<dc:Bounds height="142.0" width="480.0" x="282.0" y="214.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_3" bpmnElement="StartEvent_1">
<dc:Bounds height="36.0" width="36.0" x="324.0" y="448.0"/>
<bpmndi:BPMNLabel>
<dc:Bounds height="0.0" width="0.0" x="342.0" y="489.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Task_3" bpmnElement="Task_1">
<dc:Bounds height="80.0" width="100.0" x="360.0" y="246.0"/>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>

View File

@ -0,0 +1,39 @@
var fs = require('fs');
var BpmnModel = require('../../../lib/Model'),
Diagram = require('../../../lib/Diagram');
var Matchers = require('../Matchers');
describe('Importer', function() {
var bpmnModel = BpmnModel.instance();
function read(xml, opts, callback) {
return BpmnModel.fromXML(xml, 'bpmn:Definitions', opts, callback);
}
beforeEach(Matchers.add);
var container;
beforeEach(function() {
container = document.createElement('div');
document.getElementsByTagName('body')[0].appendChild(container);
});
afterEach(function() {
container.parentNode.removeChild(container);
});
it('should import simple process', function(done) {
var xml = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf8');
read(xml, function(err, result) {
var diagram = new Diagram(container);
diagram.importDefinitions(result, done);
});
});
});

View File

@ -0,0 +1,96 @@
var _ = require('lodash');
var BpmnModel = require('../../../../lib/Model'),
BpmnTreeWalker = require('../../../../lib/BpmnTreeWalker');
var Helper = require('../Helper'),
Matchers = require('../../Matchers');
describe('BpmnTreeWalker', function() {
function readBpmnDiagram(file) {
return Helper.readFile('test/fixtures/bpmn/' + file);
}
function read(xml, root, opts, callback) {
return BpmnModel.fromXML(xml, root, opts, callback);
}
function readFile(file, root, opts, callback) {
return read(readBpmnDiagram(file), root, opts, callback);
}
beforeEach(Matchers.add);
var bpmnModel = BpmnModel.instance();
it('should walk simple process', function(done) {
readFile('simple.bpmn', 'bpmn:Definitions', function(err, definitions) {
if (err) {
done(err);
} else {
var drawn = [];
var visitor = function(element, di, ctx) {
var id = element.id;
drawn.push({ id: id, parent: ctx });
return id;
};
new BpmnTreeWalker(visitor).handleDefinitions(definitions);
var expectedDrawn = [
{ id: 'SubProcess_1', parent: undefined },
{ id: 'StartEvent_1', parent: 'SubProcess_1' },
{ id: 'Task_1', parent: 'SubProcess_1' },
{ id: 'SequenceFlow_1', parent: 'SubProcess_1' } ];
expect(drawn).toDeepEqual(expectedDrawn);
done();
}
});
});
it('should walk collaboration', function(done) {
readFile('collaboration.bpmn', 'bpmn:Definitions', function(err, definitions) {
if (err) {
done(err);
} else {
var drawn = [];
var visitor = function(element, di, ctx) {
var id = element.id;
drawn.push({ id: id, parent: ctx });
return id;
};
new BpmnTreeWalker(visitor).handleDefinitions(definitions);
var expectedDrawn = [
{ id : '_Participant_2', parent : undefined },
{ id : 'Task_1', parent : '_Participant_2' },
{ id : 'Participant_1', parent : undefined },
{ id : 'StartEvent_1', parent : 'Participant_1' } ];
expect(drawn).toDeepEqual(expectedDrawn);
done();
}
});
});
});