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

[REFACTOR] Store part of the BpmnModel into memory for searches #1115

Merged
merged 10 commits into from
Mar 3, 2021
3 changes: 2 additions & 1 deletion src/api/public/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export { BpmnVisualization };
export { GlobalOptions, NavigationConfiguration, FitOptions, FitType, LoadOptions, ZoomConfiguration } from '../../component/options';

// Interaction
export { BpmnElementsRegistry, BpmnSemantic, BpmnElement } from '../../component/registry';
export { BpmnElement, BpmnElementsRegistry, BpmnSemantic } from '../../component/registry';
export * from '../../model/bpmn/internal/api';
8 changes: 6 additions & 2 deletions src/component/BpmnVisualization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { BpmnMxGraph } from './mxgraph/BpmnMxGraph';
import { FitOptions, GlobalOptions, LoadOptions } from './options';
import { BpmnElementsRegistry } from './registry';
import { newBpmnElementsRegistry } from './registry/bpmn-elements-registry';
import { BpmnModelRegistry } from './registry/bpmn-model-registry';
import { htmlElement } from './helpers/dom-utils';

/**
Expand All @@ -32,19 +33,22 @@ export default class BpmnVisualization {
* @experimental subject to change, feedback welcome
*/
readonly bpmnElementsRegistry: BpmnElementsRegistry;
private readonly _bpmnModelRegistry: BpmnModelRegistry;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a big fan of underscore for private fields. IMHO it is enough to have linting and modifiers.

If we decide to go with that convention, we need to perhaps do the refactoring to convert other private fields to follow this approach.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, as decided with @csouchet, I will prepare a PR to make everything consistent.


constructor(options: GlobalOptions) {
// mxgraph configuration
const configurator = new MxGraphConfigurator(htmlElement(options?.container));
this.graph = configurator.configure(options);
// other configurations
this.bpmnElementsRegistry = newBpmnElementsRegistry(this.graph);
this._bpmnModelRegistry = new BpmnModelRegistry();
this.bpmnElementsRegistry = newBpmnElementsRegistry(this._bpmnModelRegistry, this.graph);
}

public load(xml: string, options?: LoadOptions): void {
try {
const bpmnModel = newBpmnParser().parse(xml);
newMxGraphRenderer(this.graph).render(bpmnModel, options);
const renderedModel = this._bpmnModelRegistry.computeRenderedModel(bpmnModel);
newMxGraphRenderer(this.graph).render(renderedModel, options);
} catch (e) {
// TODO error handling
window.alert(`Cannot load bpmn diagram: ${e.message}`);
Expand Down
59 changes: 11 additions & 48 deletions src/component/mxgraph/MxGraphRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,41 @@
*/
import Shape from '../../model/bpmn/internal/shape/Shape';
import Edge from '../../model/bpmn/internal/edge/Edge';
import BpmnModel from '../../model/bpmn/internal/BpmnModel';
import ShapeBpmnElement, { ShapeBpmnSubProcess } from '../../model/bpmn/internal/shape/ShapeBpmnElement';
import ShapeBpmnElement from '../../model/bpmn/internal/shape/ShapeBpmnElement';
import Waypoint from '../../model/bpmn/internal/edge/Waypoint';
import Bounds from '../../model/bpmn/internal/Bounds';
import ShapeUtil from '../../model/bpmn/internal/shape/ShapeUtil';
import CoordinatesTranslator from './renderer/CoordinatesTranslator';
import StyleConfigurator from './config/StyleConfigurator';
import { MessageFlow } from '../../model/bpmn/internal/edge/Flow';
import { MessageVisibleKind } from '../../model/bpmn/internal/edge/MessageVisibleKind';
import { ShapeBpmnMarkerKind } from '../../model/bpmn/internal/shape';
import { BpmnMxGraph } from './BpmnMxGraph';
import { LoadOptions } from '../options';
import { RenderedModel } from '../registry/bpmn-model-registry';
import { mxgraph } from './initializer';
import { mxCell } from 'mxgraph'; // for types

export default class MxGraphRenderer {
constructor(readonly graph: BpmnMxGraph, readonly coordinatesTranslator: CoordinatesTranslator, readonly styleConfigurator: StyleConfigurator) {}

public render(bpmnModel: BpmnModel, loadOptions?: LoadOptions): void {
this.insertShapesAndEdges(bpmnModel);
public render(renderedModel: RenderedModel, loadOptions?: LoadOptions): void {
this.insertShapesAndEdges(renderedModel);
this.graph.customFit(loadOptions?.fit);
}

private insertShapesAndEdges(bpmnModel: BpmnModel): void {
const displayedModel = toDisplayedModel(bpmnModel);

private insertShapesAndEdges({ pools, lanes, subprocesses, otherFlowNodes, boundaryEvents, edges }: RenderedModel): void {
const model = this.graph.getModel();
model.clear(); // ensure to remove manual changes or already loaded graphs
model.beginUpdate();
try {
this.insertShapes(displayedModel.pools);
this.insertShapes(displayedModel.lanes);
this.insertShapes(displayedModel.subprocesses);
this.insertShapes(displayedModel.otherFlowNodes);
this.insertShapes(pools);
this.insertShapes(lanes);
this.insertShapes(subprocesses);
this.insertShapes(otherFlowNodes);
// last shape as the boundary event parent must be in the model (subprocess or activity)
this.insertShapes(displayedModel.boundaryEvents);
this.insertShapes(boundaryEvents);
// at last as edge source and target must be present in the model prior insertion, otherwise they are not rendered
this.insertEdges(displayedModel.edges);
this.insertEdges(edges);
} finally {
model.endUpdate();
}
Expand Down Expand Up @@ -163,37 +160,3 @@ export default class MxGraphRenderer {
export function newMxGraphRenderer(graph: BpmnMxGraph): MxGraphRenderer {
return new MxGraphRenderer(graph, new CoordinatesTranslator(graph), new StyleConfigurator(graph));
}

function toDisplayedModel(bpmnModel: BpmnModel): DisplayedModel {
const collapsedSubProcessIds: string[] = bpmnModel.flowNodes
.filter(shape => {
const bpmnElement = shape.bpmnElement;
return ShapeUtil.isSubProcess(bpmnElement?.kind) && (bpmnElement as ShapeBpmnSubProcess)?.markers.includes(ShapeBpmnMarkerKind.EXPAND);
})
.map(shape => shape.bpmnElement?.id);

const subprocesses: Shape[] = [];
const boundaryEvents: Shape[] = [];
const otherFlowNodes: Shape[] = [];
bpmnModel.flowNodes.forEach(shape => {
const kind = shape.bpmnElement?.kind;
if (ShapeUtil.isSubProcess(kind)) {
subprocesses.push(shape);
} else if (ShapeUtil.isBoundaryEvent(kind)) {
boundaryEvents.push(shape);
} else if (!collapsedSubProcessIds.includes(shape.bpmnElement?.parentId)) {
otherFlowNodes.push(shape);
}
});

return { boundaryEvents: boundaryEvents, edges: bpmnModel.edges, lanes: bpmnModel.lanes, otherFlowNodes: otherFlowNodes, pools: bpmnModel.pools, subprocesses: subprocesses };
}

interface DisplayedModel {
edges: Edge[];
boundaryEvents: Shape[];
otherFlowNodes: Shape[];
lanes: Shape[];
pools: Shape[];
subprocesses: Shape[];
}
2 changes: 1 addition & 1 deletion src/component/mxgraph/config/StyleConfigurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export default class StyleConfigurator {
}

computeStyle(bpmnCell: Shape | Edge, labelBounds: Bounds): string {
const styles: string[] = [bpmnCell.bpmnElement?.kind as string];
const styles: string[] = [bpmnCell.bpmnElement.kind as string];

let shapeStyleValues;
if (bpmnCell instanceof Shape) {
Expand Down
52 changes: 6 additions & 46 deletions src/component/registry/bpmn-elements-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,16 @@
*/
import { ensureIsArray } from '../helpers/array-utils';
import { BpmnMxGraph } from '../mxgraph/BpmnMxGraph';
import { computeBpmnBaseClassName, extractBpmnKindFromStyle } from '../mxgraph/style-helper';
import { FlowKind } from '../../model/bpmn/internal/edge/FlowKind';
import { ShapeBpmnElementKind } from '../../model/bpmn/internal/shape';
import { computeBpmnBaseClassName } from '../mxgraph/style-helper';
import { CssRegistry } from './css-registry';
import MxGraphCellUpdater from '../mxgraph/MxGraphCellUpdater';
import { BpmnQuerySelectors } from './query-selectors';
import { BpmnElement } from './types';
import { BpmnModelRegistry } from './bpmn-model-registry';
import { BpmnElementKind } from '../../model/bpmn/internal/api';

export function newBpmnElementsRegistry(graph: BpmnMxGraph): BpmnElementsRegistry {
return new BpmnElementsRegistry(
new BpmnModelRegistry(graph),
new HtmlElementRegistry(new BpmnQuerySelectors(graph.container?.id)),
new CssRegistry(),
new MxGraphCellUpdater(graph),
);
export function newBpmnElementsRegistry(bpmnModelRegistry: BpmnModelRegistry, graph: BpmnMxGraph): BpmnElementsRegistry {
return new BpmnElementsRegistry(bpmnModelRegistry, new HtmlElementRegistry(new BpmnQuerySelectors(graph.container?.id)), new CssRegistry(), new MxGraphCellUpdater(graph));
}

/**
Expand Down Expand Up @@ -183,42 +179,6 @@ export class BpmnElementsRegistry {
}
}

export type BpmnElementKind = FlowKind | ShapeBpmnElementKind;

/**
* @category Interaction
*/
export interface BpmnSemantic {
id: string;
name: string;
/** `true` when relates to a BPMN Shape, `false` when relates to a BPMN Edge. */
isShape: boolean;
// TODO use a more 'type oriented' BpmnElementKind (as part of #929)
kind: string;
}

/**
* @category Interaction
*/
export interface BpmnElement {
bpmnSemantic: BpmnSemantic;
htmlElement: HTMLElement;
}

// for now, we don't store the BpmnModel so we can use it, information are only available in the mxgraph model
class BpmnModelRegistry {
constructor(private graph: BpmnMxGraph) {}

getBpmnSemantic(bpmnElementId: string): BpmnSemantic | undefined {
const mxCell = this.graph.getModel().getCell(bpmnElementId);
if (mxCell == null) {
return undefined;
}

return { id: bpmnElementId, name: mxCell.value, isShape: mxCell.isVertex(), kind: extractBpmnKindFromStyle(mxCell) };
}
}

class HtmlElementRegistry {
constructor(private selectors: BpmnQuerySelectors) {}

Expand Down
92 changes: 92 additions & 0 deletions src/component/registry/bpmn-model-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Copyright 2021 Bonitasoft S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import BpmnModel from '../../model/bpmn/internal/BpmnModel';
import Shape from '../../model/bpmn/internal/shape/Shape';
import Edge from '../../model/bpmn/internal/edge/Edge';
import { BpmnSemantic } from './types';
import ShapeUtil from '../../model/bpmn/internal/shape/ShapeUtil';
import ShapeBpmnElement, { ShapeBpmnSubProcess } from '../../model/bpmn/internal/shape/ShapeBpmnElement';
import { ShapeBpmnMarkerKind } from '../../model/bpmn/internal/shape';

export class BpmnModelRegistry {
private searchableModel: SearchableModel;

computeRenderedModel(bpmnModel: BpmnModel): RenderedModel {
Copy link
Member

Choose a reason for hiding this comment

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

👍🏼

this.searchableModel = new SearchableModel(bpmnModel);
return toRenderedModel(bpmnModel);
}

getBpmnSemantic(bpmnElementId: string): BpmnSemantic | undefined {
const element = this.searchableModel.elementById(bpmnElementId);
if (!element) {
return undefined;
}
const bpmnElement = element.bpmnElement;
const isShape = bpmnElement instanceof ShapeBpmnElement;
return { id: bpmnElementId, name: bpmnElement.name, isShape: isShape, kind: bpmnElement.kind };
}
}

function toRenderedModel(bpmnModel: BpmnModel): RenderedModel {
Copy link
Member Author

Choose a reason for hiding this comment

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

⚠️ Sonar suggests that this function is not fully covered by unit and integration tests
We should at least model tests for all use cases.

image

Copy link
Member Author

Choose a reason for hiding this comment

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

This is managed by the removal of the 'safe navigation operator' ?. in 7dd96bf

const collapsedSubProcessIds: string[] = bpmnModel.flowNodes
.filter(shape => {
const bpmnElement = shape.bpmnElement;
return ShapeUtil.isSubProcess(bpmnElement.kind) && (bpmnElement as ShapeBpmnSubProcess).markers.includes(ShapeBpmnMarkerKind.EXPAND);
})
.map(shape => shape.bpmnElement.id);

const subprocesses: Shape[] = [];
const boundaryEvents: Shape[] = [];
const otherFlowNodes: Shape[] = [];
bpmnModel.flowNodes.forEach(shape => {
const kind = shape.bpmnElement.kind;
if (ShapeUtil.isSubProcess(kind)) {
subprocesses.push(shape);
} else if (ShapeUtil.isBoundaryEvent(kind)) {
boundaryEvents.push(shape);
} else if (!collapsedSubProcessIds.includes(shape.bpmnElement.parentId)) {
otherFlowNodes.push(shape);
}
});

return { boundaryEvents: boundaryEvents, edges: bpmnModel.edges, lanes: bpmnModel.lanes, otherFlowNodes: otherFlowNodes, pools: bpmnModel.pools, subprocesses: subprocesses };
}

export interface RenderedModel {
edges: Edge[];
boundaryEvents: Shape[];
otherFlowNodes: Shape[];
lanes: Shape[];
pools: Shape[];
subprocesses: Shape[];
}

class SearchableModel {
private elements: Map<string, Shape | Edge> = new Map();

constructor(bpmnModel: BpmnModel) {
([] as Array<Edge | Shape>)
.concat(bpmnModel.pools, bpmnModel.lanes, bpmnModel.flowNodes, bpmnModel.edges)
// use the bpmn element id and not the bpmn shape id
.forEach(e => {
this.elements.set(e.bpmnElement.id, e);
});
}

elementById(id: string): Shape | Edge | undefined {
return this.elements.get(id);
}
}
3 changes: 2 additions & 1 deletion src/component/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
* limitations under the License.
*/

export { BpmnElementsRegistry, BpmnElement, BpmnSemantic } from './bpmn-elements-registry';
export { BpmnElementsRegistry } from './bpmn-elements-registry';
export * from './types';
36 changes: 36 additions & 0 deletions src/component/registry/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright 2021 Bonitasoft S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { BpmnElementKind } from '../../model/bpmn/internal/api';

/**
* @category Interaction
*/
export interface BpmnSemantic {
id: string;
name: string;
/** `true` when relates to a BPMN Shape, `false` when relates to a BPMN Edge. */
isShape: boolean;
kind: BpmnElementKind;
}

/**
* @category Interaction
*/
export interface BpmnElement {
bpmnSemantic: BpmnSemantic;
htmlElement: HTMLElement;
}
3 changes: 2 additions & 1 deletion src/demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import BpmnVisualization from '../component/BpmnVisualization';
import { GlobalOptions, FitOptions, FitType, LoadOptions } from '../component/options';
import { log, logStartup } from './helper';
import { DropFileUserInterface } from './component/DropFileUserInterface';
import { BpmnElement, BpmnElementKind } from '../component/registry/bpmn-elements-registry';
import { BpmnElement } from '../component/registry';
import { BpmnElementKind } from '../model/bpmn/internal/api';

export * from './helper';

Expand Down
Loading