|
@@ -0,0 +1,244 @@
|
|
|
+/**
|
|
|
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
|
|
+ *
|
|
|
+ * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
|
|
+ */
|
|
|
+
|
|
|
+import { StringBuilder, deepEqual } from '../../../mol-util';
|
|
|
+import Writer from '../writer';
|
|
|
+import { Encoder, Category, Field } from '../cif/encoder';
|
|
|
+import { getCategoryInstanceData } from '../cif/encoder/util';
|
|
|
+import { ComponentBond } from '../../../mol-model-formats/structure/property/bonds/comp';
|
|
|
+
|
|
|
+// specification: http://chemyang.ccnu.edu.cn/ccb/server/AIMMS/mol2.pdf
|
|
|
+export class Mol2Encoder implements Encoder<string> {
|
|
|
+ private builder: StringBuilder;
|
|
|
+ private meta: StringBuilder;
|
|
|
+ private encoded = false;
|
|
|
+ private error = false;
|
|
|
+ private componentData: ComponentBond;
|
|
|
+ readonly isBinary = false;
|
|
|
+ binaryEncodingProvider = void 0;
|
|
|
+
|
|
|
+ setComponentBondData(componentData: ComponentBond) {
|
|
|
+ this.componentData = componentData;
|
|
|
+ }
|
|
|
+
|
|
|
+ writeTo(stream: Writer) {
|
|
|
+ const chunks = StringBuilder.getChunks(this.builder);
|
|
|
+ for (let i = 0, _i = chunks.length; i < _i; i++) {
|
|
|
+ stream.writeString(chunks[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getSize() {
|
|
|
+ return StringBuilder.getSize(this.builder);
|
|
|
+ }
|
|
|
+
|
|
|
+ getData() {
|
|
|
+ return StringBuilder.getString(this.builder);
|
|
|
+ }
|
|
|
+
|
|
|
+ startDataBlock() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx) {
|
|
|
+ if (this.encoded) {
|
|
|
+ throw new Error('The writer contents have already been encoded, no more writing.');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.metaInformation && (category.name === 'model_server_result' || category.name === 'model_server_params' || category.name === 'model_server_stats')) {
|
|
|
+ this.writeFullCategory(this.meta, category, context);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // if error: force writing of meta information
|
|
|
+ if (category.name === 'model_server_error') {
|
|
|
+ this.writeFullCategory(this.meta, category, context);
|
|
|
+ this.error = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // only care about atom_site category when writing SDF
|
|
|
+ if (category.name !== 'atom_site') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // use separate builder because we still need to write Counts and Bonds line
|
|
|
+ const ctab = StringBuilder.create();
|
|
|
+ const bonds = StringBuilder.create();
|
|
|
+ const charges = StringBuilder.create();
|
|
|
+
|
|
|
+ // write Atom block and gather data for Bonds and Charges
|
|
|
+ const { instance, source } = getCategoryInstanceData(category, context);
|
|
|
+ const sortedFields = this.getSortedFields(instance, ['Cartn_x', 'Cartn_y', 'Cartn_z', 'type_symbol']);
|
|
|
+ const label_comp_id = this.getField(instance, 'label_comp_id');
|
|
|
+ const label_atom_id = this.getField(instance, 'label_atom_id');
|
|
|
+
|
|
|
+ // all of this is used to ensure that only 1 residue is written
|
|
|
+ // TODO potentially we could use a dedicated query mode or use the 'correct' SDF definition and allow an arbitrary number of molecules in a file
|
|
|
+ const auxiliaryFields = this.getSortedFields(instance, ['label_seq_id', 'label_asym_id', 'pdbx_PDB_ins_code', 'pdbx_PDB_model_num']);
|
|
|
+
|
|
|
+ // write header
|
|
|
+ const name = label_comp_id.value(source[0].keys().move(), source[0].data, 0) as string;
|
|
|
+ StringBuilder.write(this.builder, `# Name: ${name}\n# Created by ${this.encoder}\n\n`);
|
|
|
+
|
|
|
+ const bondMap = this.componentData.entries.get(name)!;
|
|
|
+ let bondCount = 0;
|
|
|
+
|
|
|
+ // traverse once to determine all actually present atoms
|
|
|
+ const atoms = this.getAtoms(source, sortedFields, label_atom_id, auxiliaryFields, ctab);
|
|
|
+ for (let i1 = 0, il = atoms.length; i1 < il; i1++) {
|
|
|
+ bondMap.map.get(atoms[i1])!.forEach((v, k) => {
|
|
|
+ const i2 = atoms.indexOf(k);
|
|
|
+ const label2 = this.getLabel(k);
|
|
|
+ if (i1 < i2 && atoms.indexOf(k) > -1 && !this.skipHydrogen(label2)) {
|
|
|
+ const { order } = v;
|
|
|
+ StringBuilder.writeIntegerPadLeft(bonds, i1 + 1, 3);
|
|
|
+ StringBuilder.writeIntegerPadLeft(bonds, i2 + 1, 3);
|
|
|
+ StringBuilder.writeIntegerPadLeft(bonds, order, 3);
|
|
|
+ StringBuilder.writeSafe(bonds, ' 0 0 0 0\n');
|
|
|
+ // TODO 2nd value: Single bonds: 0 = not stereo, 1 = Up, 4 = Either, 6 = Down, Double bonds: 0 = Use x-, y-, z-coords from atom block to determine cis or trans, 3 = Cis or trans (either) double bond
|
|
|
+ bondCount++;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // write counts line
|
|
|
+ StringBuilder.writeIntegerPadLeft(this.builder, atoms.length, 3);
|
|
|
+ StringBuilder.writeIntegerPadLeft(this.builder, bondCount, 3);
|
|
|
+ StringBuilder.write(this.builder, ' 0 0 0 0 0 0 0999 V2000\n');
|
|
|
+ // TODO 2nd value: chiral flag: 0=not chiral, 1=chiral
|
|
|
+
|
|
|
+ StringBuilder.writeSafe(this.builder, StringBuilder.getString(ctab));
|
|
|
+ StringBuilder.writeSafe(this.builder, StringBuilder.getString(bonds));
|
|
|
+ StringBuilder.writeSafe(this.builder, StringBuilder.getString(charges));
|
|
|
+ // TODO charges?
|
|
|
+
|
|
|
+ StringBuilder.writeSafe(this.builder, 'M END\n');
|
|
|
+ }
|
|
|
+
|
|
|
+ private getAtoms(source: any, fields: Field<any, any>[], label_atom_id: Field<any, any>, auxiliaryFields: Field<any, any>[], ctab: StringBuilder): string[] {
|
|
|
+ const atoms = [];
|
|
|
+ let index = 0;
|
|
|
+ let id;
|
|
|
+
|
|
|
+ // is this loop needed?
|
|
|
+ l: for (let _c = 0; _c < source.length; _c++) {
|
|
|
+ const src = source[_c];
|
|
|
+ const data = src.data;
|
|
|
+
|
|
|
+ if (src.rowCount === 0) continue;
|
|
|
+
|
|
|
+ const it = src.keys();
|
|
|
+ while (it.hasNext) {
|
|
|
+ const key = it.move();
|
|
|
+
|
|
|
+ // ensure only a single residue is written
|
|
|
+ // TODO fairly certain this can be done at query level
|
|
|
+ if (!id) {
|
|
|
+ id = auxiliaryFields.map(af => af.value(key, data, 0));
|
|
|
+ } else {
|
|
|
+ if (!deepEqual(id, auxiliaryFields.map(af => af.value(key, data, index)))) {
|
|
|
+ break l;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const lai = label_atom_id.value(key, data, index) as string;
|
|
|
+ const label = this.getLabel(lai);
|
|
|
+ if (this.skipHydrogen(label)) {
|
|
|
+ index++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ atoms.push(lai);
|
|
|
+
|
|
|
+ for (let _f = 0, _fl = fields.length; _f < _fl; _f++) {
|
|
|
+ const f: Field<any, any> = fields[_f]!;
|
|
|
+ const v = f.value(key, data, index);
|
|
|
+ this.writeValue(ctab, v, f.type);
|
|
|
+ }
|
|
|
+
|
|
|
+ StringBuilder.writeSafe(ctab, ' 0 0 0 0 0 0 0 0 0 0 0 0\n');
|
|
|
+ index++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return atoms;
|
|
|
+ }
|
|
|
+
|
|
|
+ private skipHydrogen(label: string) {
|
|
|
+ if (this.hydrogens) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return label.startsWith('H');
|
|
|
+ }
|
|
|
+
|
|
|
+ private getLabel(s: string) {
|
|
|
+ return s.replace(/[^A-Z]+/g, '');
|
|
|
+ }
|
|
|
+
|
|
|
+ private writeFullCategory<Ctx>(sb: StringBuilder, category: Category<Ctx>, context?: Ctx) {
|
|
|
+ const { instance, source } = getCategoryInstanceData(category, context);
|
|
|
+ const fields = instance.fields;
|
|
|
+ const src = source[0];
|
|
|
+ const data = src.data;
|
|
|
+
|
|
|
+ const it = src.keys();
|
|
|
+ const key = it.move();
|
|
|
+ for (let _f = 0; _f < fields.length; _f++) {
|
|
|
+ const f = fields[_f]!;
|
|
|
+
|
|
|
+ StringBuilder.writeSafe(sb, `> <${category.name}.${f.name}>\n`);
|
|
|
+ const val = f.value(key, data, 0);
|
|
|
+ StringBuilder.writeSafe(sb, val as string);
|
|
|
+ StringBuilder.writeSafe(sb, '\n\n');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private writeValue(sb: StringBuilder, val: string | number, t: Field.Type, floatPrecision: number = 4) {
|
|
|
+ if (t === Field.Type.Str) {
|
|
|
+ // type_symbol is the only string field - width 2, right-padded
|
|
|
+ StringBuilder.whitespace1(sb);
|
|
|
+ StringBuilder.writePadRight(sb, val as string, 2);
|
|
|
+ } else if (t === Field.Type.Int) {
|
|
|
+ StringBuilder.writeInteger(sb, val as number);
|
|
|
+ } else {
|
|
|
+ // coordinates have width 10 and are left-padded
|
|
|
+ StringBuilder.writePadLeft(sb, (val as number).toFixed(floatPrecision), 10);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private getSortedFields<Ctx>(instance: Category.Instance<Ctx>, names: string[]) {
|
|
|
+ return names.map(n => this.getField(instance, n));
|
|
|
+ }
|
|
|
+
|
|
|
+ private getField<Ctx>(instance: Category.Instance<Ctx>, name: string) {
|
|
|
+ return instance.fields.find(f => f.name === name)!;
|
|
|
+ }
|
|
|
+
|
|
|
+ encode() {
|
|
|
+ // write meta-information, do so after ctab
|
|
|
+ if (this.error || this.metaInformation) {
|
|
|
+ StringBuilder.writeSafe(this.builder, StringBuilder.getString(this.meta));
|
|
|
+ }
|
|
|
+
|
|
|
+ // terminate file
|
|
|
+ StringBuilder.writeSafe(this.builder, '$$$$\n');
|
|
|
+
|
|
|
+ this.encoded = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ setFilter(filter?: Category.Filter) {}
|
|
|
+
|
|
|
+ setFormatter(formatter?: Category.Formatter) {}
|
|
|
+
|
|
|
+ isCategoryIncluded(name: string) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ constructor(readonly encoder: string, readonly metaInformation: boolean, readonly hydrogens: boolean) {
|
|
|
+ this.builder = StringBuilder.create();
|
|
|
+ this.meta = StringBuilder.create();
|
|
|
+ }
|
|
|
+}
|