From 3747e997af8edab31bf3c3face6414b89a3495be Mon Sep 17 00:00:00 2001 From: Linus Pfoch Date: Sun, 6 Aug 2023 14:08:56 +0200 Subject: [PATCH 1/7] Added process deriation from SAP BOM data to BPMN into helper modules --- src/helper-modules/process-derivation/LICENSE | 9 + .../process-derivation/README.md | 26 + .../__tests__/data/BOM.xlsx | Bin 0 -> 18577 bytes .../__tests__/data/testdata.json | 119 + .../__tests__/derivation.js | 53 + .../process-derivation/index.js | 5 + .../process-derivation/package.json | 17 + .../process-derivation/src/constants.js | 112 + .../src/graphicProcessBuilder.js | 141 + .../process-derivation/src/main.js | 175 + .../src/processUtilities.js | 255 + .../src/semanticProcessBuilder.js | 328 + .../test_with_web_app/.gitignore | 23 + .../test_with_web_app/README.md | 42 + .../test_with_web_app/babel.config.js | 3 + .../test_with_web_app/jsconfig.json | 12 + .../test_with_web_app/package.json | 56 + .../test_with_web_app/public/favicon.ico | Bin 0 -> 4286 bytes .../test_with_web_app/public/index.html | 28 + .../test_with_web_app/src/App.vue | 72 + .../test_with_web_app/src/assets/logo.png | Bin 0 -> 6849 bytes .../test_with_web_app/src/assets/logo.svg | 1 + .../src/components/ConfigurationAndImport.vue | 306 + .../src/components/ProcessPreview.vue | 72 + .../test_with_web_app/src/main.js | 10 + .../test_with_web_app/src/plugins/vuetify.js | 6 + .../test_with_web_app/vue.config.js | 4 + .../test_with_web_app/yarn.lock | 6483 +++++++++++++++++ yarn.lock | 60 +- 29 files changed, 8417 insertions(+), 1 deletion(-) create mode 100644 src/helper-modules/process-derivation/LICENSE create mode 100644 src/helper-modules/process-derivation/README.md create mode 100644 src/helper-modules/process-derivation/__tests__/data/BOM.xlsx create mode 100644 src/helper-modules/process-derivation/__tests__/data/testdata.json create mode 100644 src/helper-modules/process-derivation/__tests__/derivation.js create mode 100644 src/helper-modules/process-derivation/index.js create mode 100644 src/helper-modules/process-derivation/package.json create mode 100644 src/helper-modules/process-derivation/src/constants.js create mode 100644 src/helper-modules/process-derivation/src/graphicProcessBuilder.js create mode 100644 src/helper-modules/process-derivation/src/main.js create mode 100644 src/helper-modules/process-derivation/src/processUtilities.js create mode 100644 src/helper-modules/process-derivation/src/semanticProcessBuilder.js create mode 100644 src/helper-modules/process-derivation/test_with_web_app/.gitignore create mode 100644 src/helper-modules/process-derivation/test_with_web_app/README.md create mode 100644 src/helper-modules/process-derivation/test_with_web_app/babel.config.js create mode 100644 src/helper-modules/process-derivation/test_with_web_app/jsconfig.json create mode 100644 src/helper-modules/process-derivation/test_with_web_app/package.json create mode 100644 src/helper-modules/process-derivation/test_with_web_app/public/favicon.ico create mode 100644 src/helper-modules/process-derivation/test_with_web_app/public/index.html create mode 100644 src/helper-modules/process-derivation/test_with_web_app/src/App.vue create mode 100644 src/helper-modules/process-derivation/test_with_web_app/src/assets/logo.png create mode 100644 src/helper-modules/process-derivation/test_with_web_app/src/assets/logo.svg create mode 100644 src/helper-modules/process-derivation/test_with_web_app/src/components/ConfigurationAndImport.vue create mode 100644 src/helper-modules/process-derivation/test_with_web_app/src/components/ProcessPreview.vue create mode 100644 src/helper-modules/process-derivation/test_with_web_app/src/main.js create mode 100644 src/helper-modules/process-derivation/test_with_web_app/src/plugins/vuetify.js create mode 100644 src/helper-modules/process-derivation/test_with_web_app/vue.config.js create mode 100644 src/helper-modules/process-derivation/test_with_web_app/yarn.lock diff --git a/src/helper-modules/process-derivation/LICENSE b/src/helper-modules/process-derivation/LICENSE new file mode 100644 index 000000000..391b1f57a --- /dev/null +++ b/src/helper-modules/process-derivation/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2017-present PROCEED Project Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/helper-modules/process-derivation/README.md b/src/helper-modules/process-derivation/README.md new file mode 100644 index 000000000..e04ca4f1f --- /dev/null +++ b/src/helper-modules/process-derivation/README.md @@ -0,0 +1,26 @@ +# Process derivation + +Module that allows the derivation of BPMN processes from SAP BOM data + +## About this module + +Use src/main.js to derive processes from SAP BOM data. +It provides two functions to derive processes from an Excel file (binary) or a JSON array. +The required file format is documented in JSDOC. + +The folder "test_with_web_app" contains a test web application that allows uploading an Excel spreadsheet and mapping the spreadsheet to the expected data structure. It uses the src/main.js for process derivation and can dispplay the resulting process. + +## Usage + +Call the deriveProcessFromData() or deriveProcessFromExcel() function in main.js with the respective parameters. For more details, see also JSDOC and the provided test cases. +A BPMN XML String is returned. + +### deriveProcessFromExcel + +You can use one of the following exampels to read a Excel file as binary data to call the derivation with: + +FileReader.readAsBinaryString(file).onload((e)=> e.target.result) + +OR + +fs.readFileSync('/BOM.xlsx', 'binary'); diff --git a/src/helper-modules/process-derivation/__tests__/data/BOM.xlsx b/src/helper-modules/process-derivation/__tests__/data/BOM.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2e46c60cf37ce1daab895c853a8d818881fa31af GIT binary patch literal 18577 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 o`Pk?Vc&#pGmYrqor^Z|1&lyiitiqchNw?X&lcF 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 { + beforeEach(() => {}); + + test('test derivation', async () => { + let process = await derivation.deriveProcessFromData( + derivationData.processSettings, + derivationData.processData + ); + + expect(typeof process).toBe('string'); + expect(process).toContain(''); + expect(process).toContain(' { + let BOM = fs.readFileSync(path.join(__dirname, '/data/BOM.xlsx'), 'binary'); + let process = await derivation.deriveProcessFromExcel(derivationData.processSettings, BOM); + + expect(typeof process).toBe('string'); + expect(process).toContain(''); + expect(process).toContain(' element.id.includes('StartEvent_')); + let startGateway = Utils.getSuccessors(process.startEvent, process)[0]; + let yCenterLine = 0; + + // distinguish linear and parallel processes + if (startGateway.id.includes('Gateway_')) { + process.plane.sourcedElements = Utils.getSuccessors(startGateway, process); + + yCenterLine = process.plane.sourcedElements.length / 2 - 0.5; + Utils.createElementShape(data.model, process.startEvent, 0, yCenterLine, process.plane); + Utils.createElementShape(data.model, startGateway, 1, yCenterLine, process.plane); + + // draw all sourced tasks to let ther tasks align to them correctly (yPos fixed) + process.plane.sourcedElements.forEach((successor, index) => { + Utils.createElementShape(data.model, successor, 2, index, process.plane); + }); + } + // no gatway, just linear process + else { + process.plane.sourcedElements = [startGateway]; + Utils.createElementShape(data.model, process.startEvent, 1, 0, process.plane); + Utils.createElementShape(data.model, startGateway, 2, 0, process.plane); + } + + // calculate BOM depth to correctly place elements on x-axis + let endEvent = flowElements.find((element) => element.id.includes('EndEvent_')); + let bomDepthCalculated = Utils.calculatedBomDepth(process.startEvent, endEvent, process); + + // CALL RECURSIVE RENDER FUNCTION to draw BPMN-Elements + createNextProcessLevel(data, process, endEvent, bomDepthCalculated * 2 + 2); + + try { + // draw process flows + flowElements + .filter((flowElement) => flowElement.id.includes('SequenceFlow_')) + .forEach((flow) => Utils.createFlowShape(data.model, flow, process.plane)); + } catch (error) { + console.log('ERROR rendering the SequenceFlows. There might be a Element misspositioned'); + console.log(error); + } + + // draw annotaions & association + flowElements + .filter((flowElement) => flowElement.id.includes('TextAnnotation_')) + .forEach((annotation) => { + let association = flowElements.find((flowElement) => flowElement.targetRef === annotation); + let task = association.sourceRef; + let taskShape = process.plane + .get('planeElement') + .find((element) => element.bpmnElement === task); + let bounds = taskShape.bounds; + + Utils.createElementShape( + data.model, + annotation, + bounds.x / Constants.VisualElementOffsetX + 3, + bounds.y / Constants.VisualElementOffsetY + 1, + process.plane + ); + Utils.createFlowShape(data.model, association, process.plane); + }); +} + +// recursively draw the BPMN-Elements to the plane. +function createNextProcessLevel(data, process, currentElement, x) { + let predecessors = Utils.getPredecessors(currentElement, process); + let elementAlignment = data.processSettings.elementAlignment; + + // when currentElement is a Gateway, first draw predecessor-element and then align the gateway to them + if (predecessors.length > 1) { + // Gateway + let yVals = []; + + // draw the predecessor-element + predecessors.forEach((pred) => { + yVals.push(createNextProcessLevel(data, process, pred, x)); + }); + + // draw gateway centered to the tasks and return its position, for other elements to align to them + let yGateway = yVals[Math.floor(predecessors.length / 2)]; + switch (elementAlignment) { + case 'TOP': + yGateway = yVals[0]; + break; + case 'DOWN': // || "Waterfall" + yGateway = yVals[yVals.length - 1]; + break; + case 'Steps': + yGateway = yVals.reduce((a, b) => a + b) / yVals.length; + break; + default: + yGateway = yVals[Math.floor(predecessors.length / 2)]; + } + Utils.createElementShape(data.model, currentElement, x - 1, yGateway, process.plane); + return yGateway; + } + + // when currentElement is a Activity + else if (predecessors.length === 1) { + // on subprocess, render subprocess also as seperate diagram + if (currentElement.id.includes('SubProcess_')) build(data, currentElement); + + // on end of dependency chain, return yPos of sourced task, for other elements to align to them + if (process.plane.sourcedElements.includes(currentElement)) { + return process.plane + .get('planeElement') + .find((element) => element.id.includes(currentElement.id)).yPos; + } + // draw current Activity and return its position, for other elements to align to them + x -= 2; + let y = createNextProcessLevel(data, process, predecessors[0], x); + Utils.createElementShape(data.model, currentElement, x, y, process.plane); + return y; + } +} + +module.exports = { + build, +}; diff --git a/src/helper-modules/process-derivation/src/main.js b/src/helper-modules/process-derivation/src/main.js new file mode 100644 index 000000000..32b33585b --- /dev/null +++ b/src/helper-modules/process-derivation/src/main.js @@ -0,0 +1,175 @@ +// import modules +const XLSX = require('xlsx'); +const semanticBuilder = require('./semanticProcessBuilder.js'); +const graphicBuilder = require('./graphicProcessBuilder.js'); +const Constants = require('./constants.js'); + +/** + * Function that derives a process from given data and settings + * + * @param {Object} processSettings - Configuration object to specify derivation settings + * @param {string} processSettings.id - Id of the process to be derived (pass through) + * @param {string} processSettings.name - Name of the process to be derived (pass through) + * @param {string} processSettings.processType - Visibility of process: Public, Private, None (pass through) + * @param {boolean} processSettings.isExecutable - Executability of the process (pass through) + * @param {string} processSettings.taskType - Default type of derived tasks: Task, ManualTask, UserTask (or any other valid Task type in BPMN) + * @param {boolean} processSettings.concurrentTasks - Indication if a work step can start without all requiered materials ready + * @param {boolean} processSettings.concurrentIntermediateEvent - Indication if concurrent work steps should end with an intermediate + * @param {boolean} processSettings.useSubProcesses - Indication if subprocesses should be used + * @param {string[]} processSettings.subProcessesMaterials - Materilas that should be derived as subprocesses: only valid material-ids (H-699620) + * @param {string[]} processSettings.textAnnotationsContents - Information that should be included into the annotations: ["Material", "Workplace", "Duration", "Worksteps"] + * @param {string[]} processSettings.stopOnMaterialTypes - Indication the the derivation shoud not include the manufacotring process of sourced materials of the given type (e.g. RAW, HIBE) + * @param {string[]} processSettings.elementAlignment - How elements should be aligned behind gatways ('Aligned', 'TOP', 'DOWN', 'Steps') + * + * @param {Object} processData - Dynamic data object, containing a SINGLE BOM and ANY NUMBER OF allocation and operation fory any material within the BOM + * @param {Object[]} processData.XXXXX_BOM - BOM for the material XXXXX + * @param {string} processData.XXXXX_BOM.material - Material identifier: technical id of the material in the material master data + * @param {string} processData.XXXXX_BOM.materialName - Material Name: Human readable name of the material + * @param {number} processData.XXXXX_BOM.layer - Depth of the materila in the BOM + * @param {string} processData.XXXXX_BOM.materialType - Categroy of material in terms of production state and usage (ROH, HALB, FERT) + * @param {number} processData.XXXXX_BOM.quantity - Quantity of the component required + * @param {string} processData.XXXXX_BOM.unit - Measure for the component quantity + * @param {string} [processData.XXXXX_BOM.category] - Defines how an entry should be interpreted, e.g. as separately registered material (L) or as description for material (T) + * + * @param {Object[]} processData.YYYYY_Operations - Operations for the material YYYYY + * @param {number} processData.YYYYY_Operations.operation - Unique id of operation + * @param {string} processData.YYYYY_Operations.workCenter - Workplace a action is to be performed on + * @param {string} processData.YYYYY_Operations.description - Action that is to be performed + * @param {number} processData.YYYYY_Operations.quantity - Quantity of the component required + * @param {string} processData.YYYYY_Operations.unit - Measure for the component quantity + * + * @param {Object[]} processData.ZZZZZ_Allocations - Allocations for the material ZZZZZ + * @param {number} processData.ZZZZZ_Allocations.operation - Reference to a operation (unique id of operation) + * @param {string} processData.ZZZZZ_Allocations.component - Reference to a material (material id / XXXXX_BOM.material) + * @param {number} processData.ZZZZZ_Allocations.quantity - Quantity of the component required + * @param {string} processData.ZZZZZ_Allocations.unit - Measure for the component quantity + * @param {number} [processData.ZZZZZ_Allocations.itemNr] - Unique id of allocation + * @param {string} [processData.ZZZZZ_Allocations.itemCategory] - Defines how an entry should be interpreted, e.g. as separately registered material (L) or as description for material (T) + * @param {string} [processData.ZZZZZ_Allocations.materialDescription] - Material Name: Human readable name of the material (XXXXX_BOM.materialName) + * + * @returns {string} BPMN XML + */ + +async function deriveProcessFromData(processSettings, processData) { + // create template + let data = semanticBuilder.build(processSettings, processData); + graphicBuilder.build(data); + + // create callback Promise + let promise = new Promise((resolve, reject) => { + data.model.toXML(data.definitions, { format: true }, (saveErr, xml) => { + if (saveErr) { + reject(saveErr); + } + resolve(xml); + }); + }); + + return await promise; +} + +/** + * Function that derives a process from given settings and a Excel file with optional mappings + * + * @param {Object} processSettings - Configuration object to specify derivation settings + * @param {string} processSettings.id - Id of the process to be derived (pass through) + * @param {string} processSettings.name - Name of the process to be derived (pass through) + * @param {string} processSettings.processType - Visibility of process: Public, Private, None (pass through) + * @param {boolean} processSettings.isExecutable - Executability of the process (pass through) + * @param {string} processSettings.taskType - Default type of derived tasks: Task, ManualTask, UserTask (or any other valid Task type in BPMN) + * @param {boolean} processSettings.concurrentTasks - Indication if a work step can start without all requiered materials ready + * @param {boolean} processSettings.concurrentIntermediateEvent - Indication if concurrent work steps should end with an intermediate + * @param {boolean} processSettings.useSubProcesses - Indication if subprocesses should be used + * @param {string[]} processSettings.subProcessesMaterials - Materilas that should be derived as subprocesses: only valid material-ids (H-699620) + * @param {string[]} processSettings.textAnnotationsContents - Information that should be included into the annotations: ["Material", "Workplace", "Duration", "Worksteps"] + * @param {string[]} processSettings.stopOnMaterialTypes - Indication the the derivation shoud not include the manufacotring process of sourced materials of the given type (e.g. RAW, HIBE) + * @param {string[]} processSettings.elementAlignment - How elements should be aligned behind gatways ('Aligned', 'TOP', 'DOWN', 'Steps') + * + * @param {Buffer} excelBinary - Binary of a excel file containing BOM data. Use FileReader.readAsBinaryString(file).onload((e)=> e.target.result) as parameter OR fs.readFileSync('./__tests__/data/BOM.xlsx', 'binary'); + * + * @param {Object} colnameMappings - Mapping of column names of (several) Excel file sheet(s) + * @param {Object} columnMappings.XXXXX_BOM - Mapping of column names of a singel Sheet within the Excel file + * @param {string} columnMappings.XXXXX_BOM.material - Mapping within the Excel file for the internal name "material". for all internal names see jsdoc of deriveProcessFromData() + * @param {string} columnMappings.XXXXX_BOM.quantity - Mapping within the Excel file for the internal name "quantity". for all internal names see jsdoc of deriveProcessFromData() + * columnMappings are optinal. If no columnMappings are provided, the mapping is condcted automatically + * + * @returns {string} BPMN XML + */ +async function deriveProcessFromExcel(processSettings, excelBinary, colnameMappings) { + let processData; + let workbook = XLSX.read(excelBinary, { type: 'binary' }); + + let sheets = {}; + workbook.SheetNames.forEach((name) => { + sheets[name] = XLSX.utils.sheet_to_json(workbook.Sheets[name]); + }); + + if (colnameMappings === undefined) colnameMappings = finfColumnNameMappings(sheets); + + processData = mapColumns(sheets, colnameMappings); + + return await deriveProcessFromData(processSettings, processData); +} + +function mapColumns(sheets, columnMappings) { + let sheetNames = Object.keys(sheets); + let mappedSheets = {}; + + sheetNames.forEach((sheetName) => { + let mappedRows = []; + let sheetRows = sheets[sheetName]; + let mappings = columnMappings[sheetName]; + + sheetRows.forEach((row) => { + let mappedEntrys = {}; + Object.entries(mappings).forEach(([internalName, externalName]) => { + mappedEntrys[internalName] = row[externalName]; + }); + mappedRows.push(mappedEntrys); + }); + mappedSheets[sheetName] = mappedRows; + }); + return mappedSheets; +} + +function finfColumnNameMappings(sheets) { + let colnameMappings = {}; + let sheetNames = Object.keys(sheets); + + sheetNames.forEach((sheetName) => { + colnameMappings[sheetName] = {}; + let expectedColNames = {}; + + if (sheetName.includes('_BOM')) expectedColNames = Constants.ExpectedColNames.BOM; + else if (sheetName.includes('_Operations')) + expectedColNames = Constants.ExpectedColNames.operations; + else if (sheetName.includes('_Allocations')) + expectedColNames = Constants.ExpectedColNames.allocations; + + let providedColNames = Object.keys(sheets[sheetName][0]); + + Object.entries(expectedColNames).forEach(([internalName, externalNames]) => { + let foundMaping; + + externalNames.forEach((externalName) => { + if (providedColNames.includes(externalName)) foundMaping = externalName; + }); + colnameMappings[sheetName][internalName] = foundMaping; + + if (foundMaping === undefined) + throw error( + 'The provided File does not match the required format. \n Missing Parameter: ' + + internalName + + ' in Sheet: ' + + sheetName + ); + }); + }); + + return colnameMappings; +} + +module.exports = { + deriveProcessFromData, + deriveProcessFromExcel, +}; diff --git a/src/helper-modules/process-derivation/src/processUtilities.js b/src/helper-modules/process-derivation/src/processUtilities.js new file mode 100644 index 000000000..83b082941 --- /dev/null +++ b/src/helper-modules/process-derivation/src/processUtilities.js @@ -0,0 +1,255 @@ +const Constants = require('./constants.js'); + +function randomID() { + return Math.random().toString(36).substr(2, 7); +} + +// helper for semantic derivation +function createFlow(model, process, startElement, endElement, type = Constants.Other.sequenceFlow) { + let parameter = { + sourceRef: startElement, + targetRef: endElement, + }; + createElement(model, process, type, parameter); +} + +function createTask(model, process, name, type, parameter) { + return createElement(model, process, type, { name: name, ...parameter }); +} +function createElement(model, process, elementType, parameter) { + let elementId = elementType + '_' + randomID(); + + let element = model.create('bpmn:' + elementType, { + id: elementId, + ...parameter, + }); + process.get('flowElements').push(element); + + return element; +} + +function addAnnotation(data, process, task, operations, allocations) { + if (data.processSettings.textAnnotationsContents.length === 0) return; + + let text = ''; + if ( + data.processSettings.textAnnotationsContents.includes('Duration') && + operations !== undefined + ) { + text += 'Expected DURATION: \r'; + let units = [...new Set(operations.map((op) => op.unit))]; + for (let unit of units) { + let sum = operations.map((op) => op.quantity).reduce((a, b) => a + b); + text += sum + ' [' + unit + '] '; + } + text += '\r\r'; + } + if ( + data.processSettings.textAnnotationsContents.includes('Workplace') && + operations !== undefined + ) { + let workplaces = [...new Set(operations.map((op) => op.workCenter))]; + text += 'Required WORKPLACES: \r' + workplaces.join(', '); + text += '\r\r'; + } + if ( + data.processSettings.textAnnotationsContents.includes('Material') && + allocations !== undefined + ) { + text += 'Required MATERIALS: \r'; + allocations.forEach((alloc) => { + text += alloc.component + ' (x' + alloc.quantity + '), '; + }); + text += '\r\r'; + } + if ( + data.processSettings.textAnnotationsContents.includes('Worksteps') && + operations !== undefined + ) { + text += 'Required OPERATIONS: \r'; + let cols = ['operation', 'workCenter', 'quantity', 'unit', 'description']; + for (let op of operations) { + for (let col of cols) { + text += op[col] + ' '; + } + text += '\r'; + } + } + + // stop if no data added + if (text.length == 0) return; + + let annotaion = createElement(data.model, process, Constants.Other.textAnnotation, { + text: text, + }); + createFlow(data.model, process, task, annotaion, Constants.Other.association); +} + +// helper for graphic derivation + +// draw a BPMN-Elements to a specified position on a plane. +function createElementShape(model, element, xPos, yPos, processPlane, parameter) { + let size = getElementSize(element); + let x = xPos * Constants.VisualElementOffsetX - size.width / 2; + let y = + yPos * Constants.VisualElementOffsetY + Constants.VisualElementOffsetY / 2 - size.height / 2; + + let shape = model.create('bpmndi:BPMNShape', { + id: element.id + '_di', + bpmnElement: element, + bounds: model.create('dc:Bounds', { x: x, y: y, ...size }), + ...parameter, + }); + shape.yPos = yPos; + + processPlane.get('planeElement').push(shape); + return shape; +} + +// draw a sequenceFlow of an element on a plane. The flow is placed, based on the position of the BPMN-Elements it connects +function createFlowShape(model, flowElement, processPlane) { + let planeElements = processPlane.get('planeElement'); + + let from = flowElement.sourceRef; + let to = flowElement.targetRef; + + let fromBounds = planeElements.find((element) => element.id.includes(from.id)).bounds; + let toBounds = planeElements.find((element) => element.id.includes(to.id)).bounds; + + let fromSize = getElementSize(from); + let toSize = getElementSize(to); + + let yFrom = fromBounds.y + fromSize.height / 2; + let yTo = toBounds.y + toSize.height / 2; + + let direction = yFrom > yTo ? -1 : 1; + let waypoints; + if (yFrom === yTo) { + waypoints = [ + model.create('dc:Point', { x: fromBounds.x + fromSize.width, y: yFrom }), + model.create('dc:Point', { x: toBounds.x, y: yTo }), + ]; + } else if (from.id.includes('Gateway_') && to.id.includes('Gateway_')) { + waypoints = [ + model.create('dc:Point', { + x: fromBounds.x + fromSize.height / 2, + y: yFrom + (direction * fromSize.height) / 2, + }), + model.create('dc:Point', { x: fromBounds.x + fromSize.width / 2, y: yTo }), + model.create('dc:Point', { + x: toBounds.x + toSize.height / 2, + y: yTo - (direction * toSize.height) / 2, + }), + ]; + } else if (from.id.includes('Gateway_')) { + waypoints = [ + model.create('dc:Point', { + x: fromBounds.x + fromSize.width / 2, + y: yFrom + (direction * fromSize.height) / 2, + }), + model.create('dc:Point', { x: fromBounds.x + fromSize.width / 2, y: yTo }), + model.create('dc:Point', { x: toBounds.x, y: yTo }), + ]; + } else if (to.id.includes('Gateway_')) { + waypoints = [ + model.create('dc:Point', { x: fromBounds.x + fromSize.width, y: yFrom }), + model.create('dc:Point', { x: toBounds.x + toSize.width / 2, y: yFrom }), + model.create('dc:Point', { + x: toBounds.x + toSize.width / 2, + y: yTo - (direction * toSize.height) / 2, + }), + ]; + } else { + waypoints = [ + model.create('dc:Point', { x: fromBounds.x + fromSize.width, y: yFrom }), + model.create('dc:Point', { x: toBounds.x, y: yTo }), + ]; + } + + let shape = model.create('bpmndi:BPMNEdge', { + id: 'Edge_' + randomID(), + bpmnElement: flowElement, + waypoint: waypoints, + }); + processPlane.get('planeElement').push(shape); + return shape; +} + +// return the drawing-size of an element, based on "ShapeSize" +function getElementSize(element) { + let size = Constants.ShapeSize.default; + Object.keys(Constants.ShapeSize).forEach((shapeName) => { + if (element.id.includes(shapeName)) size = Constants.ShapeSize[shapeName]; + }); + return size; +} + +// return direct successors of an element within a process +function getSuccessors(element, process) { + return process + .get(Constants.NodeTypes.flowElements) + .filter((elem) => elem.sourceRef == element) + .map((elem) => elem.targetRef); +} + +// return direct predecessors of an element within a process +function getPredecessors(element, process) { + return process + .get(Constants.NodeTypes.flowElements) + .filter((elem) => elem.targetRef == element) + .map((elem) => elem.sourceRef); +} + +// create a BPMN diagram and plan for the graphical representation of a (sub-)process +function createProcessPlane(data, process) { + let bpmnDiagram = data.model.create('bpmndi:BPMNDiagram', { + id: 'BPMNDiagram_' + data.planeIdCounter++, //process.id.replace(/[^a-z0-9]/gi, ''), + name: 'BPMNDiagram_' + process.name.replace(/[^a-z0-9]/gi, ''), + }); + + let plane = data.model.create('bpmndi:BPMNPlane', { + id: 'BPMNPlane_' + data.planeIdCounter, + // name : "BPMNDiagram_"+ process.name.replace(/[^a-z0-9]/gi, '') + }); + plane.bpmnElement = process; + bpmnDiagram.set('plane', plane); + + data.definitions.get('diagrams').push(bpmnDiagram); + return plane; +} + +// iterate recursively through the bpmn process (from element to target) to find the path with the largest number of tasks +function calculatedBomDepth(element, target, process) { + if (element == target) return 0; + else { + let successors = process + .get(Constants.NodeTypes.flowElements) + .filter((elem) => elem.sourceRef == element) + .map((elem) => elem.targetRef); + let successorTasks = successors.flatMap((suc) => { + if (suc.id.includes('Gateway_')) { + return process + .get(Constants.NodeTypes.flowElements) + .filter((elem) => elem.sourceRef == suc) + .map((elem) => elem.targetRef); + } + return suc; + }); + + return 1 + Math.max(...successorTasks.map((suc) => calculatedBomDepth(suc, target, process))); + } +} + +module.exports = { + createFlow, + createTask, + createElement, + addAnnotation, + createElementShape, + createFlowShape, + getElementSize, + getSuccessors, + getPredecessors, + createProcessPlane, + calculatedBomDepth, +}; diff --git a/src/helper-modules/process-derivation/src/semanticProcessBuilder.js b/src/helper-modules/process-derivation/src/semanticProcessBuilder.js new file mode 100644 index 000000000..5740c0718 --- /dev/null +++ b/src/helper-modules/process-derivation/src/semanticProcessBuilder.js @@ -0,0 +1,328 @@ +// import lib for NODE and VUE +let BPMNModdle = require('bpmn-moddle'); +if (typeof BPMNModdle !== 'function') BPMNModdle = BPMNModdle.default; + +const Constants = require('./constants.js'); +const Utils = require('./processUtilities.js'); + +function build(processSettings, processData) { + let data = {}; + + // handle input Data + let inputSheetNames = Object.keys(processData); + let bomName = inputSheetNames.find((sheetName) => sheetName.includes('_BOM')); + + data.planeIdCounter = 0; + data.rootMaterial = bomName.split('_')[0]; + data.bom = processData[bomName]; + data.processSettings = processSettings; + data.operations = {}; + data.allocations = {}; + + // create process template + data.model = new BPMNModdle(); + data.definitions = data.model.create('bpmn:Definitions', { + xmlns: 'http://www.omg.org/spec/BPMN/20100524/MODEL', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xmlns:bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI', + 'xmlns:dc': 'http://www.omg.org/spec/DD/20100524/DC', + 'xmlns:di': 'http://www.omg.org/spec/DD/20100524/DI', + }); + + data.process = data.model.create('bpmn:Process', { + id: data.processSettings.id, + name: data.processSettings.name, + processType: data.processSettings.processType, + isExecutable: data.processSettings.isExecutable, + }); + data.definitions.get(Constants.NodeTypes.rootElements).push(data.process); + + inputSheetNames.forEach((name) => { + const material = name.substring(0, name.lastIndexOf('_')); + const sheet = processData[name]; + + if (name.includes('_Operations')) data.operations[material] = sheet; + else if (name.includes('_Allocations')) data.allocations[material] = sheet; + }); + + createSemanticProcess(data); + return data; +} + +/** + * Recursive function, which derives the semantic process completely on all levels. + * The process is derived starting from the final product of the BOM. From the BPMN point of view, the derived starts at the end event. + * + * @param {Array} residualBOM - BOM-Array of the branch, that remains to be derived + * @param {number} iteration - Iteration within the current (sub-)process + * @param {ModdelElement} predecessor - Reference to the last BPMN element (if existing) + * @param {ModdelElement} process - LReference current (sub-)process + * @returns {} Manipulation of the process-model + */ +function createSemanticProcess( + data, + residualBOM, + iteration = 0, + predecessor, + process = data.process +) { + // on first iteration, add final Product + if (residualBOM === undefined) { + residualBOM = data.bom; + residualBOM.unshift({ + material: data.rootMaterial, + materialName: data.rootMaterial, + quantity: 1, + unit: 'PC', + category: 'L', + layer: 0, + materialType: 'FERT', + }); + } + + // get materilas on current layer of branch + let currentElement = residualBOM[0]; + let nextLayerElements = []; + for (let i = 0; i < residualBOM.length; i++) { + if (residualBOM[i].layer === currentElement.layer + 1) nextLayerElements.push(i); + } + + let quantityUnit = ''; + if (currentElement.quantity !== 1 || !['PC', 'ST'].includes(currentElement.unit)) + quantityUnit = ' (' + currentElement.quantity + ' ' + currentElement.unit + ')'; + let allocations = data.allocations[currentElement.material]; + let operations = data.operations[currentElement.material]; + + // on first iteration of each (sub-)process: create endEvent and prepare for merge at start Event + if (iteration === 0) { + process.sourcedElements = []; + iteration++; + + //draw end event + predecessor = Utils.createElement(data.model, process, Constants.Event.end, { + name: 'Production finished for: ' + currentElement.materialName, + }); + } + + // get task type + let taskType = data.processSettings.taskType; + if ( + data.processSettings.useSubProcesses && + data.processSettings.subProcessesMaterials.length === 0 && + iteration % 2 == 0 + ) + taskType = Constants.Task.subProcess; + else if ( + data.processSettings.subProcessesMaterials.includes(currentElement.material) && + iteration !== 1 + ) + taskType = Constants.Task.subProcess; + + // on end of dependency chain, stop + if ( + nextLayerElements.length === 0 || + data.processSettings.stopOnMaterialTypes.includes(currentElement.materialType) + ) { + taskType = Constants.Task.none; + let task = Utils.createTask( + data.model, + process, + Constants.TaskNames.source + currentElement.materialName + quantityUnit, + taskType + ); + process.sourcedElements.push(task); + Utils.createFlow(data.model, process, task, predecessor); + return; + } + + // ON CONCURRENT DERIVATIONS + if (data.processSettings.concurrentTasks || allocations !== undefined) { + // if subprocess requiered, stop on curent subprocess-level and create new subprocess + if (taskType == Constants.Task.subProcess) { + let task = Utils.createTask( + data.model, + process, + Constants.TaskNames.assemble + currentElement.materialName + quantityUnit, + taskType + ); + Utils.createFlow(data.model, process, task, predecessor); + predecessor = task; + createSemanticProcess(data, residualBOM, 0, task, task); + process.sourcedElements.push(task); + return; + } else if (iteration !== 1 && data.processSettings.concurrentIntermediateEvent) { + let task = Utils.createElement(data.model, process, Constants.Event.intermediateThrow, { + name: currentElement.materialName + ' ready', + }); + Utils.createFlow(data.model, process, task, predecessor); + predecessor = task; + } + + // USE ALLOCATIONS for more accurate derivation, if available + if (allocations === undefined) { + // create gateway if more than one component + if (nextLayerElements.length > 1) { + let gateway = Utils.createElement(data.model, process, Constants.Gateway.parallel); + Utils.createFlow(data.model, process, gateway, predecessor); + predecessor = gateway; + } + + // create each component + nextLayerElements.push(residualBOM.length); + for (let i = 0; i < nextLayerElements.length - 1; i++) { + let component = residualBOM[nextLayerElements[i]]; + + let quantityUnit2 = ''; + if (component.quantity !== 1 || !['PC', 'ST'].includes(component.unit)) + quantityUnit2 = ' (' + component.quantity + ' ' + component.unit + ')'; + + let taskName = Constants.TaskNames.combineXY + .replace('X', component.materialName + quantityUnit2) + .replace('Y', currentElement.materialName); + let componentTask = Utils.createTask(data.model, process, taskName, taskType); + Utils.createFlow(data.model, process, componentTask, predecessor); + + // continue recursive call on next level + let subTree = residualBOM.slice(nextLayerElements[i], nextLayerElements[i + 1]); + createSemanticProcess(data, subTree, iteration + 1, componentTask, process); + } + } else { + // get materilas on next lower level + let components = {}; + nextLayerElements.push(residualBOM.length); + for (let i = 0; i < nextLayerElements.length - 1; i++) { + let component = residualBOM[nextLayerElements[i]]; + component.residualBOM = residualBOM.slice(nextLayerElements[i], nextLayerElements[i + 1]); + components[component.material] = component; + } + + // group allocation by requierend materials + let groups = [...new Set(allocations.map((alloc) => alloc.operation))].sort((a, b) => b - a); + let groupMaterials = {}; + groups.forEach((operation) => { + groupMaterials[operation] = allocations + .filter((alloc) => alloc.operation == operation) + .map((alloc) => alloc.component); + }); + + // group operations by requierend materials + let groupOperations = {}; + if (operations !== undefined) { + let groupBounds = [...groups]; + groupBounds[groupBounds.length - 1] = 0; + + let upperBound = Math.max(...operations.map((op) => op.operation)) + 1; + groupBounds.forEach((lowerBound, index) => { + groupOperations[groups[index]] = operations.filter( + (op) => op.operation < upperBound && op.operation >= lowerBound + ); + upperBound = lowerBound; + }); + } + + // create "ADD INTO"-task for each group + let lastGrop = groups[0] + 1; + groups.forEach((group, index) => { + let materialIds = groupMaterials[group]; + let taskName = + 'ADD ' + + materialIds + .map((id) => { + let component = components[id]; + let quantityUnit2 = ''; + if (component.quantity !== 1 || !['PC', 'ST'].includes(component.unit)) + quantityUnit2 = ' (' + component.quantity + ' ' + component.unit + ')'; + + return component.materialName + quantityUnit2; + }) + .join(' AND ') + + ' INTO ' + + currentElement.materialName; + + let task = Utils.createTask(data.model, process, taskName + quantityUnit, taskType); + Utils.createFlow(data.model, process, task, predecessor); + + let subAllocations = allocations.filter( + (alloc) => alloc.operation < lastGrop && alloc.operation >= group + ); + Utils.addAnnotation(data, process, task, groupOperations[group], subAllocations); + + // connect via Gateway, except the last one, if last one is single + if (index == groups.length - 1 && materialIds.length === 1) { + predecessor = task; + } else { + let gateway = Utils.createElement(data.model, process, Constants.Gateway.parallel); + Utils.createFlow(data.model, process, gateway, task); + predecessor = gateway; + } + + // continue recursive call on next level + for (let materialId of materialIds) { + createSemanticProcess( + data, + components[materialId].residualBOM, + iteration + 1, + predecessor, + process + ); + } + lastGrop = group; + }); + } + } + // ON SEQUENCIAL DERIVATION + else { + // create task + let task = Utils.createTask( + data.model, + process, + Constants.TaskNames.assemble + currentElement.materialName + quantityUnit, + taskType + ); + Utils.createFlow(data.model, process, task, predecessor); + Utils.addAnnotation(data, process, task, operations, allocations); + predecessor = task; + + // if subprocess requiered, stop on curent subprocess-level and create new subprocess + if (taskType == Constants.Task.subProcess) { + createSemanticProcess(data, residualBOM, 0, task, task); + process.sourcedElements.push(task); + } else { + // create gateway if more than one component + if (nextLayerElements.length > 1) { + // draw gateway if needed + let gateway = Utils.createElement(data.model, process, Constants.Gateway.parallel); + Utils.createFlow(data.model, process, gateway, predecessor); + predecessor = gateway; + } + + // create each component + nextLayerElements.push(residualBOM.length); + for (let i = 0; i < nextLayerElements.length - 1; i++) { + let subTree = residualBOM.slice(nextLayerElements[i], nextLayerElements[i + 1]); + + // continue recursive call on next level + createSemanticProcess(data, subTree, iteration + 1, predecessor, process); + } + } + } + + // at the end of each (sub-)process creation: merge all flows into Gateway / start Event + if (iteration === 1) { + let startEvent = Utils.createElement(data.model, process, Constants.Event.start); + + if (process.sourcedElements.length > 1) { + let startGateway = Utils.createElement(data.model, process, Constants.Gateway.parallel); + Utils.createFlow(data.model, process, startEvent, startGateway); + process.sourcedElements.forEach((task) => { + Utils.createFlow(data.model, process, startGateway, task); + }); + } else { + Utils.createFlow(data.model, process, startEvent, process.sourcedElements[0]); + } + } +} + +module.exports = { + build, +}; diff --git a/src/helper-modules/process-derivation/test_with_web_app/.gitignore b/src/helper-modules/process-derivation/test_with_web_app/.gitignore new file mode 100644 index 000000000..403adbc1e --- /dev/null +++ b/src/helper-modules/process-derivation/test_with_web_app/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules +/dist + + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/helper-modules/process-derivation/test_with_web_app/README.md b/src/helper-modules/process-derivation/test_with_web_app/README.md new file mode 100644 index 000000000..93b06697a --- /dev/null +++ b/src/helper-modules/process-derivation/test_with_web_app/README.md @@ -0,0 +1,42 @@ +# Tes app for process derivation + +## Project setup + +``` +yarn install +``` + +IMPORTANT: also use "yarn install" within the proceed root folder. Otherwise the derivation might not work! + +### Compiles and hot-reloads for development + +``` +vue serve + +OR (npm run serve) +``` + +### Compiles and minifies for production + +``` +npm run build +``` + +### Lints and fixes files + +``` +npm run lint +``` + +### Customize configuration + +See [Configuration Reference](https://cli.vuejs.org/config/). + +### USAGE + +1. Launch the application +2. Click "Upload File" and select a SAP BOM file (you can find a example BOM inside _tests_/data) +3. Select preferred settings for derivation +4. If necessary, assign missing column names +5. Click Start Derivation. +6. View the process on the right diff --git a/src/helper-modules/process-derivation/test_with_web_app/babel.config.js b/src/helper-modules/process-derivation/test_with_web_app/babel.config.js new file mode 100644 index 000000000..078c0056f --- /dev/null +++ b/src/helper-modules/process-derivation/test_with_web_app/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['@vue/cli-plugin-babel/preset'], +}; diff --git a/src/helper-modules/process-derivation/test_with_web_app/jsconfig.json b/src/helper-modules/process-derivation/test_with_web_app/jsconfig.json new file mode 100644 index 000000000..b6bd4c8dc --- /dev/null +++ b/src/helper-modules/process-derivation/test_with_web_app/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "baseUrl": "./", + "moduleResolution": "node", + "paths": { + "@/*": ["src/*"] + }, + "lib": ["esnext", "dom", "dom.iterable", "scripthost"] + } +} diff --git a/src/helper-modules/process-derivation/test_with_web_app/package.json b/src/helper-modules/process-derivation/test_with_web_app/package.json new file mode 100644 index 000000000..e62612711 --- /dev/null +++ b/src/helper-modules/process-derivation/test_with_web_app/package.json @@ -0,0 +1,56 @@ +{ + "name": "my-app", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "bootstrap": "^5.2.3", + "bootstrap-vue": "^2.23.1", + "bpmn-js": "^11.5.0", + "bpmn-js-cli": "^2.3.0", + "bpmn-moddle": "^6.0.7", + "core-js": "^3.8.3", + "vue": "^2.7.14", + "vue-bpmn": "^0.3.0", + "vue-splitpane": "^1.0.6", + "vuetify": "^2.6.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@babel/core": "^7.12.16", + "@babel/eslint-parser": "^7.12.16", + "@vue/cli-plugin-babel": "~5.0.0", + "@vue/cli-plugin-eslint": "~5.0.0", + "@vue/cli-service": "~5.0.0", + "eslint": "^7.32.0", + "eslint-plugin-vue": "^8.0.3", + "sass": "~1.32.0", + "sass-loader": "^10.4.1", + "vue-cli-plugin-vuetify": "~2.5.8", + "vue-template-compiler": "^2.6.14", + "vuetify-loader": "^1.7.0" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/essential", + "eslint:recommended" + ], + "parserOptions": { + "parser": "@babel/eslint-parser" + }, + "rules": {} + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ] +} diff --git a/src/helper-modules/process-derivation/test_with_web_app/public/favicon.ico b/src/helper-modules/process-derivation/test_with_web_app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/src/helper-modules/process-derivation/test_with_web_app/public/index.html b/src/helper-modules/process-derivation/test_with_web_app/public/index.html new file mode 100644 index 000000000..5a48af09b --- /dev/null +++ b/src/helper-modules/process-derivation/test_with_web_app/public/index.html @@ -0,0 +1,28 @@ + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + +
+ + + diff --git a/src/helper-modules/process-derivation/test_with_web_app/src/App.vue b/src/helper-modules/process-derivation/test_with_web_app/src/App.vue new file mode 100644 index 000000000..9dc89f7a0 --- /dev/null +++ b/src/helper-modules/process-derivation/test_with_web_app/src/App.vue @@ -0,0 +1,72 @@ + + + diff --git a/src/helper-modules/process-derivation/test_with_web_app/src/assets/logo.png b/src/helper-modules/process-derivation/test_with_web_app/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 zcmaKRcUV(fvo}bjDT-7nLI_nlK}sT_69H+`qzVWDA|yaU?}j417wLi^B1KB1SLsC& zL0ag7$U(XW5YR7p&Ux?sP$d4lvMt8C^+TcQu4F zQqv!UF!I+kw)c0jhd6+g6oCr9P?7)?!qX1ui*iL{p}sKCAGuJ{{W)0z1pLF|=>h}& zt(2Lr0Z`2ig8<5i%Zk}cO5Fm=LByqGWaS`oqChZdEFmc`0hSb#gg|Aap^{+WKOYcj zHjINK)KDG%&s?Mt4CL(T=?;~U@bU2x_mLKN!#GJuK_CzbNw5SMEJorG!}_5;?R>@1 zSl)jns3WlU7^J%=(hUtfmuUCU&C3%8B5C^f5>W2Cy8jW3#{Od{lF1}|?c61##3dzA zsPlFG;l_FzBK}8>|H_Ru_H#!_7$UH4UKo3lKOA}g1(R&|e@}GINYVzX?q=_WLZCgh z)L|eJMce`D0EIwgRaNETDsr+?vQknSGAi=7H00r`QnI%oQnFxm`G2umXso9l+8*&Q z7WqF|$p49js$mdzo^BXpH#gURy=UO;=IMrYc5?@+sR4y_?d*~0^YP7d+y0{}0)zBM zIKVM(DBvICK#~7N0a+PY6)7;u=dutmNqK3AlsrUU9U`d;msiucB_|8|2kY=(7XA;G zwDA8AR)VCA#JOkxm#6oHNS^YVuOU;8p$N)2{`;oF|rQ?B~K$%rHDxXs+_G zF5|-uqHZvSzq}L;5Kcy_P+x0${33}Ofb6+TX&=y;;PkEOpz%+_bCw_{<&~ zeLV|!bP%l1qxywfVr9Z9JI+++EO^x>ZuCK);=$VIG1`kxK8F2M8AdC$iOe3cj1fo(ce4l-9 z7*zKy3={MixvUk=enQE;ED~7tv%qh&3lR<0m??@w{ILF|e#QOyPkFYK!&Up7xWNtL zOW%1QMC<3o;G9_S1;NkPB6bqbCOjeztEc6TsBM<(q9((JKiH{01+Ud=uw9B@{;(JJ z-DxI2*{pMq`q1RQc;V8@gYAY44Z!%#W~M9pRxI(R?SJ7sy7em=Z5DbuDlr@*q|25V)($-f}9c#?D%dU^RS<(wz?{P zFFHtCab*!rl(~j@0(Nadvwg8q|4!}L^>d?0al6}Rrv9$0M#^&@zjbfJy_n!%mVHK4 z6pLRIQ^Uq~dnyy$`ay51Us6WaP%&O;@49m&{G3z7xV3dLtt1VTOMYl3UW~Rm{Eq4m zF?Zl_v;?7EFx1_+#WFUXxcK78IV)FO>42@cm@}2I%pVbZqQ}3;p;sDIm&knay03a^ zn$5}Q$G!@fTwD$e(x-~aWP0h+4NRz$KlnO_H2c< z(XX#lPuW_%H#Q+c&(nRyX1-IadKR-%$4FYC0fsCmL9ky3 zKpxyjd^JFR+vg2!=HWf}2Z?@Td`0EG`kU?{8zKrvtsm)|7>pPk9nu@2^z96aU2<#` z2QhvH5w&V;wER?mopu+nqu*n8p~(%QkwSs&*0eJwa zMXR05`OSFpfyRb!Y_+H@O%Y z0=K^y6B8Gcbl?SA)qMP3Z+=C(?8zL@=74R=EVnE?vY!1BQy2@q*RUgRx4yJ$k}MnL zs!?74QciNb-LcG*&o<9=DSL>1n}ZNd)w1z3-0Pd^4ED1{qd=9|!!N?xnXjM!EuylY z5=!H>&hSofh8V?Jofyd!h`xDI1fYAuV(sZwwN~{$a}MX^=+0TH*SFp$vyxmUv7C*W zv^3Gl0+eTFgBi3FVD;$nhcp)ka*4gSskYIqQ&+M}xP9yLAkWzBI^I%zR^l1e?bW_6 zIn{mo{dD=)9@V?s^fa55jh78rP*Ze<3`tRCN4*mpO$@7a^*2B*7N_|A(Ve2VB|)_o z$=#_=aBkhe(ifX}MLT()@5?OV+~7cXC3r!%{QJxriXo9I%*3q4KT4Xxzyd{ z9;_%=W%q!Vw$Z7F3lUnY+1HZ*lO;4;VR2+i4+D(m#01OYq|L_fbnT;KN<^dkkCwtd zF7n+O7KvAw8c`JUh6LmeIrk4`F3o|AagKSMK3))_5Cv~y2Bb2!Ibg9BO7Vkz?pAYX zoI=B}+$R22&IL`NCYUYjrdhwjnMx_v=-Qcx-jmtN>!Zqf|n1^SWrHy zK|MwJ?Z#^>)rfT5YSY{qjZ&`Fjd;^vv&gF-Yj6$9-Dy$<6zeP4s+78gS2|t%Z309b z0^fp~ue_}i`U9j!<|qF92_3oB09NqgAoehQ`)<)dSfKoJl_A6Ec#*Mx9Cpd-p#$Ez z={AM*r-bQs6*z$!*VA4|QE7bf@-4vb?Q+pPKLkY2{yKsw{&udv_2v8{Dbd zm~8VAv!G~s)`O3|Q6vFUV%8%+?ZSVUa(;fhPNg#vab@J*9XE4#D%)$UU-T5`fwjz! z6&gA^`OGu6aUk{l*h9eB?opVdrHK>Q@U>&JQ_2pR%}TyOXGq_6s56_`U(WoOaAb+K zXQr#6H}>a-GYs9^bGP2Y&hSP5gEtW+GVC4=wy0wQk=~%CSXj=GH6q z-T#s!BV`xZVxm{~jr_ezYRpqqIcXC=Oq`b{lu`Rt(IYr4B91hhVC?yg{ol4WUr3v9 zOAk2LG>CIECZ-WIs0$N}F#eoIUEtZudc7DPYIjzGqDLWk_A4#(LgacooD z2K4IWs@N`Bddm-{%oy}!k0^i6Yh)uJ1S*90>|bm3TOZxcV|ywHUb(+CeX-o1|LTZM zwU>dY3R&U)T(}5#Neh?-CWT~@{6Ke@sI)uSuzoah8COy)w)B)aslJmp`WUcjdia-0 zl2Y}&L~XfA`uYQboAJ1;J{XLhYjH){cObH3FDva+^8ioOQy%Z=xyjGLmWMrzfFoH; zEi3AG`_v+%)&lDJE;iJWJDI@-X9K5O)LD~j*PBe(wu+|%ar~C+LK1+-+lK=t# z+Xc+J7qp~5q=B~rD!x78)?1+KUIbYr^5rcl&tB-cTtj+e%{gpZZ4G~6r15+d|J(ky zjg@@UzMW0k9@S#W(1H{u;Nq(7llJbq;;4t$awM;l&(2s+$l!Ay9^Ge|34CVhr7|BG z?dAR83smef^frq9V(OH+a+ki#q&-7TkWfFM=5bsGbU(8mC;>QTCWL5ydz9s6k@?+V zcjiH`VI=59P-(-DWXZ~5DH>B^_H~;4$)KUhnmGo*G!Tq8^LjfUDO)lASN*=#AY_yS zqW9UX(VOCO&p@kHdUUgsBO0KhXxn1sprK5h8}+>IhX(nSXZKwlNsjk^M|RAaqmCZB zHBolOHYBas@&{PT=R+?d8pZu zUHfyucQ`(umXSW7o?HQ3H21M`ZJal+%*)SH1B1j6rxTlG3hx1IGJN^M7{$j(9V;MZ zRKybgVuxKo#XVM+?*yTy{W+XHaU5Jbt-UG33x{u(N-2wmw;zzPH&4DE103HV@ER86 z|FZEmQb|&1s5#`$4!Cm}&`^{(4V}OP$bk`}v6q6rm;P!H)W|2i^e{7lTk2W@jo_9q z*aw|U7#+g59Fv(5qI`#O-qPj#@_P>PC#I(GSp3DLv7x-dmYK=C7lPF8a)bxb=@)B1 zUZ`EqpXV2dR}B&r`uM}N(TS99ZT0UB%IN|0H%DcVO#T%L_chrgn#m6%x4KE*IMfjX zJ%4veCEqbXZ`H`F_+fELMC@wuy_ch%t*+Z+1I}wN#C+dRrf2X{1C8=yZ_%Pt6wL_~ zZ2NN-hXOT4P4n$QFO7yYHS-4wF1Xfr-meG9Pn;uK51?hfel`d38k{W)F*|gJLT2#T z<~>spMu4(mul-8Q3*pf=N4DcI)zzjqAgbE2eOT7~&f1W3VsdD44Ffe;3mJp-V@8UC z)|qnPc12o~$X-+U@L_lWqv-RtvB~%hLF($%Ew5w>^NR82qC_0FB z)=hP1-OEx?lLi#jnLzH}a;Nvr@JDO-zQWd}#k^an$Kwml;MrD&)sC5b`s0ZkVyPkb zt}-jOq^%_9>YZe7Y}PhW{a)c39G`kg(P4@kxjcYfgB4XOOcmezdUI7j-!gs7oAo2o zx(Ph{G+YZ`a%~kzK!HTAA5NXE-7vOFRr5oqY$rH>WI6SFvWmahFav!CfRMM3%8J&c z*p+%|-fNS_@QrFr(at!JY9jCg9F-%5{nb5Bo~z@Y9m&SHYV`49GAJjA5h~h4(G!Se zZmK{Bo7ivCfvl}@A-ptkFGcWXAzj3xfl{evi-OG(TaCn1FAHxRc{}B|x+Ua1D=I6M z!C^ZIvK6aS_c&(=OQDZfm>O`Nxsw{ta&yiYPA~@e#c%N>>#rq)k6Aru-qD4(D^v)y z*>Rs;YUbD1S8^D(ps6Jbj0K3wJw>L4m)0e(6Pee3Y?gy9i0^bZO?$*sv+xKV?WBlh zAp*;v6w!a8;A7sLB*g-^<$Z4L7|5jXxxP1}hQZ<55f9<^KJ>^mKlWSGaLcO0=$jem zWyZkRwe~u{{tU63DlCaS9$Y4CP4f?+wwa(&1ou)b>72ydrFvm`Rj-0`kBJgK@nd(*Eh!(NC{F-@=FnF&Y!q`7){YsLLHf0_B6aHc# z>WIuHTyJwIH{BJ4)2RtEauC7Yq7Cytc|S)4^*t8Va3HR zg=~sN^tp9re@w=GTx$;zOWMjcg-7X3Wk^N$n;&Kf1RgVG2}2L-(0o)54C509C&77i zrjSi{X*WV=%C17((N^6R4Ya*4#6s_L99RtQ>m(%#nQ#wrRC8Y%yxkH;d!MdY+Tw@r zjpSnK`;C-U{ATcgaxoEpP0Gf+tx);buOMlK=01D|J+ROu37qc*rD(w`#O=3*O*w9?biwNoq3WN1`&Wp8TvKj3C z3HR9ssH7a&Vr<6waJrU zdLg!ieYz%U^bmpn%;(V%%ugMk92&?_XX1K@mwnVSE6!&%P%Wdi7_h`CpScvspMx?N zQUR>oadnG17#hNc$pkTp+9lW+MBKHRZ~74XWUryd)4yd zj98$%XmIL4(9OnoeO5Fnyn&fpQ9b0h4e6EHHw*l68j;>(ya`g^S&y2{O8U>1*>4zR zq*WSI_2o$CHQ?x0!wl9bpx|Cm2+kFMR)oMud1%n2=qn5nE&t@Fgr#=Zv2?}wtEz^T z9rrj=?IH*qI5{G@Rn&}^Z{+TW}mQeb9=8b<_a`&Cm#n%n~ zU47MvCBsdXFB1+adOO)03+nczfWa#vwk#r{o{dF)QWya9v2nv43Zp3%Ps}($lA02*_g25t;|T{A5snSY?3A zrRQ~(Ygh_ebltHo1VCbJb*eOAr;4cnlXLvI>*$-#AVsGg6B1r7@;g^L zFlJ_th0vxO7;-opU@WAFe;<}?!2q?RBrFK5U{*ai@NLKZ^};Ul}beukveh?TQn;$%9=R+DX07m82gP$=}Uo_%&ngV`}Hyv8g{u z3SWzTGV|cwQuFIs7ZDOqO_fGf8Q`8MwL}eUp>q?4eqCmOTcwQuXtQckPy|4F1on8l zP*h>d+cH#XQf|+6c|S{7SF(Lg>bR~l(0uY?O{OEVlaxa5@e%T&xju=o1`=OD#qc16 zSvyH*my(dcp6~VqR;o(#@m44Lug@~_qw+HA=mS#Z^4reBy8iV?H~I;{LQWk3aKK8$bLRyt$g?-