step-to-json
Version:
A simple command line tool that extracts the system structure of a STEP (ISO 10303-44) file and outputs it as .json
316 lines (260 loc) • 10.9 kB
JavaScript
const Subject = require('rxjs').Subject;
const { v4: uuidv4 } = require('uuid');
class StepToJsonParser {
constructor(file) {
this.file = file;
this.preprocessedFile = {
header: {
fileDescription: '',
fileName: '',
fileSchema: '',
},
data: {
productDefinitions: [],
nextAssemblyUsageOccurences: [],
},
};
this.preprocessFile();
}
/**
* Parses a STEP file and outputs its contents as a JSON tree
* @param {function} visitorFunction A function that will be executed for every product occurrence of the assembly
* @param {Subject} sub A subject that can be used to track progress
*/
parse(visitorFunction = undefined, sub = new Subject()) {
this.parseProductDefinitions(this.preprocessedFile.data.productDefinitions);
this.parseNextAssemblyUsageOccurences(this.preprocessedFile.data.nextAssemblyUsageOccurences);
const rootAssembly = this.identifyRootAssembly();
const result = this.buildStructureObject(rootAssembly, sub, visitorFunction);
return result;
}
/**
* Parses a STEP file and outputs its contents as a JSON tree. Adds a UUID for every product occurrence
* @param {*} sub A subject that can be used to track progress
*/
parseWithUuid(sub = new Subject()) {
return this.parse(StepToJsonParser.uuidVisitor, sub);
}
/**
* Splits the STEP-file into single lines and stores all lines that contain product definitions and assembly relations
*/
preprocessFile() {
let lines;
try {
lines = this.file.toString().split(');');
} catch (error) {
throw new Error(`Error while reading the file, filePath: ${this.file}`, error);
}
lines.forEach((line) => {
if (line.includes('FILE_NAME')) {
this.preprocessedFile.header.fileName = StepToJsonParser.removeLinebreaks(line);
} else if (line.includes('FILE_SCHEMA')) {
this.preprocessedFile.header.fileSchema = StepToJsonParser.removeLinebreaks(line);
} else if (line.includes('FILE_DESCRIPTION')) {
this.preprocessedFile.header.fileDescription = StepToJsonParser.removeLinebreaks(line);
} else if (line.includes('PRODUCT_DEFINITION(')) {
this.preprocessedFile.data.productDefinitions.push(StepToJsonParser.removeLinebreaks(line));
} else if (line.includes('NEXT_ASSEMBLY_USAGE_OCCURRENCE(')) {
this.preprocessedFile.data.nextAssemblyUsageOccurences.push(StepToJsonParser.removeLinebreaks(line));
}
});
return this.preprocessedFile;
}
/**
* Parses the lines of the next assembly usage occurrence and extracts id of relation, container id, contained id and contained name
*
* @param {Array<string>} nextAssemblyUsageOccurences
* @param {Subject} subject Subject that can be used to track this function's state
* @returns
*/
parseNextAssemblyUsageOccurences(nextAssemblyUsageOccurences, subject = new Subject()) {
let progress = 1;
const assemblyRelations = [];
nextAssemblyUsageOccurences.forEach((element) => {
subject.next(progress++);
// get id by splitting at '=' and removing '#'
const newId = element.split('=')[0].slice(1);
const attributes = StepToJsonParser.getAttributes(element);
const container = attributes[3].slice(1); // Remove #
const contained = attributes[4].slice(1); // Remove #
const assemblyObject = {
id: newId,
container: container,
contains: contained,
};
assemblyRelations.push(assemblyObject);
});
subject.complete();
this.relations = assemblyRelations;
return assemblyRelations;
}
/**
* Parses the lines of the product definition and extracts id and name
*
* @param {Array<string>} productDefinitionLines
* @param {Subject} subject Subject that can be used to track this function's state
* @returns
*/
parseProductDefinitions(productDefinitionLines, subject = new Subject()) {
let progress = 1;
const products = [];
productDefinitionLines.forEach((pDLine) => {
subject.next(progress++);
const attributes = StepToJsonParser.getAttributes(pDLine);
const newId = pDLine.split('=')[0].slice(1); // Remove #
const name = attributes[0].slice(1, attributes[0].length - 1); // Remove ' (first and last element)
const fixedName = StepToJsonParser.fixSpecialChars(name);
const productObject = {
id: newId,
name: fixedName,
};
products.push(productObject);
});
subject.complete();
this.products = products;
return products;
}
/**
* Identifies the root component that contains all other components
*/
identifyRootAssembly() {
if (this.products.length === 1) {
return this.products[0];
}
try {
let rootComponent;
this.products.forEach((product) => {
// Look for a relation where product is the container
const productIsContainer = this.relations.some((relation) => relation.container === product.id);
// Look for a relation where product is contained
const productIsContained = this.relations.some((relation) => relation.contains === product.id);
// Root assembly acts a container, but is not contained in any other product
if (productIsContainer && !productIsContained) {
rootComponent = product;
}
});
return rootComponent;
} catch (error) {
throw new Error('Root component could not be found');
}
}
/**
* Returns the preprocessed file
*/
getPreProcessedObject() {
return this.preprocessedFile;
}
/**
* Returns a containment structure object for a given product object that has id and name
*
* @param {Object} rootAssemblyObject
* @param {Subject} buildSubject
* @returns
*/
/**
* Returns a containment structure object for a given product object that has id and name
* @param {*} rootProduct The root component of the assembly
* @param {*} buildSubject An instance of rxjs Subject that can be used to track this function's progress
* @param {*} visitorFunction A function that is executed for every component. Can be used to customize processing or add additional data
*/
buildStructureObject(rootProduct, buildSubject = new Subject(), visitorFunction = undefined) {
let relationsChecked = 0;
const structureObject = {
id: rootProduct.id,
name: rootProduct.name,
contains: [],
};
if (visitorFunction !== undefined) {
const visitorResult = visitorFunction(structureObject);
structureObject[visitorResult.key] = visitorResult.value;
}
this.relations.forEach((relation) => {
buildSubject.next(++relationsChecked);
if (relation.container === rootProduct.id) {
const containedProduct = this.getContainedProduct(relation.contains);
structureObject.contains.push(this.buildStructureObject(containedProduct, buildSubject, visitorFunction));
}
});
if (visitorFunction !== undefined) {
const visitorResult = visitorFunction(structureObject);
structureObject[visitorResult.key] = visitorResult.value;
}
buildSubject.complete();
return structureObject;
}
/**
* Checks if a productId serves as a container for other products
*
* @param {*} productId
* @returns
*/
isContainer(productId) {
const isContainer = this.relations.some((element) => element.container === productId);
return isContainer;
}
/**
* Get the contained product of a relation given a relation's 'contained-id'
* @param {string} relationContainsId 'contains-id' of the relation
*/
getContainedProduct(relationContainsId) {
return this.products.find((product) => product.id === relationContainsId);
}
/**
* Returns the name for a given product id
*
* @param {string} productId ID of the product
* @returns {string} Name of the product
*/
getProductName(productId) {
let productName = '';
this.products.forEach((element) => {
if (element.id === productId) {
productName = element.name;
}
});
return productName;
}
/**
* Removes linebreaks that are always present at the end of a line inside a STEP file
* @param {String} str String that the linebreak will be removed from
*/
static removeLinebreaks(str) {
return str.replace(/[\r\n]+/gm, '');
}
/**
* Returns attributes of a line that are defined inside parantheses
* @param {*} line One line of a STEP-file
* @returns {Array<string>} An array of attributes
*/
static getAttributes(line) {
const openParentheses = line.indexOf('(') + 1;
const closingParentheses = line.indexOf(')');
const attributes = line.slice(openParentheses, closingParentheses).split(',');
return attributes;
}
/**
* Fixes German umlauts
* @param {string} stringToFix The string that will be fixed
*/
static fixSpecialChars(stringToFix) {
let fixedString = stringToFix;
if (stringToFix.includes('\\X\\')) {
fixedString = stringToFix.replace('\\X\\C4', 'Ae')
.replace('\\X\\E4', 'ae')
.replace('\\X\\D6', 'Oe')
.replace('\\X\\F6', 'oe')
.replace('\\X\\DC', 'Ue')
.replace('\\X\\FC', 'ue');
}
return fixedString;
}
/**
* An exemplary visitor function that creates a UUID
*/
static uuidVisitor() {
const id = uuidv4();
const result = { key: 'uuid', value: id };
return result;
}
}
exports.StepToJsonParser = StepToJsonParser