Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce CasePathResolver #142

Merged
merged 4 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ As far as possible, we maintain compatibility for some minor versions.

See the dedicated [README](packages/addons/README.md).

A live demo is available at ⏩ https://process-analytics.github.io/bv-experimental-add-ons/
A live demo is available at ⏩ https://process-analytics.github.io/bv-experimental-add-ons/.
The sources of the demo are available in the [demo](./packages/demo) folder.


## ⚒️ Development Setup
Expand Down
18 changes: 15 additions & 3 deletions packages/addons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,25 @@ Limitations
- There is no guarantee that names are unique in the BPMN source. In case that there are several matches, `BpmnElementsSearcher` returns the first matching id.


### `PathResolver`

Infer BPMN path. Currently, only infer edges/flows given a list of flow node ids.
### Available implementations for `Path Resolution`

**WARNING**: this is front-end processing. It's more efficient for this type of processing to be carried out in the backend.
Use it to bypass the limitations of the tools and algorithms provided in the backend.

The `Path Resolution` infers a BPMN path from elements known to be completed or pending.

#### `PathResolver`

As it is generic and covers general use cases, its capabilities are limited.

It only infers edges/flows given a list of flowNode/shape ids.

#### `CasePathResolver`

Provides path resolution for a single process instance/case.

It is an enhanced implementation of `PathResolver` with resolution options and returns categorized `BpmnSemantic` objects.


### `ShapeUtil`

Expand Down
103 changes: 90 additions & 13 deletions packages/addons/src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,107 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import type { BpmnElementsRegistry, ShapeBpmnSemantic } from 'bpmn-visualization';
import type { EdgeBpmnSemantic, ElementsRegistry, ShapeBpmnSemantic } from 'bpmn-visualization';

// bpmn-visualization does not filter duplicates when passing an ids several times
const filterDuplicates = (ids: string[]): string[] => [...new Set(ids)];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: IMHO this is a bug in bpmn-visualization that we should fix. It applies to all methods of ElementsRegistry taking ids or kinds.
What do you think @csouchet?
If so, I will create an issue.

Copy link
Member

@csouchet csouchet Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, the bpmn-visualization APIs should check the uniqueness of the parameter

Copy link
Member Author

@tbouffard tbouffard Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine, before merging this PR, I am going to create a dedicated issue in bpmn-visualization and another one in bv-experimental-add-ons to update the implementation when the fix is available in a new release of bpmn-visualization.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug created: process-analytics/bpmn-visualization-js#2921
Refactor task created: #150


const inferEdgeIds = (shapes: ShapeBpmnSemantic[]): string[] => {
const incomingIds: string[] = [];
const outgoingIds: string[] = [];
for (const shape of shapes) {
incomingIds.push(...shape.incomingIds);
outgoingIds.push(...shape.outgoingIds);
}
return incomingIds.filter(incomingId => outgoingIds.includes(incomingId));
};

/**
* Experimental implementation for {@link https://github.com/process-analytics/bpmn-visualization-js/issues/930}
* A general implementation for Path resolution.
*
* As it is generic and covers general use cases, its capabilities are limited.
*
* For the path resolution of single case/instance of a process, prefer {@link CasePathResolver}.
*/
export class PathResolver {
constructor(private readonly bpmnElementsRegistry: BpmnElementsRegistry) {}
constructor(private readonly elementsRegistry: ElementsRegistry) {}

/**
* Currently, if the `shapeIds` parameter contains ids related to edges, these ids are ignored and not returned as part of the visited edges.
* If the `shapeIds` parameter contains ids related to edges, these ids are ignored and not returned as part of the visited edges.
*
* @param shapeIds the ids used to compute the visited edges
*/
getVisitedEdges(shapeIds: string[]): string[] {
const incomingIds = [] as string[];
const outgoingIds = [] as string[];
const shapes = this.elementsRegistry.getModelElementsByIds(filterDuplicates(shapeIds)).filter(element => element.isShape) as ShapeBpmnSemantic[];
return inferEdgeIds(shapes);
}
}

/**
* Provides path resolution for a single process instance/case.
*
* It is an enhanced implementation of {@link PathResolver} with resolution options and returns categorized `BpmnSemantic` objects.
*/
export class CasePathResolver {
constructor(private readonly elementsRegistry: ElementsRegistry) {}

compute(input: CasePathResolverInput): CasePathResolverOutput {
const completedElements = this.elementsRegistry.getModelElementsByIds(filterDuplicates(input.completedIds));

const completedShapes = completedElements.filter(element => element.isShape) as ShapeBpmnSemantic[];
const completedEdges = completedElements.filter(element => !element.isShape) as EdgeBpmnSemantic[];

const shapes = this.bpmnElementsRegistry.getModelElementsByIds(shapeIds).filter(element => element.isShape) as ShapeBpmnSemantic[];
for (const shape of shapes) {
incomingIds.push(...shape.incomingIds);
outgoingIds.push(...shape.outgoingIds);
}
const inputElementIds = new Set(completedElements.map(element => element.id));

// find edges and remove duplicates
return [...new Set(incomingIds.filter(incomingId => outgoingIds.includes(incomingId)))];
// infer edges from shapes
const computedCompletedEdgeIds = inferEdgeIds(completedShapes).filter(id => !inputElementIds.has(id));
const computedCompletedEdges = this.elementsRegistry.getModelElementsByIds(computedCompletedEdgeIds) as EdgeBpmnSemantic[];

// infer shapes from edges
const computedCompletedShapeIds = completedEdges.flatMap(edge => [edge.sourceRefId, edge.targetRefId]).filter(id => !inputElementIds.has(id));
const computedCompletedShapes = this.elementsRegistry.getModelElementsByIds(filterDuplicates(computedCompletedShapeIds)) as ShapeBpmnSemantic[];

return {
provided: {
completed: {
shapes: completedShapes,
edges: completedEdges,
},
},
computed: {
completed: {
shapes: computedCompletedShapes,
edges: computedCompletedEdges,
},
},
};
}
}

export type CasePathResolverInput = {
/**
* The IDs of elements (flowNodes/shapes and flows/edges) that are already completed. Non-existing ids will be silently ignored.
*
* `Completed` means that the element has been fully executed or definitively cancelled (for BPM engines that support this and allow cancelled elements to be continued).
* No further user action or automation will update the element.
*/
completedIds: string[];
};

export type CasePathResolverOutput = {
/**
* The `BpmnSemantic` objects retrieved from the model that relate to the ids passed in {@link CasePathResolverInput}.
*/
provided: {
completed: {
shapes: ShapeBpmnSemantic[];
edges: EdgeBpmnSemantic[];
};
};
computed: {
completed: {
shapes: ShapeBpmnSemantic[];
edges: EdgeBpmnSemantic[];
};
};
};
40 changes: 20 additions & 20 deletions packages/addons/test/fixtures/bpmn/paths/simple.bpmn
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,41 @@
</bpmn:startEvent>
<bpmn:task id="Task_1" name="Task 1">
<bpmn:incoming>Flow_StartEvent_1_Task_1</bpmn:incoming>
<bpmn:outgoing>Flow_12pv067</bpmn:outgoing>
<bpmn:outgoing>Flow_Task_1_Gateway_1</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_StartEvent_1_Task_1" sourceRef="StartEvent_1" targetRef="Task_1" />
<bpmn:exclusiveGateway id="Gateway_1">
<bpmn:incoming>Flow_12pv067</bpmn:incoming>
<bpmn:outgoing>Flow_1qsbuis</bpmn:outgoing>
<bpmn:incoming>Flow_Task_1_Gateway_1</bpmn:incoming>
<bpmn:outgoing>Flow_Gateway_1_Task_2_1</bpmn:outgoing>
<bpmn:outgoing>Flow_Gateway_1_Task_2_2</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_12pv067" sourceRef="Task_1" targetRef="Gateway_1" />
<bpmn:sequenceFlow id="Flow_1qsbuis" sourceRef="Gateway_1" targetRef="Task_2_1" />
<bpmn:sequenceFlow id="Flow_Task_1_Gateway_1" sourceRef="Task_1" targetRef="Gateway_1" />
<bpmn:sequenceFlow id="Flow_Gateway_1_Task_2_1" sourceRef="Gateway_1" targetRef="Task_2_1" />
<bpmn:sequenceFlow id="Flow_Gateway_1_Task_2_2" sourceRef="Gateway_1" targetRef="Task_2_2" />
<bpmn:sequenceFlow id="Flow_Task_2_2_IntermediateEvent_1" sourceRef="Task_2_2" targetRef="IntermediateEvent_1" />
<bpmn:exclusiveGateway id="Gateway_2">
<bpmn:incoming>Flow_IntermediateEvent_1_Gateway_2</bpmn:incoming>
<bpmn:incoming>Flow_112r0jv</bpmn:incoming>
<bpmn:outgoing>Flow_122udrp</bpmn:outgoing>
<bpmn:incoming>Flow_Task_2_1_Gateway_2</bpmn:incoming>
<bpmn:outgoing>Flow_Gateway_2_Task_3</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_IntermediateEvent_1_Gateway_2" sourceRef="IntermediateEvent_1" targetRef="Gateway_2" />
<bpmn:sequenceFlow id="Flow_112r0jv" sourceRef="Task_2_1" targetRef="Gateway_2" />
<bpmn:sequenceFlow id="Flow_122udrp" sourceRef="Gateway_2" targetRef="Task_3" />
<bpmn:sequenceFlow id="Flow_Task_2_1_Gateway_2" sourceRef="Task_2_1" targetRef="Gateway_2" />
<bpmn:sequenceFlow id="Flow_Gateway_2_Task_3" sourceRef="Gateway_2" targetRef="Task_3" />
<bpmn:endEvent id="EndEvent_1" name="End event">
<bpmn:incoming>Flow_1q25pru</bpmn:incoming>
<bpmn:incoming>Flow_Task_3_EndEvent_1</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1q25pru" sourceRef="Task_3" targetRef="EndEvent_1" />
<bpmn:sequenceFlow id="Flow_Task_3_EndEvent_1" sourceRef="Task_3" targetRef="EndEvent_1" />
<bpmn:userTask id="Task_2_1" name="Task 2.1">
<bpmn:incoming>Flow_1qsbuis</bpmn:incoming>
<bpmn:outgoing>Flow_112r0jv</bpmn:outgoing>
<bpmn:incoming>Flow_Gateway_1_Task_2_1</bpmn:incoming>
<bpmn:outgoing>Flow_Task_2_1_Gateway_2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Task_2_2" name="Task 2.2">
<bpmn:incoming>Flow_Gateway_1_Task_2_2</bpmn:incoming>
<bpmn:outgoing>Flow_Task_2_2_IntermediateEvent_1</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:serviceTask id="Task_3" name="Task 3">
<bpmn:incoming>Flow_122udrp</bpmn:incoming>
<bpmn:outgoing>Flow_1q25pru</bpmn:outgoing>
<bpmn:incoming>Flow_Gateway_2_Task_3</bpmn:incoming>
<bpmn:outgoing>Flow_Task_3_EndEvent_1</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:intermediateCatchEvent id="IntermediateEvent_1" name="Timer intermediate event">
<bpmn:incoming>Flow_Task_2_2_IntermediateEvent_1</bpmn:incoming>
Expand Down Expand Up @@ -94,11 +94,11 @@
<di:waypoint x="209" y="120" />
<di:waypoint x="260" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_12pv067_di" bpmnElement="Flow_12pv067">
<bpmndi:BPMNEdge id="Flow_Task_1_Gateway_1_di" bpmnElement="Flow_Task_1_Gateway_1">
<di:waypoint x="360" y="120" />
<di:waypoint x="415" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1qsbuis_di" bpmnElement="Flow_1qsbuis">
<bpmndi:BPMNEdge id="Flow_Gateway_1_Task_2_1_di" bpmnElement="Flow_Gateway_1_Task_2_1">
<di:waypoint x="465" y="120" />
<di:waypoint x="520" y="120" />
</bpmndi:BPMNEdge>
Expand All @@ -116,15 +116,15 @@
<di:waypoint x="790" y="230" />
<di:waypoint x="790" y="145" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_112r0jv_di" bpmnElement="Flow_112r0jv">
<bpmndi:BPMNEdge id="Flow_Task_2_1_Gateway_2_di" bpmnElement="Flow_Task_2_1_Gateway_2">
<di:waypoint x="620" y="120" />
<di:waypoint x="765" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_122udrp_di" bpmnElement="Flow_122udrp">
<bpmndi:BPMNEdge id="Flow_Gateway_2_Task_3_di" bpmnElement="Flow_Gateway_2_Task_3">
<di:waypoint x="815" y="120" />
<di:waypoint x="870" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1q25pru_di" bpmnElement="Flow_1q25pru">
<bpmndi:BPMNEdge id="Flow_Task_3_EndEvent_1_di" bpmnElement="Flow_Task_3_EndEvent_1">
<di:waypoint x="970" y="120" />
<di:waypoint x="1032" y="120" />
</bpmndi:BPMNEdge>
Expand Down
Loading