Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,115 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
function pollDeferred(process, complete, timeGap, startTimeGap) {
return new Promise(function (resolve, reject) {
function iterate() {
process().then(function (val) {
try {
var finish = complete(val);
if (finish)
resolve(val);
else
setTimeout(iterate, timeGap);
} catch (e) {
reject(e);
}
}, function (err) {
return reject(err);
});
}
setTimeout(iterate, startTimeGap || 0);
});
}
function parametrizeUrl(url, params) {
return url.replace(/:(\w+)/g, function (_, val) {
return params[val];
});
}
function api(base, defaultOptions) {
var baseUrl = !base || /\/$/.test(base) ? base : base + '/';
var info = request('GET', 'indigo/info').then(function (res) {
return { indigoVersion: res.Indigo.version };
}).catch(function () {
throw Error('Server is not compatible');
});
function request(method, url, data, headers) {
if (data && method === 'GET')
url = parametrizeUrl(url, data);
return fetch(baseUrl + url, {
method: method,
headers: Object.assign({
Accept: 'application/json'
}, headers),
body: method !== 'GET' ? data : undefined,
credentials: 'same-origin'
}).then(function (response) {
return response.json().then(function (res) {
return response.ok ? res : Promise.reject(res.error);
});
}).catch(function (err) {
throw 'Cannot parse result\n' + err;
});
}
function indigoCall(method, url, defaultData) {
return function (data, options) {
var body = Object.assign({}, defaultData, data);
body.options = Object.assign(body.options || {},
defaultOptions, options);
return info.then(function () {
return request(method, url, JSON.stringify(body), {
'Content-Type': 'application/json'
});
});
};
}
return Object.assign(info, {
convert: indigoCall('POST', 'indigo/convert'),
layout: indigoCall('POST', 'indigo/layout'),
clean: indigoCall('POST', 'indigo/clean'),
aromatize: indigoCall('POST', 'indigo/aromatize'),
dearomatize: indigoCall('POST', 'indigo/dearomatize'),
calculateCip: indigoCall('POST', 'indigo/calculate_cip'),
automap: indigoCall('POST', 'indigo/automap'),
check: indigoCall('POST', 'indigo/check'),
calculate: indigoCall('POST', 'indigo/calculate'),
recognize: function (blob) {
var req = request('POST', 'imago/uploads', blob, {
'Content-Type': blob.type || 'application/octet-stream'
});
var status = request.bind(null, 'GET', 'imago/uploads/:id');
return req.then(function (res) {
return pollDeferred(
status.bind(null, { id: res.upload_id }),
function complete(res) {
if (res.state === 'FAILURE')
throw res;
return res.state === 'SUCCESS';
}, 500, 300);
}).then(function correct(res) {
return { struct: res.metadata.mol_str };
});
}
});
}
// export default api;

View File

@ -0,0 +1,15 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,110 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var generics = {
atom: {
'any': {
labels: ['A', 'AH']
},
'no-carbon': {
labels: ['Q', 'QH']
},
'metal': {
labels: ['M', 'MH']
},
'halogen': {
labels: ['X', 'XH']
}
},
group: {
labels: ['G', 'GH', 'G*', 'GH*'],
acyclic: {
labels: ['ACY', 'ACH'],
carbo: {
labels: ['ABC', 'ABH'],
alkynyl: {
labels: ['AYL', 'AYH']
},
alkyl: {
labels: ['ALK', 'ALH']
},
alkenyl: {
labels: ['AEL', 'AEH']
}
},
hetero: {
labels: ['AHC', 'AHH'],
alkoxy: {
labels: ['AOX', 'AOH']
}
}
},
cyclic: {
'labels': ['CYC', 'CYH'],
'no-carbon': {
labels: ['CXX', 'CXH']
},
'carbo': {
labels: ['CBC', 'CBH'],
aryl: {
labels: ['ARY', 'ARH']
},
cycloalkyl: {
labels: ['CAL', 'CAH']
},
cycloalkenyl: {
labels: ['CEL', 'CEH']
}
},
'hetero': {
labels: ['CHC', 'CHH'],
aryl: {
labels: ['HAR', 'HAH']
}
}
}
},
special: {
labels: ['H+', 'D', 'T', 'R', 'Pol']
}
};
function mapify(tree, path, res) {
return Object.keys(tree).reduce(function (res, key) {
if (key === 'labels') {
return tree.labels.reduce(function (res, label) {
res[label] = path || true;
return res;
}, res);
}
return mapify(tree[key],
path ? path.concat(key) : [key], res);
}, res || {});
}
function traverse(tree, path) {
return path.reduce(function (res, cur) {
return (res && res[cur]) || null;
}, tree);
}
generics.map = mapify(generics);
generics.map['*'] = generics.map['A']; // alias
generics.get = function (path) {
return mapify(traverse(path));
};
module.exports = generics;

View File

@ -0,0 +1,269 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Set = require('../../util/set');
var v2000 = require('./v2000');
var v3000 = require('./v3000');
var Struct = require('./../struct/index');
var utils = require('./utils');
var loadRGroupFragments = true; // TODO: set to load the fragments
/* Parse Mol */
function parseMol(/* string */ ctabLines) /* Struct */ {
/* reader */
if (ctabLines[0].search('\\$MDL') == 0)
return v2000.parseRg2000(ctabLines);
var struct = parseCTab(ctabLines.slice(3));
struct.name = ctabLines[0].trim();
return struct;
}
function parseCTab(/* string */ ctabLines) /* Struct */ {
/* reader */
var countsSplit = partitionLine(ctabLines[0], utils.fmtInfo.countsLinePartition);
var version = countsSplit[11].trim();
ctabLines = ctabLines.slice(1);
if (version == 'V2000')
return v2000.parseCTabV2000(ctabLines, countsSplit);
else if (version == 'V3000')
return v3000.parseCTabV3000(ctabLines, !loadRGroupFragments);
else
throw new Error('Molfile version unknown: ' + version); // eslint-disable-line no-else-return
}
/* Parse Rxn */
function parseRxn(/* string[] */ ctabLines) /* Struct */ {
/* reader */
var split = ctabLines[0].trim().split(' ');
if (split.length > 1 && split[1] == 'V3000')
return v3000.parseRxn3000(ctabLines);
else
return v2000.parseRxn2000(ctabLines); // eslint-disable-line no-else-return
}
/* Prepare For Saving */
var prepareForSaving = {
MUL: Struct.SGroup.prepareMulForSaving,
SRU: prepareSruForSaving,
SUP: prepareSupForSaving,
DAT: prepareDatForSaving,
GEN: prepareGenForSaving
};
function prepareSruForSaving(sgroup, mol) {
var xBonds = [];
mol.bonds.each(function (bid, bond) {
var a1 = mol.atoms.get(bond.begin);
var a2 = mol.atoms.get(bond.end);
/* eslint-disable no-mixed-operators*/
if (Set.contains(a1.sgs, sgroup.id) && !Set.contains(a2.sgs, sgroup.id) ||
Set.contains(a2.sgs, sgroup.id) && !Set.contains(a1.sgs, sgroup.id))
/* eslint-enable no-mixed-operators*/
xBonds.push(bid);
}, sgroup);
if (xBonds.length != 0 && xBonds.length != 2)
throw { 'id': sgroup.id, 'error-type': 'cross-bond-number', 'message': 'Unsupported cross-bonds number' };
sgroup.bonds = xBonds;
}
function prepareSupForSaving(sgroup, mol) {
// This code is also used for GroupSru and should be moved into a separate common method
// It seems that such code should be used for any sgroup by this this should be checked
var xBonds = [];
mol.bonds.each(function (bid, bond) {
var a1 = mol.atoms.get(bond.begin);
var a2 = mol.atoms.get(bond.end);
/* eslint-disable no-mixed-operators*/
if (Set.contains(a1.sgs, sgroup.id) && !Set.contains(a2.sgs, sgroup.id) ||
Set.contains(a2.sgs, sgroup.id) && !Set.contains(a1.sgs, sgroup.id))
/* eslint-enable no-mixed-operators*/
xBonds.push(bid);
}, sgroup);
sgroup.bonds = xBonds;
}
function prepareGenForSaving(sgroup, mol) { // eslint-disable-line no-unused-vars
}
function prepareDatForSaving(sgroup, mol) {
sgroup.atoms = Struct.SGroup.getAtoms(mol, sgroup);
}
/* Save To Molfile */
var saveToMolfile = {
MUL: saveMulToMolfile,
SRU: saveSruToMolfile,
SUP: saveSupToMolfile,
DAT: saveDatToMolfile,
GEN: saveGenToMolfile
};
function saveMulToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, Object.keys(sgroup.atomSet), atomMap)); // TODO: check atomSet
lines = lines.concat(makeAtomBondLines('SPA', idstr, Object.keys(sgroup.parentAtomSet), atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
var smtLine = 'M SMT ' + idstr + ' ' + sgroup.data.mul;
lines.push(smtLine);
lines = lines.concat(bracketsToMolfile(mol, sgroup, idstr));
return lines.join('\n');
}
function saveSruToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
lines = lines.concat(bracketsToMolfile(mol, sgroup, idstr));
return lines.join('\n');
}
function saveSupToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
if (sgroup.data.name && sgroup.data.name != '')
lines.push('M SMT ' + idstr + ' ' + sgroup.data.name);
return lines.join('\n');
}
function saveDatToMolfile(sgroup, mol, sgMap, atomMap) {
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var data = sgroup.data;
var pp = sgroup.pp;
if (!data.absolute)
pp = pp.sub(Struct.SGroup.getMassCentre(mol, sgroup.atoms));
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
var sdtLine = 'M SDT ' + idstr + ' ' +
(data.fieldName || '').padEnd(30) +
(data.fieldType || '').padStart(2) +
(data.units || '').padEnd(20) +
(data.query || '').padStart(2);
if (data.queryOp) // see gitlab #184
sdtLine += data.queryOp.padEnd(80 - 65);
lines.push(sdtLine);
var sddLine = 'M SDD ' + idstr +
' ' + utils.paddedNum(pp.x, 10, 4) + utils.paddedNum(-pp.y, 10, 4) +
' ' + // ' eee'
(data.attached ? 'A' : 'D') + // f
(data.absolute ? 'A' : 'R') + // g
(data.showUnits ? 'U' : ' ') + // h
' ' + // i
(data.nCharnCharsToDisplay >= 0 ? utils.paddedNum(data.nCharnCharsToDisplay, 3) : 'ALL') + // jjj
' 1 ' + // 'kkk ll '
(data.tagChar || ' ') + // m
' ' + utils.paddedNum(data.daspPos, 1) + // n
' '; // oo
lines.push(sddLine);
var val = normalizeNewlines(data.fieldValue).replace(/\n*$/, '');
var charsPerLine = 69;
val.split('\n').forEach(function (chars) {
while (chars.length > charsPerLine) {
lines.push('M SCD ' + idstr + ' ' + chars.slice(0, charsPerLine));
chars = chars.slice(charsPerLine);
}
lines.push('M SED ' + idstr + ' ' + chars);
});
return lines.join('\n');
}
function saveGenToMolfile(sgroup, mol, sgMap, atomMap, bondMap) { // eslint-disable-line max-params
var idstr = (sgMap[sgroup.id] + '').padStart(3);
var lines = [];
lines = lines.concat(makeAtomBondLines('SAL', idstr, sgroup.atoms, atomMap));
lines = lines.concat(makeAtomBondLines('SBL', idstr, sgroup.bonds, bondMap));
lines = lines.concat(bracketsToMolfile(mol, sgroup, idstr));
return lines.join('\n');
}
function makeAtomBondLines(prefix, idstr, ids, map) {
if (!ids)
return [];
var lines = [];
for (var i = 0; i < Math.floor((ids.length + 14) / 15); ++i) {
var rem = Math.min(ids.length - 15 * i, 15); // eslint-disable-line no-mixed-operators
var salLine = 'M ' + prefix + ' ' + idstr + ' ' + utils.paddedNum(rem, 2);
for (var j = 0; j < rem; ++j)
salLine += ' ' + utils.paddedNum(map[ids[i * 15 + j]], 3); // eslint-disable-line no-mixed-operators
lines.push(salLine);
}
return lines;
}
function bracketsToMolfile(mol, sg, idstr) { // eslint-disable-line max-statements
var inBonds = [];
var xBonds = [];
var atomSet = Set.fromList(sg.atoms);
Struct.SGroup.getCrossBonds(inBonds, xBonds, mol, atomSet);
Struct.SGroup.bracketPos(sg, mol, xBonds);
var bb = sg.bracketBox;
var d = sg.bracketDir;
var n = d.rotateSC(1, 0);
var brackets = Struct.SGroup.getBracketParameters(mol, xBonds, atomSet, bb, d, n);
var lines = [];
for (var i = 0; i < brackets.length; ++i) {
var bracket = brackets[i];
var a0 = bracket.c.addScaled(bracket.n, -0.5 * bracket.h).yComplement();
var a1 = bracket.c.addScaled(bracket.n, 0.5 * bracket.h).yComplement();
var line = 'M SDI ' + idstr + utils.paddedNum(4, 3);
var coord = [a0.x, a0.y, a1.x, a1.y];
for (var j = 0; j < coord.length; ++j)
line += utils.paddedNum(coord[j], 10, 4);
lines.push(line);
}
return lines;
}
// According Unicode Consortium sould be
// nlRe = /\r\n|[\n\v\f\r\x85\u2028\u2029]/g;
// http://www.unicode.org/reports/tr18/#Line_Boundaries
var nlRe = /\r\n|[\n\r]/g;
function normalizeNewlines(str) {
return str.replace(nlRe, '\n');
}
function partitionLine(/* string*/ str, /* array of int*/ parts, /* bool*/ withspace) {
/* reader */
var res = [];
for (var i = 0, shift = 0; i < parts.length; ++i) {
res.push(str.slice(shift, shift + parts[i]));
if (withspace)
shift++;
shift += parts[i];
}
return res;
}
module.exports = {
parseCTab: parseCTab,
parseMol: parseMol,
parseRxn: parseRxn,
prepareForSaving: prepareForSaving,
saveToMolfile: saveToMolfile
};

View File

@ -0,0 +1,54 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Molfile = require('./molfile');
// TODO: reconstruct molfile string instead parsing multiple times
// merge to bottom
function parseCTFile(str, options) {
var molfile = new Molfile();
var lines = str.split(/\r\n|[\n\r]/g);
try {
return molfile.parseCTFile(lines);
} catch (ex) {
if (options.badHeaderRecover) {
try {
// check whether there's an extra empty line on top
// this often happens when molfile text is pasted into the dialog window
return molfile.parseCTFile(lines.slice(1));
} catch (ex1) { //
}
try {
// check for a missing first line
// this sometimes happens when pasting
return molfile.parseCTFile([''].concat(lines));
} catch (ex2) { //
}
}
throw ex;
}
}
module.exports = {
stringify: function (struct, options) {
var opts = options || {};
return new Molfile(opts.v3000).saveMolecule(struct, opts.ignoreErrors,
opts.noRgroups, opts.preserveIndigoDesc);
},
parse: function (str, options) {
return parseCTFile(str, options || {});
}
};

View File

@ -0,0 +1,488 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var element = require('./../element');
var common = require('./common');
var utils = require('./utils');
function Molfile(v3000) {
/* reader */
/* saver */
this.molecule = null;
this.molfile = null;
this.v3000 = v3000 || false;
}
Molfile.prototype.parseCTFile = function (molfileLines) {
var ret = null;
if (molfileLines[0].search('\\$RXN') == 0)
ret = common.parseRxn(molfileLines);
else
ret = common.parseMol(molfileLines);
ret.initHalfBonds();
ret.initNeighbors();
ret.markFragments();
return ret;
};
Molfile.prototype.prepareSGroups = function (skipErrors, preserveIndigoDesc) {
var mol = this.molecule;
var toRemove = [];
var errors = 0;
this.molecule.sGroupForest.getSGroupsBFS().reverse().forEach(function (id) {
var sgroup = mol.sgroups.get(id);
var errorIgnore = false;
try {
common.prepareForSaving[sgroup.type](sgroup, mol);
} catch (ex) {
if (!skipErrors || typeof (ex.id) != 'number')
throw ex;
errorIgnore = true;
}
/* eslint-disable no-mixed-operators*/
if (errorIgnore ||
!preserveIndigoDesc && /^INDIGO_.+_DESC$/i.test(sgroup.data.fieldName)) {
/* eslint-enable no-mixed-operators*/
errors += errorIgnore;
toRemove.push(sgroup.id);
}
}, this);
if (errors)
throw new Error('WARNING: ' + errors + ' invalid S-groups were detected. They will be omitted.');
for (var i = 0; i < toRemove.length; ++i)
mol.sGroupDelete(toRemove[i]);
return mol;
};
Molfile.prototype.getCTab = function (molecule, rgroups) {
/* saver */
this.molecule = molecule.clone();
this.molfile = '';
this.writeCTab2000(rgroups);
return this.molfile;
};
Molfile.prototype.saveMolecule = function (molecule, skipSGroupErrors, norgroups, preserveIndigoDesc) { // eslint-disable-line max-statements
/* saver */
this.reaction = molecule.rxnArrows.count() > 0;
if (molecule.rxnArrows.count() > 1)
throw new Error('Reaction may not contain more than one arrow');
this.molfile = '' + molecule.name;
if (this.reaction) {
if (molecule.rgroups.count() > 0)
throw new Error('Unable to save the structure - reactions with r-groups are not supported at the moment');
var components = molecule.getComponents();
var reactants = components.reactants;
var products = components.products;
var all = reactants.concat(products);
this.molfile = '$RXN\n\n\n\n' +
utils.paddedNum(reactants.length, 3) +
utils.paddedNum(products.length, 3) +
utils.paddedNum(0, 3) + '\n';
for (var i = 0; i < all.length; ++i) {
var saver = new Molfile(false);
var submol = molecule.clone(all[i], null, true);
var molfile = saver.saveMolecule(submol, false, true);
this.molfile += '$MOL\n' + molfile;
}
return this.molfile;
}
if (molecule.rgroups.count() > 0) {
if (norgroups) {
molecule = molecule.getScaffold();
} else {
var scaffold = new Molfile(false).getCTab(molecule.getScaffold(), molecule.rgroups);
this.molfile = '$MDL REV 1\n$MOL\n$HDR\n\n\n\n$END HDR\n';
this.molfile += '$CTAB\n' + scaffold + '$END CTAB\n';
molecule.rgroups.each(function (rgid, rg) {
this.molfile += '$RGP\n';
this.writePaddedNumber(rgid, 3);
this.molfile += '\n';
rg.frags.each(function (fnum, fid) {
var group = new Molfile(false).getCTab(molecule.getFragment(fid));
this.molfile += '$CTAB\n' + group + '$END CTAB\n';
}, this);
this.molfile += '$END RGP\n';
}, this);
this.molfile += '$END MOL\n';
return this.molfile;
}
}
this.molecule = molecule.clone();
this.prepareSGroups(skipSGroupErrors, preserveIndigoDesc);
this.writeHeader();
// TODO: saving to V3000
this.writeCTab2000();
return this.molfile;
};
Molfile.prototype.writeHeader = function () {
/* saver */
var date = new Date();
this.writeCR(); // TODO: write structure name
this.writeWhiteSpace(2);
this.write('Ketcher');
this.writeWhiteSpace();
this.writeCR(((date.getMonth() + 1) + '').padStart(2) + (date.getDate() + '').padStart(2) + ((date.getFullYear() % 100) + '').padStart(2) +
(date.getHours() + '').padStart(2) + (date.getMinutes() + '').padStart(2) + '2D 1 1.00000 0.00000 0');
this.writeCR();
};
Molfile.prototype.write = function (str) {
/* saver */
this.molfile += str;
};
Molfile.prototype.writeCR = function (str) {
/* saver */
if (arguments.length == 0)
str = '';
this.molfile += str + '\n';
};
Molfile.prototype.writeWhiteSpace = function (length) {
/* saver */
if (arguments.length == 0)
length = 1;
this.write(' '.repeat(Math.max(length, 0)));
};
Molfile.prototype.writePadded = function (str, width) {
/* saver */
this.write(str);
this.writeWhiteSpace(width - str.length);
};
Molfile.prototype.writePaddedNumber = function (number, width) {
/* saver */
var str = (number - 0).toString();
this.writeWhiteSpace(width - str.length);
this.write(str);
};
Molfile.prototype.writePaddedFloat = function (number, width, precision) {
/* saver */
this.write(utils.paddedNum(number, width, precision));
};
Molfile.prototype.writeCTab2000Header = function () {
/* saver */
this.writePaddedNumber(this.molecule.atoms.count(), 3);
this.writePaddedNumber(this.molecule.bonds.count(), 3);
this.writePaddedNumber(0, 3);
this.writeWhiteSpace(3);
this.writePaddedNumber(this.molecule.isChiral ? 1 : 0, 3);
this.writePaddedNumber(0, 3);
this.writeWhiteSpace(12);
this.writePaddedNumber(999, 3);
this.writeCR(' V2000');
};
Molfile.prototype.writeCTab2000 = function (rgroups) { // eslint-disable-line max-statements
/* saver */
this.writeCTab2000Header();
this.mapping = {};
var i = 1;
/* eslint-disable camelcase*/
var atomList_list = [];
var atomProps_list = [];
/* eslint-enable camel-case*/
this.molecule.atoms.each(function (id, atom) { // eslint-disable-line max-statements
this.writePaddedFloat(atom.pp.x, 10, 4);
this.writePaddedFloat(-atom.pp.y, 10, 4);
this.writePaddedFloat(atom.pp.z, 10, 4);
this.writeWhiteSpace();
var label = atom.label;
if (atom.atomList != null) {
label = 'L';
atomList_list.push(id);
} else if (atom['pseudo']) {
if (atom['pseudo'].length > 3) {
label = 'A';
atomProps_list.push({ id: id, value: "'" + atom['pseudo'] + "'" });
}
} else if (atom['alias']) {
atomProps_list.push({ id: id, value: atom['alias'] });
} else if (!element.map[label] && ['A', 'Q', 'X', '*', 'R#'].indexOf(label) == -1) { // search in generics?
label = 'C';
atomProps_list.push({ id: id, value: atom.label });
}
this.writePadded(label, 3);
this.writePaddedNumber(0, 2);
this.writePaddedNumber(0, 3);
this.writePaddedNumber(0, 3);
if (typeof atom.hCount === "undefined")
atom.hCount = 0;
this.writePaddedNumber(atom.hCount, 3);
if (typeof atom.stereoCare === "undefined")
atom.stereoCare = 0;
this.writePaddedNumber(atom.stereoCare, 3);
this.writePaddedNumber(atom.explicitValence < 0 ? 0 : (atom.explicitValence == 0 ? 15 : atom.explicitValence), 3); // eslint-disable-line no-nested-ternary
this.writePaddedNumber(0, 3);
this.writePaddedNumber(0, 3);
this.writePaddedNumber(0, 3);
if (typeof atom.aam === "undefined")
atom.aam = 0;
this.writePaddedNumber(atom.aam, 3);
if (typeof atom.invRet === "undefined")
atom.invRet = 0;
this.writePaddedNumber(atom.invRet, 3);
if (typeof atom.exactChangeFlag === "undefined")
atom.exactChangeFlag = 0;
this.writePaddedNumber(atom.exactChangeFlag, 3);
this.writeCR();
this.mapping[id] = i;
i++;
}, this);
this.bondMapping = {};
i = 1;
this.molecule.bonds.each(function (id, bond) {
this.bondMapping[id] = i++;
this.writePaddedNumber(this.mapping[bond.begin], 3);
this.writePaddedNumber(this.mapping[bond.end], 3);
this.writePaddedNumber(bond.type, 3);
if (typeof bond.stereo === "undefined")
bond.stereo = 0;
this.writePaddedNumber(bond.stereo, 3);
this.writePadded(bond.xxx, 3);
if (typeof bond.topology === "undefined")
bond.topology = 0;
this.writePaddedNumber(bond.topology, 3);
if (typeof bond.reactingCenterStatus === "undefined")
bond.reactingCenterStatus = 0;
this.writePaddedNumber(bond.reactingCenterStatus, 3);
this.writeCR();
}, this);
while (atomProps_list.length > 0) {
this.write('A ');
this.writePaddedNumber(atomProps_list[0].id + 1, 3);
this.writeCR();
this.writeCR(atomProps_list[0].value);
atomProps_list.splice(0, 1);
}
var chargeList = [];
var isotopeList = [];
var radicalList = [];
var rglabelList = [];
var rglogicList = [];
var aplabelList = [];
var rbcountList = [];
var unsaturatedList = [];
var substcountList = [];
this.molecule.atoms.each(function (id, atom) {
if (atom.charge != 0)
chargeList.push([id, atom.charge]);
if (atom.isotope != 0)
isotopeList.push([id, atom.isotope]);
if (atom.radical != 0)
radicalList.push([id, atom.radical]);
if (atom.rglabel != null && atom.label == 'R#') { // TODO need to force rglabel=null when label is not 'R#'
for (var rgi = 0; rgi < 32; rgi++)
if (atom.rglabel & (1 << rgi)) rglabelList.push([id, rgi + 1]);
}
if (atom.attpnt != null)
aplabelList.push([id, atom.attpnt]);
if (atom.ringBondCount != 0)
rbcountList.push([id, atom.ringBondCount]);
if (atom.substitutionCount != 0)
substcountList.push([id, atom.substitutionCount]);
if (atom.unsaturatedAtom != 0)
unsaturatedList.push([id, atom.unsaturatedAtom]);
});
if (rgroups) {
rgroups.each(function (rgid, rg) {
if (rg.resth || rg.ifthen > 0 || rg.range.length > 0) {
var line = ' 1 ' +
utils.paddedNum(rgid, 3) + ' ' +
utils.paddedNum(rg.ifthen, 3) + ' ' +
utils.paddedNum(rg.resth ? 1 : 0, 3) + ' ' + rg.range;
rglogicList.push(line);
}
});
}
function writeAtomPropList(propId, values) {
while (values.length > 0) {
var part = [];
while (values.length > 0 && part.length < 8) {
part.push(values[0]);
values.splice(0, 1);
}
this.write(propId);
this.writePaddedNumber(part.length, 3);
part.forEach(function (value) {
this.writeWhiteSpace();
this.writePaddedNumber(this.mapping[value[0]], 3);
this.writeWhiteSpace();
this.writePaddedNumber(value[1], 3);
}, this);
this.writeCR();
}
}
writeAtomPropList.call(this, 'M CHG', chargeList);
writeAtomPropList.call(this, 'M ISO', isotopeList);
writeAtomPropList.call(this, 'M RAD', radicalList);
writeAtomPropList.call(this, 'M RGP', rglabelList);
for (var j = 0; j < rglogicList.length; ++j)
this.write('M LOG' + rglogicList[j] + '\n');
writeAtomPropList.call(this, 'M APO', aplabelList);
writeAtomPropList.call(this, 'M RBC', rbcountList);
writeAtomPropList.call(this, 'M SUB', substcountList);
writeAtomPropList.call(this, 'M UNS', unsaturatedList);
if (atomList_list.length > 0) {
for (j = 0; j < atomList_list.length; ++j) {
var aid = atomList_list[j];
var atomList = this.molecule.atoms.get(aid).atomList;
this.write('M ALS');
this.writePaddedNumber(aid + 1, 4);
this.writePaddedNumber(atomList.ids.length, 3);
this.writeWhiteSpace();
this.write(atomList.notList ? 'T' : 'F');
var labelList = atomList.labelList();
for (var k = 0; k < labelList.length; ++k) {
this.writeWhiteSpace();
this.writePadded(labelList[k], 3);
}
this.writeCR();
}
}
var sgmap = {};
var cnt = 1;
var sgmapback = {};
var sgorder = this.molecule.sGroupForest.getSGroupsBFS();
sgorder.forEach(function (id) {
sgmapback[cnt] = id;
sgmap[id] = cnt++;
}, this);
for (var q = 1; q < cnt; ++q) { // each group on its own
var id = sgmapback[q];
var sgroup = this.molecule.sgroups.get(id);
this.write('M STY');
this.writePaddedNumber(1, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeWhiteSpace(1);
this.writePadded(sgroup.type, 3);
this.writeCR();
// TODO: write subtype, M SST
this.write('M SLB');
this.writePaddedNumber(1, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeCR();
var parentid = this.molecule.sGroupForest.parent.get(id);
if (parentid >= 0) {
this.write('M SPL');
this.writePaddedNumber(1, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(q, 3);
this.writeWhiteSpace(1);
this.writePaddedNumber(sgmap[parentid], 3);
this.writeCR();
}
// connectivity
if (sgroup.type == 'SRU' && sgroup.data.connectivity) {
var connectivity = '';
connectivity += ' ';
connectivity += q.toString().padStart(3);
connectivity += ' ';
connectivity += (sgroup.data.connectivity || '').padEnd(3);
this.write('M SCN');
this.writePaddedNumber(1, 3);
this.write(connectivity.toUpperCase());
this.writeCR();
}
if (sgroup.type == 'SRU') {
this.write('M SMT ');
this.writePaddedNumber(q, 3);
this.writeWhiteSpace();
this.write(sgroup.data.subscript || 'n');
this.writeCR();
}
this.writeCR(common.saveToMolfile[sgroup.type](sgroup, this.molecule, sgmap, this.mapping, this.bondMapping));
}
// TODO: write M APO
// TODO: write M AAL
// TODO: write M RGP
// TODO: write M LOG
this.writeCR('M END');
};
module.exports = Molfile;

View File

@ -0,0 +1,301 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Struct = require('./../struct/index');
var utils = require('./utils');
function readKeyValuePairs(str, /* bool */ valueString) {
/* reader */
var ret = {};
var partition = utils.partitionLineFixed(str, 3, true);
var count = utils.parseDecimalInt(partition[0]);
for (var i = 0; i < count; ++i) {
/* eslint-disable no-mixed-operators*/
ret[utils.parseDecimalInt(partition[2 * i + 1]) - 1] =
valueString ? partition[2 * i + 2].trim() :
utils.parseDecimalInt(partition[2 * i + 2]);
/* eslint-enable no-mixed-operators*/
}
return ret;
}
function readKeyMultiValuePairs(str, /* bool */ valueString) {
/* reader */
var ret = [];
var partition = utils.partitionLineFixed(str, 3, true);
var count = utils.parseDecimalInt(partition[0]);
for (var i = 0; i < count; ++i) {
ret.push([
/* eslint-disable no-mixed-operators*/
utils.parseDecimalInt(partition[2 * i + 1]) - 1,
valueString ? partition[2 * i + 2].trim() : utils.parseDecimalInt(partition[2 * i + 2])
/* eslint-enable no-mixed-operators*/
]);
}
return ret;
}
function postLoadMul(sgroup, mol, atomMap) { // eslint-disable-line max-statements
sgroup.data.mul = sgroup.data.subscript - 0;
var atomReductionMap = {};
sgroup.atoms = Struct.SGroup.filterAtoms(sgroup.atoms, atomMap);
sgroup.patoms = Struct.SGroup.filterAtoms(sgroup.patoms, atomMap);
// mark repetitions for removal
for (var k = 1; k < sgroup.data.mul; ++k) {
for (var m = 0; m < sgroup.patoms.length; ++m) {
var raid = sgroup.atoms[k * sgroup.patoms.length + m]; // eslint-disable-line no-mixed-operators
if (raid < 0)
continue; // eslint-disable-line no-continue
if (sgroup.patoms[m] < 0)
throw new Error('parent atom missing');
atomReductionMap[raid] = sgroup.patoms[m]; // "merge" atom in parent
}
}
sgroup.patoms = Struct.SGroup.removeNegative(sgroup.patoms);
var patomsMap = identityMap(sgroup.patoms);
var bondsToRemove = [];
mol.bonds.each(function (bid, bond) {
var beginIn = bond.begin in atomReductionMap;
var endIn = bond.end in atomReductionMap;
// if both adjacent atoms of a bond are to be merged, remove it
/* eslint-disable no-mixed-operators*/
if (beginIn && endIn ||
beginIn && bond.end in patomsMap ||
endIn && bond.begin in patomsMap)
bondsToRemove.push(bid);
/* eslint-enable no-mixed-operators*/
// if just one atom is merged, modify the bond accordingly
else if (beginIn)
bond.begin = atomReductionMap[bond.begin];
else if (endIn)
bond.end = atomReductionMap[bond.end];
}, sgroup);
// apply removal lists
for (var b = 0; b < bondsToRemove.length; ++b)
mol.bonds.remove(bondsToRemove[b]);
for (var a in atomReductionMap) {
mol.atoms.remove(a);
atomMap[a] = -1;
}
sgroup.atoms = sgroup.patoms;
sgroup.patoms = null;
}
function postLoadSru(sgroup) {
sgroup.data.connectivity = (sgroup.data.connectivity || 'EU').trim().toLowerCase();
}
function postLoadSup(sgroup) {
sgroup.data.name = (sgroup.data.subscript || '').trim();
sgroup.data.subscript = '';
}
function postLoadGen(sgroup, mol, atomMap) { // eslint-disable-line no-unused-vars
}
function postLoadDat(sgroup, mol) {
if (!sgroup.data.absolute)
sgroup.pp = sgroup.pp.add(Struct.SGroup.getMassCentre(mol, sgroup.atoms));
}
function loadSGroup(mol, sg, atomMap) {
var postLoadMap = {
MUL: postLoadMul,
SRU: postLoadSru,
SUP: postLoadSup,
DAT: postLoadDat,
GEN: postLoadGen
};
// add the group to the molecule
sg.id = mol.sgroups.add(sg);
// apply type-specific post-processing
postLoadMap[sg.type](sg, mol, atomMap);
// mark atoms in the group as belonging to it
for (var s = 0; s < sg.atoms.length; ++s) {
if (mol.atoms.has(sg.atoms[s]))
Set.add(mol.atoms.get(sg.atoms[s]).sgs, sg.id);
}
if (sg.type == 'DAT')
mol.sGroupForest.insert(sg.id, -1, []);
else
mol.sGroupForest.insert(sg.id);
return sg.id;
}
function initSGroup(sGroups, propData) {
/* reader */
var kv = readKeyValuePairs(propData, true);
for (var key in kv) {
var type = kv[key];
if (!(type in Struct.SGroup.TYPES))
throw new Error('Unsupported S-group type');
var sg = new Struct.SGroup(type);
sg.number = key;
sGroups[key] = sg;
}
}
function applySGroupProp(sGroups, propName, propData, numeric, core) { // eslint-disable-line max-params
var kv = readKeyValuePairs(propData, !(numeric));
for (var key in kv)
// "core" properties are stored directly in an sgroup, not in sgroup.data
(core ? sGroups[key] : sGroups[key].data)[propName] = kv[key];
}
function applySGroupArrayProp(sGroups, propName, propData, shift) {
/* reader */
var sid = utils.parseDecimalInt(propData.slice(1, 4)) - 1;
var num = utils.parseDecimalInt(propData.slice(4, 8));
var part = toIntArray(utils.partitionLineFixed(propData.slice(8), 3, true));
if (part.length != num)
throw new Error('File format invalid');
if (shift) {
part = part.map(function (v) {
return v + shift;
});
}
sGroups[sid][propName] = sGroups[sid][propName].concat(part);
}
function applyDataSGroupName(sg, name) {
/* reader */
sg.data.fieldName = name;
}
function applyDataSGroupQuery(sg, query) {
/* reader */
sg.data.query = query;
}
function applyDataSGroupQueryOp(sg, queryOp) {
/* reader */
sg.data.queryOp = queryOp;
}
function applyDataSGroupDesc(sGroups, propData) {
/* reader */
var split = utils.partitionLine(propData, [4, 31, 2, 20, 2, 3], false);
var id = utils.parseDecimalInt(split[0]) - 1;
var fieldName = split[1].trim();
var fieldType = split[2].trim();
var units = split[3].trim();
var query = split[4].trim();
var queryOp = split[5].trim();
var sGroup = sGroups[id];
sGroup.data.fieldType = fieldType;
sGroup.data.fieldName = fieldName;
sGroup.data.units = units;
sGroup.data.query = query;
sGroup.data.queryOp = queryOp;
}
function applyDataSGroupInfo(sg, propData) { // eslint-disable-line max-statements
/* reader */
var split = utils.partitionLine(propData, [10/* x.x*/, 10/* y.y*/, 4/* eee*/, 1/* f*/, 1/* g*/, 1/* h*/, 3/* i */, 3/* jjj*/, 3/* kkk*/, 3/* ll*/, 2/* m*/, 3/* n*/, 2/* oo*/], false);
var x = parseFloat(split[0]);
var y = parseFloat(split[1]);
var attached = split[3].trim() == 'A';
var absolute = split[4].trim() == 'A';
var showUnits = split[5].trim() == 'U';
var nCharsToDisplay = split[7].trim();
nCharsToDisplay = nCharsToDisplay == 'ALL' ? -1 : utils.parseDecimalInt(nCharsToDisplay);
var tagChar = split[10].trim();
var daspPos = utils.parseDecimalInt(split[11].trim());
sg.pp = new Vec2(x, -y);
sg.data.attached = attached;
sg.data.absolute = absolute;
sg.data.showUnits = showUnits;
sg.data.nCharsToDisplay = nCharsToDisplay;
sg.data.tagChar = tagChar;
sg.data.daspPos = daspPos;
}
function applyDataSGroupInfoLine(sGroups, propData) {
/* reader */
var id = utils.parseDecimalInt(propData.substr(0, 4)) - 1;
var sg = sGroups[id];
applyDataSGroupInfo(sg, propData.substr(5));
}
function applyDataSGroupData(sg, data, finalize) {
/* reader */
sg.data.fieldValue = (sg.data.fieldValue || '') + data;
if (finalize) {
sg.data.fieldValue = trimRight(sg.data.fieldValue);
if (sg.data.fieldValue.startsWith('"') && sg.data.fieldValue.endsWith('"'))
sg.data.fieldValue = sg.data.fieldValue.substr(1, sg.data.fieldValue.length - 2);
}
}
function applyDataSGroupDataLine(sGroups, propData, finalize) {
/* reader */
var id = utils.parseDecimalInt(propData.substr(0, 5)) - 1;
var data = propData.substr(5);
var sg = sGroups[id];
applyDataSGroupData(sg, data, finalize);
}
// Utilities functions
function toIntArray(strArray) {
/* reader */
var ret = [];
for (var j = 0; j < strArray.length; ++j)
ret[j] = utils.parseDecimalInt(strArray[j]);
return ret;
}
function trimRight(str) {
return str.replace(/\s+$/, '');
}
function identityMap(array) {
var map = {};
for (var i = 0; i < array.length; ++i)
map[array[i]] = array[i];
return map;
}
module.exports = {
readKeyValuePairs: readKeyValuePairs,
readKeyMultiValuePairs: readKeyMultiValuePairs,
loadSGroup: loadSGroup,
initSGroup: initSGroup,
applySGroupProp: applySGroupProp,
applySGroupArrayProp: applySGroupArrayProp,
applyDataSGroupName: applyDataSGroupName,
applyDataSGroupQuery: applyDataSGroupQuery,
applyDataSGroupQueryOp: applyDataSGroupQueryOp,
applyDataSGroupDesc: applyDataSGroupDesc,
applyDataSGroupInfo: applyDataSGroupInfo,
applyDataSGroupData: applyDataSGroupData,
applyDataSGroupInfoLine: applyDataSGroupInfoLine,
applyDataSGroupDataLine: applyDataSGroupDataLine
};

View File

@ -0,0 +1,274 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Struct = require('./../struct/index');
function paddedNum(number, width, precision) {
var numStr = number.toFixed(precision || 0).replace(',', '.'); // Really need to replace?
if (numStr.length > width)
throw new Error('number does not fit');
return numStr.padStart(width);
}
function parseDecimalInt(str) {
/* reader */
var val = parseInt(str, 10);
return isNaN(val) ? 0 : val;
}
function partitionLine(/* string*/ str, /* array of int*/ parts, /* bool*/ withspace) {
/* reader */
var res = [];
for (var i = 0, shift = 0; i < parts.length; ++i) {
res.push(str.slice(shift, shift + parts[i]));
if (withspace)
shift++;
shift += parts[i];
}
return res;
}
function partitionLineFixed(/* string*/ str, /* int*/ itemLength, /* bool*/ withspace) {
/* reader */
var res = [];
for (var shift = 0; shift < str.length; shift += itemLength) {
res.push(str.slice(shift, shift + itemLength));
if (withspace)
shift++;
}
return res;
}
var fmtInfo = {
bondTypeMap: {
1: Struct.Bond.PATTERN.TYPE.SINGLE,
2: Struct.Bond.PATTERN.TYPE.DOUBLE,
3: Struct.Bond.PATTERN.TYPE.TRIPLE,
4: Struct.Bond.PATTERN.TYPE.AROMATIC,
5: Struct.Bond.PATTERN.TYPE.SINGLE_OR_DOUBLE,
6: Struct.Bond.PATTERN.TYPE.SINGLE_OR_AROMATIC,
7: Struct.Bond.PATTERN.TYPE.DOUBLE_OR_AROMATIC,
8: Struct.Bond.PATTERN.TYPE.ANY
},
bondStereoMap: {
0: Struct.Bond.PATTERN.STEREO.NONE,
1: Struct.Bond.PATTERN.STEREO.UP,
4: Struct.Bond.PATTERN.STEREO.EITHER,
6: Struct.Bond.PATTERN.STEREO.DOWN,
3: Struct.Bond.PATTERN.STEREO.CIS_TRANS
},
v30bondStereoMap: {
0: Struct.Bond.PATTERN.STEREO.NONE,
1: Struct.Bond.PATTERN.STEREO.UP,
2: Struct.Bond.PATTERN.STEREO.EITHER,
3: Struct.Bond.PATTERN.STEREO.DOWN
},
bondTopologyMap: {
0: Struct.Bond.PATTERN.TOPOLOGY.EITHER,
1: Struct.Bond.PATTERN.TOPOLOGY.RING,
2: Struct.Bond.PATTERN.TOPOLOGY.CHAIN
},
countsLinePartition: [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6],
atomLinePartition: [10, 10, 10, 1, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
bondLinePartition: [3, 3, 3, 3, 3, 3, 3],
atomListHeaderPartition: [3, 1, 1, 4, 1, 1],
atomListHeaderLength: 11, // = atomListHeaderPartition.reduce(function(a,b) { return a + b; }, 0)
atomListHeaderItemLength: 4,
chargeMap: [0, +3, +2, +1, 0, -1, -2, -3],
valenceMap: [undefined, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0],
implicitHydrogenMap: [undefined, 0, 1, 2, 3, 4],
v30atomPropMap: {
CHG: 'charge',
RAD: 'radical',
MASS: 'isotope',
VAL: 'explicitValence',
HCOUNT: 'hCount',
INVRET: 'invRet',
SUBST: 'substitutionCount',
UNSAT: 'unsaturatedAtom',
RBCNT: 'ringBondCount'
},
rxnItemsPartition: [3, 3, 3]
};
var FRAGMENT = {
NONE: 0,
REACTANT: 1,
PRODUCT: 2,
AGENT: 3
};
var SHOULD_REACTION_FRAGMENT_RELAYOUT = true;
var SHOULD_RESCALE_MOLECULES = true;
function rxnMerge(mols, nReactants, nProducts) /* Struct */ { // eslint-disable-line max-statements
/* reader */
var ret = new Struct();
var bbReact = [],
bbAgent = [],
bbProd = [];
var molReact = [],
molAgent = [],
molProd = [];
var j;
var bondLengthData = { cnt: 0, totalLength: 0 };
for (j = 0; j < mols.length; ++j) {
var mol = mols[j];
var bondLengthDataMol = mol.getBondLengthData();
bondLengthData.cnt += bondLengthDataMol.cnt;
bondLengthData.totalLength += bondLengthDataMol.totalLength;
}
if (SHOULD_RESCALE_MOLECULES) {
var avgBondLength = 1 / (bondLengthData.cnt == 0 ? 1 : bondLengthData.totalLength / bondLengthData.cnt);
for (j = 0; j < mols.length; ++j) {
mol = mols[j];
mol.scale(avgBondLength);
}
}
for (j = 0; j < mols.length; ++j) {
mol = mols[j];
var bb = mol.getCoordBoundingBoxObj();
if (!bb)
continue; // eslint-disable-line no-continue
var fragmentType = (j < nReactants ? FRAGMENT.REACTANT : // eslint-disable-line no-nested-ternary
(j < nReactants + nProducts ? FRAGMENT.PRODUCT :
FRAGMENT.AGENT));
if (fragmentType == FRAGMENT.REACTANT) {
bbReact.push(bb);
molReact.push(mol);
} else if (fragmentType == FRAGMENT.AGENT) {
bbAgent.push(bb);
molAgent.push(mol);
} else if (fragmentType == FRAGMENT.PRODUCT) {
bbProd.push(bb);
molProd.push(mol);
}
mol.atoms.each(function (aid, atom) {
atom.rxnFragmentType = fragmentType;
});
}
function shiftMol(ret, mol, bb, xorig, over) { // eslint-disable-line max-params
var d = new Vec2(xorig - bb.min.x, over ? 1 - bb.min.y : -(bb.min.y + bb.max.y) / 2);
mol.atoms.each(function (aid, atom) {
atom.pp.add_(d); // eslint-disable-line no-underscore-dangle
});
mol.sgroups.each(function (id, item) {
if (item.pp)
item.pp.add_(d); // eslint-disable-line no-underscore-dangle
});
bb.min.add_(d); // eslint-disable-line no-underscore-dangle
bb.max.add_(d); // eslint-disable-line no-underscore-dangle
mol.mergeInto(ret);
return bb.max.x - bb.min.x;
}
if (SHOULD_REACTION_FRAGMENT_RELAYOUT) {
// reaction fragment layout
var xorig = 0;
for (j = 0; j < molReact.length; ++j)
xorig += shiftMol(ret, molReact[j], bbReact[j], xorig, false) + 2.0;
xorig += 2.0;
for (j = 0; j < molAgent.length; ++j)
xorig += shiftMol(ret, molAgent[j], bbAgent[j], xorig, true) + 2.0;
xorig += 2.0;
for (j = 0; j < molProd.length; ++j)
xorig += shiftMol(ret, molProd[j], bbProd[j], xorig, false) + 2.0;
} else {
for (j = 0; j < molReact.length; ++j)
molReact[j].mergeInto(ret);
for (j = 0; j < molAgent.length; ++j)
molAgent[j].mergeInto(ret);
for (j = 0; j < molProd.length; ++j)
molProd[j].mergeInto(ret);
}
var bb1;
var bb2;
var x;
var y;
var bbReactAll = null;
var bbProdAll = null;
for (j = 0; j < bbReact.length - 1; ++j) {
bb1 = bbReact[j];
bb2 = bbReact[j + 1];
x = (bb1.max.x + bb2.min.x) / 2;
y = (bb1.max.y + bb1.min.y + bb2.max.y + bb2.min.y) / 4;
ret.rxnPluses.add(new Struct.RxnPlus({ pp: new Vec2(x, y) }));
}
for (j = 0; j < bbReact.length; ++j) {
if (j == 0) {
bbReactAll = {};
bbReactAll.max = new Vec2(bbReact[j].max);
bbReactAll.min = new Vec2(bbReact[j].min);
} else {
bbReactAll.max = Vec2.max(bbReactAll.max, bbReact[j].max);
bbReactAll.min = Vec2.min(bbReactAll.min, bbReact[j].min);
}
}
for (j = 0; j < bbProd.length - 1; ++j) {
bb1 = bbProd[j];
bb2 = bbProd[j + 1];
x = (bb1.max.x + bb2.min.x) / 2;
y = (bb1.max.y + bb1.min.y + bb2.max.y + bb2.min.y) / 4;
ret.rxnPluses.add(new Struct.RxnPlus({ pp: new Vec2(x, y) }));
}
for (j = 0; j < bbProd.length; ++j) {
if (j == 0) {
bbProdAll = {};
bbProdAll.max = new Vec2(bbProd[j].max);
bbProdAll.min = new Vec2(bbProd[j].min);
} else {
bbProdAll.max = Vec2.max(bbProdAll.max, bbProd[j].max);
bbProdAll.min = Vec2.min(bbProdAll.min, bbProd[j].min);
}
}
bb1 = bbReactAll;
bb2 = bbProdAll;
if (!bb1 && !bb2) {
ret.rxnArrows.add(new Struct.RxnArrow({ pp: new Vec2(0, 0) }));
} else {
var v1 = bb1 ? new Vec2(bb1.max.x, (bb1.max.y + bb1.min.y) / 2) : null;
var v2 = bb2 ? new Vec2(bb2.min.x, (bb2.max.y + bb2.min.y) / 2) : null;
var defaultOffset = 3;
if (!v1)
v1 = new Vec2(v2.x - defaultOffset, v2.y);
if (!v2)
v2 = new Vec2(v1.x + defaultOffset, v1.y);
ret.rxnArrows.add(new Struct.RxnArrow({ pp: Vec2.lc2(v1, 0.5, v2, 0.5) }));
}
ret.isReaction = true;
return ret;
}
module.exports = {
fmtInfo: fmtInfo,
paddedNum: paddedNum,
parseDecimalInt: parseDecimalInt,
partitionLine: partitionLine,
partitionLineFixed: partitionLineFixed,
rxnMerge: rxnMerge
};

View File

@ -0,0 +1,426 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Map = require('../../util/map');
var element = require('./../element');
var Struct = require('./../struct/index');
var sGroup = require('./parseSGroup');
var utils = require('./utils');
var loadRGroupFragments = true; // TODO: set to load the fragments
function parseAtomLine(atomLine) {
/* reader */
var atomSplit = utils.partitionLine(atomLine, utils.fmtInfo.atomLinePartition);
var params =
{
// generic
pp: new Vec2(parseFloat(atomSplit[0]), -parseFloat(atomSplit[1]), parseFloat(atomSplit[2])),
label: atomSplit[4].trim(),
explicitValence: utils.fmtInfo.valenceMap[utils.parseDecimalInt(atomSplit[10])],
// obsolete
massDifference: utils.parseDecimalInt(atomSplit[5]),
charge: utils.fmtInfo.chargeMap[utils.parseDecimalInt(atomSplit[6])],
// query
hCount: utils.parseDecimalInt(utils.parseDecimalInt(atomSplit[8])),
stereoCare: utils.parseDecimalInt(atomSplit[9]) != 0,
// reaction
aam: utils.parseDecimalInt(atomSplit[14]),
invRet: utils.parseDecimalInt(atomSplit[15]),
// reaction query
exactChangeFlag: utils.parseDecimalInt(atomSplit[16]) != 0
};
return new Struct.Atom(params);
}
function parseBondLine(bondLine) {
/* reader */
var bondSplit = utils.partitionLine(bondLine, utils.fmtInfo.bondLinePartition);
var params =
{
begin: utils.parseDecimalInt(bondSplit[0]) - 1,
end: utils.parseDecimalInt(bondSplit[1]) - 1,
type: utils.fmtInfo.bondTypeMap[utils.parseDecimalInt(bondSplit[2])],
stereo: utils.fmtInfo.bondStereoMap[utils.parseDecimalInt(bondSplit[3])],
xxx: bondSplit[4],
topology: utils.fmtInfo.bondTopologyMap[utils.parseDecimalInt(bondSplit[5])],
reactingCenterStatus: utils.parseDecimalInt(bondSplit[6])
};
return new Struct.Bond(params);
}
function parseAtomListLine(/* string */atomListLine) {
/* reader */
var split = utils.partitionLine(atomListLine, utils.fmtInfo.atomListHeaderPartition);
var number = utils.parseDecimalInt(split[0]) - 1;
var notList = (split[2].trim() == 'T');
var count = utils.parseDecimalInt(split[4].trim());
var ids = atomListLine.slice(utils.fmtInfo.atomListHeaderLength);
var list = [];
var itemLength = utils.fmtInfo.atomListHeaderItemLength;
for (var i = 0; i < count; ++i)
list[i] = utils.parseDecimalInt(ids.slice(i * itemLength, ((i + 1) * itemLength) - 1));
return {
aid: number,
atomList: new Struct.AtomList({
notList: notList,
ids: list
})
};
}
function parsePropertyLines(ctab, ctabLines, shift, end, sGroups, rLogic) { // eslint-disable-line max-statements, max-params
/* reader */
var props = new Map();
while (shift < end) {
var line = ctabLines[shift];
if (line.charAt(0) == 'A') {
var propValue = ctabLines[++shift];
var isPseudo = /'.+'/.test(propValue);
if (isPseudo && !props.get('pseudo'))
props.set('pseudo', new Map());
if (!isPseudo && !props.get('alias'))
props.set('alias', new Map());
if (isPseudo) propValue = propValue.replace(/'/g, '');
props.get(isPseudo ? 'pseudo' : 'alias').set(utils.parseDecimalInt(line.slice(3, 6)) - 1, propValue);
} else if (line.charAt(0) == 'M') {
var type = line.slice(3, 6);
var propertyData = line.slice(6);
if (type == 'END') {
break;
} else if (type == 'CHG') {
if (!props.get('charge'))
props.set('charge', new Map());
props.get('charge').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'RAD') {
if (!props.get('radical'))
props.set('radical', new Map());
props.get('radical').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'ISO') {
if (!props.get('isotope'))
props.set('isotope', new Map());
props.get('isotope').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'RBC') {
if (!props.get('ringBondCount'))
props.set('ringBondCount', new Map());
props.get('ringBondCount').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'SUB') {
if (!props.get('substitutionCount'))
props.set('substitutionCount', new Map());
props.get('substitutionCount').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'UNS') {
if (!props.get('unsaturatedAtom'))
props.set('unsaturatedAtom', new Map());
props.get('unsaturatedAtom').update(sGroup.readKeyValuePairs(propertyData));
// else if (type == "LIN") // link atom
} else if (type == 'RGP') { // rgroup atom
if (!props.get('rglabel'))
props.set('rglabel', new Map());
var rglabels = props.get('rglabel');
var a2rs = sGroup.readKeyMultiValuePairs(propertyData);
for (var a2ri = 0; a2ri < a2rs.length; a2ri++) {
var a2r = a2rs[a2ri];
rglabels.set(a2r[0], (rglabels.get(a2r[0]) || 0) | (1 << (a2r[1] - 1)));
}
} else if (type == 'LOG') { // rgroup atom
propertyData = propertyData.slice(4);
var rgid = utils.parseDecimalInt(propertyData.slice(0, 3).trim());
var iii = utils.parseDecimalInt(propertyData.slice(4, 7).trim());
var hhh = utils.parseDecimalInt(propertyData.slice(8, 11).trim());
var ooo = propertyData.slice(12).trim();
var logic = {};
if (iii > 0)
logic.ifthen = iii;
logic.resth = hhh == 1;
logic.range = ooo;
rLogic[rgid] = logic;
} else if (type == 'APO') {
if (!props.get('attpnt'))
props.set('attpnt', new Map());
props.get('attpnt').update(sGroup.readKeyValuePairs(propertyData));
} else if (type == 'ALS') { // atom list
if (!props.get('atomList'))
props.set('atomList', new Map());
var list = parsePropertyLineAtomList(
utils.partitionLine(propertyData, [1, 3, 3, 1, 1, 1]),
utils.partitionLineFixed(propertyData.slice(10), 4, false));
props.get('atomList').update(
list);
if (!props.get('label'))
props.set('label', new Map());
for (var aid in list) props.get('label').set(aid, 'L#');
} else if (type == 'STY') { // introduce s-group
sGroup.initSGroup(sGroups, propertyData);
} else if (type == 'SST') {
sGroup.applySGroupProp(sGroups, 'subtype', propertyData);
} else if (type == 'SLB') {
sGroup.applySGroupProp(sGroups, 'label', propertyData, true);
} else if (type == 'SPL') {
sGroup.applySGroupProp(sGroups, 'parent', propertyData, true, true);
} else if (type == 'SCN') {
sGroup.applySGroupProp(sGroups, 'connectivity', propertyData);
} else if (type == 'SAL') {
sGroup.applySGroupArrayProp(sGroups, 'atoms', propertyData, -1);
} else if (type == 'SBL') {
sGroup.applySGroupArrayProp(sGroups, 'bonds', propertyData, -1);
} else if (type == 'SPA') {
sGroup.applySGroupArrayProp(sGroups, 'patoms', propertyData, -1);
} else if (type == 'SMT') {
var sid = utils.parseDecimalInt(propertyData.slice(0, 4)) - 1;
sGroups[sid].data.subscript = propertyData.slice(4).trim();
} else if (type == 'SDT') {
sGroup.applyDataSGroupDesc(sGroups, propertyData);
} else if (type == 'SDD') {
sGroup.applyDataSGroupInfoLine(sGroups, propertyData);
} else if (type == 'SCD') {
sGroup.applyDataSGroupDataLine(sGroups, propertyData, false);
} else if (type == 'SED') {
sGroup.applyDataSGroupDataLine(sGroups, propertyData, true);
}
}
++shift;
}
return props;
}
function applyAtomProp(atoms /* Pool */, values /* Map */, propId /* string */) {
/* reader */
values.each(function (aid, propVal) {
atoms.get(aid)[propId] = propVal;
});
}
function parseCTabV2000(ctabLines, countsSplit) { // eslint-disable-line max-statements
/* reader */
var ctab = new Struct();
var i;
var atomCount = utils.parseDecimalInt(countsSplit[0]);
var bondCount = utils.parseDecimalInt(countsSplit[1]);
var atomListCount = utils.parseDecimalInt(countsSplit[2]);
ctab.isChiral = utils.parseDecimalInt(countsSplit[4]) != 0;
var stextLinesCount = utils.parseDecimalInt(countsSplit[5]);
var propertyLinesCount = utils.parseDecimalInt(countsSplit[10]);
var shift = 0;
var atomLines = ctabLines.slice(shift, shift + atomCount);
shift += atomCount;
var bondLines = ctabLines.slice(shift, shift + bondCount);
shift += bondCount;
var atomListLines = ctabLines.slice(shift, shift + atomListCount);
shift += atomListCount + stextLinesCount;
var atoms = atomLines.map(parseAtomLine);
for (i = 0; i < atoms.length; ++i)
ctab.atoms.add(atoms[i]);
var bonds = bondLines.map(parseBondLine);
for (i = 0; i < bonds.length; ++i)
ctab.bonds.add(bonds[i]);
var atomLists = atomListLines.map(parseAtomListLine);
atomLists.forEach(function (pair) {
ctab.atoms.get(pair.aid).atomList = pair.atomList;
ctab.atoms.get(pair.aid).label = 'L#';
});
var sGroups = {};
var rLogic = {};
var props = parsePropertyLines(ctab, ctabLines, shift,
Math.min(ctabLines.length, shift + propertyLinesCount), sGroups, rLogic);
props.each(function (propId, values) {
applyAtomProp(ctab.atoms, values, propId);
});
var atomMap = {};
var sid;
for (sid in sGroups) {
var sg = sGroups[sid];
if (sg.type === 'DAT' && sg.atoms.length === 0) {
var parent = sGroups[sid].parent;
if (parent >= 0) {
var psg = sGroups[parent - 1];
if (psg.type === 'GEN')
sg.atoms = [].slice.call(psg.atoms);
}
}
}
for (sid in sGroups)
sGroup.loadSGroup(ctab, sGroups[sid], atomMap);
var emptyGroups = [];
for (sid in sGroups) { // TODO: why do we need that?
Struct.SGroup.filter(ctab, sGroups[sid], atomMap);
if (sGroups[sid].atoms.length == 0 && !sGroups[sid].allAtoms)
emptyGroups.push(sid);
}
for (i = 0; i < emptyGroups.length; ++i) {
ctab.sGroupForest.remove(emptyGroups[i]);
ctab.sgroups.remove(emptyGroups[i]);
}
for (var rgid in rLogic)
ctab.rgroups.set(rgid, new Struct.RGroup(rLogic[rgid]));
return ctab;
}
function parseRg2000(/* string[] */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
ctabLines = ctabLines.slice(7);
if (ctabLines[0].trim() != '$CTAB')
throw new Error('RGFile format invalid');
var i = 1;
while (ctabLines[i].charAt(0) != '$') i++;
if (ctabLines[i].trim() != '$END CTAB')
throw new Error('RGFile format invalid');
var coreLines = ctabLines.slice(1, i);
ctabLines = ctabLines.slice(i + 1);
var fragmentLines = {};
while (true) { // eslint-disable-line no-constant-condition
if (ctabLines.length == 0)
throw new Error('Unexpected end of file');
var line = ctabLines[0].trim();
if (line == '$END MOL') {
ctabLines = ctabLines.slice(1);
break;
}
if (line != '$RGP')
throw new Error('RGFile format invalid');
var rgid = ctabLines[1].trim() - 0;
fragmentLines[rgid] = [];
ctabLines = ctabLines.slice(2);
while (true) { // eslint-disable-line no-constant-condition
if (ctabLines.length == 0)
throw new Error('Unexpected end of file');
line = ctabLines[0].trim();
if (line == '$END RGP') {
ctabLines = ctabLines.slice(1);
break;
}
if (line != '$CTAB')
throw new Error('RGFile format invalid');
i = 1;
while (ctabLines[i].charAt(0) != '$') i++;
if (ctabLines[i].trim() != '$END CTAB')
throw new Error('RGFile format invalid');
fragmentLines[rgid].push(ctabLines.slice(1, i));
ctabLines = ctabLines.slice(i + 1);
}
}
var core = parseCTab(coreLines);
var frag = {};
if (loadRGroupFragments) {
for (var id in fragmentLines) {
frag[id] = [];
for (var j = 0; j < fragmentLines[id].length; ++j)
frag[id].push(parseCTab(fragmentLines[id][j]));
}
}
return rgMerge(core, frag);
}
function parseRxn2000(/* string[] */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
/* reader */
ctabLines = ctabLines.slice(4);
var countsSplit = utils.partitionLine(ctabLines[0], utils.fmtInfo.rxnItemsPartition);
var nReactants = countsSplit[0] - 0,
nProducts = countsSplit[1] - 0,
nAgents = countsSplit[2] - 0;
ctabLines = ctabLines.slice(1); // consume counts line
var mols = [];
while (ctabLines.length > 0 && ctabLines[0].substr(0, 4) == '$MOL') {
ctabLines = ctabLines.slice(1);
var n = 0;
while (n < ctabLines.length && ctabLines[n].substr(0, 4) != '$MOL') n++;
var lines = ctabLines.slice(0, n);
var struct;
if (lines[0].search('\\$MDL') == 0) {
struct = parseRg2000(lines);
} else {
struct = parseCTab(lines.slice(3));
struct.name = lines[0].trim();
}
mols.push(struct);
ctabLines = ctabLines.slice(n);
}
return utils.rxnMerge(mols, nReactants, nProducts, nAgents);
}
function parseCTab(/* string */ ctabLines) /* Struct */ {
/* reader */
var countsSplit = utils.partitionLine(ctabLines[0], utils.fmtInfo.countsLinePartition);
ctabLines = ctabLines.slice(1);
return parseCTabV2000(ctabLines, countsSplit);
}
function rgMerge(scaffold, rgroups) /* Struct */ {
/* reader */
var ret = new Struct();
scaffold.mergeInto(ret, null, null, false, true);
for (var rgid in rgroups) {
for (var j = 0; j < rgroups[rgid].length; ++j) {
var ctab = rgroups[rgid][j];
ctab.rgroups.set(rgid, new Struct.RGroup());
var frag = {};
var frid = ctab.frags.add(frag);
ctab.rgroups.get(rgid).frags.add(frid);
ctab.atoms.each(function (aid, atom) {
atom.fragment = frid;
});
ctab.mergeInto(ret);
}
}
return ret;
}
function labelsListToIds(labels) {
/* reader */
var ids = [];
for (var i = 0; i < labels.length; ++i)
ids.push(element.map[labels[i].trim()]);
return ids;
}
function parsePropertyLineAtomList(hdr, lst) {
/* reader */
var aid = utils.parseDecimalInt(hdr[1]) - 1;
var count = utils.parseDecimalInt(hdr[2]);
var notList = hdr[4].trim() == 'T';
var ids = labelsListToIds(lst.slice(0, count));
var ret = {};
ret[aid] = new Struct.AtomList({
notList: notList,
ids: ids
});
return ret;
}
module.exports = {
parseCTabV2000: parseCTabV2000,
parseRg2000: parseRg2000,
parseRxn2000: parseRxn2000
};

View File

@ -0,0 +1,482 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var element = require('./../element');
var Struct = require('./../struct/index');
var sGroup = require('./parseSGroup');
var utils = require('./utils');
function parseAtomLineV3000(line) { // eslint-disable-line max-statements
/* reader */
var split, subsplit, key, value, i;
split = spaceparsplit(line);
var params = {
pp: new Vec2(parseFloat(split[2]), -parseFloat(split[3]), parseFloat(split[4])),
aam: split[5].trim()
};
var label = split[1].trim();
if (label.charAt(0) == '"' && label.charAt(label.length - 1) == '"')
label = label.substr(1, label.length - 2); // strip qutation marks
if (label.charAt(label.length - 1) == ']') { // assume atom list
label = label.substr(0, label.length - 1); // remove ']'
var atomListParams = {};
atomListParams.notList = false;
if (label.substr(0, 5) == 'NOT [') {
atomListParams.notList = true;
label = label.substr(5); // remove 'NOT ['
} else if (label.charAt(0) != '[') {
throw new Error('Error: atom list expected, found \'' + label + '\'');
} else {
label = label.substr(1); // remove '['
}
atomListParams.ids = labelsListToIds(label.split(','));
params['atomList'] = new Struct.AtomList(atomListParams);
params['label'] = 'L#';
} else {
params['label'] = label;
}
split.splice(0, 6);
for (i = 0; i < split.length; ++i) {
subsplit = splitonce(split[i], '=');
key = subsplit[0];
value = subsplit[1];
if (key in utils.fmtInfo.v30atomPropMap) {
var ival = utils.parseDecimalInt(value);
if (key == 'VAL') {
if (ival == 0)
continue; // eslint-disable-line no-continue
if (ival == -1)
ival = 0;
}
params[utils.fmtInfo.v30atomPropMap[key]] = ival;
} else if (key == 'RGROUPS') {
value = value.trim().substr(1, value.length - 2);
var rgrsplit = value.split(' ').slice(1);
params.rglabel = 0;
for (var j = 0; j < rgrsplit.length; ++j)
params.rglabel |= 1 << (rgrsplit[j] - 1);
} else if (key == 'ATTCHPT') {
params.attpnt = value.trim() - 0;
}
}
return new Struct.Atom(params);
}
function parseBondLineV3000(line) {
/* reader */
var split, subsplit, key, value, i;
split = spaceparsplit(line);
var params = {
begin: utils.parseDecimalInt(split[2]) - 1,
end: utils.parseDecimalInt(split[3]) - 1,
type: utils.fmtInfo.bondTypeMap[utils.parseDecimalInt(split[1])]
};
split.splice(0, 4);
for (i = 0; i < split.length; ++i) {
subsplit = splitonce(split[i], '=');
key = subsplit[0];
value = subsplit[1];
if (key == 'CFG') {
params.stereo = utils.fmtInfo.v30bondStereoMap[utils.parseDecimalInt(value)];
if (params.type == Struct.Bond.PATTERN.TYPE.DOUBLE && params.stereo == Struct.Bond.PATTERN.STEREO.EITHER)
params.stereo = Struct.Bond.PATTERN.STEREO.CIS_TRANS;
} else if (key == 'TOPO') {
params.topology = utils.fmtInfo.bondTopologyMap[utils.parseDecimalInt(value)];
} else if (key == 'RXCTR') {
params.reactingCenterStatus = utils.parseDecimalInt(value);
} else if (key == 'STBOX') {
params.stereoCare = utils.parseDecimalInt(value);
}
}
return new Struct.Bond(params);
}
function v3000parseCollection(ctab, ctabLines, shift) {
/* reader */
shift++;
while (ctabLines[shift].trim() != 'M V30 END COLLECTION')
shift++;
shift++;
return shift;
}
function v3000parseSGroup(ctab, ctabLines, sgroups, atomMap, shift) { // eslint-disable-line max-params, max-statements
/* reader */
var line = '';
shift++;
while (shift < ctabLines.length) {
line = stripV30(ctabLines[shift++]).trim();
if (line.trim() == 'END SGROUP')
return shift;
while (line.charAt(line.length - 1) == '-')
line = (line.substr(0, line.length - 1) + stripV30(ctabLines[shift++])).trim();
var split = splitSGroupDef(line);
var type = split[1];
var sg = new Struct.SGroup(type);
sg.number = split[0] - 0;
sg.type = type;
sg.label = split[2] - 0;
sgroups[sg.number] = sg;
var props = {};
for (var i = 3; i < split.length; ++i) {
var subsplit = splitonce(split[i], '=');
if (subsplit.length != 2)
throw new Error('A record of form AAA=BBB or AAA=(...) expected, got \'' + split[i] + '\'');
var name = subsplit[0];
if (!(name in props))
props[name] = [];
props[name].push(subsplit[1]);
}
sg.atoms = parseBracedNumberList(props['ATOMS'][0], -1);
if (props['PATOMS'])
sg.patoms = parseBracedNumberList(props['PATOMS'][0], -1);
sg.bonds = props['BONDS'] ? parseBracedNumberList(props['BONDS'][0], -1) : [];
var brkxyzStrs = props['BRKXYZ'];
sg.brkxyz = [];
if (brkxyzStrs) {
for (var j = 0; j < brkxyzStrs.length; ++j)
sg.brkxyz.push(parseBracedNumberList(brkxyzStrs[j]));
}
if (props['MULT'])
sg.data.subscript = props['MULT'][0] - 0;
if (props['LABEL'])
sg.data.subscript = props['LABEL'][0].trim();
if (props['CONNECT'])
sg.data.connectivity = props['CONNECT'][0].toLowerCase();
if (props['FIELDDISP'])
sGroup.applyDataSGroupInfo(sg, stripQuotes(props['FIELDDISP'][0]));
if (props['FIELDDATA'])
sGroup.applyDataSGroupData(sg, props['FIELDDATA'][0], true);
if (props['FIELDNAME'])
sGroup.applyDataSGroupName(sg, props['FIELDNAME'][0]);
if (props['QUERYTYPE'])
sGroup.applyDataSGroupQuery(sg, props['QUERYTYPE'][0]);
if (props['QUERYOP'])
sGroup.applyDataSGroupQueryOp(sg, props['QUERYOP'][0]);
sGroup.loadSGroup(ctab, sg, atomMap);
}
throw new Error('S-group declaration incomplete.');
}
function parseCTabV3000(ctabLines, norgroups) { // eslint-disable-line max-statements
/* reader */
var ctab = new Struct();
var shift = 0;
if (ctabLines[shift++].trim() != 'M V30 BEGIN CTAB')
throw Error('CTAB V3000 invalid');
if (ctabLines[shift].slice(0, 13) != 'M V30 COUNTS')
throw Error('CTAB V3000 invalid');
var vals = ctabLines[shift].slice(14).split(' ');
ctab.isChiral = (utils.parseDecimalInt(vals[4]) == 1);
shift++;
if (ctabLines[shift].trim() == 'M V30 BEGIN ATOM') {
shift++;
var line;
while (shift < ctabLines.length) {
line = stripV30(ctabLines[shift++]).trim();
if (line == 'END ATOM')
break;
while (line.charAt(line.length - 1) == '-')
line = (line.substring(0, line.length - 1) + stripV30(ctabLines[shift++])).trim();
ctab.atoms.add(parseAtomLineV3000(line));
}
if (ctabLines[shift].trim() == 'M V30 BEGIN BOND') {
shift++;
while (shift < ctabLines.length) {
line = stripV30(ctabLines[shift++]).trim();
if (line == 'END BOND')
break;
while (line.charAt(line.length - 1) == '-')
line = (line.substring(0, line.length - 1) + stripV30(ctabLines[shift++])).trim();
ctab.bonds.add(parseBondLineV3000(line));
}
}
// TODO: let sections follow in arbitrary order
var sgroups = {};
var atomMap = {};
while (ctabLines[shift].trim() != 'M V30 END CTAB') {
if (ctabLines[shift].trim() == 'M V30 BEGIN COLLECTION')
// TODO: read collection information
shift = v3000parseCollection(ctab, ctabLines, shift);
else if (ctabLines[shift].trim() == 'M V30 BEGIN SGROUP')
shift = v3000parseSGroup(ctab, ctabLines, sgroups, atomMap, shift);
else
throw Error('CTAB V3000 invalid');
}
}
if (ctabLines[shift++].trim() != 'M V30 END CTAB')
throw Error('CTAB V3000 invalid');
if (!norgroups)
readRGroups3000(ctab, ctabLines.slice(shift));
return ctab;
}
function readRGroups3000(ctab, /* string */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
/* reader */
var rfrags = {};
var rLogic = {};
var shift = 0;
while (shift < ctabLines.length && ctabLines[shift].search('M V30 BEGIN RGROUP') == 0) {
var id = ctabLines[shift++].split(' ').pop();
rfrags[id] = [];
rLogic[id] = {};
while (true) { // eslint-disable-line no-constant-condition
var line = ctabLines[shift].trim();
if (line.search('M V30 RLOGIC') == 0) {
line = line.slice(13);
var rlsplit = line.trim().split(/\s+/g);
var iii = utils.parseDecimalInt(rlsplit[0]);
var hhh = utils.parseDecimalInt(rlsplit[1]);
var ooo = rlsplit.slice(2).join(' ');
var logic = {};
if (iii > 0)
logic.ifthen = iii;
logic.resth = hhh == 1;
logic.range = ooo;
rLogic[id] = logic;
shift++;
continue; // eslint-disable-line no-continue
}
if (line != 'M V30 BEGIN CTAB')
throw Error('CTAB V3000 invalid');
for (var i = 0; i < ctabLines.length; ++i) {
if (ctabLines[shift + i].trim() == 'M V30 END CTAB')
break;
}
var lines = ctabLines.slice(shift, shift + i + 1);
var rfrag = parseCTabV3000(lines, true);
rfrags[id].push(rfrag);
shift = shift + i + 1;
if (ctabLines[shift].trim() == 'M V30 END RGROUP') {
shift++;
break;
}
}
}
for (var rgid in rfrags) {
for (var j = 0; j < rfrags[rgid].length; ++j) {
var rg = rfrags[rgid][j];
rg.rgroups.set(rgid, new Struct.RGroup(rLogic[rgid]));
var frid = rg.frags.add({});
rg.rgroups.get(rgid).frags.add(frid);
rg.atoms.each(function (aid, atom) {
atom.fragment = frid;
});
rg.mergeInto(ctab);
}
}
}
function parseRxn3000(/* string[] */ ctabLines) /* Struct */ { // eslint-disable-line max-statements
/* reader */
ctabLines = ctabLines.slice(4);
var countsSplit = ctabLines[0].split(/\s+/g).slice(3);
var nReactants = countsSplit[0] - 0,
nProducts = countsSplit[1] - 0,
nAgents = countsSplit.length > 2 ? countsSplit[2] - 0 : 0;
function findCtabEnd(i) {
for (var j = i; j < ctabLines.length; ++j) {
if (ctabLines[j].trim() == 'M V30 END CTAB')
return j;
}
console.error('CTab format invalid');
}
function findRGroupEnd(i) {
for (var j = i; j < ctabLines.length; ++j) {
if (ctabLines[j].trim() == 'M V30 END RGROUP')
return j;
}
console.error('CTab format invalid');
}
var molLinesReactants = [];
var molLinesProducts = [];
var current = null;
var rGroups = [];
for (var i = 0; i < ctabLines.length; ++i) {
var line = ctabLines[i].trim();
var j;
if (line.startsWith('M V30 COUNTS')) {
// do nothing
} else if (line == 'M END') {
break; // stop reading
} else if (line == 'M V30 BEGIN PRODUCT') {
console.assert(current == null, 'CTab format invalid');
current = molLinesProducts;
} else if (line == 'M V30 END PRODUCT') {
console.assert(current === molLinesProducts, 'CTab format invalid');
current = null;
} else if (line == 'M V30 BEGIN REACTANT') {
console.assert(current == null, 'CTab format invalid');
current = molLinesReactants;
} else if (line == 'M V30 END REACTANT') {
console.assert(current === molLinesReactants, 'CTab format invalid');
current = null;
} else if (line.startsWith('M V30 BEGIN RGROUP')) {
console.assert(current == null, 'CTab format invalid');
j = findRGroupEnd(i);
rGroups.push(ctabLines.slice(i, j + 1));
i = j;
} else if (line == 'M V30 BEGIN CTAB') {
j = findCtabEnd(i);
current.push(ctabLines.slice(i, j + 1));
i = j;
} else {
throw new Error('line unrecognized: ' + line);
}
}
var mols = [];
var molLines = molLinesReactants.concat(molLinesProducts);
for (j = 0; j < molLines.length; ++j) {
var mol = parseCTabV3000(molLines[j], countsSplit);
mols.push(mol);
}
var ctab = utils.rxnMerge(mols, nReactants, nProducts, nAgents);
readRGroups3000(ctab, function (array) {
var res = [];
for (var k = 0; k < array.length; ++k)
res = res.concat(array[k]);
return res;
}(rGroups));
return ctab;
}
// split a line by spaces outside parentheses
function spaceparsplit(line) { // eslint-disable-line max-statements
/* reader */
var split = [];
var pc = 0;
var c;
var i;
var i0 = -1;
var quoted = false;
for (i = 0; i < line.length; ++i) {
c = line[i];
if (c == '(')
pc++;
else if (c == ')')
pc--;
if (c == '"')
quoted = !quoted;
if (!quoted && line[i] == ' ' && pc == 0) {
if (i > i0 + 1)
split.push(line.slice(i0 + 1, i));
i0 = i;
}
}
if (i > i0 + 1)
split.push(line.slice(i0 + 1, i));
return split;
}
// utils
function stripQuotes(str) {
if (str[0] === '"' && str[str.length - 1] === '"')
return str.substr(1, str.length - 2);
return str;
}
function splitonce(line, delim) {
/* reader */
var p = line.indexOf(delim);
return [line.slice(0, p), line.slice(p + 1)];
}
function splitSGroupDef(line) { // eslint-disable-line max-statements
/* reader */
var split = [];
var braceBalance = 0;
var quoted = false;
for (var i = 0; i < line.length; ++i) {
var c = line.charAt(i);
if (c == '"') {
quoted = !quoted;
} else if (!quoted) {
if (c == '(') {
braceBalance++;
} else if (c == ')') {
braceBalance--;
} else if (c == ' ' && braceBalance == 0) {
split.push(line.slice(0, i));
line = line.slice(i + 1).trim();
i = 0;
}
}
}
if (braceBalance != 0)
throw new Error('Brace balance broken. S-group properies invalid!');
if (line.length > 0)
split.push(line.trim());
return split;
}
function parseBracedNumberList(line, shift) {
/* reader */
if (!line)
return null;
var list = [];
line = line.trim();
line = line.substr(1, line.length - 2);
var split = line.split(' ');
shift = shift || 0;
for (var i = 1; i < split.length; ++i) {
var value = parseInt(split[i]);
if (!isNaN(value))
list.push(value + shift);
}
return list;
}
function stripV30(line) {
/* reader */
if (line.slice(0, 7) != 'M V30 ')
throw new Error('Prefix invalid');
return line.slice(7);
}
function labelsListToIds(labels) {
/* reader */
var ids = [];
for (var i = 0; i < labels.length; ++i)
ids.push(element.map[labels[i].trim()]);
return ids;
}
module.exports = {
parseCTabV3000: parseCTabV3000,
readRGroups3000: readRGroups3000,
parseRxn3000: parseRxn3000
};

View File

@ -0,0 +1,64 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var molfile = require('./molfile');
function parse(str, options) {
var regexp = /^[^]+?\$\$\$\$$/gm;
var m, chunk;
var result = [];
while ((m = regexp.exec(str)) !== null) {
chunk = m[0].replace(/\r/g, ''); // TODO: normalize newline?
chunk = chunk.trim();
var end = chunk.indexOf('M END');
if (end !== -1) {
var item = {};
var propChunks = chunk.substr(end + 7).trim().split(/^$\n?/m);
item.struct = molfile.parse(chunk.substring(0, end + 6), options);
item.props = propChunks.reduce(function (props, pc) {
var m = pc.match(/^> [ \d]*<(\S+)>/);
if (m) {
var field = m[1];
var value = pc.split('\n')[1].trim();
props[field] = value;
}
return props;
}, {});
result.push(item);
}
}
return result;
}
function stringify(items, options) {
return items.reduce(function (res, item) {
res += molfile.stringify(item.struct, options);
for (var prop in item.props) {
res += "> <" + prop + ">\n";
res += item.props[prop] + "\n\n";
}
return res + '\$\$\$\$';
}, '');
}
module.exports = {
stringify: stringify,
parse: parse
};

View File

@ -0,0 +1,212 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Map = require('../../util/map');
var Vec2 = require('../../util/vec2');
var Struct = require('../struct');
function CisTrans(mol, neighborsFunc, context) {
this.molecule = mol;
this.bonds = new Map();
this.getNeighbors = neighborsFunc;
this.context = context;
}
CisTrans.PARITY = {
NONE: 0,
CIS: 1,
TRANS: 2
};
CisTrans.prototype.each = function (func, context) {
this.bonds.each(func, context);
};
CisTrans.prototype.getParity = function (idx) {
return this.bonds.get(idx).parity;
};
CisTrans.prototype.getSubstituents = function (idx) {
return this.bonds.get(idx).substituents;
};
CisTrans.prototype.sameside = function (beg, end, neiBeg, neiEnd) {
var diff = Vec2.diff(beg, end);
var norm = new Vec2(-diff.y, diff.x);
if (!norm.normalize())
return 0;
var normBeg = Vec2.diff(neiBeg, beg);
var normEnd = Vec2.diff(neiEnd, end);
if (!normBeg.normalize())
return 0;
if (!normEnd.normalize())
return 0;
var prodBeg = Vec2.dot(normBeg, norm);
var prodEnd = Vec2.dot(normEnd, norm);
if (Math.abs(prodBeg) < 0.001 || Math.abs(prodEnd) < 0.001)
return 0;
return (prodBeg * prodEnd > 0) ? 1 : -1;
};
CisTrans.prototype.samesides = function (iBeg, iEnd, iNeiBeg, iNeiEnd) {
return this.sameside(this.molecule.atoms.get(iBeg).pp, this.molecule.atoms.get(iEnd).pp,
this.molecule.atoms.get(iNeiBeg).pp, this.molecule.atoms.get(iNeiEnd).pp);
};
CisTrans.prototype.sortSubstituents = function (substituents) { // eslint-disable-line max-statements
var h0 = this.molecule.atoms.get(substituents[0]).pureHydrogen();
var h1 = substituents[1] < 0 || this.molecule.atoms.get(substituents[1]).pureHydrogen();
var h2 = this.molecule.atoms.get(substituents[2]).pureHydrogen();
var h3 = substituents[3] < 0 || this.molecule.atoms.get(substituents[3]).pureHydrogen();
if (h0 && h1)
return false;
if (h2 && h3)
return false;
if (h1) {
substituents[1] = -1;
} else if (h0) {
substituents[0] = substituents[1];
substituents[1] = -1;
} else if (substituents[0] > substituents[1]) {
swap(substituents, 0, 1);
}
if (h3) {
substituents[3] = -1;
} else if (h2) {
substituents[2] = substituents[3];
substituents[3] = -1;
} else if (substituents[2] > substituents[3]) {
swap(substituents, 2, 3);
}
return true;
};
CisTrans.prototype.isGeomStereoBond = function (bondIdx, substituents) { // eslint-disable-line max-statements
// it must be [C,N,Si]=[C,N,Si] bond
var bond = this.molecule.bonds.get(bondIdx);
if (bond.type != Struct.Bond.PATTERN.TYPE.DOUBLE)
return false;
var label1 = this.molecule.atoms.get(bond.begin).label;
var label2 = this.molecule.atoms.get(bond.end).label;
if (label1 != 'C' && label1 != 'N' && label1 != 'Si' && label1 != 'Ge')
return false;
if (label2 != 'C' && label2 != 'N' && label2 != 'Si' && label2 != 'Ge')
return false;
// the atoms should have 1 or 2 single bonds
// (apart from the double bond under consideration)
var neiBegin = this.getNeighbors.call(this.context, bond.begin);
var neiЕnd = this.getNeighbors.call(this.context, bond.end);
if (
neiBegin.length < 2 || neiBegin.length > 3 ||
neiЕnd.length < 2 || neiЕnd.length > 3
)
return false;
substituents[0] = -1;
substituents[1] = -1;
substituents[2] = -1;
substituents[3] = -1;
var i;
var nei;
for (i = 0; i < neiBegin.length; i++) {
nei = neiBegin[i];
if (nei.bid == bondIdx)
continue; // eslint-disable-line no-continue
if (this.molecule.bonds.get(nei.bid).type != Struct.Bond.PATTERN.TYPE.SINGLE)
return false;
if (substituents[0] == -1)
substituents[0] = nei.aid;
else // (substituents[1] == -1)
substituents[1] = nei.aid;
}
for (i = 0; i < neiЕnd.length; i++) {
nei = neiЕnd[i];
if (nei.bid == bondIdx)
continue; // eslint-disable-line no-continue
if (this.molecule.bonds.get(nei.bid).type != Struct.Bond.PATTERN.TYPE.SINGLE)
return false;
if (substituents[2] == -1)
substituents[2] = nei.aid;
else // (substituents[3] == -1)
substituents[3] = nei.aid;
}
if (substituents[1] != -1 && this.samesides(bond.begin, bond.end, substituents[0], substituents[1]) != -1)
return false;
if (substituents[3] != -1 && this.samesides(bond.begin, bond.end, substituents[2], substituents[3]) != -1)
return false;
return true;
};
CisTrans.prototype.build = function (excludeBonds) {
this.molecule.bonds.each(function (bid, bond) {
var ct = this.bonds.set(bid,
{
parity: 0,
substituents: []
});
if (Array.isArray(excludeBonds) && excludeBonds[bid])
return;
if (!this.isGeomStereoBond(bid, ct.substituents))
return;
if (!this.sortSubstituents(ct.substituents))
return;
var sign = this.samesides(bond.begin, bond.end, ct.substituents[0], ct.substituents[2]);
if (sign == 1)
ct.parity = CisTrans.PARITY.CIS;
else if (sign == -1)
ct.parity = CisTrans.PARITY.TRANS;
}, this);
};
function swap(arr, i1, i2) {
var tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
module.exports = CisTrans;

View File

@ -0,0 +1,177 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Set = require('../../util/set');
function Dfs(mol, atomData, components, nReactants) {
this.molecule = mol;
this.atom_data = atomData;
this.components = components;
this.nComponentsInReactants = -1;
this.nReactants = nReactants;
this.vertices = new Array(this.molecule.atoms.count()); // Minimum size
this.molecule.atoms.each(function (aid) {
this.vertices[aid] = new Dfs.VertexDesc();
}, this);
this.edges = new Array(this.molecule.bonds.count()); // Minimum size
this.molecule.bonds.each(function (bid) {
this.edges[bid] = new Dfs.EdgeDesc();
}, this);
this.v_seq = [];
}
Dfs.VertexDesc = function () {
this.dfs_state = 0; // 0 -- not on stack
// 1 -- on stack
// 2 -- removed from stack
this.parent_vertex = 0; // parent vertex in DFS tree
this.parent_edge = 0; // edge to parent vertex
this.branches = 0; // how many DFS branches go out from this vertex}
};
Dfs.EdgeDesc = function () {
this.opening_cycles = 0; // how many cycles are
// (i) starting with this edge
// and (ii) ending in this edge's first vertex
this.closing_cycle = 0; // 1 if this edge closes a cycle
};
Dfs.SeqElem = function (vIdx, parVertex, parEdge) {
this.idx = vIdx; // index of vertex in _graph
this.parent_vertex = parVertex; // parent vertex in DFS tree
this.parent_edge = parEdge; // edge to parent vertex
};
Dfs.prototype.walk = function () { // eslint-disable-line max-statements
var vStack = [];
var i, j;
var cid = 0;
var component = 0;
while (true) { // eslint-disable-line no-constant-condition
if (vStack.length < 1) {
var selected = -1;
var findFunc = function (aid) { // eslint-disable-line func-style
if (this.vertices[aid].dfs_state == 0) {
selected = aid;
return true;
}
return false;
};
while (cid < this.components.length && selected == -1) {
selected = Set.find(this.components[cid], findFunc, this);
if (selected === null) {
selected = -1;
cid++;
}
if (cid == this.nReactants)
this.nComponentsInReactants = component;
}
if (selected < -1)
this.molecule.atoms.find(findFunc, this);
if (selected == -1)
break;
this.vertices[selected].parent_vertex = -1;
this.vertices[selected].parent_edge = -1;
vStack.push(selected);
component++;
}
var vIdx = vStack.pop();
var parentVertex = this.vertices[vIdx].parent_vertex;
var seqElem = new Dfs.SeqElem(vIdx, parentVertex, this.vertices[vIdx].parent_edge);
this.v_seq.push(seqElem);
this.vertices[vIdx].dfs_state = 2;
var atomD = this.atom_data[vIdx];
for (i = 0; i < atomD.neighbours.length; i++) {
var neiIdx = atomD.neighbours[i].aid;
var edgeIdx = atomD.neighbours[i].bid;
if (neiIdx == parentVertex)
continue; // eslint-disable-line no-continue
if (this.vertices[neiIdx].dfs_state == 2) {
this.edges[edgeIdx].closing_cycle = 1;
j = vIdx;
while (j != -1 && this.vertices[j].parent_vertex != neiIdx)
j = this.vertices[j].parent_vertex;
if (j == -1)
throw new Error('cycle unwind error');
this.edges[this.vertices[j].parent_edge].opening_cycles++;
this.vertices[vIdx].branches++;
seqElem = new Dfs.SeqElem(neiIdx, vIdx, edgeIdx);
this.v_seq.push(seqElem);
} else {
if (this.vertices[neiIdx].dfs_state == 1) {
j = vStack.indexOf(neiIdx);
if (j == -1) // eslint-disable-line max-depth
throw new Error('internal: removing vertex from stack');
vStack.splice(j, 1);
var parent = this.vertices[neiIdx].parent_vertex;
if (parent >= 0) // eslint-disable-line max-depth
this.vertices[parent].branches--;
}
this.vertices[vIdx].branches++;
this.vertices[neiIdx].parent_vertex = vIdx;
this.vertices[neiIdx].parent_edge = edgeIdx;
this.vertices[neiIdx].dfs_state = 1;
vStack.push(neiIdx);
}
}
}
};
Dfs.prototype.edgeClosingCycle = function (eIdx) {
return this.edges[eIdx].closing_cycle != 0;
};
Dfs.prototype.numBranches = function (vIdx) {
return this.vertices[vIdx].branches;
};
Dfs.prototype.numOpeningCycles = function (eIdx) {
return this.edges[eIdx].opening_cycles;
};
Dfs.prototype.toString = function () {
var str = '';
this.v_seq.each(function (seqElem) {
str += seqElem.idx + ' -> ';
});
str += '*';
return str;
};
module.exports = Dfs;

View File

@ -0,0 +1,735 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Set = require('../../util/set');
var Struct = require('../struct');
var CisTrans = require('./cis_trans');
var Dfs = require('./dfs');
var Stereocenters = require('./stereocenters');
function Smiles() {
this.smiles = '';
this.writtenAtoms = [];
this.writtenComponents = 0;
this.ignore_errors = false;
}
Smiles._Atom = function (hСount) { // eslint-disable-line no-underscore-dangle
this.neighbours = []; // Array of integer pairs {a, b}
this.aromatic = false; // has aromatic bond
this.lowercase = false; // aromatic and has to be written lowercase
this.chirality = 0; // 0 means no chirality, 1 means CCW pyramid, 2 means CW pyramid
this.branch_cnt = 0; // runs from 0 to (branches - 1)
this.paren_written = false;
this.h_count = hСount;
this.parent = -1;
};
// NB: only loops of length up to 6 are included here
Smiles.prototype.isBondInRing = function (bid) {
console.assert(this.inLoop, 'Init this.inLoop prior to calling this method');
return this.inLoop[bid];
};
Smiles.prototype.saveMolecule = function (molecule, ignoreErrors) { // eslint-disable-line max-statements
var i, j, k;
if (!ignoreErrors) this.ignore_errors = ignoreErrors;
// [RB]: KETCHER-498 (Incorrect smile-string for multiple Sgroup)
// TODO the fix is temporary, still need to implement error handling/reporting
// BEGIN
molecule = molecule.clone();
molecule.initHalfBonds();
molecule.initNeighbors();
molecule.sortNeighbors();
molecule.setImplicitHydrogen();
molecule.sgroups.each(function (sgid, sg) {
if (sg.type == 'MUL') {
try {
Struct.SGroup.prepareMulForSaving(sg, molecule);
} catch (ex) {
throw { message: 'Bad s-group (' + ex.message + ')' };
}
}
// 'SMILES data format doesn\'t support s-groups'
}, this);
// END
this.atoms = new Array(molecule.atoms.count());
molecule.atoms.each(function (aid, atom) {
this.atoms[aid] = new Smiles._Atom(atom.implicitH); // eslint-disable-line no-underscore-dangle
}, this);
// From the SMILES specification:
// Please note that only atoms on the following list
// can be considered aromatic: C, N, O, P, S, As, Se, and * (wildcard).
var allowedLowercase = ['B', 'C', 'N', 'O', 'P', 'S', 'Se', 'As'];
// Detect atoms that have aromatic bonds and count neighbours
molecule.bonds.each(function (bid, bond) {
if (bond.type == Struct.Bond.PATTERN.TYPE.AROMATIC) {
this.atoms[bond.begin].aromatic = true;
if (allowedLowercase.indexOf(molecule.atoms.get(bond.begin).label) != -1)
this.atoms[bond.begin].lowercase = true;
this.atoms[bond.end].aromatic = true;
if (allowedLowercase.indexOf(molecule.atoms.get(bond.end).label) != -1)
this.atoms[bond.end].lowercase = true;
}
this.atoms[bond.begin].neighbours.push({ aid: bond.end, bid: bid });
this.atoms[bond.end].neighbours.push({ aid: bond.begin, bid: bid });
}, this);
this.inLoop = (function () {
molecule.prepareLoopStructure();
var bondsInLoops = Set.empty();
molecule.loops.each(function (lid, loop) {
if (loop.hbs.length <= 6) {
Set.mergeIn(bondsInLoops, Set.fromList(loop.hbs.map(function (hbid) {
return molecule.halfBonds.get(hbid).bid;
})));
}
});
var inLoop = {};
Set.each(bondsInLoops, function (bid) {
inLoop[bid] = 1;
}, this);
return inLoop;
})();
this.touchedCistransbonds = 0;
this.markCisTrans(molecule);
var components = molecule.getComponents();
var componentsAll = components.reactants.concat(components.products);
var walk = new Dfs(molecule, this.atoms, componentsAll, components.reactants.length);
walk.walk();
this.atoms.forEach(function (atom) {
atom.neighbours = [];
}, this);
// fill up neighbor lists for the stereocenters calculation
for (i = 0; i < walk.v_seq.length; i++) {
var seqEl = walk.v_seq[i];
var vIdx = seqEl.idx;
var eIdx = seqEl.parent_edge;
var vPrevIdx = seqEl.parent_vertex;
if (eIdx >= 0) {
var atom = this.atoms[vIdx];
var openingCycles = walk.numOpeningCycles(eIdx);
for (j = 0; j < openingCycles; j++)
this.atoms[vPrevIdx].neighbours.push({ aid: -1, bid: -1 });
if (walk.edgeClosingCycle(eIdx)) {
for (k = 0; k < atom.neighbours.length; k++) {
if (atom.neighbours[k].aid == -1) { // eslint-disable-line max-depth
atom.neighbours[k].aid = vPrevIdx;
atom.neighbours[k].bid = eIdx;
break;
}
}
if (k == atom.neighbours.length)
throw new Error('internal: can not put closing bond to its place');
} else {
atom.neighbours.push({ aid: vPrevIdx, bid: eIdx });
atom.parent = vPrevIdx;
}
this.atoms[vPrevIdx].neighbours.push({ aid: vIdx, bid: eIdx });
}
}
try {
// detect chiral configurations
var stereocenters = new Stereocenters(molecule, function (idx) {
return this.atoms[idx].neighbours;
}, this);
stereocenters.buildFromBonds(this.ignore_errors);
stereocenters.each(function (atomIdx, sc) { // eslint-disable-line max-statements
// if (sc.type < MoleculeStereocenters::ATOM_AND)
// continue;
var implicitHIdx = -1;
if (sc.pyramid[3] == -1)
implicitHIdx = 3;
/*
else for (j = 0; j < 4; j++)
if (ignored_vertices[pyramid[j]])
{
implicit_h_idx = j;
break;
}
*/
var pyramidMapping = [];
var counter = 0;
var atom = this.atoms[atomIdx];
if (atom.parent != -1) {
for (k = 0; k < 4; k++) {
if (sc.pyramid[k] == atom.parent) {
pyramidMapping[counter++] = k;
break;
}
}
}
if (implicitHIdx != -1)
pyramidMapping[counter++] = implicitHIdx;
for (j = 0; j != atom.neighbours.length; j++) {
if (atom.neighbours[j].aid == atom.parent)
continue; // eslint-disable-line no-continue
for (k = 0; k < 4; k++) {
if (atom.neighbours[j].aid == sc.pyramid[k]) {
if (counter >= 4)
throw new Error('internal: pyramid overflow');
pyramidMapping[counter++] = k;
break;
}
}
}
if (counter == 4) {
// move the 'from' atom to the end
counter = pyramidMapping[0];
pyramidMapping[0] = pyramidMapping[1];
pyramidMapping[1] = pyramidMapping[2];
pyramidMapping[2] = pyramidMapping[3];
pyramidMapping[3] = counter;
} else if (counter != 3) {
throw new Error('cannot calculate chirality');
}
if (Stereocenters.isPyramidMappingRigid(pyramidMapping))
this.atoms[atomIdx].chirality = 1;
else
this.atoms[atomIdx].chirality = 2;
}, this);
} catch (ex) {
alert('Warning: ' + ex.message);
}
// write the SMILES itself
// cycle_numbers[i] == -1 means that the number is available
// cycle_numbers[i] == n means that the number is used by vertex n
var cycleNumbers = [];
cycleNumbers.push(0); // never used
var firstComponent = true;
for (i = 0; i < walk.v_seq.length; i++) {
seqEl = walk.v_seq[i];
vIdx = seqEl.idx;
eIdx = seqEl.parent_edge;
vPrevIdx = seqEl.parent_vertex;
var writeAtom = true;
if (vPrevIdx >= 0) {
if (walk.numBranches(vPrevIdx) > 1) {
if (this.atoms[vPrevIdx].branch_cnt > 0 && this.atoms[vPrevIdx].paren_written)
this.smiles += ')';
}
openingCycles = walk.numOpeningCycles(eIdx);
for (j = 0; j < openingCycles; j++) {
for (k = 1; k < cycleNumbers.length; k++) {
if (cycleNumbers[k] == -1) // eslint-disable-line max-depth
break;
}
if (k == cycleNumbers.length)
cycleNumbers.push(vPrevIdx);
else
cycleNumbers[k] = vPrevIdx;
this.writeCycleNumber(k);
}
if (vPrevIdx >= 0) {
var branches = walk.numBranches(vPrevIdx);
if (branches > 1 && this.atoms[vPrevIdx].branch_cnt < branches - 1) {
if (walk.edgeClosingCycle(eIdx)) { // eslint-disable-line max-depth
this.atoms[vPrevIdx].paren_written = false;
} else {
this.smiles += '(';
this.atoms[vPrevIdx].paren_written = true;
}
}
this.atoms[vPrevIdx].branch_cnt++;
if (this.atoms[vPrevIdx].branch_cnt > branches)
throw new Error('unexpected branch');
}
var bond = molecule.bonds.get(eIdx);
var dir = 0;
if (bond.type == Struct.Bond.PATTERN.TYPE.SINGLE)
dir = this.calcBondDirection(molecule, eIdx, vPrevIdx);
if ((dir == 1 && vIdx == bond.end) || (dir == 2 && vIdx == bond.begin))
this.smiles += '/';
else if ((dir == 2 && vIdx == bond.end) || (dir == 1 && vIdx == bond.begin))
this.smiles += '\\';
else if (bond.type == Struct.Bond.PATTERN.TYPE.ANY)
this.smiles += '~';
else if (bond.type == Struct.Bond.PATTERN.TYPE.DOUBLE)
this.smiles += '=';
else if (bond.type == Struct.Bond.PATTERN.TYPE.TRIPLE)
this.smiles += '#';
else if (bond.type == Struct.Bond.PATTERN.TYPE.AROMATIC &&
(!this.atoms[bond.begin].lowercase || !this.atoms[bond.end].lowercase || !this.isBondInRing(eIdx)))
this.smiles += ':'; // TODO: Check if this : is needed
else if (bond.type == Struct.Bond.PATTERN.TYPE.SINGLE && this.atoms[bond.begin].aromatic && this.atoms[bond.end].aromatic)
this.smiles += '-';
if (walk.edgeClosingCycle(eIdx)) {
for (j = 1; j < cycleNumbers.length; j++) {
if (cycleNumbers[j] == vIdx)
break;
}
if (j == cycleNumbers.length)
throw new Error('cycle number not found');
this.writeCycleNumber(j);
cycleNumbers[j] = -1;
writeAtom = false;
}
} else {
if (!firstComponent) {
this.smiles += (this.writtenComponents === walk.nComponentsInReactants &&
walk.nReactants !== 0) ? '>>' : '.'; // when walk.nReactants === 0 - not reaction
}
firstComponent = false;
this.writtenComponents++;
}
if (writeAtom) {
this.writeAtom(molecule, vIdx, this.atoms[vIdx].aromatic, this.atoms[vIdx].lowercase, this.atoms[vIdx].chirality);
this.writtenAtoms.push(seqEl.idx);
}
}
this.comma = false;
// this._writeStereogroups(mol, atoms);
this.writeRadicals(molecule);
// this._writePseudoAtoms(mol);
// this._writeHighlighting();
if (this.comma)
this.smiles += '|';
return this.smiles;
};
Smiles.prototype.writeCycleNumber = function (n) {
if (n > 0 && n < 10)
this.smiles += n;
else if (n >= 10 && n < 100)
this.smiles += '%' + n;
else if (n >= 100 && n < 1000)
this.smiles += '%%' + n;
else
throw new Error('bad cycle number: ' + n);
};
Smiles.prototype.writeAtom = function (mol, idx, aromatic, lowercase, chirality) { // eslint-disable-line max-params, max-statements
var atom = mol.atoms.get(idx);
var needBrackets = false;
var hydro = -1;
var aam = 0;
/*
if (mol.haveQueryAtoms())
{
query_atom = &mol.getQueryAtom(idx);
if (query_atom->type == QUERY_ATOM_RGROUP)
{
if (mol.getRGroups()->isRGroupAtom(idx))
{
const Array<int> &rg = mol.getRGroups()->getSiteRGroups(idx);
if (rg.length != 1)
throw Error("rgroup count %d", rg.length);
_output.printf("[&%d]", rg[0] + 1);
}
else
_output.printf("[&%d]", 1);
return;
}
}
*/
if (atom.label == 'A') {
this.smiles += '*';
return;
}
if (atom.label == 'R' || atom.label == 'R#') {
this.smiles += '[*]';
return;
}
// KETCHER-598 (Ketcher does not save AAM into reaction SMILES)
// BEGIN
// if (this.atom_atom_mapping)
// aam = atom_atom_mapping[idx];
aam = atom.aam;
// END
if (atom.label != 'C' && atom.label != 'P' &&
atom.label != 'N' && atom.label != 'S' &&
atom.label != 'O' && atom.label != 'Cl' &&
atom.label != 'F' && atom.label != 'Br' &&
atom.label != 'B' && atom.label != 'I')
needBrackets = true;
if (atom.explicitValence >= 0 || atom.radical != 0 || chirality > 0 ||
(aromatic && atom.label != 'C' && atom.label != 'O') ||
(aromatic && atom.label == 'C' && this.atoms[idx].neighbours.length < 3 && this.atoms[idx].h_count == 0))
hydro = this.atoms[idx].h_count;
var label = atom.label;
if (atom.atomList && !atom.atomList.notList) {
label = atom.atomList.label();
needBrackets = false; // atom list label already has brackets
} else if (atom.isPseudo() || (atom.atomList && atom.atomList.notList)) {
label = '*';
needBrackets = true;
} else if (chirality || atom.charge != 0 || atom.isotope > 0 || hydro >= 0 || aam > 0) {
needBrackets = true;
}
if (needBrackets) {
if (hydro == -1)
hydro = this.atoms[idx].h_count;
this.smiles += '[';
}
if (atom.isotope > 0)
this.smiles += atom.isotope;
if (lowercase)
this.smiles += label.toLowerCase();
else
this.smiles += label;
if (chirality > 0) {
if (chirality == 1)
this.smiles += '@';
else // chirality == 2
this.smiles += '@@';
if (atom.implicitH > 1)
throw new Error(atom.implicitH + ' implicit H near stereocenter');
}
if (atom.label != 'H') {
if (hydro > 1 || (hydro == 0 && !needBrackets))
this.smiles += 'H' + hydro;
else if (hydro == 1)
this.smiles += 'H';
}
if (atom.charge > 1)
this.smiles += '+' + atom.charge;
else if (atom.charge < -1)
this.smiles += atom.charge;
else if (atom.charge == 1)
this.smiles += '+';
else if (atom.charge == -1)
this.smiles += '-';
if (aam > 0)
this.smiles += ':' + aam;
if (needBrackets)
this.smiles += ']';
/*
if (mol.getRGroupFragment() != 0)
{
for (i = 0; i < 2; i++)
{
int j;
for (j = 0; mol.getRGroupFragment()->getAttachmentPoint(i, j) != -1; j++)
if (idx == mol.getRGroupFragment()->getAttachmentPoint(i, j))
{
_output.printf("([*])");
break;
}
if (mol.getRGroupFragment()->getAttachmentPoint(i, j) != -1)
break;
}
}
*/
};
Smiles.prototype.markCisTrans = function (mol) {
this.cis_trans = new CisTrans(mol, function (idx) {
return this.atoms[idx].neighbours;
}, this);
this.cis_trans.build();
this.dbonds = new Array(mol.bonds.count());
mol.bonds.each(function (bid) {
this.dbonds[bid] =
{
ctbond_beg: -1,
ctbond_end: -1,
saved: 0
};
}, this);
this.cis_trans.each(function (bid, ct) {
var bond = mol.bonds.get(bid);
if (ct.parity != 0 && !this.isBondInRing(bid)) {
var neiBeg = this.atoms[bond.begin].neighbours;
var neiEnd = this.atoms[bond.end].neighbours;
var aromFailBeg = true;
var aromFailEnd = true;
neiBeg.forEach(function (nei) {
if (nei.bid !== bid && mol.bonds.get(nei.bid).type === Struct.Bond.PATTERN.TYPE.SINGLE)
aromFailBeg = false;
}, this);
neiEnd.forEach(function (nei) {
if (nei.bid !== bid && mol.bonds.get(nei.bid).type === Struct.Bond.PATTERN.TYPE.SINGLE)
aromFailEnd = false;
}, this);
if (aromFailBeg || aromFailEnd)
return;
neiBeg.forEach(function (nei) {
if (nei.bid === bid) return;
if (mol.bonds.get(nei.bid).begin === bond.begin)
this.dbonds[nei.bid].ctbond_beg = bid;
else
this.dbonds[nei.bid].ctbond_end = bid;
}, this);
neiEnd.forEach(function (nei) {
if (nei.bid === bid) return;
if (mol.bonds.get(nei.bid).begin === bond.end)
this.dbonds[nei.bid].ctbond_beg = bid;
else
this.dbonds[nei.bid].ctbond_end = bid;
}, this);
}
}, this);
};
Smiles.prototype.updateSideBonds = function (mol, bondIdx) { // eslint-disable-line max-statements
var bond = mol.bonds.get(bondIdx);
var subst = this.cis_trans.getSubstituents(bondIdx);
var parity = this.cis_trans.getParity(bondIdx);
var sidebonds = [-1, -1, -1, -1];
sidebonds[0] = mol.findBondId(subst[0], bond.begin);
if (subst[1] != -1)
sidebonds[1] = mol.findBondId(subst[1], bond.begin);
sidebonds[2] = mol.findBondId(subst[2], bond.end);
if (subst[3] != -1)
sidebonds[3] = mol.findBondId(subst[3], bond.end);
var n1 = 0;
var n2 = 0;
var n3 = 0;
var n4 = 0;
if (this.dbonds[sidebonds[0]].saved != 0) {
if ((this.dbonds[sidebonds[0]].saved == 1 && mol.bonds.get(sidebonds[0]).begin == bond.begin) ||
(this.dbonds[sidebonds[0]].saved == 2 && mol.bonds.get(sidebonds[0]).end == bond.begin))
n1++;
else
n2++;
}
if (sidebonds[1] != -1 && this.dbonds[sidebonds[1]].saved != 0) {
if ((this.dbonds[sidebonds[1]].saved == 2 && mol.bonds.get(sidebonds[1]).begin == bond.begin) ||
(this.dbonds[sidebonds[1]].saved == 1 && mol.bonds.get(sidebonds[1]).end == bond.begin))
n1++;
else
n2++;
}
if (this.dbonds[sidebonds[2]].saved != 0) {
if ((this.dbonds[sidebonds[2]].saved == 1 && mol.bonds.get(sidebonds[2]).begin == bond.end) ||
(this.dbonds[sidebonds[2]].saved == 2 && mol.bonds.get(sidebonds[2]).end == bond.end))
n3++;
else
n4++;
}
if (sidebonds[3] != -1 && this.dbonds[sidebonds[3]].saved != 0) {
if ((this.dbonds[sidebonds[3]].saved == 2 && mol.bonds.get(sidebonds[3]).begin == bond.end) ||
(this.dbonds[sidebonds[3]].saved == 1 && mol.bonds.get(sidebonds[3]).end == bond.end))
n3++;
else
n4++;
}
if (parity == CisTrans.PARITY.CIS) {
n1 += n3;
n2 += n4;
} else {
n1 += n4;
n2 += n3;
}
if (n1 > 0 && n2 > 0)
throw new Error('incompatible cis-trans configuration');
if (n1 == 0 && n2 == 0)
return false;
if (n1 > 0) {
this.dbonds[sidebonds[0]].saved =
(mol.bonds.get(sidebonds[0]).begin == bond.begin) ? 1 : 2;
if (sidebonds[1] != -1) {
this.dbonds[sidebonds[1]].saved =
(mol.bonds.get(sidebonds[1]).begin == bond.begin) ? 2 : 1;
}
this.dbonds[sidebonds[2]].saved =
((mol.bonds.get(sidebonds[2]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 1 : 2;
if (sidebonds[3] != -1) {
this.dbonds[sidebonds[3]].saved =
((mol.bonds.get(sidebonds[3]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 2 : 1;
}
}
if (n2 > 0) {
this.dbonds[sidebonds[0]].saved =
(mol.bonds.get(sidebonds[0]).begin == bond.begin) ? 2 : 1;
if (sidebonds[1] != -1) {
this.dbonds[sidebonds[1]].saved =
(mol.bonds.get(sidebonds[1]).begin == bond.begin) ? 1 : 2;
}
this.dbonds[sidebonds[2]].saved =
((mol.bonds.get(sidebonds[2]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 2 : 1;
if (sidebonds[3] != -1) {
this.dbonds[sidebonds[3]].saved =
((mol.bonds.get(sidebonds[3]).begin == bond.end) == (parity == CisTrans.PARITY.CIS)) ? 1 : 2;
}
}
return true;
};
Smiles.prototype.calcBondDirection = function (mol, idx, vprev) {
var ntouched;
if (this.dbonds[idx].ctbond_beg == -1 && this.dbonds[idx].ctbond_end == -1)
return 0;
if (mol.bonds.get(idx).type != Struct.Bond.PATTERN.TYPE.SINGLE)
throw new Error('internal: directed bond type ' + mol.bonds.get(idx).type);
while (true) { // eslint-disable-line no-constant-condition
ntouched = 0;
this.cis_trans.each(function (bid, ct) {
if (ct.parity != 0 && !this.isBondInRing(bid)) {
if (this.updateSideBonds(mol, bid))
ntouched++;
}
}, this);
if (ntouched == this.touchedCistransbonds)
break;
this.touchedCistransbonds = ntouched;
}
if (this.dbonds[idx].saved == 0) {
if (vprev == mol.bonds.get(idx).begin)
this.dbonds[idx].saved = 1;
else
this.dbonds[idx].saved = 2;
}
return this.dbonds[idx].saved;
};
Smiles.prototype.writeRadicals = function (mol) { // eslint-disable-line max-statements
var marked = new Array(this.writtenAtoms.length);
var i, j;
for (i = 0; i < this.writtenAtoms.length; i++) {
if (marked[i])
continue; // eslint-disable-line no-continue
var radical = mol.atoms.get(this.writtenAtoms[i]).radical;
if (radical == 0)
continue; // eslint-disable-line no-continue
if (this.comma) {
this.smiles += ',';
} else {
this.smiles += ' |';
this.comma = true;
}
if (radical == Struct.Atom.PATTERN.RADICAL.SINGLET)
this.smiles += '^3:';
else if (radical == Struct.Atom.PATTERN.RADICAL.DOUPLET)
this.smiles += '^1:';
else // RADICAL_TRIPLET
this.smiles += '^4:';
this.smiles += i;
for (j = i + 1; j < this.writtenAtoms.length; j++) {
if (mol.atoms.get(this.writtenAtoms[j]).radical == radical) {
marked[j] = true;
this.smiles += ',' + j;
}
}
}
};
module.exports = {
stringify: function (struct, options) {
var opts = options || {};
return new Smiles().saveMolecule(struct, opts.ignoreErrors);
}
};

View File

@ -0,0 +1,519 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Map = require('../../util/map');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Struct = require('../struct');
function Stereocenters(mol, neighborsFunc, context) {
this.molecule = mol;
this.atoms = new Map();
this.getNeighbors = neighborsFunc;
this.context = context;
}
Stereocenters.prototype.each = function (func, context) {
this.atoms.each(func, context);
};
Stereocenters.prototype.buildFromBonds = function (/* const int *atom_types, const int *atom_groups, const int *bond_orientations, */ignoreErrors) {
var atoms = this.molecule.atoms;
var bonds = this.molecule.bonds;
// this is a set of atoms that are likely to belong to allene structures and
// therefore should not be considered as potential stereocenters in the code below,
// as allenes cannot be encoded in the SMILES notation
var alleneMask = Set.empty();
atoms.each(function (aid) {
var neiList = this.getNeighbors.call(this.context, aid);
if (neiList.length != 2)
return false;
var nei1 = neiList[0];
var nei2 = neiList[1];
// check atom labels
if ([aid, nei1.aid, nei2.aid].findIndex(function (aid) {
return ['C', 'Si'].indexOf(atoms.get(aid).label) < 0;
}, this) >= 0)
return false;
// check adjacent bond types
if ([nei1.bid, nei2.bid].findIndex(function (bid) {
return bonds.get(bid).type != Struct.Bond.PATTERN.TYPE.DOUBLE;
}, this) >= 0)
return false;
// get the other neighbors of the two adjacent atoms except for the central atom
var nei1nei = this.getNeighbors.call(this.context, nei1.aid).filter(function (nei) {
return nei.aid != aid;
});
var nei2nei = this.getNeighbors.call(this.context, nei2.aid).filter(function (nei) {
return nei.aid != aid;
});
if (nei1nei.length < 1 || nei1nei.length > 2 || nei2nei.length < 1 || nei2nei.length > 2)
return false;
if (nei1nei.concat(nei2nei).findIndex(function (nei) {
return bonds.get(nei.bid).type != Struct.Bond.PATTERN.TYPE.SINGLE;
}, this) >= 0)
return false;
if (nei1nei.concat(nei2nei).findIndex(function (nei) {
return bonds.get(nei.bid).stereo == Struct.Bond.PATTERN.STEREO.EITHER;
}, this) >= 0)
return false;
Set.add(alleneMask, nei1.aid);
Set.add(alleneMask, nei2.aid);
}, this);
if (Set.size(alleneMask) > 0)
alert('This structure may contain allenes, which cannot be represented in the SMILES notation. Relevant stereo-information will be discarded.');
atoms.each(function (aid) {
if (Set.contains(alleneMask, aid))
return;
/*
if (atom_types[atom_idx] == 0)
continue;
*/
var neiList = this.getNeighbors.call(this.context, aid);
var stereocenter = false;
neiList.find(function (nei) {
var bond = this.molecule.bonds.get(nei.bid);
if (bond.type == Struct.Bond.PATTERN.TYPE.SINGLE && bond.begin == aid) {
if (bond.stereo == Struct.Bond.PATTERN.STEREO.UP || bond.stereo == Struct.Bond.PATTERN.STEREO.DOWN) {
stereocenter = true;
return true;
}
}
return false;
}, this);
if (!stereocenter)
return;
if (ignoreErrors)
// try
// {
this.buildOneCenter(aid/* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations*/);
// }
// catch (er)
// {
// }
else
this.buildOneCenter(aid/* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations*/);
}, this);
};
Stereocenters.allowed_stereocenters =
[
{ elem: 'C', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'C', charge: 0, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'Si', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'Si', charge: 0, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 1, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 1, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'S', charge: 0, degree: 4, n_double_bonds: 2, implicit_degree: 4 },
{ elem: 'S', charge: 1, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'S', charge: 0, degree: 3, n_double_bonds: 1, implicit_degree: 3 },
{ elem: 'P', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'P', charge: 1, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'P', charge: 0, degree: 4, n_double_bonds: 1, implicit_degree: 4 }
];
Stereocenters.prototype.buildOneCenter = function (atomIdx/* , int group, int type, const int *bond_orientations*/) { // eslint-disable-line max-statements
var atom = this.molecule.atoms.get(atomIdx);
var neiList = this.getNeighbors.call(this.context, atomIdx);
var degree = neiList.length;
var implicitDegree = -1;
var stereocenter = {
group: 0, // = group;
type: 0, // = type;
pyramid: []
};
var edgeIds = [];
var lastAtomDir = 0;
var nDoubleBonds = 0;
stereocenter.pyramid[0] = -1;
stereocenter.pyramid[1] = -1;
stereocenter.pyramid[2] = -1;
stereocenter.pyramid[3] = -1;
var nPureHydrogens = 0;
if (degree > 4)
throw new Error('stereocenter with %d bonds are not supported' + degree);
neiList.forEach(function (nei, neiIdx) {
var neiAtom = this.molecule.atoms.get(nei.aid);
var bond = this.molecule.bonds.get(nei.bid);
edgeIds[neiIdx] = {
edge_idx: nei.bid,
nei_idx: nei.aid,
rank: nei.aid,
vec: Vec2.diff(neiAtom.pp, atom.pp).yComplement()
};
if (neiAtom.pureHydrogen()) {
nPureHydrogens++;
edgeIds[neiIdx].rank = 10000;
} else if (neiAtom.label === 'H') {
edgeIds[neiIdx].rank = 5000;
}
if (!edgeIds[neiIdx].vec.normalize())
throw new Error('zero bond length');
if (bond.type === Struct.Bond.PATTERN.TYPE.TRIPLE)
throw new Error('non-single bonds not allowed near stereocenter');
else if (bond.type === Struct.Bond.PATTERN.TYPE.AROMATIC)
throw new Error('aromatic bonds not allowed near stereocenter');
else if (bond.type === Struct.Bond.PATTERN.TYPE.DOUBLE)
nDoubleBonds++;
}, this);
Stereocenters.allowed_stereocenters.find(function (as) {
if (as.elem == atom.label && as.charge == atom.charge &&
as.degree == degree && as.n_double_bonds == nDoubleBonds) {
implicitDegree = as.implicit_degree;
return true;
}
return false;
}, this);
if (implicitDegree === -1)
throw new Error('unknown stereocenter configuration: ' + atom.label + ', charge ' + atom.charge + ', ' + degree + ' bonds (' + nDoubleBonds + ' double)');
if (degree === 4 && nPureHydrogens > 1)
throw new Error(nPureHydrogens + ' hydrogens near stereocenter');
if (degree === 3 && implicitDegree == 4 && nPureHydrogens > 0)
throw new Error('have hydrogen(s) besides implicit hydrogen near stereocenter');
/*
if (stereocenter.type == ATOM_ANY)
{
_stereocenters.insert(atom_idx, stereocenter);
return;
}
*/
if (degree === 4) {
// sort by neighbor atom index (ascending)
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[2].rank > edgeIds[3].rank)
swap(edgeIds, 2, 3);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
var main1 = -1;
var main2 = -1;
var side1 = -1;
var side2 = -1;
var mainDir = 0;
for (var neiIdx = 0; neiIdx < 4; neiIdx++) {
var stereo = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (stereo == Struct.Bond.PATTERN.STEREO.UP || stereo == Struct.Bond.PATTERN.STEREO.DOWN) {
main1 = neiIdx;
mainDir = stereo;
break;
}
}
if (main1 == -1)
throw new Error('none of 4 bonds going from stereocenter is stereobond');
var xyz1, xyz2;
// find main2 as opposite to main1
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 1) % 4].vec, edgeIds[(main1 + 2) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 1) % 4].vec, edgeIds[(main1 + 3) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 1) % 4;
side1 = (main1 + 2) % 4;
side2 = (main1 + 3) % 4;
}
}
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 2) % 4].vec, edgeIds[(main1 + 1) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 2) % 4].vec, edgeIds[(main1 + 3) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 2) % 4;
side1 = (main1 + 1) % 4;
side2 = (main1 + 3) % 4;
}
}
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 3) % 4].vec, edgeIds[(main1 + 1) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 3) % 4].vec, edgeIds[(main1 + 2) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 3) % 4;
side1 = (main1 + 2) % 4;
side2 = (main1 + 1) % 4;
}
}
if (main2 == -1)
throw new Error('internal error: can not find opposite bond');
if (mainDir == Struct.Bond.PATTERN.STEREO.UP && this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) == Struct.Bond.PATTERN.STEREO.DOWN)
throw new Error('stereo types of the opposite bonds mismatch');
if (mainDir == Struct.Bond.PATTERN.STEREO.DOWN && this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) == Struct.Bond.PATTERN.STEREO.UP)
throw new Error('stereo types of the opposite bonds mismatch');
if (mainDir == this.getBondStereo(atomIdx, edgeIds[side1].edge_idx))
throw new Error('stereo types of non-opposite bonds match');
if (mainDir == this.getBondStereo(atomIdx, edgeIds[side2].edge_idx))
throw new Error('stereo types of non-opposite bonds match');
if (main1 == 3 || main2 == 3)
lastAtomDir = mainDir;
else
lastAtomDir = (mainDir == Struct.Bond.PATTERN.STEREO.UP ? Struct.Bond.PATTERN.STEREO.DOWN : Struct.Bond.PATTERN.STEREO.UP);
sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if ((lastAtomDir == Struct.Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir == Struct.Bond.PATTERN.STEREO.DOWN && sign < 0)) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
}
stereocenter.pyramid[3] = edgeIds[3].nei_idx;
} else if (degree === 3) {
// sort by neighbor atom index (ascending)
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
var stereo0 = this.getBondStereo(atomIdx, edgeIds[0].edge_idx);
var stereo1 = this.getBondStereo(atomIdx, edgeIds[1].edge_idx);
var stereo2 = this.getBondStereo(atomIdx, edgeIds[2].edge_idx);
var nUp = 0;
var nDown = 0;
nUp += (stereo0 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nUp += (stereo1 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nUp += (stereo2 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nDown += (stereo0 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
nDown += (stereo1 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
nDown += (stereo2 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
if (implicitDegree == 4) { // have implicit hydrogen
if (nUp == 3)
throw new Error('all 3 bonds up near stereoatom');
if (nDown == 3)
throw new Error('all 3 bonds down near stereoatom');
if (nUp == 0 && nDown == 0)
throw new Error('no up/down bonds near stereoatom -- indefinite case');
if (nUp == 1 && nDown == 1)
throw new Error('one bond up, one bond down -- indefinite case');
mainDir = 0;
if (nUp == 2) {
lastAtomDir = Struct.Bond.PATTERN.STEREO.DOWN;
} else if (nDown == 2) {
lastAtomDir = Struct.Bond.PATTERN.STEREO.UP;
} else {
main1 = -1;
side1 = -1;
side2 = -1;
for (neiIdx = 0; neiIdx < 3; neiIdx++) {
dir = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (dir == Struct.Bond.PATTERN.STEREO.UP || dir == Struct.Bond.PATTERN.STEREO.DOWN) { // eslint-disable-line max-depth
main1 = neiIdx;
mainDir = dir;
side1 = (neiIdx + 1) % 3;
side2 = (neiIdx + 2) % 3;
break;
}
}
if (main1 == -1)
throw new Error('internal error: can not find up or down bond');
var xyz = Stereocenters.xyzzy(edgeIds[side1].vec, edgeIds[side2].vec, edgeIds[main1].vec);
if (xyz == 3 || xyz == 4)
throw new Error('degenerate case for 3 bonds near stereoatom');
if (xyz == 1)
lastAtomDir = mainDir;
else
lastAtomDir = (mainDir == Struct.Bond.PATTERN.STEREO.UP ? Struct.Bond.PATTERN.STEREO.DOWN : Struct.Bond.PATTERN.STEREO.UP);
}
var sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if ((lastAtomDir == Struct.Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir == Struct.Bond.PATTERN.STEREO.DOWN && sign < 0)) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
}
stereocenter.pyramid[3] = -1;
} else { // 3-connected P, N or S; no implicit hydrogens
var dir;
if (nDown > 0 && nUp > 0)
throw new Error('one bond up, one bond down -- indefinite case');
else if (nDown == 0 && nUp == 0)
throw new Error('no up-down bonds attached to stereocenter');
else if (nUp > 0)
dir = 1;
else
dir = -1;
if (Stereocenters.xyzzy(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec) === 1 ||
Stereocenters.xyzzy(edgeIds[0].vec, edgeIds[2].vec, edgeIds[1].vec) === 1 ||
Stereocenters.xyzzy(edgeIds[2].vec, edgeIds[1].vec, edgeIds[0].vec) === 1)
// all bonds belong to the same half-plane
dir = -dir;
sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if (sign == dir) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
}
stereocenter.pyramid[3] = -1;
}
}
this.atoms.set(atomIdx, stereocenter);
};
Stereocenters.prototype.getBondStereo = function (centerIdx, edgeIdx) {
var bond = this.molecule.bonds.get(edgeIdx);
if (centerIdx != bond.begin) // TODO: check this
return 0;
return bond.stereo;
};
// 1 -- in the smaller angle, 2 -- in the bigger angle,
// 4 -- in the 'positive' straight angle, 8 -- in the 'negative' straight angle
Stereocenters.xyzzy = function (v1, v2, u) {
var eps = 0.001;
var sine1 = Vec2.cross(v1, v2);
var cosine1 = Vec2.dot(v1, v2);
var sine2 = Vec2.cross(v1, u);
var cosine2 = Vec2.dot(v1, u);
if (Math.abs(sine1) < eps) {
if (Math.abs(sine2) < eps)
throw new Error('degenerate case -- bonds overlap');
return (sine2 > 0) ? 4 : 8;
}
if (sine1 * sine2 < -eps * eps)
return 2;
if (cosine2 < cosine1)
return 2;
return 1;
};
Stereocenters.sign = function (v1, v2, v3) {
var res = (v1.x - v3.x) * (v2.y - v3.y) - (v1.y - v3.y) * (v2.x - v3.x); // eslint-disable-line no-mixed-operators
var eps = 0.001;
if (res > eps)
return 1;
if (res < -eps)
return -1;
throw new Error('degenerate triangle');
};
Stereocenters.isPyramidMappingRigid = function (mapping) {
var arr = mapping.slice();
var rigid = true;
if (arr[0] > arr[1])
swap(arr, 0, 1), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
if (arr[2] > arr[3])
swap(arr, 2, 3), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
if (arr[0] > arr[1])
swap(arr, 0, 1), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
return rigid;
};
function swap(arr, i1, i2) {
var tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
module.exports = Stereocenters;

View File

@ -0,0 +1,435 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var element = require('../element');
var AtomList = require('./atomlist');
function Atom(params) { // eslint-disable-line max-statements
var def = Atom.attrGetDefault;
console.assert(params || 'label' in params, 'label must be specified!');
this.label = params.label;
this.fragment = ('fragment' in params) ? params.fragment : -1;
this.pseudo = params.pseudo || checkPseudo(params.label);
ifDef(this, params, 'alias', def('alias'));
ifDef(this, params, 'isotope', def('isotope'));
ifDef(this, params, 'radical', def('radical'));
ifDef(this, params, 'charge', def('charge'));
ifDef(this, params, 'rglabel', def('rglabel')); // r-group index mask, i-th bit stands for i-th r-site
ifDef(this, params, 'attpnt', def('attpnt')); // attachment point
ifDef(this, params, 'explicitValence', def('explicitValence'));
this.valence = 0;
this.implicitH = 0; // implicitH is not an attribute
this.pp = params.pp ? new Vec2(params.pp) : new Vec2();
// sgs should only be set when an atom is added to an s-group by an appropriate method,
// or else a copied atom might think it belongs to a group, but the group be unaware of the atom
// TODO: make a consistency check on atom/s-group assignments
this.sgs = {};
// query
ifDef(this, params, 'ringBondCount', def('ringBondCount'));
ifDef(this, params, 'substitutionCount', def('substitutionCount'));
ifDef(this, params, 'unsaturatedAtom', def('unsaturatedAtom'));
ifDef(this, params, 'hCount', def('hCount'));
// reaction
ifDef(this, params, 'aam', def('aam'));
ifDef(this, params, 'invRet', def('invRet'));
ifDef(this, params, 'exactChangeFlag', def('exactChangeFlag'));
ifDef(this, params, 'rxnFragmentType', -1); // this isn't really an attribute
this.atomList = params.atomList ? new AtomList(params.atomList) : null;
this.neighbors = []; // set of half-bonds having this atom as their origin
this.badConn = false;
}
Atom.getAttrHash = function (atom) {
var attrs = {};
for (var attr in Atom.attrlist) {
if (typeof (atom[attr]) !== 'undefined')
attrs[attr] = atom[attr];
}
return attrs;
};
Atom.attrGetDefault = function (attr) {
if (attr in Atom.attrlist)
return Atom.attrlist[attr];
console.assert(false, 'Attribute unknown');
};
Atom.PATTERN =
{
RADICAL:
{
NONE: 0,
SINGLET: 1,
DOUPLET: 2,
TRIPLET: 3
}
};
Atom.attrlist = {
alias: null,
label: 'C',
pseudo: null,
isotope: 0,
radical: 0,
charge: 0,
explicitValence: -1,
ringBondCount: 0,
substitutionCount: 0,
unsaturatedAtom: 0,
hCount: 0,
atomList: null,
invRet: 0,
exactChangeFlag: 0,
rglabel: null,
attpnt: null,
aam: 0
};
function radicalElectrons(radical) {
radical -= 0;
if (radical === Atom.PATTERN.RADICAL.NONE)
return 0;
else if (radical === Atom.PATTERN.RADICAL.DOUPLET)
return 1;
else if (radical === Atom.PATTERN.RADICAL.SINGLET ||
radical === Atom.PATTERN.RADICAL.TRIPLET)
return 2;
console.assert(false, 'Unknown radical value');
}
Atom.prototype.clone = function (fidMap) {
var ret = new Atom(this);
if (fidMap && this.fragment in fidMap)
ret.fragment = fidMap[this.fragment];
return ret;
};
Atom.prototype.isQuery = function () {
return this.atomList != null || this.label === 'A' || this.attpnt || this.hCount;
};
Atom.prototype.pureHydrogen = function () {
return this.label === 'H' && this.isotope === 0;
};
Atom.prototype.isPlainCarbon = function () {
return this.label === 'C' && this.isotope === 0 && this.radical == 0 && this.charge == 0 &&
this.explicitValence < 0 && this.ringBondCount == 0 && this.substitutionCount == 0 &&
this.unsaturatedAtom == 0 && this.hCount == 0 && !this.atomList;
};
Atom.prototype.isPseudo = function () {
// TODO: handle reaxys generics separately
return !this.atomList && !this.rglabel && !element.map[this.label];
};
Atom.prototype.hasRxnProps = function () {
return !!(this.invRet || this.exactChangeFlag || this.attpnt != null || this.aam);
};
Atom.prototype.calcValence = function (conn) { // eslint-disable-line max-statements
var atom = this;
var charge = atom.charge;
var label = atom.label;
if (atom.isQuery()) {
this.implicitH = 0;
return true;
}
var elem = element.map[label];
if (elem === undefined) {
this.implicitH = 0;
return true;
}
var groupno = element[elem].group;
var rad = radicalElectrons(atom.radical);
var valence = conn;
var hyd = 0;
var absCharge = Math.abs(charge);
if (groupno === 1) {
if (label === 'H' ||
label === 'Li' || label === 'Na' || label === 'K' ||
label === 'Rb' || label === 'Cs' || label === 'Fr') {
valence = 1;
hyd = 1 - rad - conn - absCharge;
}
} else if (groupno === 2) {
if (conn + rad + absCharge === 2 || conn + rad + absCharge === 0)
valence = 2;
else
hyd = -1;
} else if (groupno === 3) {
if (label === 'B' || label === 'Al' || label === 'Ga' || label === 'In') {
if (charge === -1) {
valence = 4;
hyd = 4 - rad - conn;
} else {
valence = 3;
hyd = 3 - rad - conn - absCharge;
}
} else if (label === 'Tl') {
if (charge === -1) {
if (rad + conn <= 2) {
valence = 2;
hyd = 2 - rad - conn;
} else {
valence = 4;
hyd = 4 - rad - conn;
}
} else if (charge === -2) {
if (rad + conn <= 3) {
valence = 3;
hyd = 3 - rad - conn;
} else {
valence = 5;
hyd = 5 - rad - conn;
}
} else if (rad + conn + absCharge <= 1) {
valence = 1;
hyd = 1 - rad - conn - absCharge;
} else {
valence = 3;
hyd = 3 - rad - conn - absCharge;
}
}
} else if (groupno === 4) {
if (label === 'C' || label === 'Si' || label === 'Ge') {
valence = 4;
hyd = 4 - rad - conn - absCharge;
} else if (label === 'Sn' || label === 'Pb') {
if (conn + rad + absCharge <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
} else {
valence = 4;
hyd = 4 - rad - conn - absCharge;
}
}
} else if (groupno === 5) {
if (label === 'N' || label === 'P') {
if (charge === 1) {
valence = 4;
hyd = 4 - rad - conn;
} else if (charge === 2) {
valence = 3;
hyd = 3 - rad - conn;
} else if (label === 'N' || rad + conn + absCharge <= 3) {
valence = 3;
hyd = 3 - rad - conn - absCharge;
} else { // ELEM_P && rad + conn + absCharge > 3
valence = 5;
hyd = 5 - rad - conn - absCharge;
}
} else if (label === 'Bi' || label === 'Sb' || label === 'As') {
if (charge === 1) {
if (rad + conn <= 2 && label !== 'As') {
valence = 2;
hyd = 2 - rad - conn;
} else {
valence = 4;
hyd = 4 - rad - conn;
}
} else if (charge === 2) {
valence = 3;
hyd = 3 - rad - conn;
} else if (rad + conn <= 3) {
valence = 3;
hyd = 3 - rad - conn - absCharge;
} else {
valence = 5;
hyd = 5 - rad - conn - absCharge;
}
}
} else if (groupno === 6) {
if (label === 'O') {
if (charge >= 1) {
valence = 3;
hyd = 3 - rad - conn;
} else {
valence = 2;
hyd = 2 - rad - conn - absCharge;
}
} else if (label === 'S' || label === 'Se' || label === 'Po') {
if (charge === 1) {
if (conn <= 3) {
valence = 3;
hyd = 3 - rad - conn;
} else {
valence = 5;
hyd = 5 - rad - conn;
}
} else if (conn + rad + absCharge <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
} else if (conn + rad + absCharge <= 4) {
// See examples in PubChem
// [S] : CID 16684216
// [Se]: CID 5242252
// [Po]: no example, just following ISIS/Draw logic here
valence = 4;
hyd = 4 - rad - conn - absCharge;
} else {
// See examples in PubChem
// [S] : CID 46937044
// [Se]: CID 59786
// [Po]: no example, just following ISIS/Draw logic here
valence = 6;
hyd = 6 - rad - conn - absCharge;
}
} else if (label === 'Te') {
if (charge === -1) {
if (conn <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
}
} else if (charge === 0 || charge === 2) {
if (conn <= 2) {
valence = 2;
hyd = 2 - rad - conn - absCharge;
} else if (conn <= 4) {
valence = 4;
hyd = 4 - rad - conn - absCharge;
} else if (charge === 0 && conn <= 6) {
valence = 6;
hyd = 6 - rad - conn - absCharge;
} else {
hyd = -1;
}
}
}
} else if (groupno === 7) {
if (label === 'F') {
valence = 1;
hyd = 1 - rad - conn - absCharge;
} else if (label === 'Cl' || label === 'Br' ||
label === 'I' || label === 'At') {
if (charge === 1) {
if (conn <= 2) {
valence = 2;
hyd = 2 - rad - conn;
} else if (conn === 3 || conn === 5 || conn >= 7) {
hyd = -1;
}
} else if (charge === 0) {
if (conn <= 1) {
valence = 1;
hyd = 1 - rad - conn;
// While the halogens can have valence 3, they can not have
// hydrogens in that case.
} else if (conn === 2 || conn === 4 || conn === 6) {
if (rad === 1) {
valence = conn;
hyd = 0;
} else {
hyd = -1; // will throw an error in the end
}
} else if (conn > 7) {
hyd = -1; // will throw an error in the end
}
}
}
} else if (groupno === 8) {
if (conn + rad + absCharge === 0)
valence = 1;
else
hyd = -1;
}
this.valence = valence;
this.implicitH = hyd;
if (this.implicitH < 0) {
this.valence = conn;
this.implicitH = 0;
this.badConn = true;
return false;
}
return true;
};
Atom.prototype.calcValenceMinusHyd = function (conn) { // eslint-disable-line max-statements
var atom = this;
var charge = atom.charge;
var label = atom.label;
var elem = element.map[label];
if (elem === null)
console.assert('Element ' + label + ' unknown');
if (elem < 0) { // query atom, skip
this.implicitH = 0;
return null;
}
var groupno = element[elem].group;
var rad = radicalElectrons(atom.radical);
if (groupno === 3) {
if (label === 'B' || label === 'Al' || label === 'Ga' || label === 'In') {
if (charge === -1) {
if (rad + conn <= 4)
return rad + conn;
}
}
} else if (groupno === 5) {
if (label === 'N' || label === 'P') {
if (charge === 1)
return rad + conn;
if (charge === 2)
return rad + conn;
} else if (label === 'Sb' || label === 'Bi' || label === 'As') {
if (charge === 1)
return rad + conn;
else if (charge === 2)
return rad + conn;
}
} else if (groupno === 6) {
if (label === 'O') {
if (charge >= 1)
return rad + conn;
} else if (label === 'S' || label === 'Se' || label === 'Po') {
if (charge === 1)
return rad + conn;
}
} else if (groupno === 7) {
if (label === 'Cl' || label === 'Br' ||
label === 'I' || label === 'At') {
if (charge === 1)
return rad + conn;
}
}
return rad + conn + Math.abs(charge);
};
function ifDef(dst, src, prop, def) {
dst[prop] = !(typeof src[prop] === 'undefined') ? src[prop] : def;
}
function checkPseudo(label) {
return !element.map[label] && label !== 'L' && label !== 'L#' && label !== 'R#' ? label : null;
}
module.exports = Atom;

View File

@ -0,0 +1,44 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var element = require('../element');
function AtomList(params) {
console.assert(params && 'notList' in params && 'ids' in params, '\'notList\' and \'ids\' must be specified!');
this.notList = params.notList; /* boolean*/
this.ids = params.ids; /* Array of integers*/
}
AtomList.prototype.labelList = function () {
var labels = [];
for (var i = 0; i < this.ids.length; ++i)
labels.push(element[this.ids[i]].label);
return labels;
};
AtomList.prototype.label = function () {
var label = '[' + this.labelList().join(',') + ']';
if (this.notList)
label = '!' + label;
return label;
};
AtomList.prototype.equals = function (x) {
return this.notList == x.notList && (this.ids || []).sort().toString() === (x.ids || []).sort().toString();
};
module.exports = AtomList;

View File

@ -0,0 +1,145 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
function Bond(params) { // eslint-disable-line max-statements
console.assert(params && 'begin' in params && 'end' in params && 'type' in params,
'\'begin\', \'end\' and \'type\' properties must be specified!');
this.begin = params.begin;
this.end = params.end;
this.type = params.type;
this.xxx = params.xxx || '';
this.stereo = Bond.PATTERN.STEREO.NONE;
this.topology = Bond.PATTERN.TOPOLOGY.EITHER;
this.reactingCenterStatus = 0;
this.hb1 = null; // half-bonds
this.hb2 = null;
this.len = 0;
this.sb = 0;
this.sa = 0;
this.angle = 0;
if (params.stereo)
this.stereo = params.stereo;
if (params.topology)
this.topology = params.topology;
if (params.reactingCenterStatus)
this.reactingCenterStatus = params.reactingCenterStatus;
this.center = new Vec2();
}
Bond.PATTERN =
{
TYPE:
{
SINGLE: 1,
DOUBLE: 2,
TRIPLE: 3,
AROMATIC: 4,
SINGLE_OR_DOUBLE: 5,
SINGLE_OR_AROMATIC: 6,
DOUBLE_OR_AROMATIC: 7,
ANY: 8
},
STEREO:
{
NONE: 0,
UP: 1,
EITHER: 4,
DOWN: 6,
CIS_TRANS: 3
},
TOPOLOGY:
{
EITHER: 0,
RING: 1,
CHAIN: 2
},
REACTING_CENTER:
{
NOT_CENTER: -1,
UNMARKED: 0,
CENTER: 1,
UNCHANGED: 2,
MADE_OR_BROKEN: 4,
ORDER_CHANGED: 8,
MADE_OR_BROKEN_AND_CHANGED: 12
}
};
Bond.attrlist = {
type: Bond.PATTERN.TYPE.SINGLE,
stereo: Bond.PATTERN.STEREO.NONE,
topology: Bond.PATTERN.TOPOLOGY.EITHER,
reactingCenterStatus: 0
};
// TODO: not used
Bond.getAttrHash = function (bond) {
var attrs = {};
for (var attr in Bond.attrlist) {
if (typeof (bond[attr]) !== 'undefined')
attrs[attr] = bond[attr];
}
return attrs;
};
Bond.attrGetDefault = function (attr) {
if (attr in Bond.attrlist)
return Bond.attrlist[attr];
console.error('Attribute unknown');
};
Bond.prototype.hasRxnProps = function () {
return !!this.reactingCenterStatus;
};
Bond.prototype.getCenter = function (struct) {
var p1 = struct.atoms.get(this.begin).pp;
var p2 = struct.atoms.get(this.end).pp;
return Vec2.lc2(p1, 0.5, p2, 0.5);
};
Bond.prototype.getDir = function (struct) {
var p1 = struct.atoms.get(this.begin).pp;
var p2 = struct.atoms.get(this.end).pp;
return p2.sub(p1).normalized();
};
Bond.prototype.clone = function (aidMap) {
var cp = new Bond(this);
if (aidMap) {
cp.begin = aidMap[cp.begin];
cp.end = aidMap[cp.end];
}
return cp;
};
Bond.prototype.findOtherEnd = function (i) {
if (i == this.begin)
return this.end;
if (i == this.end)
return this.begin;
console.error('bond end not found');
};
module.exports = Bond;

View File

@ -0,0 +1,975 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Map = require('../../util/map');
var Pool = require('../../util/pool');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var element = require('../element');
var Atom = require('./atom');
var AtomList = require('./atomlist');
var Bond = require('./bond');
var SGroup = require('./sgroup');
var RGroup = require('./rgroup');
var SGroupForest = require('./sgforest');
function Struct() {
this.atoms = new Pool();
this.bonds = new Pool();
this.sgroups = new Pool();
this.halfBonds = new Map();
this.loops = new Pool();
this.isChiral = false;
this.isReaction = false;
this.rxnArrows = new Pool();
this.rxnPluses = new Pool();
this.frags = new Pool();
this.rgroups = new Map();
this.name = '';
this.sGroupForest = new SGroupForest(this);
}
Struct.prototype.hasRxnProps = function () {
return this.atoms.find(function (aid, atom) {
return atom.hasRxnProps();
}, this) >= 0 || this.bonds.find(function (bid, bond) {
return bond.hasRxnProps();
}, this) >= 0;
};
Struct.prototype.hasRxnArrow = function () {
return this.rxnArrows.count() > 0;
};
// returns a list of id's of s-groups, which contain only atoms in the given list
Struct.prototype.getSGroupsInAtomSet = function (atoms/* Array*/) {
var sgCounts = {};
atoms.forEach(function (aid) {
var sg = Set.list(this.atoms.get(aid).sgs);
sg.forEach(function (sid) {
sgCounts[sid] = sgCounts[sid] ? (sgCounts[sid] + 1) : 1;
}, this);
}, this);
var sgroupList = [];
for (var key in sgCounts) {
var sid = parseInt(key, 10);
var sgroup = this.sgroups.get(sid);
var sgAtoms = SGroup.getAtoms(this, sgroup);
if (sgCounts[key] === sgAtoms.length)
sgroupList.push(sid);
}
return sgroupList;
};
Struct.prototype.isBlank = function () {
return this.atoms.count() === 0 &&
this.rxnArrows.count() === 0 &&
this.rxnPluses.count() === 0 && !this.isChiral;
};
Struct.prototype.toLists = function () {
var aidMap = {};
var atomList = [];
this.atoms.each(function (aid, atom) {
aidMap[aid] = atomList.length;
atomList.push(atom);
});
var bondList = [];
this.bonds.each(function (bid, bond) {
var b = new Bond(bond);
b.begin = aidMap[bond.begin];
b.end = aidMap[bond.end];
bondList.push(b);
});
return {
atoms: atomList,
bonds: bondList
};
};
Struct.prototype.clone = function (atomSet, bondSet, dropRxnSymbols, aidMap) {
var cp = new Struct();
return this.mergeInto(cp, atomSet, bondSet, dropRxnSymbols, false, aidMap);
};
Struct.prototype.getScaffold = function () {
var atomSet = Set.empty();
this.atoms.each(function (aid) {
Set.add(atomSet, aid);
}, this);
this.rgroups.each(function (rgid, rg) {
rg.frags.each(function (fnum, fid) {
this.atoms.each(function (aid, atom) {
if (atom.fragment === fid)
Set.remove(atomSet, aid);
}, this);
}, this);
}, this);
return this.clone(atomSet);
};
Struct.prototype.getFragmentIds = function (fid) {
const atomSet = Set.empty();
this.atoms.each((aid, atom) => {
if (atom.fragment === fid)
Set.add(atomSet, aid);
});
return atomSet;
};
Struct.prototype.getFragment = function (fid) {
return this.clone(this.getFragmentIds(fid));
};
Struct.prototype.mergeInto = function (cp, atomSet, bondSet, dropRxnSymbols, keepAllRGroups, aidMap) { // eslint-disable-line max-params, max-statements
atomSet = atomSet || Set.keySetInt(this.atoms);
bondSet = bondSet || Set.keySetInt(this.bonds);
bondSet = Set.filter(bondSet, function (bid) {
var bond = this.bonds.get(bid);
return Set.contains(atomSet, bond.begin) && Set.contains(atomSet, bond.end);
}, this);
var fidMask = {};
this.atoms.each(function (aid, atom) {
if (Set.contains(atomSet, aid))
fidMask[atom.fragment] = 1;
});
var fidMap = {};
this.frags.each(function (fid, frag) {
if (fidMask[fid])
fidMap[fid] = cp.frags.add(Object.assign({}, frag));
});
var rgroupsIds = [];
this.rgroups.each(function (rgid, rgroup) {
var keepGroup = keepAllRGroups;
if (!keepGroup) {
rgroup.frags.each(function (fnum, fid) {
rgroupsIds.push(fid);
if (fidMask[fid])
keepGroup = true;
});
if (!keepGroup)
return;
}
var rg = cp.rgroups.get(rgid);
if (rg) {
rgroup.frags.each(function (fnum, fid) {
rgroupsIds.push(fid);
if (fidMask[fid])
rg.frags.add(fidMap[fid]);
});
} else {
cp.rgroups.set(rgid, rgroup.clone(fidMap));
}
});
if (typeof aidMap === 'undefined' || aidMap === null)
aidMap = {};
// atoms in not RGroup
this.atoms.each(function (aid, atom) {
if (Set.contains(atomSet, aid) && rgroupsIds.indexOf(atom.fragment) === -1)
aidMap[aid] = cp.atoms.add(atom.clone(fidMap));
});
// atoms in RGroup
this.atoms.each(function (aid, atom) {
if (Set.contains(atomSet, aid) && rgroupsIds.indexOf(atom.fragment) !== -1)
aidMap[aid] = cp.atoms.add(atom.clone(fidMap));
});
var bidMap = {};
this.bonds.each(function (bid, bond) {
if (Set.contains(bondSet, bid))
bidMap[bid] = cp.bonds.add(bond.clone(aidMap));
});
this.sgroups.each(function (sid, sg) {
var i;
for (i = 0; i < sg.atoms.length; ++i) {
if (!Set.contains(atomSet, sg.atoms[i]))
return;
}
sg = SGroup.clone(sg, aidMap, bidMap);
var id = cp.sgroups.add(sg);
sg.id = id;
for (i = 0; i < sg.atoms.length; ++i)
Set.add(cp.atoms.get(sg.atoms[i]).sgs, id);
if (sg.type === 'DAT')
cp.sGroupForest.insert(sg.id, -1, []);
else
cp.sGroupForest.insert(sg.id);
});
cp.isChiral = this.isChiral;
if (!dropRxnSymbols) {
cp.isReaction = this.isReaction;
this.rxnArrows.each(function (id, item) {
cp.rxnArrows.add(item.clone());
});
this.rxnPluses.each(function (id, item) {
cp.rxnPluses.add(item.clone());
});
}
return cp;
};
Struct.prototype.findBondId = function (begin, end) {
var id = -1;
this.bonds.find(function (bid, bond) {
if ((bond.begin === begin && bond.end === end) ||
(bond.begin === end && bond.end === begin)) {
id = bid;
return true;
}
return false;
}, this);
return id;
};
function HalfBond(/* num*/begin, /* num*/end, /* num*/bid) { // eslint-disable-line max-params, max-statements
console.assert(arguments.length === 3, 'Invalid parameter number!');
this.begin = begin - 0;
this.end = end - 0;
this.bid = bid - 0;
// rendering properties
this.dir = new Vec2(); // direction
this.norm = new Vec2(); // left normal
this.ang = 0; // angle to (1,0), used for sorting the bonds
this.p = new Vec2(); // corrected origin position
this.loop = -1; // left loop id if the half-bond is in a loop, otherwise -1
this.contra = -1; // the half bond contrary to this one
this.next = -1; // the half-bond next ot this one in CCW order
this.leftSin = 0;
this.leftCos = 0;
this.leftNeighbor = 0;
this.rightSin = 0;
this.rightCos = 0;
this.rightNeighbor = 0;
}
Struct.prototype.initNeighbors = function () {
this.atoms.each(function (aid, atom) {
atom.neighbors = [];
});
this.bonds.each(function (bid, bond) {
var a1 = this.atoms.get(bond.begin);
var a2 = this.atoms.get(bond.end);
a1.neighbors.push(bond.hb1);
a2.neighbors.push(bond.hb2);
}, this);
};
Struct.prototype.bondInitHalfBonds = function (bid, /* opt*/ bond) {
bond = bond || this.bonds.get(bid);
bond.hb1 = 2 * bid;
bond.hb2 = 2 * bid + 1; // eslint-disable-line no-mixed-operators
this.halfBonds.set(bond.hb1, new HalfBond(bond.begin, bond.end, bid));
this.halfBonds.set(bond.hb2, new HalfBond(bond.end, bond.begin, bid));
var hb1 = this.halfBonds.get(bond.hb1);
var hb2 = this.halfBonds.get(bond.hb2);
hb1.contra = bond.hb2;
hb2.contra = bond.hb1;
};
Struct.prototype.halfBondUpdate = function (hbid) {
var hb = this.halfBonds.get(hbid);
var p1 = this.atoms.get(hb.begin).pp;
var p2 = this.atoms.get(hb.end).pp;
var d = Vec2.diff(p2, p1).normalized();
hb.dir = Vec2.dist(p2, p1) > 1e-4 ? d : new Vec2(1, 0);
hb.norm = hb.dir.turnLeft();
hb.ang = hb.dir.oxAngle();
if (hb.loop < 0)
hb.loop = -1;
};
Struct.prototype.initHalfBonds = function () {
this.halfBonds.clear();
this.bonds.each(this.bondInitHalfBonds, this);
};
Struct.prototype.setHbNext = function (hbid, next) {
this.halfBonds.get(this.halfBonds.get(hbid).contra).next = next;
};
Struct.prototype.halfBondSetAngle = function (hbid, left) {
var hb = this.halfBonds.get(hbid);
var hbl = this.halfBonds.get(left);
hbl.rightCos = hb.leftCos = Vec2.dot(hbl.dir, hb.dir);
hbl.rightSin = hb.leftSin = Vec2.cross(hbl.dir, hb.dir);
hb.leftNeighbor = left;
hbl.rightNeighbor = hbid;
};
Struct.prototype.atomAddNeighbor = function (hbid) {
var hb = this.halfBonds.get(hbid);
var atom = this.atoms.get(hb.begin);
var i = 0;
for (i = 0; i < atom.neighbors.length; ++i) {
if (this.halfBonds.get(atom.neighbors[i]).ang > hb.ang)
break;
}
atom.neighbors.splice(i, 0, hbid);
var ir = atom.neighbors[(i + 1) % atom.neighbors.length];
var il = atom.neighbors[(i + atom.neighbors.length - 1) %
atom.neighbors.length];
this.setHbNext(il, hbid);
this.setHbNext(hbid, ir);
this.halfBondSetAngle(hbid, il);
this.halfBondSetAngle(ir, hbid);
};
Struct.prototype.atomSortNeighbors = function (aid) {
var atom = this.atoms.get(aid);
var halfBonds = this.halfBonds;
atom.neighbors = atom.neighbors.sort(function (nei, nei2) {
return halfBonds.get(nei).ang - halfBonds.get(nei2).ang;
});
var i;
for (i = 0; i < atom.neighbors.length; ++i) {
this.halfBonds.get(this.halfBonds.get(atom.neighbors[i]).contra).next =
atom.neighbors[(i + 1) % atom.neighbors.length];
}
for (i = 0; i < atom.neighbors.length; ++i) {
this.halfBondSetAngle(atom.neighbors[(i + 1) % atom.neighbors.length],
atom.neighbors[i]);
}
};
Struct.prototype.sortNeighbors = function (list) {
function f(aid) {
this.atomSortNeighbors(aid);
}
if (!list)
this.atoms.each(f, this);
else
list.forEach(f, this);
};
Struct.prototype.atomUpdateHalfBonds = function (aid) {
var nei = this.atoms.get(aid).neighbors;
for (var i = 0; i < nei.length; ++i) {
var hbid = nei[i];
this.halfBondUpdate(hbid);
this.halfBondUpdate(this.halfBonds.get(hbid).contra);
}
};
Struct.prototype.updateHalfBonds = function (list) {
function f(aid) {
this.atomUpdateHalfBonds(aid);
}
if (!list)
this.atoms.each(f, this);
else
list.forEach(f, this);
};
Struct.prototype.sGroupsRecalcCrossBonds = function () {
this.sgroups.each(function (sgid, sg) {
sg.xBonds = [];
sg.neiAtoms = [];
}, this);
this.bonds.each(function (bid, bond) {
var a1 = this.atoms.get(bond.begin);
var a2 = this.atoms.get(bond.end);
Set.each(a1.sgs, function (sgid) {
if (!Set.contains(a2.sgs, sgid)) {
var sg = this.sgroups.get(sgid);
sg.xBonds.push(bid);
arrayAddIfMissing(sg.neiAtoms, bond.end);
}
}, this);
Set.each(a2.sgs, function (sgid) {
if (!Set.contains(a1.sgs, sgid)) {
var sg = this.sgroups.get(sgid);
sg.xBonds.push(bid);
arrayAddIfMissing(sg.neiAtoms, bond.begin);
}
}, this);
}, this);
};
Struct.prototype.sGroupDelete = function (sgid) {
var sg = this.sgroups.get(sgid);
for (var i = 0; i < sg.atoms.length; ++i)
Set.remove(this.atoms.get(sg.atoms[i]).sgs, sgid);
this.sGroupForest.remove(sgid);
this.sgroups.remove(sgid);
};
Struct.prototype.atomSetPos = function (id, pp) {
var itemId = this['atoms'].get(id);
itemId.pp = pp;
};
Struct.prototype.rxnPlusSetPos = function (id, pp) {
var itemId = this['rxnPluses'].get(id);
itemId.pp = pp;
};
Struct.prototype.rxnArrowSetPos = function (id, pp) {
var itemId = this['rxnArrows'].get(id);
itemId.pp = pp;
};
Struct.prototype.getCoordBoundingBox = function (atomSet) {
var bb = null;
function extend(pp) {
if (!bb) {
bb = {
min: pp,
max: pp
};
} else {
bb.min = Vec2.min(bb.min, pp);
bb.max = Vec2.max(bb.max, pp);
}
}
var global = typeof (atomSet) === 'undefined';
this.atoms.each(function (aid, atom) {
if (global || Set.contains(atomSet, aid))
extend(atom.pp);
});
if (global) {
this.rxnPluses.each(function (id, item) {
extend(item.pp);
});
this.rxnArrows.each(function (id, item) {
extend(item.pp);
});
}
if (!bb && global) {
bb = {
min: new Vec2(0, 0),
max: new Vec2(1, 1)
};
}
return bb;
};
Struct.prototype.getCoordBoundingBoxObj = function () {
var bb = null;
function extend(pp) {
if (!bb) {
bb = {
min: new Vec2(pp),
max: new Vec2(pp)
};
} else {
bb.min = Vec2.min(bb.min, pp);
bb.max = Vec2.max(bb.max, pp);
}
}
this.atoms.each(function (aid, atom) {
extend(atom.pp);
});
return bb;
};
Struct.prototype.getBondLengthData = function () {
var totalLength = 0;
var cnt = 0;
this.bonds.each(function (bid, bond) {
totalLength += Vec2.dist(
this.atoms.get(bond.begin).pp,
this.atoms.get(bond.end).pp);
cnt++;
}, this);
return { cnt: cnt, totalLength: totalLength };
};
Struct.prototype.getAvgBondLength = function () {
var bld = this.getBondLengthData();
return bld.cnt > 0 ? bld.totalLength / bld.cnt : -1;
};
Struct.prototype.getAvgClosestAtomDistance = function () {
var totalDist = 0;
var minDist;
var dist = 0;
var keys = this.atoms.keys();
var k;
var j;
for (k = 0; k < keys.length; ++k) {
minDist = -1;
for (j = 0; j < keys.length; ++j) {
if (j == k)
continue; // eslint-disable-line no-continue
dist = Vec2.dist(this.atoms.get(keys[j]).pp, this.atoms.get(keys[k]).pp);
if (minDist < 0 || minDist > dist)
minDist = dist;
}
totalDist += minDist;
}
return keys.length > 0 ? totalDist / keys.length : -1;
};
Struct.prototype.checkBondExists = function (begin, end) {
var bondExists = false;
this.bonds.each(function (bid, bond) {
if ((bond.begin == begin && bond.end == end) ||
(bond.end == begin && bond.begin == end))
bondExists = true;
}, this);
return bondExists;
};
function Loop(/* Array of num*/hbs, /* Struct*/struct, /* bool*/convex) {
this.hbs = hbs; // set of half-bonds involved
this.dblBonds = 0; // number of double bonds in the loop
this.aromatic = true;
this.convex = convex || false;
hbs.forEach(function (hb) {
var bond = struct.bonds.get(struct.halfBonds.get(hb).bid);
if (bond.type != Bond.PATTERN.TYPE.AROMATIC)
this.aromatic = false;
if (bond.type == Bond.PATTERN.TYPE.DOUBLE)
this.dblBonds++;
}, this);
}
Struct.prototype.findConnectedComponent = function (aid) {
var map = {};
var list = [aid];
var ids = Set.empty();
while (list.length > 0) {
(function () {
var aid = list.pop();
map[aid] = 1;
Set.add(ids, aid);
var atom = this.atoms.get(aid);
for (var i = 0; i < atom.neighbors.length; ++i) {
var neiId = this.halfBonds.get(atom.neighbors[i]).end;
if (!Set.contains(ids, neiId))
list.push(neiId);
}
}).apply(this);
}
return ids;
};
Struct.prototype.findConnectedComponents = function (discardExistingFragments) {
// NB: this is a hack
// TODO: need to maintain half-bond and neighbor structure permanently
if (!this.halfBonds.count()) {
this.initHalfBonds();
this.initNeighbors();
this.updateHalfBonds(this.atoms.keys());
this.sortNeighbors(this.atoms.keys());
}
var map = {};
this.atoms.each(function (aid) {
map[aid] = -1;
}, this);
var components = [];
this.atoms.each(function (aid, atom) {
if ((discardExistingFragments || atom.fragment < 0) && map[aid] < 0) {
var component = this.findConnectedComponent(aid);
components.push(component);
Set.each(component, function (aid) {
map[aid] = 1;
}, this);
}
}, this);
return components;
};
Struct.prototype.markFragment = function (ids) {
var frag = {};
var fid = this.frags.add(frag);
Set.each(ids, function (aid) {
this.atoms.get(aid).fragment = fid;
}, this);
};
Struct.prototype.markFragmentByAtomId = function (aid) {
this.markFragment(this.findConnectedComponent(aid));
};
Struct.prototype.markFragments = function () {
var components = this.findConnectedComponents();
for (var i = 0; i < components.length; ++i)
this.markFragment(components[i]);
};
Struct.prototype.scale = function (scale) {
if (scale != 1) {
this.atoms.each(function (aid, atom) {
atom.pp = atom.pp.scaled(scale);
}, this);
this.rxnPluses.each(function (id, item) {
item.pp = item.pp.scaled(scale);
}, this);
this.rxnArrows.each(function (id, item) {
item.pp = item.pp.scaled(scale);
}, this);
this.sgroups.each(function (id, item) {
item.pp = item.pp ? item.pp.scaled(scale) : null;
}, this);
}
};
Struct.prototype.rescale = function () {
var avg = this.getAvgBondLength();
if (avg < 0 && !this.isReaction) // TODO [MK] this doesn't work well for reactions as the distances between
// the atoms in different components are generally larger than those between atoms of a single component
// (KETCHER-341)
avg = this.getAvgClosestAtomDistance();
if (avg < 1e-3)
avg = 1;
var scale = 1 / avg;
this.scale(scale);
};
Struct.prototype.loopHasSelfIntersections = function (hbs) {
for (var i = 0; i < hbs.length; ++i) {
var hbi = this.halfBonds.get(hbs[i]);
var ai = this.atoms.get(hbi.begin).pp;
var bi = this.atoms.get(hbi.end).pp;
var set = Set.fromList([hbi.begin, hbi.end]);
for (var j = i + 2; j < hbs.length; ++j) {
var hbj = this.halfBonds.get(hbs[j]);
if (Set.contains(set, hbj.begin) || Set.contains(set, hbj.end))
/* eslint-disable no-continue*/
continue; // skip edges sharing an atom
/* eslint-enable no-continue*/
var aj = this.atoms.get(hbj.begin).pp;
var bj = this.atoms.get(hbj.end).pp;
if (Vec2.segmentIntersection(ai, bi, aj, bj))
return true;
}
}
return false;
};
// partition a cycle into simple cycles
// TODO: [MK] rewrite the detection algorithm to only find simple ones right away?
Struct.prototype.partitionLoop = function (loop) { // eslint-disable-line max-statements
var subloops = [];
var continueFlag = true;
search: while (continueFlag) { // eslint-disable-line no-restricted-syntax
var atomToHalfBond = {}; // map from every atom in the loop to the index of the first half-bond starting from that atom in the uniqHb array
for (var l = 0; l < loop.length; ++l) {
var hbid = loop[l];
var aid1 = this.halfBonds.get(hbid).begin;
var aid2 = this.halfBonds.get(hbid).end;
if (aid2 in atomToHalfBond) { // subloop found
var s = atomToHalfBond[aid2]; // where the subloop begins
var subloop = loop.slice(s, l + 1);
subloops.push(subloop);
if (l < loop.length) // remove half-bonds corresponding to the subloop
loop.splice(s, l - s + 1);
continue search; // eslint-disable-line no-continue
}
atomToHalfBond[aid1] = l;
}
continueFlag = false; // we're done, no more subloops found
subloops.push(loop);
}
return subloops;
};
Struct.prototype.halfBondAngle = function (hbid1, hbid2) {
var hba = this.halfBonds.get(hbid1);
var hbb = this.halfBonds.get(hbid2);
return Math.atan2(
Vec2.cross(hba.dir, hbb.dir),
Vec2.dot(hba.dir, hbb.dir));
};
Struct.prototype.loopIsConvex = function (loop) {
for (var k = 0; k < loop.length; ++k) {
var angle = this.halfBondAngle(loop[k], loop[(k + 1) % loop.length]);
if (angle > 0)
return false;
}
return true;
};
// check whether a loop is on the inner or outer side of the polygon
// by measuring the total angle between bonds
Struct.prototype.loopIsInner = function (loop) {
var totalAngle = 2 * Math.PI;
for (var k = 0; k < loop.length; ++k) {
var hbida = loop[k];
var hbidb = loop[(k + 1) % loop.length];
var hbb = this.halfBonds.get(hbidb);
var angle = this.halfBondAngle(hbida, hbidb);
if (hbb.contra == loop[k]) // back and forth along the same edge
totalAngle += Math.PI;
else
totalAngle += angle;
}
return Math.abs(totalAngle) < Math.PI;
};
Struct.prototype.findLoops = function () {
var newLoops = [];
var bondsToMark = Set.empty();
/*
Starting from each half-bond not known to be in a loop yet,
follow the 'next' links until the initial half-bond is reached or
the length of the sequence exceeds the number of half-bonds available.
In a planar graph, as long as every bond is a part of some "loop" -
either an outer or an inner one - every iteration either yields a loop
or doesn't start at all. Thus this has linear complexity in the number
of bonds for planar graphs.
*/
var hbIdNext, c, loop, loopId;
this.halfBonds.each(function (hbId, hb) {
if (hb.loop !== -1)
return;
for (hbIdNext = hbId, c = 0, loop = []; c <= this.halfBonds.count(); hbIdNext = this.halfBonds.get(hbIdNext).next, ++c) {
if (!(c > 0 && hbIdNext === hbId)) {
loop.push(hbIdNext);
continue;
}
// loop found
var subloops = this.partitionLoop(loop);
subloops.forEach(function (loop) {
if (this.loopIsInner(loop) && !this.loopHasSelfIntersections(loop)) {
/*
loop is internal
use lowest half-bond id in the loop as the loop id
this ensures that the loop gets the same id if it is discarded and then recreated,
which in turn is required to enable redrawing while dragging, as actions store item id's
*/
loopId = Math.min.apply(Math, loop);
this.loops.set(loopId, new Loop(loop, this, this.loopIsConvex(loop)));
} else {
loopId = -2;
}
loop.forEach(function (hbid) {
this.halfBonds.get(hbid).loop = loopId;
Set.add(bondsToMark, this.halfBonds.get(hbid).bid);
}, this);
if (loopId >= 0)
newLoops.push(loopId);
}, this);
break;
}
}, this);
return {
newLoops: newLoops,
bondsToMark: Set.list(bondsToMark)
};
};
// NB: this updates the structure without modifying the corresponding ReStruct.
// To be applied to standalone structures only.
Struct.prototype.prepareLoopStructure = function () {
this.initHalfBonds();
this.initNeighbors();
this.updateHalfBonds(this.atoms.keys());
this.sortNeighbors(this.atoms.keys());
this.findLoops();
};
Struct.prototype.atomAddToSGroup = function (sgid, aid) {
// TODO: [MK] make sure the addition does not break the hierarchy?
SGroup.addAtom(this.sgroups.get(sgid), aid);
Set.add(this.atoms.get(aid).sgs, sgid);
};
Struct.prototype.calcConn = function (aid) {
var conn = 0;
var atom = this.atoms.get(aid);
var oddLoop = false;
var hasAromatic = false;
for (var i = 0; i < atom.neighbors.length; ++i) {
var hb = this.halfBonds.get(atom.neighbors[i]);
var bond = this.bonds.get(hb.bid);
switch (bond.type) {
case Bond.PATTERN.TYPE.SINGLE:
conn += 1;
break;
case Bond.PATTERN.TYPE.DOUBLE:
conn += 2;
break;
case Bond.PATTERN.TYPE.TRIPLE:
conn += 3;
break;
case Bond.PATTERN.TYPE.AROMATIC:
conn += 1;
hasAromatic = true;
this.loops.each(function (id, item) {
if (item.hbs.indexOf(atom.neighbors[i]) != -1 && item.hbs.length % 2 == 1)
oddLoop = true;
}, this);
break;
default:
return -1;
}
}
if (hasAromatic && !atom.hasImplicitH && !oddLoop)
conn += 1;
return conn;
};
Struct.prototype.calcImplicitHydrogen = function (aid) {
var conn = this.calcConn(aid);
var atom = this.atoms.get(aid);
atom.badConn = false;
if (conn < 0 || atom.isQuery()) {
atom.implicitH = 0;
return;
}
if (atom.explicitValence >= 0) {
var elem = element.map[atom.label];
atom.implicitH = 0;
if (elem != null) {
atom.implicitH = atom.explicitValence - atom.calcValenceMinusHyd(conn);
if (atom.implicitH < 0) {
atom.implicitH = 0;
atom.badConn = true;
}
}
} else {
atom.calcValence(conn);
}
};
Struct.prototype.setImplicitHydrogen = function (list) {
this.sgroups.each(function (id, item) {
if (item.data.fieldName === "MRV_IMPLICIT_H")
this.atoms.get(item.atoms[0]).hasImplicitH = true;
}, this);
function f(aid) {
this.calcImplicitHydrogen(aid);
}
if (!list)
this.atoms.each(f, this);
else
list.forEach(f, this);
};
Struct.prototype.getComponents = function () { // eslint-disable-line max-statements
/* saver */
var ccs = this.findConnectedComponents(true);
var barriers = [];
var arrowPos = null;
this.rxnArrows.each(function (id, item) { // there's just one arrow
arrowPos = item.pp.x;
});
this.rxnPluses.each(function (id, item) {
barriers.push(item.pp.x);
});
if (arrowPos != null)
barriers.push(arrowPos);
barriers.sort(function (a, b) {
return a - b;
});
var components = [];
var i;
for (i = 0; i < ccs.length; ++i) {
var bb = this.getCoordBoundingBox(ccs[i]);
var c = Vec2.lc2(bb.min, 0.5, bb.max, 0.5);
var j = 0;
while (c.x > barriers[j])
++j;
components[j] = components[j] || {};
Set.mergeIn(components[j], ccs[i]);
}
var submolTexts = [];
var reactants = [];
var products = [];
for (i = 0; i < components.length; ++i) {
if (!components[i]) {
submolTexts.push('');
continue; // eslint-disable-line no-continue
}
bb = this.getCoordBoundingBox(components[i]);
c = Vec2.lc2(bb.min, 0.5, bb.max, 0.5);
if (c.x < arrowPos)
reactants.push(components[i]);
else
products.push(components[i]);
}
return {
reactants: reactants,
products: products
};
};
// Other struct objects
function RxnPlus(params) {
params = params || {};
this.pp = params.pp ? new Vec2(params.pp) : new Vec2();
}
RxnPlus.prototype.clone = function () {
return new RxnPlus(this);
};
function RxnArrow(params) {
params = params || {};
this.pp = params.pp ? new Vec2(params.pp) : new Vec2();
}
RxnArrow.prototype.clone = function () {
return new RxnArrow(this);
};
function arrayAddIfMissing(array, item) {
for (var i = 0; i < array.length; ++i) {
if (array[i] === item)
return false;
}
array.push(item);
return true;
}
module.exports = Object.assign(Struct, {
Atom: Atom,
AtomList: AtomList,
Bond: Bond,
SGroup: SGroup,
RGroup: RGroup,
RxnPlus: RxnPlus,
RxnArrow: RxnArrow
});

View File

@ -0,0 +1,51 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Pool = require('../../util/pool');
function RGroup(logic) {
logic = logic || {};
this.frags = new Pool();
this.resth = logic.resth || false;
this.range = logic.range || '';
this.ifthen = logic.ifthen || 0;
}
RGroup.prototype.getAttrs = function () {
return {
resth: this.resth,
range: this.range,
ifthen: this.ifthen
};
};
RGroup.findRGroupByFragment = function (rgroups, frid) {
var ret;
rgroups.each(function (rgid, rgroup) {
if (rgroup.frags.keyOf(frid)) ret = rgid;
});
return ret;
};
RGroup.prototype.clone = function (fidMap) {
var ret = new RGroup(this);
this.frags.each(function (fnum, fid) {
ret.frags.add(fidMap ? fidMap[fid] : fid);
});
return ret;
};
module.exports = RGroup;

View File

@ -0,0 +1,164 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Map = require('../../util/map');
var Set = require('../../util/set');
function SGroupForest(molecule) {
this.parent = new Map(); // child id -> parent id
this.children = new Map(); // parent id -> list of child ids
this.children.set(-1, []); // extra root node
this.molecule = molecule;
}
// returns an array or s-group ids in the order of breadth-first search
SGroupForest.prototype.getSGroupsBFS = function () {
var order = [];
var id = -1;
var queue = [].slice.call(this.children.get(-1));
while (queue.length > 0) {
id = queue.shift();
queue = queue.concat(this.children.get(id));
order.push(id);
}
return order;
};
SGroupForest.prototype.getAtomSets = function () {
return this.molecule.sgroups.map(function (sgid, sgroup) {
return Set.fromList(sgroup.atoms);
});
};
SGroupForest.prototype.getAtomSetRelations = function (newId, atoms /* Set */) {
// find the lowest superset in the hierarchy
var isStrictSuperset = new Map();
var isSubset = new Map();
var atomSets = this.getAtomSets();
atomSets.unset(newId);
atomSets.each(function (id, atomSet) {
isSubset.set(id, Set.subset(atoms, atomSet));
isStrictSuperset.set(id, Set.subset(atomSet, atoms) && !Set.eq(atomSet, atoms));
}, this);
var parents = atomSets.findAll(function (id) {
if (!isSubset.get(id))
return false;
if (this.children.get(id).findIndex(function (childId) {
return isSubset.get(childId);
}, this) >= 0)
return false;
return true;
}, this);
console.assert(parents.length <= 1, "We are here"); // there should be only one parent
var children = atomSets.findAll(function (id) {
return isStrictSuperset.get(id) && !isStrictSuperset.get(this.parent.get(id));
}, this);
return {
children: children,
parent: parents.length === 0 ? -1 : parents[0]
};
};
SGroupForest.prototype.getPathToRoot = function (sgid) {
var path = [];
for (var id = sgid; id >= 0; id = this.parent.get(id)) {
console.assert(path.indexOf(id) < 0, 'SGroupForest: loop detected');
path.push(id);
}
return path;
};
SGroupForest.prototype.validate = function () {
var atomSets = this.getAtomSets();
this.molecule.sgroups.each(function (id) {
this.getPathToRoot(id); // this will throw an exception if there is a loop in the path to root
}, this);
var valid = true;
// 1) child group's atom set is a subset of the parent one's
this.parent.each(function (id, parentId) {
if (parentId >= 0 && !Set.subset(atomSets.get(id), atomSets.get(parentId)))
valid = false;
}, this);
// 2) siblings have disjoint atom sets
this.children.each(function (parentId) {
var list = this.children.get(parentId);
for (var i = 0; i < list.length; ++i) {
for (var j = i + 1; j < list.length; ++j) {
var id1 = list[i];
var id2 = list[j];
var sg1 = this.molecule.sgroups.get(id1);
var sg2 = this.molecule.sgroups.get(id2);
if (!Set.disjoint(atomSets.get(id1), atomSets.get(id2)) && sg1.type != 'DAT' && sg2.type != 'DAT')
valid = false;
}
}
}, this);
return valid;
};
SGroupForest.prototype.insert = function (id, parent /* int, optional */, children /* [int], optional */) {
console.assert(!this.parent.has(id), 'sgid already present in the forest');
console.assert(!this.children.has(id), 'sgid already present in the forest');
console.assert(this.validate(), 's-group forest invalid');
var atomSets = this.getAtomSets();
var atoms = Set.fromList(this.molecule.sgroups.get(id).atoms);
if (!parent || !children) { // if these are not provided, deduce automatically
var guess = this.getAtomSetRelations(id, atoms, atomSets);
parent = guess.parent;
children = guess.children;
}
// TODO: make children Map<int, Set> instead of Map<int, []>?
children.forEach(function (childId) { // reset parent links
var childs = this.children.get(this.parent.get(childId));
var i = childs.indexOf(childId);
console.assert(i >= 0 && childs.indexOf(childId, i + 1) < 0, 'Assertion failed'); // one element
childs.splice(i, 1);
this.parent.set(childId, id);
}, this);
this.children.set(id, children);
this.parent.set(id, parent);
this.children.get(parent).push(id);
console.assert(this.validate(), 's-group forest invalid');
return { parent: parent, children: children };
};
SGroupForest.prototype.remove = function (id) {
console.assert(this.parent.has(id), 'sgid is not in the forest');
console.assert(this.children.has(id), 'sgid is not in the forest');
console.assert(this.validate(), 's-group forest invalid');
var parentId = this.parent.get(id);
this.children.get(id).forEach(function (childId) { // reset parent links
this.parent.set(childId, parentId);
this.children.get(parentId).push(childId);
}, this);
var childs = this.children.get(parentId);
var i = childs.indexOf(id);
console.assert(i >= 0 && childs.indexOf(id, i + 1) < 0, 'Assertion failed'); // one element
childs.splice(i, 1);
this.children.unset(id);
this.parent.unset(id);
console.assert(this.validate(), 's-group forest invalid');
};
module.exports = SGroupForest;

View File

@ -0,0 +1,383 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Box2Abs = require('../../util/box2abs');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Atom = require('./atom');
var Bond = require('./bond');
function SGroup(type) { // eslint-disable-line max-statements
console.assert(type && type in SGroup.TYPES, 'Invalid or unsupported s-group type');
this.type = type;
this.id = -1;
this.label = -1;
this.bracketBox = null;
this.bracketDir = new Vec2(1, 0);
this.areas = [];
this.highlight = false;
this.highlighting = null;
this.selected = false;
this.selectionPlate = null;
this.atoms = [];
this.patoms = [];
this.bonds = [];
this.xBonds = [];
this.neiAtoms = [];
this.pp = null;
this.data = {
mul: 1, // multiplication count for MUL group
connectivity: 'ht', // head-to-head, head-to-tail or either-unknown
name: '',
subscript: 'n',
// data s-group fields
attached: false,
absolute: true,
showUnits: false,
nCharsToDisplay: -1,
tagChar: '',
daspPos: 1,
fieldType: 'F',
fieldName: '',
fieldValue: '',
units: '',
query: '',
queryOp: ''
};
}
SGroup.TYPES = {
MUL: 1,
SRU: 2,
SUP: 3,
DAT: 4,
GEN: 5
};
// TODO: these methods should be overridden
// and should only accept valid attributes for each S-group type.
// The attributes should be accessed via these methods only and not directly through this.data.
// stub
SGroup.prototype.getAttr = function (attr) {
return this.data[attr];
};
// TODO: should be group-specific
SGroup.prototype.getAttrs = function () {
var attrs = {};
for (var attr in this.data) {
if (this.data.hasOwnProperty(attr))
attrs[attr] = this.data[attr];
}
return attrs;
};
// stub
SGroup.prototype.setAttr = function (attr, value) {
var oldValue = this.data[attr];
this.data[attr] = value;
return oldValue;
};
// stub
SGroup.prototype.checkAttr = function (attr, value) {
return this.data[attr] == value;
};
// SGroup.numberArrayToString = function (numbers, map) {
// var str = util.stringPadded(numbers.length, 3);
// for (var i = 0; i < numbers.length; ++i) {
// str += ' ' + util.stringPadded(map[numbers[i]], 3);
// }
// return str;
// };
SGroup.filterAtoms = function (atoms, map) {
var newAtoms = [];
for (var i = 0; i < atoms.length; ++i) {
var aid = atoms[i];
if (typeof (map[aid]) !== 'number')
newAtoms.push(aid);
else if (map[aid] >= 0)
newAtoms.push(map[aid]);
else
newAtoms.push(-1);
}
return newAtoms;
};
SGroup.removeNegative = function (atoms) {
var newAtoms = [];
for (var j = 0; j < atoms.length; ++j) {
if (atoms[j] >= 0)
newAtoms.push(atoms[j]);
}
return newAtoms;
};
SGroup.filter = function (mol, sg, atomMap) {
sg.atoms = SGroup.removeNegative(SGroup.filterAtoms(sg.atoms, atomMap));
};
SGroup.clone = function (sgroup, aidMap) {
var cp = new SGroup(sgroup.type);
for (var field in sgroup.data) // TODO: remove all non-primitive properties from 'data'
cp.data[field] = sgroup.data[field];
cp.atoms = sgroup.atoms.map(function (elem) {
return aidMap[elem];
});
cp.pp = sgroup.pp;
cp.bracketBox = sgroup.bracketBox;
cp.patoms = null;
cp.bonds = null;
cp.allAtoms = sgroup.allAtoms;
return cp;
};
SGroup.addAtom = function (sgroup, aid) {
sgroup.atoms.push(aid);
};
SGroup.removeAtom = function (sgroup, aid) {
for (var i = 0; i < sgroup.atoms.length; ++i) {
if (sgroup.atoms[i] === aid) {
sgroup.atoms.splice(i, 1);
return;
}
}
console.error('The atom is not found in the given s-group');
};
SGroup.getCrossBonds = function (inBonds, xBonds, mol, parentAtomSet) {
mol.bonds.each(function (bid, bond) {
if (Set.contains(parentAtomSet, bond.begin) && Set.contains(parentAtomSet, bond.end)) {
if (inBonds != null)
inBonds.push(bid);
} else if (Set.contains(parentAtomSet, bond.begin) || Set.contains(parentAtomSet, bond.end)) {
if (xBonds != null)
xBonds.push(bid);
}
}, this);
};
SGroup.bracketPos = function (sg, mol, xbonds) { // eslint-disable-line max-statements
var atoms = sg.atoms;
if (!xbonds || xbonds.length !== 2) {
sg.bracketDir = new Vec2(1, 0);
} else {
var p1 = mol.bonds.get(xbonds[0]).getCenter(mol);
var p2 = mol.bonds.get(xbonds[1]).getCenter(mol);
sg.bracketDir = Vec2.diff(p2, p1).normalized();
}
var d = sg.bracketDir;
var bb = null;
var contentBoxes = [];
atoms.forEach(function (aid) {
var atom = mol.atoms.get(aid);
var pos = new Vec2(atom.pp);
var ext = new Vec2(0.05 * 3, 0.05 * 3);
var bba = new Box2Abs(pos, pos).extend(ext, ext);
contentBoxes.push(bba);
}, this);
contentBoxes.forEach(function (bba) {
var bbb = null;
[bba.p0.x, bba.p1.x].forEach(function (x) {
[bba.p0.y, bba.p1.y].forEach(function (y) {
var v = new Vec2(x, y);
var p = new Vec2(Vec2.dot(v, d), Vec2.dot(v, d.rotateSC(1, 0)));
bbb = (bbb === null) ? new Box2Abs(p, p) : bbb.include(p);
}, this);
}, this);
bb = (bb === null) ? bbb : Box2Abs.union(bb, bbb);
}, this);
var vext = new Vec2(0.2, 0.4);
if (bb !== null) bb = bb.extend(vext, vext);
sg.bracketBox = bb;
};
SGroup.getBracketParameters = function (mol, xbonds, atomSet, bb, d, n) { // eslint-disable-line max-params
function BracketParams(c, d, w, h) {
this.c = c;
this.d = d;
this.n = d.rotateSC(1, 0);
this.w = w;
this.h = h;
}
var brackets = [];
if (xbonds.length < 2) {
(function () {
d = d || new Vec2(1, 0);
n = n || d.rotateSC(1, 0);
var bracketWidth = Math.min(0.25, bb.sz().x * 0.3);
var cl = Vec2.lc2(d, bb.p0.x, n, 0.5 * (bb.p0.y + bb.p1.y));
var cr = Vec2.lc2(d, bb.p1.x, n, 0.5 * (bb.p0.y + bb.p1.y));
var bracketHeight = bb.sz().y;
brackets.push(new BracketParams(cl, d.negated(), bracketWidth, bracketHeight), new BracketParams(cr, d, bracketWidth, bracketHeight));
})();
} else if (xbonds.length === 2) {
(function () { // eslint-disable-line max-statements
var b1 = mol.bonds.get(xbonds[0]);
var b2 = mol.bonds.get(xbonds[1]);
var cl0 = b1.getCenter(mol);
var cr0 = b2.getCenter(mol);
var dr = Vec2.diff(cr0, cl0).normalized();
var dl = dr.negated();
var bracketWidth = 0.25;
var bracketHeight = 1.5;
brackets.push(new BracketParams(cl0.addScaled(dl, 0), dl, bracketWidth, bracketHeight),
new BracketParams(cr0.addScaled(dr, 0), dr, bracketWidth, bracketHeight));
})();
} else {
(function () {
for (var i = 0; i < xbonds.length; ++i) {
var b = mol.bonds.get(xbonds[i]);
var c = b.getCenter(mol);
var d = Set.contains(atomSet, b.begin) ? b.getDir(mol) : b.getDir(mol).negated();
brackets.push(new BracketParams(c, d, 0.2, 1.0));
}
})();
}
return brackets;
};
SGroup.getObjBBox = function (atoms, mol) {
console.assert(atoms.length != 0, 'Atom list is empty');
var a0 = mol.atoms.get(atoms[0]).pp;
var bb = new Box2Abs(a0, a0);
for (var i = 1; i < atoms.length; ++i) {
var aid = atoms[i];
var atom = mol.atoms.get(aid);
var p = atom.pp;
bb = bb.include(p);
}
return bb;
};
SGroup.getAtoms = function (mol, sg) {
/* shoud we use prototype? */
if (!sg.allAtoms)
return sg.atoms;
var atoms = [];
mol.atoms.each(function (aid) {
atoms.push(aid);
});
return atoms;
};
SGroup.getBonds = function (mol, sg) {
var atoms = SGroup.getAtoms(mol, sg);
var bonds = [];
mol.bonds.each(function (bid, bond) {
if (atoms.indexOf(bond.begin) >= 0 && atoms.indexOf(bond.end) >= 0) bonds.push(bid);
});
return bonds;
};
SGroup.prepareMulForSaving = function (sgroup, mol) { // eslint-disable-line max-statements
sgroup.atoms.sort((a, b) => a - b);
sgroup.atomSet = Set.fromList(sgroup.atoms);
sgroup.parentAtomSet = Set.clone(sgroup.atomSet);
var inBonds = [];
var xBonds = [];
mol.bonds.each(function (bid, bond) {
if (Set.contains(sgroup.parentAtomSet, bond.begin) && Set.contains(sgroup.parentAtomSet, bond.end))
inBonds.push(bid);
else if (Set.contains(sgroup.parentAtomSet, bond.begin) || Set.contains(sgroup.parentAtomSet, bond.end))
xBonds.push(bid);
}, sgroup);
if (xBonds.length !== 0 && xBonds.length !== 2) {
throw {
'id': sgroup.id,
'error-type': 'cross-bond-number',
'message': 'Unsupported cross-bonds number'
};
}
var xAtom1 = -1;
var xAtom2 = -1;
var crossBond = null;
if (xBonds.length === 2) {
var bond1 = mol.bonds.get(xBonds[0]);
xAtom1 = Set.contains(sgroup.parentAtomSet, bond1.begin) ? bond1.begin : bond1.end;
var bond2 = mol.bonds.get(xBonds[1]);
xAtom2 = Set.contains(sgroup.parentAtomSet, bond2.begin) ? bond2.begin : bond2.end;
crossBond = bond2;
}
var amap = null;
var tailAtom = xAtom2;
var newAtoms = [];
for (var j = 0; j < sgroup.data.mul - 1; j++) {
amap = {};
sgroup.atoms.forEach(function (aid) {
var atom = mol.atoms.get(aid);
var aid2 = mol.atoms.add(new Atom(atom));
newAtoms.push(aid2);
sgroup.atomSet[aid2] = 1;
amap[aid] = aid2;
});
inBonds.forEach(function (bid) {
var bond = mol.bonds.get(bid);
var newBond = new Bond(bond);
newBond.begin = amap[newBond.begin];
newBond.end = amap[newBond.end];
mol.bonds.add(newBond);
});
if (crossBond !== null) {
var newCrossBond = new Bond(crossBond);
newCrossBond.begin = tailAtom;
newCrossBond.end = amap[xAtom1];
mol.bonds.add(newCrossBond);
tailAtom = amap[xAtom2];
}
}
if (tailAtom >= 0) {
var xBond2 = mol.bonds.get(xBonds[1]);
if (xBond2.begin === xAtom2)
xBond2.begin = tailAtom;
else
xBond2.end = tailAtom;
}
sgroup.bonds = xBonds;
newAtoms.forEach(function (aid) {
mol.sGroupForest.getPathToRoot(sgroup.id).reverse().forEach(function (sgid) {
mol.atomAddToSGroup(sgid, aid);
});
});
};
SGroup.getMassCentre = function (mol, atoms) {
var c = new Vec2(); // mass centre
for (var i = 0; i < atoms.length; ++i)
c = c.addScaled(mol.atoms.get(atoms[i]).pp, 1.0 / atoms.length);
return c;
};
module.exports = SGroup;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,253 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../util/vec2');
const SELECTION_DISTANCE_COEFFICIENT = 0.4;
function findClosestAtom(restruct, pos, skip, minDist) {
var closestAtom = null;
var maxMinDist = SELECTION_DISTANCE_COEFFICIENT;
var skipId = skip && skip.map === 'atoms' ? skip.id : null;
minDist = minDist || maxMinDist;
minDist = Math.min(minDist, maxMinDist);
restruct.atoms.each(function (aid, atom) {
if (aid !== skipId) {
var dist = Vec2.dist(pos, atom.a.pp);
if (dist < minDist) {
closestAtom = aid;
minDist = dist;
}
}
});
if (closestAtom !== null) {
return {
id: closestAtom,
dist: minDist
};
}
return null;
}
function findClosestBond(restruct, pos, skip, minDist, scale) { // eslint-disable-line max-params
var closestBond = null;
var closestBondCenter = null;
var maxMinDist = SELECTION_DISTANCE_COEFFICIENT;
minDist = minDist || maxMinDist;
minDist = Math.min(minDist, maxMinDist);
var minCDist = minDist;
restruct.bonds.each(function (bid, bond) {
var p1 = restruct.atoms.get(bond.b.begin).a.pp,
p2 = restruct.atoms.get(bond.b.end).a.pp;
var mid = Vec2.lc2(p1, 0.5, p2, 0.5);
var cdist = Vec2.dist(pos, mid);
if (cdist < minCDist) {
minCDist = cdist;
closestBondCenter = bid;
}
});
restruct.bonds.each(function (bid, bond) {
var hb = restruct.molecule.halfBonds.get(bond.b.hb1);
var d = hb.dir;
var n = hb.norm;
var p1 = restruct.atoms.get(bond.b.begin).a.pp,
p2 = restruct.atoms.get(bond.b.end).a.pp;
var inStripe = Vec2.dot(pos.sub(p1), d) * Vec2.dot(pos.sub(p2), d) < 0;
if (inStripe) {
var dist = Math.abs(Vec2.dot(pos.sub(p1), n));
if (dist < minDist) {
closestBond = bid;
minDist = dist;
}
}
});
if (closestBondCenter !== null) {
return {
id: closestBondCenter,
dist: minCDist
};
}
if (closestBond !== null &&
minDist > SELECTION_DISTANCE_COEFFICIENT * scale) { // hack (ported from old code)
return {
id: closestBond,
dist: minDist
};
}
return null;
}
function findClosestChiralFlag(restruct, pos) {
var minDist;
var ret = null;
// there is only one chiral flag, but we treat it as a "map" for convenience
restruct.chiralFlags.each(function (id, item) {
var p = item.pp;
if (Math.abs(pos.x - p.x) < 1.0) {
var dist = Math.abs(pos.y - p.y);
if (dist < 0.3 && (!ret || dist < minDist)) {
minDist = dist;
ret = { id: id, dist: minDist };
}
}
});
return ret;
}
function findClosestDataSGroupData(restruct, pos) {
var minDist = null;
var ret = null;
restruct.sgroupData.each(function (id, item) {
if (item.sgroup.type !== 'DAT')
throw new Error('Data group expected');
if (item.sgroup.data.fieldName !== "MRV_IMPLICIT_H") {
var box = item.sgroup.dataArea;
var inBox = box.p0.y < pos.y && box.p1.y > pos.y && box.p0.x < pos.x && box.p1.x > pos.x;
var xDist = Math.min(Math.abs(box.p0.x - pos.x), Math.abs(box.p1.x - pos.x));
if (inBox && (ret == null || xDist < minDist)) {
ret = { id: id, dist: xDist };
minDist = xDist;
}
}
});
return ret;
}
function findClosestFrag(restruct, pos, skip, minDist) {
minDist = Math.min(minDist || SELECTION_DISTANCE_COEFFICIENT,
SELECTION_DISTANCE_COEFFICIENT);
var ret = null;
var skipId = skip && skip.map === 'frags' ? skip.id : null;
restruct.frags.each(function (fid, frag) {
if (fid != skipId) {
var bb = frag.calcBBox(restruct, fid); // TODO any faster way to obtain bb?
if (bb.p0.y < pos.y && bb.p1.y > pos.y && bb.p0.x < pos.x && bb.p1.x > pos.x) {
var xDist = Math.min(Math.abs(bb.p0.x - pos.x), Math.abs(bb.p1.x - pos.x));
if (!ret || xDist < minDist) {
minDist = xDist;
ret = { id: fid, dist: minDist };
}
}
}
});
return ret;
}
function findClosestRGroup(restruct, pos, skip, minDist) {
minDist = Math.min(minDist || SELECTION_DISTANCE_COEFFICIENT,
SELECTION_DISTANCE_COEFFICIENT);
var ret = null;
restruct.rgroups.each(function (rgid, rgroup) {
if (rgid != skip && rgroup.labelBox && rgroup.labelBox.contains(pos, 0.5)) {
var dist = Vec2.dist(rgroup.labelBox.centre(), pos);
if (!ret || dist < minDist) {
minDist = dist;
ret = { id: rgid, dist: minDist };
}
}
});
return ret;
}
function findClosestRxnArrow(restruct, pos) {
var minDist;
var ret = null;
restruct.rxnArrows.each(function (id, arrow) {
var p = arrow.item.pp;
if (Math.abs(pos.x - p.x) < 1.0) {
var dist = Math.abs(pos.y - p.y);
if (dist < 0.3 && (!ret || dist < minDist)) {
minDist = dist;
ret = { id: id, dist: minDist };
}
}
});
return ret;
}
function findClosestRxnPlus(restruct, pos) {
var minDist;
var ret = null;
restruct.rxnPluses.each(function (id, plus) {
var p = plus.item.pp;
var dist = Math.max(Math.abs(pos.x - p.x), Math.abs(pos.y - p.y));
if (dist < 0.3 && (!ret || dist < minDist)) {
minDist = dist;
ret = { id: id, dist: minDist };
}
});
return ret;
}
function findClosestSGroup(restruct, pos) {
var ret = null;
var minDist = SELECTION_DISTANCE_COEFFICIENT;
restruct.molecule.sgroups.each(function (sgid, sg) {
var d = sg.bracketDir,
n = d.rotateSC(1, 0);
var pg = new Vec2(Vec2.dot(pos, d), Vec2.dot(pos, n));
for (var i = 0; i < sg.areas.length; ++i) {
var box = sg.areas[i];
var inBox = box.p0.y < pg.y && box.p1.y > pg.y && box.p0.x < pg.x && box.p1.x > pg.x;
var xDist = Math.min(Math.abs(box.p0.x - pg.x), Math.abs(box.p1.x - pg.x));
if (inBox && (ret == null || xDist < minDist)) {
ret = sgid;
minDist = xDist;
}
}
});
if (ret != null) {
return {
id: ret,
dist: minDist
};
}
return null;
}
var findMaps = {
atoms: findClosestAtom,
bonds: findClosestBond,
chiralFlags: findClosestChiralFlag,
sgroupData: findClosestDataSGroupData,
sgroups: findClosestSGroup,
rxnArrows: findClosestRxnArrow,
rxnPluses: findClosestRxnPlus,
frags: findClosestFrag,
rgroups: findClosestRGroup
};
function findClosestItem(restruct, pos, maps, skip, scale) { // eslint-disable-line max-params
maps = maps || Object.keys(findMaps);
return maps.reduce(function (res, mp) {
var minDist = res ? res.dist : null;
var item = findMaps[mp](restruct, pos, skip, minDist, scale);
if (item !== null && (res === null || item.dist < res.dist)) {
return {
map: mp,
id: item.id,
dist: item.dist
};
}
return res;
}, null);
}
module.exports = {
atom: findClosestAtom, // used in Actions
item: findClosestItem
};

View File

@ -0,0 +1,364 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var s = require('subscription');
var Set = require('../util/set');
var Vec2 = require('../util/vec2');
var Struct = require('../chem/struct');
var Render = require('../render');
var Action = require('./action');
var closest = require('./closest');
var toolMap = {
rgroupatom: require('./tool/rgroupatom'),
select: require('./tool/select'),
sgroup: require('./tool/sgroup'),
eraser: require('./tool/eraser'),
atom: require('./tool/atom'),
bond: require('./tool/bond'),
chain: require('./tool/chain'),
chiralFlag: require('./tool/chiral-flag'),
template: require('./tool/template'),
charge: require('./tool/charge'),
rgroupfragment: require('./tool/rgroupfragment'),
apoint: require('./tool/apoint'),
attach: require('./tool/attach'),
reactionarrow: require('./tool/reactionarrow'),
reactionplus: require('./tool/reactionplus'),
reactionmap: require('./tool/reactionmap'),
reactionunmap: require('./tool/reactionunmap'),
paste: require('./tool/paste'),
rotate: require('./tool/rotate')
};
const SCALE = 40; // const
const HISTORY_SIZE = 32; // put me to options
var structObjects = ['atoms', 'bonds', 'frags', 'sgroups', 'sgroupData', 'rgroups', 'rxnArrows', 'rxnPluses', 'chiralFlags'];
function Editor(clientArea, options) {
this.render = new Render(clientArea, Object.assign({
scale: SCALE
}, options));
this._selection = null; // eslint-disable-line
this._tool = null; // eslint-disable-line
this.historyStack = [];
this.historyPtr = 0;
this.event = {
message: new s.Subscription(),
elementEdit: new s.PipelineSubscription(),
bondEdit: new s.PipelineSubscription(),
rgroupEdit: new s.PipelineSubscription(),
sgroupEdit: new s.PipelineSubscription(),
sdataEdit: new s.PipelineSubscription(),
quickEdit: new s.PipelineSubscription(),
attachEdit: new s.PipelineSubscription(),
change: new s.PipelineSubscription(),
selectionChange: new s.PipelineSubscription()
};
domEventSetup(this, clientArea);
}
Editor.prototype.tool = function (name, opts) {
/* eslint-disable no-underscore-dangle */
if (arguments.length > 0) {
if (this._tool && this._tool.cancel)
this._tool.cancel();
var tool = toolMap[name](this, opts);
if (!tool)
return null;
this._tool = tool;
}
return this._tool;
/* eslint-enable no-underscore-dangle */
};
Editor.prototype.struct = function (value) {
if (arguments.length > 0) {
this.selection(null);
this.update(Action.fromNewCanvas(this.render.ctab,
value || new Struct()));
recoordinate(this, getStructCenter(this.render.ctab));
}
return this.render.ctab.molecule;
};
Editor.prototype.options = function (value) {
if (arguments.length > 0) {
var struct = this.render.ctab.molecule;
var zoom = this.render.options.zoom;
this.render.clientArea.innerHTML = '';
this.render = new Render(this.render.clientArea, Object.assign({ scale: SCALE }, value));
this.render.setMolecule(struct); // TODO: reuse this.struct here?
this.render.setZoom(zoom);
this.render.update();
}
return this.render.options;
};
Editor.prototype.zoom = function (value) {
if (arguments.length > 0) {
this.render.setZoom(value);
recoordinate(this, getStructCenter(this.render.ctab,
this.selection()));
this.render.update();
}
return this.render.options.zoom;
};
Editor.prototype.selection = function (ci) {
var restruct = this.render.ctab;
if (arguments.length > 0) {
this._selection = null; // eslint-disable-line
if (ci === 'all') { // TODO: better way will be this.struct()
ci = structObjects.reduce(function (res, key) {
res[key] = restruct[key].ikeys();
return res;
}, {});
}
if (ci === 'descriptors') {
restruct = this.render.ctab;
ci = { sgroupData: restruct['sgroupData'].ikeys() };
}
if (ci) {
var res = {};
for (var key in ci) {
if (ci.hasOwnProperty(key) && ci[key].length > 0) // TODO: deep merge
res[key] = ci[key].slice();
}
if (Object.keys(res) !== 0)
this._selection = res; // eslint-disable-line
}
this.render.ctab.setSelection(this._selection); // eslint-disable-line
this.event.selectionChange.dispatch(this._selection); // eslint-disable-line
this.render.update();
}
return this._selection; // eslint-disable-line
};
Editor.prototype.hover = function (ci) {
var tool = this._tool; // eslint-disable-line
if ('ci' in tool && (!ci || tool.ci.map !== ci.map || tool.ci.id !== ci.id)) {
this.highlight(tool.ci, false);
delete tool.ci;
}
if (ci && this.highlight(ci, true))
tool.ci = ci;
};
Editor.prototype.highlight = function (ci, visible) {
if (['atoms', 'bonds', 'rxnArrows', 'rxnPluses', 'chiralFlags', 'frags',
'rgroups', 'sgroups', 'sgroupData'].indexOf(ci.map) === -1)
return false;
var rnd = this.render;
var item = rnd.ctab[ci.map].get(ci.id);
if (!item)
return true; // TODO: fix, attempt to highlight a deleted item
if ((ci.map === 'sgroups' && item.item.type === 'DAT') || ci.map === 'sgroupData') {
// set highlight for both the group and the data item
var item1 = rnd.ctab.sgroups.get(ci.id);
var item2 = rnd.ctab.sgroupData.get(ci.id);
if (item1)
item1.setHighlight(visible, rnd);
if (item2)
item2.setHighlight(visible, rnd);
} else {
item.setHighlight(visible, rnd);
}
return true;
};
Editor.prototype.update = function (action, ignoreHistory) {
if (action === true) {
this.render.update(true); // force
} else {
if (!ignoreHistory && !action.isDummy()) {
this.historyStack.splice(this.historyPtr, HISTORY_SIZE + 1, action);
if (this.historyStack.length > HISTORY_SIZE)
this.historyStack.shift();
this.historyPtr = this.historyStack.length;
this.event.change.dispatch(action); // TODO: stoppable here
}
this.render.update();
}
};
Editor.prototype.historySize = function () {
return {
undo: this.historyPtr,
redo: this.historyStack.length - this.historyPtr
};
};
Editor.prototype.undo = function () {
if (this.historyPtr === 0)
throw new Error('Undo stack is empty');
if (this.tool() && this.tool().cancel)
this.tool().cancel();
this.selection(null);
this.historyPtr--;
var action = this.historyStack[this.historyPtr].perform(this.render.ctab);
this.historyStack[this.historyPtr] = action;
this.event.change.dispatch(action);
this.render.update();
};
Editor.prototype.redo = function () {
if (this.historyPtr === this.historyStack.length)
throw new Error('Redo stack is empty');
if (this.tool() && this.tool().cancel)
this.tool().cancel();
this.selection(null);
var action = this.historyStack[this.historyPtr].perform(this.render.ctab);
this.historyStack[this.historyPtr] = action;
this.historyPtr++;
this.event.change.dispatch(action);
this.render.update();
};
Editor.prototype.on = function (eventName, handler) {
this.event[eventName].add(handler);
};
function domEventSetup(editor, clientArea) {
// TODO: addEventListener('resize', ...);
['click', 'dblclick', 'mousedown', 'mousemove',
'mouseup', 'mouseleave'].forEach(eventName => {
const subs = editor.event[eventName] = new s.DOMSubscription();
clientArea.addEventListener(eventName, subs.dispatch.bind(subs));
subs.add(event => {
editor.lastEvent = event;
if (editor.tool() && eventName in editor.tool())
editor.tool()[eventName](event);
return true;
}, -1);
});
}
Editor.prototype.findItem = function (event, maps, skip) {
var pos = global._ui_editor ? new Vec2(this.render.page2obj(event)) : // eslint-disable-line
new Vec2(event.pageX, event.pageY).sub(elementOffset(this.render.clientArea));
var options = this.render.options;
return closest.item(this.render.ctab, pos, maps, skip, options.scale);
};
Editor.prototype.explicitSelected = function () {
var selection = this.selection() || {};
var res = structObjects.reduce(function (res, key) {
res[key] = selection[key] ? selection[key].slice() : [];
return res;
}, {});
var struct = this.render.ctab.molecule;
// "auto-select" the atoms for the bonds in selection
if ('bonds' in res) {
res.bonds.forEach(function (bid) {
var bond = struct.bonds.get(bid);
res.atoms = res.atoms || [];
if (res.atoms.indexOf(bond.begin) < 0) res.atoms.push(bond.begin);
if (res.atoms.indexOf(bond.end) < 0) res.atoms.push(bond.end);
});
}
// "auto-select" the bonds with both atoms selected
if ('atoms' in res && 'bonds' in res) {
struct.bonds.each(function (bid) {
if (!('bonds' in res) || res.bonds.indexOf(bid) < 0) {
var bond = struct.bonds.get(bid);
if (res.atoms.indexOf(bond.begin) >= 0 && res.atoms.indexOf(bond.end) >= 0) {
res.bonds = res.bonds || [];
res.bonds.push(bid);
}
}
});
}
return res;
};
Editor.prototype.structSelected = function () {
var struct = this.render.ctab.molecule;
var selection = this.explicitSelected();
var dst = struct.clone(Set.fromList(selection.atoms),
Set.fromList(selection.bonds), true);
// Copy by its own as Struct.clone doesn't support
// arrows/pluses id sets
struct.rxnArrows.each(function (id, item) {
if (selection.rxnArrows.indexOf(id) != -1)
dst.rxnArrows.add(item.clone());
});
struct.rxnPluses.each(function (id, item) {
if (selection.rxnPluses.indexOf(id) != -1)
dst.rxnPluses.add(item.clone());
});
dst.isChiral = struct.isChiral;
// TODO: should be reaction only if arrwos? check this logic
dst.isReaction = struct.isReaction &&
(dst.rxnArrows.count() || dst.rxnPluses.count());
return dst;
};
Editor.prototype.alignDescriptors = function () {
this.selection(null);
const action = Action.fromDescriptorsAlign(this.render.ctab);
this.update(action);
this.render.update(true);
};
function recoordinate(editor, rp/* , vp*/) {
// rp is a point in scaled coordinates, which will be positioned
// vp is the point where the reference point should now be (in view coordinates)
// or the center if not set
console.assert(rp, 'Reference point not specified');
editor.render.setScrollOffset(0, 0);
}
function getStructCenter(restruct, selection) {
var bb = restruct.getVBoxObj(selection || {});
return Vec2.lc2(bb.p0, 0.5, bb.p1, 0.5);
}
// TODO: find DOM shorthand
function elementOffset(element) {
var top = 0,
left = 0;
do {
top += element.offsetTop || 0;
left += element.offsetLeft || 0;
element = element.offsetParent;
} while (element);
return new Vec2(left, top);
}
module.exports = Editor;

View File

@ -0,0 +1,980 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../util/vec2');
var Set = require('../util/set');
var scale = require('../util/scale');
var Struct = require('../chem/struct');
var ReStruct = require('../render/restruct');
var DEBUG = { debug: false, logcnt: 0, logmouse: false, hl: false };
DEBUG.logMethod = function () { };
function Base() {
this.type = 'OpBase';
// assert here?
this.execute = function () {
throw new Error('Operation.execute() is not implemented');
};
this.invert = function () {
throw new Error('Operation.invert() is not implemented');
};
this.perform = function (restruct) {
this.execute(restruct);
/* eslint-disable no-underscore-dangle */
if (!this._inverted) {
this._inverted = this.invert();
this._inverted._inverted = this;
}
return this._inverted;
};
this.isDummy = function (restruct) {
return this._isDummy ? this._isDummy(restruct) : false;
/* eslint-enable no-underscore-dangle */
};
}
function AtomAdd(atom, pos) {
this.data = { aid: null, atom: atom, pos: pos };
this.execute = function (restruct) {
var struct = restruct.molecule;
var pp = {};
if (this.data.atom) {
for (var p in this.data.atom)
if (this.data.atom.hasOwnProperty(p)) pp[p] = this.data.atom[p];
}
pp.label = pp.label || 'C';
if (!(typeof this.data.aid === "number"))
this.data.aid = struct.atoms.add(new Struct.Atom(pp));
else
struct.atoms.set(this.data.aid, new Struct.Atom(pp));
// notifyAtomAdded
var atomData = new ReStruct.Atom(restruct.molecule.atoms.get(this.data.aid));
atomData.component = restruct.connectedComponents.add(Set.single(this.data.aid));
restruct.atoms.set(this.data.aid, atomData);
restruct.markAtom(this.data.aid, 1);
struct.atomSetPos(this.data.aid, new Vec2(this.data.pos));
};
this.invert = function () {
var ret = new AtomDelete();
ret.data = this.data;
return ret;
};
}
AtomAdd.prototype = new Base();
function AtomDelete(aid) {
this.data = { aid: aid, atom: null, pos: null };
this.execute = function (restruct) {
var struct = restruct.molecule;
if (!this.data.atom) {
this.data.atom = struct.atoms.get(this.data.aid);
this.data.pos = this.data.atom.pp;
}
// notifyAtomRemoved(this.data.aid);
var atom = restruct.atoms.get(this.data.aid);
var set = restruct.connectedComponents.get(atom.component);
Set.remove(set, this.data.aid);
if (Set.size(set) == 0)
restruct.connectedComponents.remove(atom.component);
restruct.clearVisel(atom.visel);
restruct.atoms.unset(this.data.aid);
restruct.markItemRemoved();
struct.atoms.remove(this.data.aid);
};
this.invert = function () {
var ret = new AtomAdd();
ret.data = this.data;
return ret;
};
}
AtomDelete.prototype = new Base();
function AtomAttr(aid, attribute, value) {
this.data = { aid: aid, attribute: attribute, value: value };
this.data2 = null;
this.execute = function (restruct) {
var atom = restruct.molecule.atoms.get(this.data.aid);
if (!this.data2)
this.data2 = { aid: this.data.aid, attribute: this.data.attribute, value: atom[this.data.attribute] };
atom[this.data.attribute] = this.data.value;
invalidateAtom(restruct, this.data.aid);
};
this._isDummy = function (restruct) { // eslint-disable-line no-underscore-dangle
return restruct.molecule.atoms.get(this.data.aid)[this.data.attribute] == this.data.value;
};
this.invert = function () {
var ret = new AtomAttr();
ret.data = this.data2;
ret.data2 = this.data;
return ret;
};
}
AtomAttr.prototype = new Base();
function AtomMove(aid, d, noinvalidate) {
this.data = { aid: aid, d: d, noinvalidate: noinvalidate };
this.execute = function (restruct) {
var struct = restruct.molecule;
var aid = this.data.aid;
var d = this.data.d;
struct.atoms.get(aid).pp.add_(d); // eslint-disable-line no-underscore-dangle
restruct.atoms.get(aid).visel.translate(scale.obj2scaled(d, restruct.render.options));
this.data.d = d.negated();
if (!this.data.noinvalidate)
invalidateAtom(restruct, aid, 1);
};
this._isDummy = function () { // eslint-disable-line no-underscore-dangle
return this.data.d.x == 0 && this.data.d.y == 0;
};
this.invert = function () {
var ret = new AtomMove();
ret.data = this.data;
return ret;
};
}
AtomMove.prototype = new Base();
function BondMove(bid, d) {
this.data = { bid: bid, d: d };
this.execute = function (restruct) {
restruct.bonds.get(this.data.bid).visel.translate(scale.obj2scaled(this.data.d, restruct.render.options));
this.data.d = this.data.d.negated();
};
this.invert = function () {
var ret = new BondMove();
ret.data = this.data;
return ret;
};
}
BondMove.prototype = new Base();
function LoopMove(id, d) {
this.data = { id: id, d: d };
this.execute = function (restruct) {
// not sure if there should be an action to move a loop in the first place
// but we have to somehow move the aromatic ring, which is associated with the loop, rather than with any of the bonds
if (restruct.reloops.get(this.data.id) && restruct.reloops.get(this.data.id).visel)
restruct.reloops.get(this.data.id).visel.translate(scale.obj2scaled(this.data.d, restruct.render.options));
this.data.d = this.data.d.negated();
};
this.invert = function () {
var ret = new LoopMove();
ret.data = this.data;
return ret;
};
}
LoopMove.prototype = new Base();
function SGroupAtomAdd(sgid, aid) {
this.type = 'OpSGroupAtomAdd';
this.data = { sgid, aid };
this.execute = function (restruct) {
const struct = restruct.molecule;
const aid = this.data.aid;
const sgid = this.data.sgid;
const atom = struct.atoms.get(aid);
const sg = struct.sgroups.get(sgid);
if (sg.atoms.indexOf(aid) >= 0)
throw new Error('The same atom cannot be added to an S-group more than once');
if (!atom)
throw new Error('OpSGroupAtomAdd: Atom ' + aid + ' not found');
struct.atomAddToSGroup(sgid, aid);
invalidateAtom(restruct, aid);
};
this.invert = function () {
const ret = new SGroupAtomRemove();
ret.data = this.data;
return ret;
};
}
SGroupAtomAdd.prototype = new Base();
function SGroupAtomRemove(sgid, aid) {
this.type = 'OpSGroupAtomRemove';
this.data = { sgid, aid };
this.execute = function (restruct) {
const aid = this.data.aid;
const sgid = this.data.sgid;
const struct = restruct.molecule;
const atom = struct.atoms.get(aid);
const sg = struct.sgroups.get(sgid);
Struct.SGroup.removeAtom(sg, aid);
Set.remove(atom.sgs, sgid);
invalidateAtom(restruct, aid);
};
this.invert = function () {
const ret = new SGroupAtomAdd();
ret.data = this.data;
return ret;
};
}
SGroupAtomRemove.prototype = new Base();
function SGroupAttr(sgid, attr, value) {
this.type = 'OpSGroupAttr';
this.data = { sgid, attr, value };
this.execute = function (restruct) {
const struct = restruct.molecule;
const sgid = this.data.sgid;
const sg = struct.sgroups.get(sgid);
if (sg.type === 'DAT' && restruct.sgroupData.has(sgid)) {
// clean the stuff here, else it might be left behind if the sgroups is set to "attached"
restruct.clearVisel(restruct.sgroupData.get(sgid).visel);
restruct.sgroupData.unset(sgid);
}
this.data.value = sg.setAttr(this.data.attr, this.data.value);
};
this.invert = function () {
const ret = new SGroupAttr();
ret.data = this.data;
return ret;
};
}
SGroupAttr.prototype = new Base();
function SGroupCreate(sgid, type, pp) {
this.type = 'OpSGroupCreate';
this.data = { sgid, type, pp };
this.execute = function (restruct) {
const struct = restruct.molecule;
const sg = new Struct.SGroup(this.data.type);
const sgid = this.data.sgid;
sg.id = sgid;
struct.sgroups.set(sgid, sg);
if (this.data.pp)
struct.sgroups.get(sgid).pp = new Vec2(this.data.pp);
restruct.sgroups.set(sgid, new ReStruct.SGroup(struct.sgroups.get(sgid)));
this.data.sgid = sgid;
};
this.invert = function () {
const ret = new SGroupDelete();
ret.data = this.data;
return ret;
};
}
SGroupCreate.prototype = new Base();
function SGroupDelete(sgid) {
this.type = 'OpSGroupDelete';
this.data = { sgid };
this.execute = function (restruct) {
const struct = restruct.molecule;
const sgid = this.data.sgid;
const sg = restruct.sgroups.get(sgid);
this.data.type = sg.item.type;
this.data.pp = sg.item.pp;
if (sg.item.type === 'DAT' && restruct.sgroupData.has(sgid)) {
restruct.clearVisel(restruct.sgroupData.get(sgid).visel);
restruct.sgroupData.unset(sgid);
}
restruct.clearVisel(sg.visel);
if (sg.item.atoms.length !== 0)
throw new Error('S-Group not empty!');
restruct.sgroups.unset(sgid);
struct.sgroups.remove(sgid);
};
this.invert = function () {
const ret = new SGroupCreate();
ret.data = this.data;
return ret;
};
}
SGroupDelete.prototype = new Base();
function SGroupAddToHierarchy(sgid, parent, children) {
this.type = 'OpSGroupAddToHierarchy';
this.data = { sgid, parent, children };
this.execute = function (restruct) {
const struct = restruct.molecule;
const sgid = this.data.sgid;
const relations = struct.sGroupForest.insert(sgid, parent, children);
this.data.parent = relations.parent;
this.data.children = relations.children;
};
this.invert = function () {
const ret = new SGroupRemoveFromHierarchy();
ret.data = this.data;
return ret;
};
}
SGroupAddToHierarchy.prototype = new Base();
function SGroupRemoveFromHierarchy(sgid) {
this.type = 'OpSGroupRemoveFromHierarchy';
this.data = { sgid };
this.execute = function (restruct) {
const struct = restruct.molecule;
const sgid = this.data.sgid;
this.data.parent = struct.sGroupForest.parent.get(sgid);
this.data.children = struct.sGroupForest.children.get(sgid);
struct.sGroupForest.remove(sgid);
};
this.invert = function () {
const ret = new SGroupAddToHierarchy();
ret.data = this.data;
return ret;
};
}
SGroupRemoveFromHierarchy.prototype = new Base();
function BondAdd(begin, end, bond) {
this.data = { bid: null, bond: bond, begin: begin, end: end };
this.execute = function (restruct) { // eslint-disable-line max-statements
var struct = restruct.molecule;
if (this.data.begin == this.data.end)
throw new Error('Distinct atoms expected');
if (DEBUG.debug && this.molecule.checkBondExists(this.data.begin, this.data.end))
throw new Error('Bond already exists');
invalidateAtom(restruct, this.data.begin, 1);
invalidateAtom(restruct, this.data.end, 1);
var pp = {};
if (this.data.bond) {
for (var p in this.data.bond)
if (this.data.bond.hasOwnProperty(p)) pp[p] = this.data.bond[p];
}
pp.type = pp.type || Struct.Bond.PATTERN.TYPE.SINGLE;
pp.begin = this.data.begin;
pp.end = this.data.end;
if (!(typeof this.data.bid === "number"))
this.data.bid = struct.bonds.add(new Struct.Bond(pp));
else
struct.bonds.set(this.data.bid, new Struct.Bond(pp));
struct.bondInitHalfBonds(this.data.bid);
struct.atomAddNeighbor(struct.bonds.get(this.data.bid).hb1);
struct.atomAddNeighbor(struct.bonds.get(this.data.bid).hb2);
// notifyBondAdded
restruct.bonds.set(this.data.bid, new ReStruct.Bond(restruct.molecule.bonds.get(this.data.bid)));
restruct.markBond(this.data.bid, 1);
};
this.invert = function () {
var ret = new BondDelete();
ret.data = this.data;
return ret;
};
}
BondAdd.prototype = new Base();
function BondDelete(bid) {
this.data = { bid: bid, bond: null, begin: null, end: null };
this.execute = function (restruct) { // eslint-disable-line max-statements
var struct = restruct.molecule;
if (!this.data.bond) {
this.data.bond = struct.bonds.get(this.data.bid);
this.data.begin = this.data.bond.begin;
this.data.end = this.data.bond.end;
}
invalidateBond(restruct, this.data.bid);
// notifyBondRemoved
var rebond = restruct.bonds.get(this.data.bid);
[rebond.b.hb1, rebond.b.hb2].forEach(function (hbid) {
var hb = restruct.molecule.halfBonds.get(hbid);
if (hb.loop >= 0)
restruct.loopRemove(hb.loop);
}, restruct);
restruct.clearVisel(rebond.visel);
restruct.bonds.unset(this.data.bid);
restruct.markItemRemoved();
var bond = struct.bonds.get(this.data.bid);
[bond.hb1, bond.hb2].forEach(function (hbid) {
var hb = struct.halfBonds.get(hbid);
var atom = struct.atoms.get(hb.begin);
var pos = atom.neighbors.indexOf(hbid);
var prev = (pos + atom.neighbors.length - 1) % atom.neighbors.length;
var next = (pos + 1) % atom.neighbors.length;
struct.setHbNext(atom.neighbors[prev], atom.neighbors[next]);
atom.neighbors.splice(pos, 1);
}, this);
struct.halfBonds.unset(bond.hb1);
struct.halfBonds.unset(bond.hb2);
struct.bonds.remove(this.data.bid);
};
this.invert = function () {
var ret = new BondAdd();
ret.data = this.data;
return ret;
};
}
BondDelete.prototype = new Base();
function BondAttr(bid, attribute, value) {
this.data = { bid: bid, attribute: attribute, value: value };
this.data2 = null;
this.execute = function (restruct) {
var bond = restruct.molecule.bonds.get(this.data.bid);
if (!this.data2)
this.data2 = { bid: this.data.bid, attribute: this.data.attribute, value: bond[this.data.attribute] };
bond[this.data.attribute] = this.data.value;
invalidateBond(restruct, this.data.bid);
if (this.data.attribute === 'type')
invalidateLoop(restruct, this.data.bid);
};
this._isDummy = function (restruct) { // eslint-disable-line no-underscore-dangle
return restruct.molecule.bonds.get(this.data.bid)[this.data.attribute] == this.data.value;
};
this.invert = function () {
var ret = new BondAttr();
ret.data = this.data2;
ret.data2 = this.data;
return ret;
};
}
BondAttr.prototype = new Base();
function FragmentAdd(frid) {
this.frid = (typeof frid === 'undefined') ? null : frid;
this.execute = function (restruct) {
var struct = restruct.molecule;
var frag = {};
if (this.frid == null)
this.frid = struct.frags.add(frag);
else
struct.frags.set(this.frid, frag);
restruct.frags.set(this.frid, new ReStruct.Frag(frag)); // TODO add ReStruct.notifyFragmentAdded
};
this.invert = function () {
return new FragmentDelete(this.frid);
};
}
FragmentAdd.prototype = new Base();
function FragmentDelete(frid) {
this.frid = frid;
this.execute = function (restruct) {
var struct = restruct.molecule;
invalidateItem(restruct, 'frags', this.frid, 1);
restruct.frags.unset(this.frid);
struct.frags.remove(this.frid); // TODO add ReStruct.notifyFragmentRemoved
};
this.invert = function () {
return new FragmentAdd(this.frid);
};
}
FragmentDelete.prototype = new Base();
function RGroupAttr(rgid, attribute, value) {
this.data = { rgid: rgid, attribute: attribute, value: value };
this.data2 = null;
this.execute = function (restruct) {
var rgp = restruct.molecule.rgroups.get(this.data.rgid);
if (!this.data2)
this.data2 = { rgid: this.data.rgid, attribute: this.data.attribute, value: rgp[this.data.attribute] };
rgp[this.data.attribute] = this.data.value;
invalidateItem(restruct, 'rgroups', this.data.rgid);
};
this._isDummy = function (restruct) { // eslint-disable-line no-underscore-dangle
return restruct.molecule.rgroups.get(this.data.rgid)[this.data.attribute] == this.data.value;
};
this.invert = function () {
var ret = new RGroupAttr();
ret.data = this.data2;
ret.data2 = this.data;
return ret;
};
}
RGroupAttr.prototype = new Base();
function RGroupFragment(rgid, frid, rg) {
this.type = 'OpAddOrDeleteRGFragment';
this.rgid_new = rgid;
this.rg_new = rg;
this.rgid_old = null;
this.rg_old = null;
this.frid = frid;
this.execute = function (restruct) { // eslint-disable-line max-statements
const struct = restruct.molecule;
this.rgid_old = this.rgid_old || Struct.RGroup.findRGroupByFragment(struct.rgroups, this.frid);
this.rg_old = (this.rgid_old ? struct.rgroups.get(this.rgid_old) : null);
if (this.rg_old) {
this.rg_old.frags.remove(this.rg_old.frags.keyOf(this.frid));
restruct.clearVisel(restruct.rgroups.get(this.rgid_old).visel);
if (this.rg_old.frags.count() === 0) {
restruct.rgroups.unset(this.rgid_old);
struct.rgroups.unset(this.rgid_old);
restruct.markItemRemoved();
} else {
restruct.markItem('rgroups', this.rgid_old, 1);
}
}
if (this.rgid_new) {
let rgNew = struct.rgroups.get(this.rgid_new);
if (!rgNew) {
rgNew = this.rg_new || new Struct.RGroup();
struct.rgroups.set(this.rgid_new, rgNew);
restruct.rgroups.set(this.rgid_new, new ReStruct.RGroup(rgNew));
} else {
restruct.markItem('rgroups', this.rgid_new, 1);
}
rgNew.frags.add(this.frid);
}
};
this.invert = function () {
return new RGroupFragment(this.rgid_old, this.frid, this.rg_old);
};
}
RGroupFragment.prototype = new Base();
function UpdateIfThen(rgNew, rgOld) {
this.type = 'OpUpdateIfThenValues';
this.rgid_new = rgNew;
this.rgid_old = rgOld;
this.ifThenHistory = {};
this.execute = function (restruct) {
const struct = restruct.molecule;
struct.rgroups.keys().forEach(rgKey => {
const rgValue = struct.rgroups.get(rgKey);
if (rgValue.ifthen === this.rgid_old) {
rgValue.ifthen = this.rgid_new;
this.ifThenHistory[rgKey] = this.rgid_old;
struct.rgroups.set(rgKey, rgValue);
}
});
};
this.invert = function () {
return new RestoreIfThen(this.rgid_new, this.rgid_old, this.ifThenHistory);
};
}
UpdateIfThen.prototype = new Base();
function RestoreIfThen(rgNew, rgOld, history) {
this.type = 'OpRestoreIfThenValues';
this.rgid_new = rgNew;
this.rgid_old = rgOld;
this.ifThenHistory = history || {};
this.execute = function (restruct) {
const struct = restruct.molecule;
Object.keys(this.ifThenHistory).forEach(rgid => {
const rgValue = struct.rgroups.get(rgid);
rgValue.ifthen = this.ifThenHistory[rgid];
struct.rgroups.set(rgid, rgValue);
});
};
this.invert = function () {
return new UpdateIfThen(this.rgid_old, this.rgid_new);
};
}
RestoreIfThen.prototype = new Base();
function RxnArrowAdd(pos) {
this.data = { arid: null, pos: pos };
this.execute = function (restruct) {
var struct = restruct.molecule;
if (!(typeof this.data.arid === 'number'))
this.data.arid = struct.rxnArrows.add(new Struct.RxnArrow());
else
struct.rxnArrows.set(this.data.arid, new Struct.RxnArrow());
// notifyRxnArrowAdded
restruct.rxnArrows.set(this.data.arid, new ReStruct.RxnArrow(restruct.molecule.rxnArrows.get(this.data.arid)));
struct.rxnArrowSetPos(this.data.arid, new Vec2(this.data.pos));
invalidateItem(restruct, 'rxnArrows', this.data.arid, 1);
};
this.invert = function () {
var ret = new RxnArrowDelete();
ret.data = this.data;
return ret;
};
}
RxnArrowAdd.prototype = new Base();
function RxnArrowDelete(arid) {
this.data = { arid: arid, pos: null };
this.execute = function (restruct) {
var struct = restruct.molecule;
if (!this.data.pos)
this.data.pos = struct.rxnArrows.get(this.data.arid).pp;
// notifyRxnArrowRemoved
restruct.markItemRemoved();
restruct.clearVisel(restruct.rxnArrows.get(this.data.arid).visel);
restruct.rxnArrows.unset(this.data.arid);
struct.rxnArrows.remove(this.data.arid);
};
this.invert = function () {
var ret = new RxnArrowAdd();
ret.data = this.data;
return ret;
};
}
RxnArrowDelete.prototype = new Base();
function RxnArrowMove(id, d, noinvalidate) {
this.data = { id: id, d: d, noinvalidate: noinvalidate };
this.execute = function (restruct) {
var struct = restruct.molecule;
var id = this.data.id;
var d = this.data.d;
struct.rxnArrows.get(id).pp.add_(d); // eslint-disable-line no-underscore-dangle
restruct.rxnArrows.get(id).visel.translate(scale.obj2scaled(d, restruct.render.options));
this.data.d = d.negated();
if (!this.data.noinvalidate)
invalidateItem(restruct, 'rxnArrows', id, 1);
};
this.invert = function () {
var ret = new RxnArrowMove();
ret.data = this.data;
return ret;
};
}
RxnArrowMove.prototype = new Base();
function RxnPlusAdd(pos) {
this.data = { plid: null, pos: pos };
this.execute = function (restruct) {
var struct = restruct.molecule;
if (!(typeof this.data.plid === 'number'))
this.data.plid = struct.rxnPluses.add(new Struct.RxnPlus());
else
struct.rxnPluses.set(this.data.plid, new Struct.RxnPlus());
// notifyRxnPlusAdded
restruct.rxnPluses.set(this.data.plid, new ReStruct.RxnPlus(restruct.molecule.rxnPluses.get(this.data.plid)));
struct.rxnPlusSetPos(this.data.plid, new Vec2(this.data.pos));
invalidateItem(restruct, 'rxnPluses', this.data.plid, 1);
};
this.invert = function () {
var ret = new RxnPlusDelete();
ret.data = this.data;
return ret;
};
}
RxnPlusAdd.prototype = new Base();
function RxnPlusDelete(plid) {
this.data = { plid: plid, pos: null };
this.execute = function (restruct) {
var struct = restruct.molecule;
if (!this.data.pos)
this.data.pos = struct.rxnPluses.get(this.data.plid).pp;
// notifyRxnPlusRemoved
restruct.markItemRemoved();
restruct.clearVisel(restruct.rxnPluses.get(this.data.plid).visel);
restruct.rxnPluses.unset(this.data.plid);
struct.rxnPluses.remove(this.data.plid);
};
this.invert = function () {
var ret = new RxnPlusAdd();
ret.data = this.data;
return ret;
};
}
RxnPlusDelete.prototype = new Base();
function RxnPlusMove(id, d, noinvalidate) {
this.data = { id: id, d: d, noinvalidate: noinvalidate };
this.execute = function (restruct) {
var struct = restruct.molecule;
var id = this.data.id;
var d = this.data.d;
struct.rxnPluses.get(id).pp.add_(d); // eslint-disable-line no-underscore-dangle
restruct.rxnPluses.get(id).visel.translate(scale.obj2scaled(d, restruct.render.options));
this.data.d = d.negated();
if (!this.data.noinvalidate)
invalidateItem(restruct, 'rxnPluses', id, 1);
};
this.invert = function () {
var ret = new RxnPlusMove();
ret.data = this.data;
return ret;
};
}
RxnPlusMove.prototype = new Base();
function SGroupDataMove(id, d) {
this.data = { id: id, d: d };
this.execute = function (restruct) {
var struct = restruct.molecule;
struct.sgroups.get(this.data.id).pp.add_(this.data.d); // eslint-disable-line no-underscore-dangle
this.data.d = this.data.d.negated();
invalidateItem(restruct, 'sgroupData', this.data.id, 1); // [MK] this currently does nothing since the DataSGroupData Visel only contains the highlighting/selection and SGroups are redrawn every time anyway
};
this.invert = function () {
var ret = new SGroupDataMove();
ret.data = this.data;
return ret;
};
}
SGroupDataMove.prototype = new Base();
function CanvasLoad(struct) {
this.data = { struct: struct };
this.execute = function (restruct) {
var oldStruct = restruct.molecule;
restruct.clearVisels(); // TODO: What is it?
restruct.render.setMolecule(this.data.struct);
this.data.struct = oldStruct;
};
this.invert = function () {
var ret = new CanvasLoad();
ret.data = this.data;
return ret;
};
}
CanvasLoad.prototype = new Base();
function ChiralFlagAdd(pos) {
this.data = { pos: pos };
this.execute = function (restruct) {
var struct = restruct.molecule;
if (restruct.chiralFlags.count() > 0) {
// throw new Error('Cannot add more than one Chiral flag');
restruct.clearVisel(restruct.chiralFlags.get(0).visel);
restruct.chiralFlags.unset(0);
}
restruct.chiralFlags.set(0, new ReStruct.ChiralFlag(pos));
struct.isChiral = true;
invalidateItem(restruct, 'chiralFlags', 0, 1);
};
this.invert = function () {
var ret = new ChiralFlagDelete();
ret.data = this.data;
return ret;
};
}
ChiralFlagAdd.prototype = new Base();
function ChiralFlagDelete() {
this.data = { pos: null };
this.execute = function (restruct) {
var struct = restruct.molecule;
if (restruct.chiralFlags.count() < 1)
throw new Error('Cannot remove chiral flag');
restruct.clearVisel(restruct.chiralFlags.get(0).visel);
this.data.pos = restruct.chiralFlags.get(0).pp;
restruct.chiralFlags.unset(0);
struct.isChiral = false;
};
this.invert = function () {
var ret = new ChiralFlagAdd(this.data.pos);
ret.data = this.data;
return ret;
};
}
ChiralFlagDelete.prototype = new Base();
function ChiralFlagMove(d) {
this.data = { d: d };
this.execute = function (restruct) {
restruct.chiralFlags.get(0).pp.add_(this.data.d); // eslint-disable-line no-underscore-dangle
this.data.d = this.data.d.negated();
invalidateItem(restruct, 'chiralFlags', 0, 1);
};
this.invert = function () {
var ret = new ChiralFlagMove();
ret.data = this.data;
return ret;
};
}
ChiralFlagMove.prototype = new Base();
function AlignDescriptors() {
this.type = 'OpAlignDescriptors';
this.history = {};
this.execute = function (restruct) {
const sgroups = restruct.molecule.sgroups.values().reverse();
let alignPoint = sgroups.reduce(
(acc, sg) => new Vec2(
Math.max(sg.bracketBox.p1.x, acc.x),
Math.min(sg.bracketBox.p0.y, acc.y)
), new Vec2(0.0, Infinity)
)
.add(new Vec2(0.5, -0.5));
sgroups.forEach(sg => {
this.history[sg.id] = sg.pp;
alignPoint = alignPoint.add(new Vec2(0.0, 0.5));
sg.pp = alignPoint;
restruct.molecule.sgroups.set(sg.id, sg);
});
};
this.invert = function () {
return new RestoreDescriptorsPosition(this.history);
};
}
AlignDescriptors.prototype = new Base();
function RestoreDescriptorsPosition(history) {
this.type = 'OpRestoreDescriptorsPosition';
this.history = history;
this.execute = function (restruct) {
const sgroups = restruct.molecule.sgroups.values();
sgroups.forEach(sg => {
sg.pp = this.history[sg.id];
restruct.molecule.sgroups.set(sg.id, sg);
});
};
this.invert = function () {
return new AlignDescriptors();
};
}
RestoreDescriptorsPosition.prototype = new Base();
function invalidateAtom(restruct, aid, level) {
var atom = restruct.atoms.get(aid);
restruct.markAtom(aid, level ? 1 : 0);
var hbs = restruct.molecule.halfBonds;
for (var i = 0; i < atom.a.neighbors.length; ++i) {
var hbid = atom.a.neighbors[i];
if (hbs.has(hbid)) {
var hb = hbs.get(hbid);
restruct.markBond(hb.bid, 1);
restruct.markAtom(hb.end, 0);
if (level)
invalidateLoop(restruct, hb.bid);
}
}
}
function invalidateLoop(restruct, bid) {
var bond = restruct.bonds.get(bid);
var lid1 = restruct.molecule.halfBonds.get(bond.b.hb1).loop;
var lid2 = restruct.molecule.halfBonds.get(bond.b.hb2).loop;
if (lid1 >= 0)
restruct.loopRemove(lid1);
if (lid2 >= 0)
restruct.loopRemove(lid2);
}
function invalidateBond(restruct, bid) {
var bond = restruct.bonds.get(bid);
invalidateLoop(restruct, bid);
invalidateAtom(restruct, bond.b.begin, 0);
invalidateAtom(restruct, bond.b.end, 0);
}
function invalidateItem(restruct, map, id, level) {
if (map === 'atoms') {
invalidateAtom(restruct, id, level);
} else if (map === 'bonds') {
invalidateBond(restruct, id);
if (level > 0)
invalidateLoop(restruct, id);
} else {
restruct.markItem(map, id, level);
}
}
module.exports = {
AtomAdd: AtomAdd,
AtomDelete: AtomDelete,
AtomAttr: AtomAttr,
AtomMove: AtomMove,
BondMove: BondMove,
LoopMove: LoopMove,
SGroupAtomAdd: SGroupAtomAdd,
SGroupAtomRemove: SGroupAtomRemove,
SGroupAttr: SGroupAttr,
SGroupCreate: SGroupCreate,
SGroupDelete: SGroupDelete,
SGroupAddToHierarchy: SGroupAddToHierarchy,
SGroupRemoveFromHierarchy: SGroupRemoveFromHierarchy,
BondAdd: BondAdd,
BondDelete: BondDelete,
BondAttr: BondAttr,
FragmentAdd: FragmentAdd,
FragmentDelete: FragmentDelete,
RGroupAttr: RGroupAttr,
RGroupFragment: RGroupFragment,
RxnArrowAdd: RxnArrowAdd,
RxnArrowDelete: RxnArrowDelete,
RxnArrowMove: RxnArrowMove,
RxnPlusAdd: RxnPlusAdd,
RxnPlusDelete: RxnPlusDelete,
RxnPlusMove: RxnPlusMove,
SGroupDataMove: SGroupDataMove,
CanvasLoad: CanvasLoad,
ChiralFlagAdd: ChiralFlagAdd,
ChiralFlagDelete: ChiralFlagDelete,
ChiralFlagMove: ChiralFlagMove,
UpdateIfThen: UpdateIfThen,
AlignDescriptors: AlignDescriptors,
RestoreDescriptorsPosition: RestoreDescriptorsPosition
};

View File

@ -0,0 +1,52 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
function APointTool(editor) {
if (!(this instanceof APointTool))
return new APointTool(editor);
this.editor = editor;
this.editor.selection(null);
}
APointTool.prototype.mousemove = function (event) {
this.editor.hover(this.editor.findItem(event, ['atoms']));
};
APointTool.prototype.mouseup = function (event) {
var editor = this.editor;
var struct = editor.render.ctab.molecule;
var ci = editor.findItem(event, ['atoms']);
if (ci && ci.map === 'atoms') {
this.editor.hover(null);
var atom = struct.atoms.get(ci.id);
var res = editor.event.elementEdit.dispatch({
attpnt: atom.attpnt
});
Promise.resolve(res).then(function (newatom) {
if (atom.attpnt != newatom.attpnt) {
var action = Action.fromAtomsAttrs(editor.render.ctab, ci.id, newatom);
editor.update(action);
}
});
return true;
}
};
module.exports = APointTool;

View File

@ -0,0 +1,119 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Struct = require('../../chem/struct');
var Action = require('../action');
var utils = require('./utils');
function AtomTool(editor, atomProps) {
if (!(this instanceof AtomTool)) {
if (!editor.selection() || !editor.selection().atoms)
return new AtomTool(editor, atomProps);
var action = Action.fromAtomsAttrs(editor.render.ctab, editor.selection().atoms,
atomProps, true);
editor.update(action);
editor.selection(null);
return null;
}
this.editor = editor;
this.atomProps = atomProps;
this.bondProps = { type: 1, stereo: Struct.Bond.PATTERN.STEREO.NONE };
}
AtomTool.prototype.mousedown = function (event) {
this.editor.hover(null);
var ci = this.editor.findItem(event, ['atoms']);
if (!ci) { // ci.type == 'Canvas'
this.dragCtx = {};
} else if (ci.map === 'atoms') {
this.dragCtx = { item: ci };
}
};
AtomTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
if (!this.dragCtx || !this.dragCtx.item) {
this.editor.hover(this.editor.findItem(event, ['atoms']));
return;
}
var dragCtx = this.dragCtx;
var ci = this.editor.findItem(event, ['atoms']);
if (ci && ci.map === 'atoms' && ci.id === dragCtx.item.id) {
// fromAtomsAttrs
this.editor.hover(this.editor.findItem(event, ['atoms']));
return;
}
// fromAtomAddition
var atom = rnd.ctab.molecule.atoms.get(dragCtx.item.id);
var newAtomPos = utils.calcNewAtomPos(atom.pp, rnd.page2obj(event));
if (dragCtx.action)
dragCtx.action.perform(rnd.ctab);
dragCtx.action = Action.fromBondAddition(rnd.ctab,
this.bondProps, dragCtx.item.id, Object.assign({}, this.atomProps), newAtomPos, newAtomPos
)[0];
this.editor.update(dragCtx.action, true);
};
AtomTool.prototype.mouseup = function (event) {
if (this.dragCtx) {
var dragCtx = this.dragCtx;
var rnd = this.editor.render;
this.editor.update(dragCtx.action || (
dragCtx.item ?
Action.fromAtomsAttrs(rnd.ctab, dragCtx.item.id, this.atomProps, true) :
Action.fromAtomAddition(rnd.ctab, rnd.page2obj(event), this.atomProps)
));
delete this.dragCtx;
}
};
function atomLongtapEvent(tool, render) {
const dragCtx = tool.dragCtx;
const editor = tool.editor;
const atomid = dragCtx.item && dragCtx.item.id;
const atom = atomid ? // edit atom or add atom
render.ctab.molecule.atoms.get(atomid) :
new Struct.Atom({ label: '' });
// TODO: longtab event
dragCtx.timeout = setTimeout(function () {
delete tool.dragCtx;
editor.selection(null);
const res = editor.event.quickEdit.dispatch(atom);
Promise.resolve(res).then(function (newatom) {
const action = atomid ?
Action.fromAtomsAttrs(render.ctab, atomid, newatom) :
Action.fromAtomAddition(render.ctab, dragCtx.xy0, newatom);
editor.update(action);
});
}, 750);
dragCtx.stopTapping = function () {
if (dragCtx.timeout) {
clearTimeout(dragCtx.timeout);
delete tool.dragCtx.timeout;
}
};
}
module.exports = Object.assign(AtomTool, {
atomLongtapEvent: atomLongtapEvent
});

View File

@ -0,0 +1,65 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var element = require('../../chem/element');
function AttachTool(editor, attachPoints) {
if (!(this instanceof AttachTool))
return new AttachTool(editor, attachPoints);
this.attach = attachPoints || { atomid: 0, bondid: 0 };
this.editor = editor;
this.editor.selection({
atoms: [this.attach.atomid],
bonds: [this.attach.bondid]
});
}
AttachTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['atoms', 'bonds']);
var struct = rnd.ctab.molecule;
if (ci && ((ci.map === 'atoms' && element.map[struct.atoms.get(ci.id).label]) || ci.map === 'bonds'))
this.editor.hover(ci);
else
this.editor.hover(null);
return true;
};
AttachTool.prototype.mouseup = function (event) {
var editor = this.editor;
var rnd = editor.render;
var struct = rnd.ctab.molecule;
var ci = editor.findItem(event, ['atoms', 'bonds']);
if (ci && ((ci.map === 'atoms' && element.map[struct.atoms.get(ci.id).label]) || ci.map === 'bonds')) {
if (ci.map === 'atoms')
this.attach.atomid = ci.id;
else
this.attach.bondid = ci.id;
this.editor.selection({
atoms: [this.attach.atomid],
bonds: [this.attach.bondid]
});
this.editor.event.attachEdit.dispatch(this.attach);
}
return true;
};
module.exports = AttachTool;

View File

@ -0,0 +1,171 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Struct = require('../../chem/struct');
var Action = require('../action');
var utils = require('./utils');
function BondTool(editor, bondProps) {
if (!(this instanceof BondTool)) {
// Action.fromBondAttrs(editor.render.ctab,
// editor.selection().bonds, {
// type: bondType(mode).type,
// stereo: Bond.PATTERN.STEREO.NONE })
editor.selection(null);
return new BondTool(editor, bondProps);
}
this.editor = editor;
this.atomProps = { label: 'C' };
this.bondProps = bondProps;
}
BondTool.prototype.mousedown = function (event) {
var rnd = this.editor.render;
this.editor.hover(null);
this.dragCtx = {
xy0: rnd.page2obj(event),
item: this.editor.findItem(event, ['atoms', 'bonds'])
};
if (!this.dragCtx.item) // ci.type == 'Canvas'
delete this.dragCtx.item;
return true;
};
BondTool.prototype.mousemove = function (event) { // eslint-disable-line max-statements
var editor = this.editor;
var rnd = editor.render;
if ('dragCtx' in this) {
var dragCtx = this.dragCtx;
if (!('item' in dragCtx) || dragCtx.item.map === 'atoms') {
if ('action' in dragCtx)
dragCtx.action.perform(rnd.ctab);
var i1, i2, p1, p2;
if (('item' in dragCtx && dragCtx.item.map === 'atoms')) {
// first mousedown event intersect with any atom
i1 = dragCtx.item.id;
i2 = editor.findItem(event, ['atoms'], dragCtx.item);
} else {
// first mousedown event intersect with any canvas
i1 = this.atomProps;
p1 = dragCtx.xy0;
i2 = editor.findItem(event, ['atoms']);
}
var dist = Number.MAX_VALUE;
if (i2 && i2.map === 'atoms') {
// after mousedown events is appered, cursor is moved and then cursor intersects any atoms
i2 = i2.id;
} else {
i2 = this.atomProps;
var xy1 = rnd.page2obj(event);
dist = Vec2.dist(dragCtx.xy0, xy1);
if (p1) {
// rotation only, leght of bond = 1;
p2 = utils.calcNewAtomPos(p1, xy1);
} else {
// first mousedown event intersect with any atom and
// rotation only, leght of bond = 1;
var atom = rnd.ctab.molecule.atoms.get(i1);
p1 = utils.calcNewAtomPos(atom.pp.get_xy0(), xy1);
}
}
// don't rotate the bond if the distance between the start and end point is too small
if (dist > 0.3)
dragCtx.action = Action.fromBondAddition(rnd.ctab, this.bondProps, i1, i2, p1, p2)[0];
else
delete dragCtx.action;
this.editor.update(dragCtx.action, true);
return true;
}
}
this.editor.hover(this.editor.findItem(event, ['atoms', 'bonds']));
return true;
};
BondTool.prototype.mouseup = function (event) { // eslint-disable-line max-statements
if ('dragCtx' in this) {
var dragCtx = this.dragCtx;
var rnd = this.editor.render;
var struct = rnd.ctab.molecule;
if ('action' in dragCtx) {
this.editor.update(dragCtx.action);
} else if (!('item' in dragCtx)) {
var xy = rnd.page2obj(event);
var v = new Vec2(1.0 / 2, 0).rotate(
this.bondProps.type == Struct.Bond.PATTERN.TYPE.SINGLE ? -Math.PI / 6 : 0
);
var bondAddition = Action.fromBondAddition(rnd.ctab,
this.bondProps, { label: 'C' }, { label: 'C' },
Vec2.diff(xy, v), Vec2.sum(xy, v));
this.editor.update(bondAddition[0]);
} else if (dragCtx.item.map === 'atoms') {
// when does it hapend?
this.editor.update(Action.fromBondAddition(rnd.ctab, this.bondProps, dragCtx.item.id)[0]);
} else if (dragCtx.item.map === 'bonds') {
var bondProps = Object.assign({}, this.bondProps);
var bond = struct.bonds.get(dragCtx.item.id);
this.editor.update(bondChangingAction(rnd.ctab, dragCtx.item.id, bond, bondProps));
}
delete this.dragCtx;
}
return true;
};
/**
* @param itemID - bond id in structure
* @param bond - bond for change
* @param bondProps - bondTool properties
* @returns Action
*/
function bondChangingAction(restruct, itemID, bond, bondProps) {
if (bondProps.stereo !== Struct.Bond.PATTERN.STEREO.NONE && //
bondProps.type === Struct.Bond.PATTERN.TYPE.SINGLE &&
bond.type === bondProps.type && bond.stereo === bondProps.stereo)
// if bondTool is stereo and equal to bond for change
return Action.fromBondFlipping(restruct, itemID);
var loop = plainBondTypes.indexOf(bondProps.type) >= 0 ? plainBondTypes : null;
if (bondProps.stereo === Struct.Bond.PATTERN.STEREO.NONE &&
bondProps.type === Struct.Bond.PATTERN.TYPE.SINGLE &&
bond.stereo === Struct.Bond.PATTERN.STEREO.NONE &&
loop)
// if `Single bond` tool is chosen and bond for change in `plainBondTypes`
bondProps.type = loop[(loop.indexOf(bond.type) + 1) % loop.length];
return Action.fromBondAttrs(restruct, itemID, bondProps,
bondFlipRequired(restruct.molecule, bond, bondProps));
}
function bondFlipRequired(struct, bond, attrs) {
return attrs.type == Struct.Bond.PATTERN.TYPE.SINGLE &&
bond.stereo == Struct.Bond.PATTERN.STEREO.NONE &&
attrs.stereo != Struct.Bond.PATTERN.STEREO.NONE &&
struct.atoms.get(bond.begin).neighbors.length <
struct.atoms.get(bond.end).neighbors.length;
}
var plainBondTypes = [
Struct.Bond.PATTERN.TYPE.SINGLE,
Struct.Bond.PATTERN.TYPE.DOUBLE,
Struct.Bond.PATTERN.TYPE.TRIPLE
];
module.exports = Object.assign(BondTool, {
bondChangingAction: bondChangingAction
});

View File

@ -0,0 +1,111 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Struct = require('../../chem/struct');
var Action = require('../action');
var utils = require('./utils');
var Atom = require('./atom');
var Bond = require('./bond');
function ChainTool(editor) {
if (!(this instanceof ChainTool))
return new ChainTool(editor);
this.editor = editor;
this.editor.selection(null);
}
ChainTool.prototype.mousedown = function (event) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['atoms', 'bonds']);
this.editor.hover(null);
this.dragCtx = {
xy0: rnd.page2obj(event),
item: ci
};
if (ci && ci.map === 'atoms') {
this.editor.selection({ atoms: [ci.id] }); // for change atom
// this event has to be stopped in others events by `tool.dragCtx.stopTapping()`
Atom.atomLongtapEvent(this, rnd);
}
if (!this.dragCtx.item) // ci.type == 'Canvas'
delete this.dragCtx.item;
return true;
};
ChainTool.prototype.mousemove = function (event) { // eslint-disable-line max-statements
var editor = this.editor;
var rnd = editor.render;
if (this.dragCtx) {
if ('stopTapping' in this.dragCtx)
this.dragCtx.stopTapping();
this.editor.selection(null);
var dragCtx = this.dragCtx;
if (!('item' in dragCtx) || dragCtx.item.map === 'atoms') {
if ('action' in dragCtx)
dragCtx.action.perform(rnd.ctab);
var atoms = rnd.ctab.molecule.atoms;
var pos0 = dragCtx.item ? atoms.get(dragCtx.item.id).pp :
dragCtx.xy0;
var pos1 = rnd.page2obj(event);
var sectCount = Math.ceil(Vec2.diff(pos1, pos0).length());
var angle = event.ctrlKey ? utils.calcAngle(pos0, pos1) :
utils.fracAngle(pos0, pos1);
dragCtx.action = Action.fromChain(rnd.ctab, pos0, angle, sectCount,
dragCtx.item ? dragCtx.item.id : null);
editor.event.message.dispatch({
info: sectCount + " sectors"
});
this.editor.update(dragCtx.action, true);
return true;
}
}
this.editor.hover(this.editor.findItem(event, ['atoms', 'bonds']));
return true;
};
ChainTool.prototype.mouseup = function () {
var rnd = this.editor.render;
var struct = rnd.ctab.molecule;
if (this.dragCtx) {
if ('stopTapping' in this.dragCtx)
this.dragCtx.stopTapping();
var dragCtx = this.dragCtx;
var action = dragCtx.action;
if (!action && dragCtx.item && dragCtx.item.map === 'bonds') {
var bond = struct.bonds.get(dragCtx.item.id);
action = Bond.bondChangingAction(rnd.ctab, dragCtx.item.id, bond, {
type: Struct.Bond.PATTERN.TYPE.SINGLE,
stereo: Struct.Bond.PATTERN.STEREO.NONE
});
}
delete this.dragCtx;
if (action)
this.editor.update(action);
}
return true;
};
ChainTool.prototype.cancel = ChainTool.prototype.mouseleave =
ChainTool.prototype.mouseup;
module.exports = ChainTool;

View File

@ -0,0 +1,53 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
var element = require('../../chem/element');
function ChargeTool(editor, charge) {
if (!(this instanceof ChargeTool))
return new ChargeTool(editor, charge);
this.editor = editor;
this.editor.selection(null);
this.charge = charge;
}
ChargeTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['atoms']);
var struct = rnd.ctab.molecule;
if (ci && ci.map === 'atoms' && element.map[struct.atoms.get(ci.id).label])
this.editor.hover(ci);
else
this.editor.hover(null);
return true;
};
ChargeTool.prototype.mouseup = function (event) {
var editor = this.editor;
var rnd = editor.render;
var struct = rnd.ctab.molecule;
var ci = editor.findItem(event, ['atoms']);
if (ci && ci.map === 'atoms' && element.map[struct.atoms.get(ci.id).label]) {
this.editor.hover(null);
this.editor.update(Action.fromAtomsAttrs(rnd.ctab, ci.id, {
charge: struct.atoms.get(ci.id).charge + this.charge
}));
}
return true;
};
module.exports = ChargeTool;

View File

@ -0,0 +1,34 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
function ChiralFlagTool(editor) {
if (!(this instanceof ChiralFlagTool)) {
this.editor = editor;
const rnd = this.editor.render;
let action = null;
if (rnd.ctab.molecule.isChiral === false)
action = Action.fromChiralFlagAddition(rnd.ctab);
else
action = Action.fromChiralFlagDeletion(rnd.ctab);
this.editor.update(action);
}
}
module.exports = ChiralFlagTool;

View File

@ -0,0 +1,86 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
var LassoHelper = require('./helper/lasso');
function EraserTool(editor, mode) {
if (!(this instanceof EraserTool)) {
if (!editor.selection())
return new EraserTool(editor, mode);
var action = Action.fromFragmentDeletion(editor.render.ctab, editor.selection());
editor.update(action);
editor.selection(null);
return null;
}
this.editor = editor;
this.maps = ['atoms', 'bonds', 'rxnArrows', 'rxnPluses', 'sgroups', 'sgroupData', 'chiralFlags'];
this.lassoHelper = new LassoHelper(mode || 0, editor);
}
EraserTool.prototype.mousedown = function (event) {
var ci = this.editor.findItem(event, this.maps);
if (!ci) // ci.type == 'Canvas'
this.lassoHelper.begin(event);
};
EraserTool.prototype.mousemove = function (event) {
if (this.lassoHelper.running())
this.editor.selection(this.lassoHelper.addPoint(event));
else
this.editor.hover(this.editor.findItem(event, this.maps));
};
EraserTool.prototype.mouseleave = function (event) {
if (this.lassoHelper.running(event))
this.lassoHelper.end(event);
};
EraserTool.prototype.mouseup = function (event) { // eslint-disable-line max-statements
var rnd = this.editor.render;
if (this.lassoHelper.running()) { // TODO it catches more events than needed, to be re-factored
this.editor.update(Action.fromFragmentDeletion(rnd.ctab, this.lassoHelper.end(event)));
this.editor.selection(null);
} else {
var ci = this.editor.findItem(event, this.maps);
if (ci) { // ci.type != 'Canvas'
this.editor.hover(null);
if (ci.map === 'atoms') {
this.editor.update(Action.fromAtomDeletion(rnd.ctab, ci.id));
} else if (ci.map === 'bonds') {
this.editor.update(Action.fromBondDeletion(rnd.ctab, ci.id));
} else if (ci.map === 'sgroups' || ci.map === 'sgroupData') {
this.editor.update(Action.fromSgroupDeletion(rnd.ctab, ci.id));
} else if (ci.map === 'rxnArrows') {
this.editor.update(Action.fromArrowDeletion(rnd.ctab, ci.id));
} else if (ci.map === 'rxnPluses') {
this.editor.update(Action.fromPlusDeletion(rnd.ctab, ci.id));
} else if (ci.map === 'chiralFlags') {
this.editor.update(Action.fromChiralFlagDeletion(rnd.ctab));
} else {
// TODO re-factoring needed - should be "map-independent"
console.error('EraserTool: unable to delete the object ' + ci.map + '[' + ci.id + ']');
return;
}
this.editor.selection(null);
}
}
};
module.exports = EraserTool;

View File

@ -0,0 +1,83 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var locate = require('./locate');
var draw = require('../../../render/draw');
var scale = require('../../../util/scale');
function LassoHelper(mode, editor, fragment) {
this.mode = mode;
this.fragment = fragment;
this.editor = editor;
}
LassoHelper.prototype.getSelection = function () {
var rnd = this.editor.render;
if (this.mode == 0)
return locate.inPolygon(rnd.ctab, this.points);
else if (this.mode == 1)
return locate.inRectangle(rnd.ctab, this.points[0], this.points[1]);
else
throw new Error('Selector mode unknown'); // eslint-disable-line no-else-return
};
LassoHelper.prototype.begin = function (event) {
var rnd = this.editor.render;
this.points = [rnd.page2obj(event)];
if (this.mode == 1)
this.points.push(this.points[0]);
};
LassoHelper.prototype.running = function () {
return !!this.points;
};
LassoHelper.prototype.addPoint = function (event) {
if (this.points) {
var rnd = this.editor.render;
if (this.mode == 0)
this.points.push(rnd.page2obj(event));
else if (this.mode == 1)
this.points = [this.points[0], rnd.page2obj(event)];
this.update();
return this.getSelection();
}
return null;
};
LassoHelper.prototype.update = function () {
if (this.selection) {
this.selection.remove();
this.selection = null;
}
if (this.points && this.points.length > 1) {
var rnd = this.editor.render;
var dp = this.points.map(function (p) {
return scale.obj2scaled(p, rnd.options).add(rnd.options.offset);
});
this.selection = this.mode == 0 ?
draw.selectionPolygon(rnd.paper, dp, rnd.options) :
draw.selectionRectangle(rnd.paper, dp[0], dp[1], rnd.options);
}
};
LassoHelper.prototype.end = function () {
var ret = this.getSelection();
this.points = null;
this.update(null);
return ret;
};
module.exports = LassoHelper;

View File

@ -0,0 +1,160 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../../util/vec2');
function getElementsInRectangle(restruct, p0, p1) {
var bondList = [];
var atomList = [];
var x0 = Math.min(p0.x, p1.x),
x1 = Math.max(p0.x, p1.x),
y0 = Math.min(p0.y, p1.y),
y1 = Math.max(p0.y, p1.y);
restruct.bonds.each(function (bid, bond) {
var centre = Vec2.lc2(restruct.atoms.get(bond.b.begin).a.pp, 0.5,
restruct.atoms.get(bond.b.end).a.pp, 0.5);
if (centre.x > x0 && centre.x < x1 && centre.y > y0 && centre.y < y1)
bondList.push(bid);
});
restruct.atoms.each(function (aid, atom) {
if (atom.a.pp.x > x0 && atom.a.pp.x < x1 && atom.a.pp.y > y0 && atom.a.pp.y < y1)
atomList.push(aid);
});
var rxnArrowsList = [];
var rxnPlusesList = [];
restruct.rxnArrows.each(function (id, item) {
if (item.item.pp.x > x0 && item.item.pp.x < x1 && item.item.pp.y > y0 && item.item.pp.y < y1)
rxnArrowsList.push(id);
});
restruct.rxnPluses.each(function (id, item) {
if (item.item.pp.x > x0 && item.item.pp.x < x1 && item.item.pp.y > y0 && item.item.pp.y < y1)
rxnPlusesList.push(id);
});
var chiralFlagList = [];
restruct.chiralFlags.each(function (id, item) {
if (item.pp.x > x0 && item.pp.x < x1 && item.pp.y > y0 && item.pp.y < y1)
chiralFlagList.push(id);
});
var sgroupDataList = [];
restruct.sgroupData.each(function (id, item) {
if (item.sgroup.pp.x > x0 && item.sgroup.pp.x < x1 && item.sgroup.pp.y > y0 && item.sgroup.pp.y < y1)
sgroupDataList.push(id);
});
return {
atoms: atomList,
bonds: bondList,
rxnArrows: rxnArrowsList,
rxnPluses: rxnPlusesList,
chiralFlags: chiralFlagList,
sgroupData: sgroupDataList
};
}
function getElementsInPolygon(restruct, rr) { // eslint-disable-line max-statements
var bondList = [];
var atomList = [];
var r = [];
for (var i = 0; i < rr.length; ++i)
r[i] = new Vec2(rr[i].x, rr[i].y);
restruct.bonds.each(function (bid, bond) {
var centre = Vec2.lc2(restruct.atoms.get(bond.b.begin).a.pp, 0.5,
restruct.atoms.get(bond.b.end).a.pp, 0.5);
if (isPointInPolygon(r, centre))
bondList.push(bid);
});
restruct.atoms.each(function (aid, atom) {
if (isPointInPolygon(r, atom.a.pp))
atomList.push(aid);
});
var rxnArrowsList = [];
var rxnPlusesList = [];
restruct.rxnArrows.each(function (id, item) {
if (isPointInPolygon(r, item.item.pp))
rxnArrowsList.push(id);
});
restruct.rxnPluses.each(function (id, item) {
if (isPointInPolygon(r, item.item.pp))
rxnPlusesList.push(id);
});
var chiralFlagList = [];
restruct.chiralFlags.each(function (id, item) {
if (isPointInPolygon(r, item.pp))
chiralFlagList.push(id);
});
var sgroupDataList = [];
restruct.sgroupData.each(function (id, item) {
if (isPointInPolygon(r, item.sgroup.pp))
sgroupDataList.push(id);
});
return {
atoms: atomList,
bonds: bondList,
rxnArrows: rxnArrowsList,
rxnPluses: rxnPlusesList,
chiralFlags: chiralFlagList,
sgroupData: sgroupDataList
};
}
// TODO: test me see testPolygon from
// 'Remove unused methods from render' commit
function isPointInPolygon(r, p) { // eslint-disable-line max-statements
var d = new Vec2(0, 1);
var n = d.rotate(Math.PI / 2);
var v0 = Vec2.diff(r[r.length - 1], p);
var n0 = Vec2.dot(n, v0);
var d0 = Vec2.dot(d, v0);
var w0 = null;
var counter = 0;
var eps = 1e-5;
var flag1 = false,
flag0 = false;
for (var i = 0; i < r.length; ++i) {
var v1 = Vec2.diff(r[i], p);
var w1 = Vec2.diff(v1, v0);
var n1 = Vec2.dot(n, v1);
var d1 = Vec2.dot(d, v1);
flag1 = false;
if (n1 * n0 < 0) {
if (d1 * d0 > -eps) {
if (d0 > -eps)
flag1 = true;
/* eslint-disable no-mixed-operators*/
} else if ((Math.abs(n0) * Math.abs(d1) - Math.abs(n1) * Math.abs(d0)) * d1 > 0) {
/* eslint-enable no-mixed-operators*/
flag1 = true;
}
}
if (flag1 && flag0 && Vec2.dot(w1, n) * Vec2.dot(w0, n) >= 0)
flag1 = false;
if (flag1)
counter++;
v0 = v1;
n0 = n1;
d0 = d1;
w0 = w1;
flag0 = flag1;
}
return (counter % 2) != 0;
}
module.exports = {
inRectangle: getElementsInRectangle,
inPolygon: getElementsInPolygon
};

View File

@ -0,0 +1,59 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
function PasteTool(editor, struct) {
if (!(this instanceof PasteTool))
return new PasteTool(editor, struct);
this.editor = editor;
this.editor.selection(null);
this.struct = struct;
var rnd = editor.render;
var point = editor.lastEvent ?
rnd.page2obj(editor.lastEvent) : null;
this.action = Action.fromPaste(rnd.ctab, this.struct, point);
this.editor.update(this.action, true);
}
PasteTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
if (this.action)
this.action.perform(rnd.ctab);
this.action = Action.fromPaste(rnd.ctab, this.struct, rnd.page2obj(event));
this.editor.update(this.action, true);
};
PasteTool.prototype.mouseup = function () {
if (this.action) {
var action = this.action;
delete this.action;
this.editor.update(action);
}
};
PasteTool.prototype.cancel = PasteTool.prototype.mouseleave = function () {
var rnd = this.editor.render;
if (this.action) {
this.action.perform(rnd.ctab); // revert the action
delete this.action;
rnd.update();
}
};
module.exports = PasteTool;

View File

@ -0,0 +1,65 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
function ReactionArrowTool(editor) {
if (!(this instanceof ReactionArrowTool))
return new ReactionArrowTool(editor);
this.editor = editor;
this.editor.selection(null);
}
ReactionArrowTool.prototype.mousedown = function (event) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['rxnArrows']);
if (ci && ci.map === 'rxnArrows') {
this.editor.hover(null);
this.editor.selection({ rxnArrows: [ci.id] });
this.dragCtx = {
xy0: rnd.page2obj(event),
action: new Action()
};
}
};
ReactionArrowTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
if ('dragCtx' in this) {
if (this.dragCtx.action)
this.dragCtx.action.perform(rnd.ctab);
this.dragCtx.action = Action.fromMultipleMove(
rnd.ctab,
this.editor.selection() || {},
rnd.page2obj(event).sub(this.dragCtx.xy0)
);
this.editor.update(this.dragCtx.action, true);
} else {
this.editor.hover(this.editor.findItem(event, ['rxnArrows']));
}
};
ReactionArrowTool.prototype.mouseup = function (event) {
var rnd = this.editor.render;
if (this.dragCtx) {
this.editor.update(this.dragCtx.action); // TODO investigate, subsequent undo/redo fails
delete this.dragCtx;
} else if (rnd.ctab.molecule.rxnArrows.count() < 1) {
this.editor.update(Action.fromArrowAddition(rnd.ctab, rnd.page2obj(event)));
}
};
module.exports = ReactionArrowTool;

View File

@ -0,0 +1,129 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Set = require('../../util/set');
var scale = require('../../util/scale');
var Action = require('../action');
var draw = require('../../render/draw');
function ReactionMapTool(editor) {
if (!(this instanceof ReactionMapTool))
return new ReactionMapTool(editor);
this.editor = editor;
this.editor.selection(null);
this.rcs = this.editor.render.ctab.molecule.getComponents();
}
ReactionMapTool.prototype.mousedown = function (event) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['atoms']);
if (ci && ci.map === 'atoms') {
this.editor.hover(null);
this.dragCtx = {
item: ci,
xy0: rnd.page2obj(event)
};
}
};
ReactionMapTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
if ('dragCtx' in this) {
var ci = this.editor.findItem(event, ['atoms'], this.dragCtx.item);
var atoms = rnd.ctab.molecule.atoms;
if (ci && ci.map === 'atoms' && isValidMap(this.rcs, this.dragCtx.item.id, ci.id)) {
this.editor.hover(ci);
this.updateLine(atoms.get(this.dragCtx.item.id).pp, atoms.get(ci.id).pp);
} else {
this.editor.hover(null);
this.updateLine(atoms.get(this.dragCtx.item.id).pp, rnd.page2obj(event));
}
} else {
this.editor.hover(this.editor.findItem(event, ['atoms']));
}
};
ReactionMapTool.prototype.updateLine = function (p1, p2) {
if (this.line) {
this.line.remove();
this.line = null;
}
if (p1 && p2) {
var rnd = this.editor.render;
this.line = draw.selectionLine(rnd.paper,
scale.obj2scaled(p1, rnd.options).add(rnd.options.offset),
scale.obj2scaled(p2, rnd.options).add(rnd.options.offset),
rnd.options);
}
};
ReactionMapTool.prototype.mouseup = function (event) { // eslint-disable-line max-statements
if ('dragCtx' in this) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['atoms'], this.dragCtx.item);
if (ci && ci.map === 'atoms' && isValidMap(this.rcs, this.dragCtx.item.id, ci.id)) {
var action = new Action();
var atoms = rnd.ctab.molecule.atoms;
var atom1 = atoms.get(this.dragCtx.item.id);
var atom2 = atoms.get(ci.id);
var aam1 = atom1.aam;
var aam2 = atom2.aam;
if (!aam1 || aam1 != aam2) {
if (aam1 && aam1 != aam2 || !aam1 && aam2) { // eslint-disable-line no-mixed-operators
atoms.each(
function (aid, atom) {
if (aid != this.dragCtx.item.id && (aam1 && atom.aam == aam1 || aam2 && atom.aam == aam2)) // eslint-disable-line no-mixed-operators
action.mergeWith(Action.fromAtomsAttrs(rnd.ctab, aid, { aam: 0 }));
},
this
);
}
if (aam1) {
action.mergeWith(Action.fromAtomsAttrs(rnd.ctab, ci.id, { aam: aam1 }));
} else {
var aam = 0;
atoms.each(function (aid, atom) {
aam = Math.max(aam, atom.aam || 0);
});
action.mergeWith(Action.fromAtomsAttrs(rnd.ctab, this.dragCtx.item.id, { aam: aam + 1 }));
action.mergeWith(Action.fromAtomsAttrs(rnd.ctab, ci.id, { aam: aam + 1 }));
}
this.editor.update(action);
}
}
this.updateLine(null);
delete this.dragCtx;
}
this.editor.hover(null);
};
function isValidMap(rcs, aid1, aid2) {
var t1, t2;
for (var ri = 0; (!t1 || !t2) && ri < rcs.reactants.length; ri++) {
var ro = Set.list(rcs.reactants[ri]);
if (!t1 && ro.indexOf(aid1) >= 0) t1 = 'r';
if (!t2 && ro.indexOf(aid2) >= 0) t2 = 'r';
}
for (var pi = 0; (!t1 || !t2) && pi < rcs.products.length; pi++) {
var po = Set.list(rcs.products[pi]);
if (!t1 && po.indexOf(aid1) >= 0) t1 = 'p';
if (!t2 && po.indexOf(aid2) >= 0) t2 = 'p';
}
return t1 && t2 && t1 != t2;
}
module.exports = ReactionMapTool;

View File

@ -0,0 +1,60 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
function ReactionPlusTool(editor) {
if (!(this instanceof ReactionPlusTool))
return new ReactionPlusTool(editor);
this.editor = editor;
this.editor.selection(null);
}
ReactionPlusTool.prototype.mousedown = function (event) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['rxnPluses']);
if (ci && ci.map === 'rxnPluses') {
this.editor.hover(null);
this.editor.selection({ rxnPluses: [ci.id] });
this.dragCtx = { xy0: rnd.page2obj(event) };
}
};
ReactionPlusTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
if ('dragCtx' in this) {
if (this.dragCtx.action)
this.dragCtx.action.perform(rnd.ctab);
this.dragCtx.action = Action.fromMultipleMove(
rnd.ctab,
this.editor.selection() || {},
rnd.page2obj(event).sub(this.dragCtx.xy0)
);
this.editor.update(this.dragCtx.action, true);
} else {
this.editor.hover(this.editor.findItem(event, ['rxnPluses']));
}
};
ReactionPlusTool.prototype.mouseup = function (event) {
var rnd = this.editor.render;
if (this.dragCtx) {
this.editor.update(this.dragCtx.action); // TODO investigate, subsequent undo/redo fails
delete this.dragCtx;
} else {
this.editor.update(Action.fromPlusAddition(rnd.ctab, rnd.page2obj(event)));
}
};
module.exports = ReactionPlusTool;

View File

@ -0,0 +1,51 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Action = require('../action');
function ReactionUnmapTool(editor) {
if (!(this instanceof ReactionUnmapTool))
return new ReactionUnmapTool(editor);
this.editor = editor;
this.editor.selection(null);
}
ReactionUnmapTool.prototype.mousemove = function (event) {
var ci = this.editor.findItem(event, ['atoms']);
if (ci && ci.map === 'atoms')
this.editor.hover(this.editor.render.ctab.molecule.atoms.get(ci.id).aam ? ci : null);
else
this.editor.hover(null);
};
ReactionUnmapTool.prototype.mouseup = function (event) {
var ci = this.editor.findItem(event, ['atoms']);
var atoms = this.editor.render.ctab.molecule.atoms;
if (ci && ci.map === 'atoms' && atoms.get(ci.id).aam) {
var action = new Action();
var aam = atoms.get(ci.id).aam;
atoms.each(
function (aid, atom) {
if (atom.aam == aam)
action.mergeWith(Action.fromAtomsAttrs(this.editor.render.ctab, aid, { aam: 0 }));
},
this
);
this.editor.update(action);
}
this.editor.hover(null);
};
module.exports = ReactionUnmapTool;

View File

@ -0,0 +1,69 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Struct = require('../../chem/struct');
var Action = require('../action');
function RGroupAtomTool(editor) {
if (!(this instanceof RGroupAtomTool)) {
// TODO: map atoms with labels
editor.selection(null);
return new RGroupAtomTool(editor);
}
this.editor = editor;
}
RGroupAtomTool.prototype.mousemove = function (event) {
this.editor.hover(this.editor.findItem(event, ['atoms']));
};
RGroupAtomTool.prototype.mouseup = function (event) {
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['atoms']);
if (!ci) { // ci.type == 'Canvas'
this.editor.hover(null);
propsDialog(this.editor, null, rnd.page2obj(event));
return true;
} else if (ci.map === 'atoms') {
this.editor.hover(null);
propsDialog(this.editor, ci.id);
return true;
}
};
function propsDialog(editor, id, pos) {
var struct = editor.render.ctab.molecule;
var atom = (id || id === 0) ? struct.atoms.get(id) : null;
var rglabel = atom ? atom.rglabel : 0;
var label = atom ? atom.label : 'R#';
var res = editor.event.elementEdit.dispatch({
label: 'R#', rglabel: rglabel
});
Promise.resolve(res).then(function (elem) {
elem = Object.assign({}, Struct.Atom.attrlist, elem); // TODO review: using Atom.attrlist as a source of default property values
if (!id && id !== 0 && elem.rglabel) {
editor.update(Action.fromAtomAddition(editor.render.ctab, pos, elem));
} else if (rglabel != elem.rglabel || label !== 'R#') {
elem.aam = atom.aam; // WTF??
editor.update(Action.fromAtomsAttrs(editor.render.ctab, id, elem));
}
});
}
module.exports = RGroupAtomTool;

View File

@ -0,0 +1,71 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Struct = require('../../chem/struct');
var Action = require('../action');
function RGroupFragmentTool(editor) {
if (!(this instanceof RGroupFragmentTool)) {
// TODO: check if it's a fragments already
editor.selection(null);
return new RGroupFragmentTool(editor);
}
this.editor = editor;
}
RGroupFragmentTool.prototype.mousemove = function (event) {
this.editor.hover(this.editor.findItem(event, ['frags', 'rgroups']));
};
RGroupFragmentTool.prototype.mouseup = function (event) {
const editor = this.editor;
const struct = editor.render.ctab.molecule;
const ci = editor.findItem(event, ['frags', 'rgroups']);
if (ci) {
this.editor.hover(null);
const label = (ci.map === 'rgroups') ? ci.id :
Struct.RGroup.findRGroupByFragment(struct.rgroups, ci.id) || null;
const rg = Object.assign({ label: label },
ci.map === 'frags' ? null :
struct.rgroups.get(ci.id));
const res = editor.event.rgroupEdit.dispatch(rg);
Promise.resolve(res).then(newRg => {
const restruct = editor.render.ctab;
let action = null;
if (ci.map !== 'rgroups') {
const rgidOld = Struct.RGroup.findRGroupByFragment(restruct.molecule.rgroups, ci.id);
action = Action.fromRGroupFragment(restruct, newRg.label, ci.id)
.mergeWith(Action.fromUpdateIfThen(restruct, newRg.label, rgidOld));
} else {
action = Action.fromRGroupAttrs(restruct, ci.id, newRg);
}
editor.update(action);
});
return true;
}
};
module.exports = RGroupFragmentTool;

View File

@ -0,0 +1,154 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Action = require('../action');
var utils = require('./utils');
function RotateTool(editor, dir) {
if (!(this instanceof RotateTool)) {
if (!dir)
return new RotateTool(editor);
var restruct = editor.render.ctab;
var selection = editor.selection();
var singleBond = selection && selection.bonds &&
Object.keys(selection).length === 1 &&
selection.bonds.length == 1;
var action = !singleBond ? Action.fromFlip(restruct, selection, dir) :
Action.fromBondAlign(restruct, selection.bonds[0], dir);
editor.update(action);
return null;
}
this.editor = editor;
if (!editor.selection() || !editor.selection().atoms)
// otherwise, clear selection
this.editor.selection(null);
}
RotateTool.prototype.mousedown = function (event) {
var xy0 = new Vec2();
var selection = this.editor.selection();
var rnd = this.editor.render;
var struct = rnd.ctab.molecule;
if (selection && selection.atoms) {
console.assert(selection.atoms.length > 0);
var rotId = null;
var rotAll = false;
selection.atoms.forEach(function (aid) {
var atom = struct.atoms.get(aid);
xy0.add_(atom.pp); // eslint-disable-line no-underscore-dangle
if (rotAll)
return;
atom.neighbors.find(function (nei) {
var hb = struct.halfBonds.get(nei);
if (selection.atoms.indexOf(hb.end) === -1) {
if (hb.loop >= 0) {
var neiAtom = struct.atoms.get(aid);
if (!neiAtom.neighbors.find(function (neiNei) {
var neiHb = struct.halfBonds.get(neiNei);
return neiHb.loop >= 0 && selection.atoms.indexOf(neiHb.end) !== -1;
})) {
rotAll = true;
return true;
}
}
if (rotId == null) {
rotId = aid;
} else if (rotId != aid) {
rotAll = true;
return true;
}
}
return false;
});
});
if (!rotAll && rotId != null)
xy0 = struct.atoms.get(rotId).pp;
else
xy0 = xy0.scaled(1 / selection.atoms.length);
} else {
struct.atoms.each(function (id, atom) {
xy0.add_(atom.pp); // eslint-disable-line no-underscore-dangle
});
// poor man struct center (without chiral, sdata, etc)
xy0 = xy0.scaled(1 / struct.atoms.count());
}
this.dragCtx = {
xy0: xy0,
angle1: utils.calcAngle(xy0, rnd.page2obj(event))
};
return true;
};
RotateTool.prototype.mousemove = function (event) { // eslint-disable-line max-statements
if ('dragCtx' in this) {
var rnd = this.editor.render;
var dragCtx = this.dragCtx;
var pos = rnd.page2obj(event);
var angle = utils.calcAngle(dragCtx.xy0, pos) - dragCtx.angle1;
if (!event.ctrlKey)
angle = utils.fracAngle(angle);
var degrees = utils.degrees(angle);
if ('angle' in dragCtx && dragCtx.angle == degrees) return true;
if ('action' in dragCtx)
dragCtx.action.perform(rnd.ctab);
dragCtx.angle = degrees;
dragCtx.action = Action.fromRotate(rnd.ctab, this.editor.selection(), dragCtx.xy0, angle);
if (degrees > 180)
degrees -= 360;
else if (degrees <= -180)
degrees += 360;
this.editor.event.message.dispatch({ info: degrees + 'º' });
this.editor.update(dragCtx.action, true);
}
return true;
};
RotateTool.prototype.mouseup = function () {
if (this.dragCtx) {
var action = this.dragCtx.action;
delete this.dragCtx;
if (action)
this.editor.update(action);
else
this.editor.selection(null);
}
return true;
};
RotateTool.prototype.cancel = RotateTool.prototype.mouseleave =
RotateTool.prototype.mouseup;
module.exports = RotateTool;

View File

@ -0,0 +1,254 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Set = require('../../util/set');
var Action = require('../action');
var Struct = require('../../chem/struct');
var LassoHelper = require('./helper/lasso');
var SGroup = require('./sgroup');
var Atom = require('./atom');
function SelectTool(editor, mode) {
if (!(this instanceof SelectTool))
return new SelectTool(editor, mode);
this.editor = editor;
this.lassoHelper = new LassoHelper(mode === 'lasso' ? 0 : 1, editor, mode === 'fragment');
}
SelectTool.prototype.mousedown = function (event) { // eslint-disable-line max-statements
var rnd = this.editor.render;
var ctab = rnd.ctab;
var struct = ctab.molecule;
this.editor.hover(null); // TODO review hovering for touch devicess
var selectFragment = (this.lassoHelper.fragment || event.ctrlKey);
var ci = this.editor.findItem(
event,
selectFragment ?
['frags', 'sgroups', 'sgroupData', 'rgroups', 'rxnArrows', 'rxnPluses', 'chiralFlags'] :
['atoms', 'bonds', 'sgroups', 'sgroupData', 'rgroups', 'rxnArrows', 'rxnPluses', 'chiralFlags']
);
this.dragCtx = {
item: ci,
xy0: rnd.page2obj(event)
};
if (!ci) { // ci.type == 'Canvas'
Atom.atomLongtapEvent(this, rnd);
delete this.dragCtx.item;
if (!this.lassoHelper.fragment)
this.lassoHelper.begin(event);
} else {
this.editor.hover(null);
if (!isSelected(rnd, this.editor.selection(), ci)) {
var sel = closestToSel(ci);
if (ci.map === 'frags') {
var frag = ctab.frags.get(ci.id);
sel = {
atoms: frag.fragGetAtoms(rnd, ci.id),
bonds: frag.fragGetBonds(rnd, ci.id)
};
} else if (ci.map === 'sgroups') {
var sgroup = ctab.sgroups.get(ci.id).item;
sel = {
atoms: Struct.SGroup.getAtoms(struct, sgroup),
bonds: Struct.SGroup.getBonds(struct, sgroup)
};
} else if (ci.map === 'rgroups') {
var rgroup = ctab.rgroups.get(ci.id);
sel = {
atoms: rgroup.getAtoms(rnd),
bonds: rgroup.getBonds(rnd)
};
}
this.editor.selection(!event.shiftKey ? sel :
selMerge(sel, this.editor.selection()));
}
if (ci.map === 'atoms')
// this event has to be stopped in others events by `tool.dragCtx.stopTapping()`
Atom.atomLongtapEvent(this, rnd);
}
return true;
};
SelectTool.prototype.mousemove = function (event) {
var rnd = this.editor.render;
if (this.dragCtx && this.dragCtx.stopTapping)
this.dragCtx.stopTapping();
if (this.dragCtx && this.dragCtx.item) {
// moving selected objects
if (this.dragCtx.action) {
this.dragCtx.action.perform(rnd.ctab);
this.editor.update(this.dragCtx.action, true); // redraw the elements in unshifted position, lest the have different offset
}
this.dragCtx.action = Action.fromMultipleMove(
rnd.ctab,
this.editor.explicitSelected(),
rnd.page2obj(event).sub(this.dragCtx.xy0));
// finding & highlighting object to stick to
if (['atoms'/* , 'bonds'*/].indexOf(this.dragCtx.item.map) >= 0) {
// TODO add bond-to-bond fusing
var ci = this.editor.findItem(event, [this.dragCtx.item.map], this.dragCtx.item);
this.editor.hover((ci && ci.map == this.dragCtx.item.map) ? ci : null);
}
this.editor.update(this.dragCtx.action, true);
} else if (this.lassoHelper.running()) {
var sel = this.lassoHelper.addPoint(event);
this.editor.selection(!event.shiftKey ? sel :
selMerge(sel, this.editor.selection()));
} else {
this.editor.hover(
this.editor.findItem(event,
(this.lassoHelper.fragment || event.ctrlKey) ?
['frags', 'sgroups', 'sgroupData', 'rgroups', 'rxnArrows', 'rxnPluses', 'chiralFlags'] :
['atoms', 'bonds', 'sgroups', 'sgroupData', 'rgroups', 'rxnArrows', 'rxnPluses', 'chiralFlags']
)
);
}
return true;
};
SelectTool.prototype.mouseup = function (event) { // eslint-disable-line max-statements
if (this.dragCtx && this.dragCtx.stopTapping)
this.dragCtx.stopTapping();
if (this.dragCtx && this.dragCtx.item) {
if (['atoms'/* , 'bonds'*/].indexOf(this.dragCtx.item.map) >= 0) {
// TODO add bond-to-bond fusing
var ci = this.editor.findItem(event, [this.dragCtx.item.map], this.dragCtx.item);
if (ci && ci.map == this.dragCtx.item.map) {
var restruct = this.editor.render.ctab;
this.editor.hover(null);
this.editor.selection(null);
this.dragCtx.action = this.dragCtx.action ?
Action.fromAtomMerge(restruct, this.dragCtx.item.id, ci.id).mergeWith(this.dragCtx.action) :
Action.fromAtomMerge(restruct, this.dragCtx.item.id, ci.id);
}
}
if (this.dragCtx.action)
this.editor.update(this.dragCtx.action);
delete this.dragCtx;
} else if (this.lassoHelper.running()) { // TODO it catches more events than needed, to be re-factored
var sel = this.lassoHelper.end();
this.editor.selection(!event.shiftKey ? sel :
selMerge(sel, this.editor.selection()));
} else if (this.lassoHelper.fragment) {
this.editor.selection(null);
}
return true;
};
SelectTool.prototype.dblclick = function (event) { // eslint-disable-line max-statements
var editor = this.editor;
var rnd = this.editor.render;
var ci = this.editor.findItem(event, ['atoms', 'bonds', 'sgroups', 'sgroupData']);
if (!ci) return;
var struct = rnd.ctab.molecule;
if (ci.map === 'atoms') {
this.editor.selection(closestToSel(ci));
var atom = struct.atoms.get(ci.id);
var ra = editor.event.elementEdit.dispatch(atom);
Promise.resolve(ra).then(function (newatom) {
// TODO: deep compare to not produce dummy, e.g.
// atom.label != attrs.label || !atom.atomList.equals(attrs.atomList)
editor.update(Action.fromAtomsAttrs(rnd.ctab, ci.id, newatom));
});
} else if (ci.map === 'bonds') {
this.editor.selection(closestToSel(ci));
var bond = rnd.ctab.bonds.get(ci.id).b;
var rb = editor.event.bondEdit.dispatch(bond);
Promise.resolve(rb).then(function (newbond) {
editor.update(Action.fromBondAttrs(rnd.ctab, ci.id, newbond));
});
} else if (ci.map === 'sgroups' || ci.map === 'sgroupData') {
this.editor.selection(closestToSel(ci));
SGroup.dialog(this.editor, ci.id);
// } else if (ci.map == 'sgroupData') {
// SGroup.dialog(this.editor, ci.sgid);
}
return true;
};
SelectTool.prototype.cancel = SelectTool.prototype.mouseleave = function () {
if (this.dragCtx && this.dragCtx.stopTapping)
this.dragCtx.stopTapping();
if (this.dragCtx && this.dragCtx.action) {
var action = this.dragCtx.action;
this.editor.update(action);
}
if (this.lassoHelper.running())
this.editor.selection(this.lassoHelper.end());
delete this.dragCtx;
this.editor.hover(null);
};
function closestToSel(ci) {
var res = {};
res[ci.map] = [ci.id];
return res;
}
// TODO: deep-merge?
function selMerge(selection, add) {
if (add) {
for (var item in add) {
if (add.hasOwnProperty(item)) {
if (!selection[item]) {
selection[item] = add[item].slice();
} else {
selection[item] = uniqArray(selection[item],
add[item]);
}
}
}
}
return selection;
}
function uniqArray(dest, add) {
for (var i = 0; i < add.length; i++) {
if (dest.indexOf(add[i]) < 0)
dest.push(add[i]);
}
return dest;
}
function isSelected(render, selection, item) {
if (!selection)
return false;
var ctab = render.ctab;
if (item.map === 'frags' || item.map === 'rgroups') {
var atoms = item.map === 'frags' ?
ctab.frags.get(item.id).fragGetAtoms(render, item.id) :
ctab.rgroups.get(item.id).getAtoms(render);
return !!selection['atoms'] &&
Set.subset(Set.fromList(atoms), Set.fromList(selection['atoms']));
}
return !!selection[item.map] &&
selection[item.map].indexOf(item.id) > -1;
}
module.exports = SelectTool;

View File

@ -0,0 +1,346 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
const isEqual = require('lodash/fp/isEqual');
const uniq = require('lodash/fp/uniq');
const LassoHelper = require('./helper/lasso');
const Action = require('../action');
const Struct = require('../../chem/struct');
const Set = require('../../util/set');
const Contexts = require('../../util/constants').SgContexts;
const searchMaps = ['atoms', 'bonds', 'sgroups', 'sgroupData'];
function SGroupTool(editor, type) {
if (!(this instanceof SGroupTool)) {
var selection = editor.selection() || {};
if (!selection.atoms && !selection.bonds)
return new SGroupTool(editor, type);
var sgroups = editor.render.ctab.molecule.sgroups;
var selectedAtoms = editor.selection().atoms;
var id = sgroups.find(function (_, sgroup) {
return isEqual(sgroup.atoms, selectedAtoms);
});
propsDialog(editor, id !== undefined ? id : null, type);
editor.selection(null);
return null;
}
this.editor = editor;
this.type = type;
this.lassoHelper = new LassoHelper(1, editor);
this.editor.selection(null);
}
SGroupTool.prototype.mousedown = function (event) {
var ci = this.editor.findItem(event, searchMaps);
if (!ci) // ci.type == 'Canvas'
this.lassoHelper.begin(event);
};
SGroupTool.prototype.mousemove = function (event) {
if (this.lassoHelper.running(event))
this.editor.selection(this.lassoHelper.addPoint(event));
else
this.editor.hover(this.editor.findItem(event, searchMaps));
};
SGroupTool.prototype.mouseleave = function (event) {
if (this.lassoHelper.running(event))
this.lassoHelper.end(event);
};
SGroupTool.prototype.mouseup = function (event) {
var id = null; // id of an existing group, if we're editing one
var selection = null; // atoms to include in a newly created group
if (this.lassoHelper.running(event)) { // TODO it catches more events than needed, to be re-factored
selection = this.lassoHelper.end(event);
} else {
var ci = this.editor.findItem(event, searchMaps);
if (!ci) // ci.type == 'Canvas'
return;
this.editor.hover(null);
if (ci.map === 'atoms') {
// if we click the SGroup tool on a single atom or bond, make a group out of those
selection = { atoms: [ci.id] };
} else if (ci.map === 'bonds') {
var bond = this.editor.render.ctab.bonds.get(ci.id);
selection = { atoms: [bond.b.begin, bond.b.end] };
} else if (ci.map === 'sgroups' || ci.map === 'sgroupData') {
id = ci.id;
} else {
return;
}
}
// TODO: handle click on an existing group?
if (id !== null || (selection && selection.atoms))
propsDialog(this.editor, id, this.type);
};
function propsDialog(editor, id, defaultType) {
const restruct = editor.render.ctab;
const struct = restruct.molecule;
const selection = editor.selection() || {};
const sg = id !== null ? struct.sgroups.get(id) : null;
const type = sg ? sg.type : defaultType;
const eventName = type === 'DAT' ? 'sdataEdit' : 'sgroupEdit';
if (!selection.atoms && !selection.bonds && !sg) {
console.info('There is no selection or sgroup');
return;
}
let attrs = null;
if (sg) {
attrs = sg.getAttrs();
attrs.context = getContextBySgroup(restruct, sg.atoms);
} else {
attrs = {
context: getContextBySelection(restruct, selection)
};
}
const res = editor.event[eventName].dispatch({
type: type,
attrs: attrs
});
Promise.resolve(res).then(newSg => {
// TODO: check before signal
if (newSg.type !== 'DAT' && // when data s-group separates
checkOverlapping(struct, selection.atoms || [])) {
editor.event.message.dispatch({
error: 'Partial S-group overlapping is not allowed.'
});
} else {
if (!sg && newSg.type !== 'DAT' && (!selection.atoms || selection.atoms.length === 0))
return;
const isDataSg = sg && sg.getAttrs().context === newSg.attrs.context;
if (isDataSg) {
const action = Action.fromSeveralSgroupAddition(restruct, newSg.type, sg.atoms, newSg.attrs)
.mergeWith(Action.fromSgroupDeletion(restruct, id));
editor.update(action);
editor.selection(selection);
return;
}
const result = fromContextType(id, editor, newSg, selection);
editor.update(result.action);
editor.selection(result.selection);
}
}).catch(result => {
console.info('rejected', result);
});
}
function getContextBySgroup(restruct, sgAtoms) {
const struct = restruct.molecule;
if (sgAtoms.length === 1)
return Contexts.Atom;
if (manyComponentsSelected(restruct, sgAtoms))
return Contexts.Multifragment;
if (singleComponentSelected(restruct, sgAtoms))
return Contexts.Fragment;
const atomMap = sgAtoms.reduce((acc, aid) => {
acc[aid] = true;
return acc;
}, {});
const sgBonds = struct.bonds
.values()
.filter(bond => atomMap[bond.begin] && atomMap[bond.end]);
return anyChainedBonds(sgBonds) ? Contexts.Group : Contexts.Bond;
}
function getContextBySelection(restruct, selection) {
const struct = restruct.molecule;
if (selection.atoms && !selection.bonds)
return Contexts.Atom;
const bonds = selection.bonds.map(bondid => struct.bonds.get(bondid));
if (!anyChainedBonds(bonds))
return Contexts.Bond;
selection.atoms = selection.atoms || [];
const atomSelectMap = atomMap(selection.atoms);
const allBondsSelected = bonds.every(bond =>
atomSelectMap[bond.begin] !== undefined && atomSelectMap[bond.end] !== undefined
);
if (singleComponentSelected(restruct, selection.atoms) && allBondsSelected)
return Contexts.Fragment;
return manyComponentsSelected(restruct, selection.atoms) ? Contexts.Multifragment : Contexts.Group;
}
function fromContextType(id, editor, newSg, currSelection) {
const restruct = editor.render.ctab;
const sg = restruct.molecule.sgroups.get(id);
const sourceAtoms = (sg && sg.atoms) || currSelection.atoms || [];
const context = newSg.attrs.context;
const result = getActionForContext(context, restruct, newSg, sourceAtoms, currSelection);
result.selection = result.selection || currSelection;
if (id !== null && id !== undefined)
result.action = result.action.mergeWith(Action.fromSgroupDeletion(restruct, id));
editor.selection(result.selection);
return result;
}
function getActionForContext(context, restruct, newSg, sourceAtoms, selection) {
if (context === Contexts.Bond)
return Action.fromBondAction(restruct, newSg, sourceAtoms, selection);
const atomsFromBonds = getAtomsFromBonds(restruct.molecule, selection.bonds);
const newSourceAtoms = uniq(sourceAtoms.concat(atomsFromBonds));
if (context === Contexts.Fragment)
return Action.fromGroupAction(restruct, newSg, newSourceAtoms, restruct.atoms.keys());
if (context === Contexts.Multifragment)
return Action.fromMultiFragmentAction(restruct, newSg, newSourceAtoms);
if (context === Contexts.Group)
return Action.fromGroupAction(restruct, newSg, newSourceAtoms, newSourceAtoms);
if (context === Contexts.Atom)
return Action.fromAtomAction(restruct, newSg, newSourceAtoms);
return {
action: Action.fromSeveralSgroupAddition(restruct, newSg.type, sourceAtoms, newSg.attrs)
};
}
// tools
function atomMap(atoms) {
atoms = atoms || [];
return atoms.reduce((acc, atomid) => {
acc[atomid] = atomid;
return acc;
}, {});
}
function anyChainedBonds(bonds) {
if (bonds.length === 0)
return true;
for (let i = 0; i < bonds.length; ++i) {
const fixedBond = bonds[i];
for (let j = 0; j < bonds.length; ++j) {
if (i === j)
continue;
const bond = bonds[j];
if (fixedBond.end === bond.begin || fixedBond.end === bond.end)
return true;
}
}
return false;
}
function singleComponentSelected(restruct, atoms) {
return countOfSelectedComponents(restruct, atoms) === 1;
}
function manyComponentsSelected(restruct, atoms) {
return countOfSelectedComponents(restruct, atoms) > 1;
}
function countOfSelectedComponents(restruct, atoms) {
const atomSelectMap = atomMap(atoms);
return restruct.connectedComponents.values()
.reduce((acc, component) => {
const componentAtoms = Object.keys(component);
const count = componentAtoms
.reduce((acc, atom) => acc + (atomSelectMap[atom] === undefined), 0);
return acc + (count === 0 ? 1 : 0);
}, 0);
}
function getAtomsFromBonds(struct, bonds) {
bonds = bonds || [];
return bonds.reduce((acc, bondid) => {
const bond = struct.bonds.get(bondid);
acc = acc.concat([bond.begin, bond.end]);
return acc;
}, []);
}
function checkOverlapping(struct, atoms) {
var verified = {};
var atomsHash = {};
atoms.forEach(function (id) {
atomsHash[id] = true;
});
return 0 <= atoms.findIndex(function (id) {
var atom = struct.atoms.get(id);
var sgroups = Set.list(atom.sgs);
return 0 <= sgroups.findIndex(function (sid) {
var sg = struct.sgroups.get(sid);
if (sg.type === 'DAT' || sid in verified)
return false;
var sgAtoms = Struct.SGroup.getAtoms(struct, sg);
if (sgAtoms.length < atoms.length) {
var ind = sgAtoms.findIndex(function (aid) {
return !(aid in atomsHash);
});
if (0 <= ind) return true;
}
return 0 <= atoms.findIndex(function (aid) {
return (sgAtoms.indexOf(aid) === -1);
});
});
});
}
module.exports = Object.assign(SGroupTool, {
dialog: propsDialog
});

View File

@ -0,0 +1,258 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Action = require('../action');
var utils = require('./utils');
function TemplateTool(editor, tmpl) {
if (!(this instanceof TemplateTool))
return new TemplateTool(editor, tmpl);
this.editor = editor;
this.editor.selection(null);
this.template = {
aid: parseInt(tmpl.aid) || 0,
bid: parseInt(tmpl.bid) || 0
};
var frag = tmpl.struct;
frag.rescale();
var xy0 = new Vec2();
frag.atoms.each(function (aid, atom) {
xy0.add_(atom.pp); // eslint-disable-line no-underscore-dangle
});
this.template.molecule = frag; // preloaded struct
this.findItems = [];
this.template.xy0 = xy0.scaled(1 / (frag.atoms.count() || 1)); // template center
var atom = frag.atoms.get(this.template.aid);
if (atom) {
this.template.angle0 = utils.calcAngle(atom.pp, this.template.xy0); // center tilt
this.findItems.push('atoms');
}
var bond = frag.bonds.get(this.template.bid);
if (bond) {
this.template.sign = getSign(frag, bond, this.template.xy0); // template location sign against attachment bond
this.findItems.push('bonds');
}
}
TemplateTool.prototype.mousedown = function (event) { // eslint-disable-line max-statements
var editor = this.editor;
var rnd = editor.render;
this.editor.hover(null);
this.dragCtx = {
xy0: rnd.page2obj(event),
item: editor.findItem(event, this.findItems)
};
var dragCtx = this.dragCtx;
var ci = dragCtx.item;
if (!ci) { // ci.type == 'Canvas'
delete dragCtx.item;
} else if (ci.map === 'bonds') {
// calculate fragment center
var molecule = rnd.ctab.molecule;
var xy0 = new Vec2();
var bond = molecule.bonds.get(ci.id);
var frid = molecule.atoms.get(bond.begin).fragment;
var frIds = molecule.getFragmentIds(frid);
var count = 0;
var loop = molecule.halfBonds.get(bond.hb1).loop;
if (loop < 0)
loop = molecule.halfBonds.get(bond.hb2).loop;
if (loop >= 0) {
var loopHbs = molecule.loops.get(loop).hbs;
loopHbs.forEach(function (hb) {
xy0.add_(molecule.atoms.get(molecule.halfBonds.get(hb).begin).pp); // eslint-disable-line no-underscore-dangle
count++;
});
} else {
Set.each(frIds, function (id) {
xy0.add_(molecule.atoms.get(id).pp); // eslint-disable-line no-underscore-dangle
count++;
});
}
dragCtx.v0 = xy0.scaled(1 / count);
var sign = getSign(molecule, bond, dragCtx.v0);
// calculate default template flip
dragCtx.sign1 = sign || 1;
dragCtx.sign2 = this.template.sign;
}
return true;
};
TemplateTool.prototype.mousemove = function (event) { // eslint-disable-line max-statements
var editor = this.editor;
var rnd = editor.render;
if (this.dragCtx) {
var dragCtx = this.dragCtx;
var ci = dragCtx.item;
var pos0;
var pos1 = rnd.page2obj(event);
var angle;
var extraBond;
dragCtx.mouse_moved = true;
var struct = rnd.ctab.molecule;
// calc initial pos and is extra bond needed
if (!ci) { // ci.type == 'Canvas'
pos0 = dragCtx.xy0;
} else if (ci.map === 'atoms') {
pos0 = struct.atoms.get(ci.id).pp;
extraBond = Vec2.dist(pos0, pos1) > 1;
} else if (ci.map === 'bonds') {
var bond = struct.bonds.get(ci.id);
var sign = getSign(struct, bond, pos1);
if (dragCtx.sign1 * this.template.sign > 0)
sign = -sign;
if (sign != dragCtx.sign2 || !dragCtx.action) {
// undo previous action
if ('action' in dragCtx)
dragCtx.action.perform(rnd.ctab);
dragCtx.sign2 = sign;
dragCtx.action = Action.fromTemplateOnBond(rnd.ctab, ci.id, this.template, dragCtx.sign1 * dragCtx.sign2 > 0);
this.editor.update(dragCtx.action, true);
}
return true;
}
angle = utils.calcAngle(pos0, pos1);
if (!event.ctrlKey)
angle = utils.fracAngle(angle);
var degrees = utils.degrees(angle);
// check if anything changed since last time
if (dragCtx.hasOwnProperty('angle') && dragCtx.angle === degrees) {
if (!dragCtx.hasOwnProperty('extra_bond') || dragCtx.extra_bond === extraBond)
return true;
}
// undo previous action
if (dragCtx.action)
dragCtx.action.perform(rnd.ctab);
// create new action
dragCtx.angle = degrees;
if (!ci) { // ci.type == 'Canvas'
dragCtx.action = Action.fromTemplateOnCanvas(
rnd.ctab,
pos0,
angle,
this.template
);
} else if (ci.map === 'atoms') {
dragCtx.action = Action.fromTemplateOnAtom(
rnd.ctab,
ci.id,
angle,
extraBond,
this.template
);
dragCtx.extra_bond = extraBond;
}
this.editor.update(dragCtx.action, true);
return true;
}
this.editor.hover(this.editor.findItem(event, this.findItems));
return true;
};
TemplateTool.prototype.mouseup = function (event) { // eslint-disable-line max-statements
var editor = this.editor;
var rnd = editor.render;
if (this.dragCtx) {
var dragCtx = this.dragCtx;
var ci = dragCtx.item;
var restruct = rnd.ctab;
var struct = restruct.molecule;
if (!dragCtx.action) {
if (!ci) { // ci.type == 'Canvas'
dragCtx.action = Action.fromTemplateOnCanvas(rnd.ctab, dragCtx.xy0, 0, this.template);
} else if (ci.map === 'atoms') {
var degree = restruct.atoms.get(ci.id).a.neighbors.length;
if (degree > 1) { // common case
dragCtx.action = Action.fromTemplateOnAtom(
restruct,
ci.id,
null,
true,
this.template
);
} else if (degree == 1) { // on chain end
var neiId = struct.halfBonds.get(struct.atoms.get(ci.id).neighbors[0]).end;
var atom = struct.atoms.get(ci.id);
var nei = struct.atoms.get(neiId);
var angle = utils.calcAngle(nei.pp, atom.pp);
dragCtx.action = Action.fromTemplateOnAtom(
restruct,
ci.id,
event.ctrlKey ? angle : utils.fracAngle(angle),
false,
this.template
);
} else { // on single atom
dragCtx.action = Action.fromTemplateOnAtom(
restruct,
ci.id,
0,
false,
this.template
);
}
} else if (ci.map === 'bonds') {
dragCtx.action = Action.fromTemplateOnBond(restruct, ci.id, this.template, dragCtx.sign1 * dragCtx.sign2 > 0);
}
this.editor.update(dragCtx.action, true);
}
var action = this.dragCtx.action;
delete this.dragCtx;
if (action && !action.isDummy())
this.editor.update(action);
}
};
TemplateTool.prototype.cancel = TemplateTool.prototype.mouseleave =
TemplateTool.prototype.mouseup;
function getSign(molecule, bond, v) {
var begin = molecule.atoms.get(bond.begin).pp;
var end = molecule.atoms.get(bond.end).pp;
var sign = Vec2.cross(Vec2.diff(begin, end), Vec2.diff(v, end));
if (sign > 0) return 1;
if (sign < 0) return -1;
return 0;
}
module.exports = TemplateTool;

View File

@ -0,0 +1,52 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var FRAC = Math.PI / 12; // '15º'
function setFracAngle(angle) {
FRAC = Math.PI / 180 * angle;
}
function calcAngle(pos0, pos1) {
var v = Vec2.diff(pos1, pos0);
return Math.atan2(v.y, v.x);
}
function fracAngle(angle) {
if (arguments.length > 1)
angle = calcAngle(arguments[0], arguments[1]);
return Math.round(angle / FRAC) * FRAC;
}
function calcNewAtomPos(pos0, pos1) {
var v = new Vec2(1, 0).rotate(fracAngle(pos0, pos1));
v.add_(pos0); // eslint-disable-line no-underscore-dangle
return v;
}
function degrees(angle) {
return Math.round(angle / Math.PI * 180);
}
module.exports = {
calcAngle: calcAngle,
fracAngle: fracAngle,
calcNewAtomPos: calcNewAtomPos,
degrees: degrees,
setFracAngle: setFracAngle
};

View File

@ -0,0 +1,128 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var safda = 0;
import 'babel-polyfill';
import 'whatwg-fetch';
import queryString from 'query-string';
import api from './api.js';
import * as molfile from './chem/molfile';
import * as smiles from './chem/smiles';
import * as structformat from './ui/structformat';
import ui from './ui';
import Render from './render';
function getSmiles() {
return smiles.stringify(ketcher.editor.struct(),
{ ignoreErrors: true });
}
function saveSmiles() {
const struct = ketcher.editor.struct();
return structformat.toString(struct, 'smiles', ketcher.server);
}
function getMolfile() {
return molfile.stringify(ketcher.editor.struct(),
{ ignoreErrors: true });
}
function setMolecule(molString) {
if (!(typeof molString === "string"))
return;
ketcher.ui.load(molString, {
rescale: true
});
}
function addFragment(molString) {
if (!(typeof molString === "string"))
return;
ketcher.ui.load(molString, {
rescale: true,
fragment: true
});
}
function showMolfile(clientArea, molString, options) {
const render = new Render(clientArea, Object.assign({
scale: options.bondLength || 75
}, options));
if (molString) {
const mol = molfile.parse(molString);
render.setMolecule(mol);
}
render.update();
// not sure we need to expose guts
return render;
}
// TODO: replace window.onload with something like <https://github.com/ded/domready>
// to start early
window.onload = function () {
var params = queryString.parse(document.location.search);
if (params.api_path)
ketcher.apiPath = params.api_path;
// Url is something similar http://localhost:8080/js/ketcher2/...
// To access server split on "/js/"
var requestUrl = document.location.href.split("/static")[0]
fetch(requestUrl,{
method: "GET",
//async: false,
body: {
getMLServerPath: true
}
}).then(function (response) {
ketcher.api = response.mlServerPath;
}).catch(function (err) {
throw 'Cannot parse result\n' + err;
});
ketcher.server = api(ketcher.apiPath, {
'smart-layout': true,
'ignore-stereochemistry-errors': true,
'mass-skip-error-on-pseudoatoms': false,
'gross-formula-add-rsites': true
});
ketcher.ui = ui(Object.assign({}, params, buildInfo), ketcher.server);
ketcher.editor = global._ui_editor;
ketcher.server.then(function () {
if (params.mol)
ketcher.ui.load(params.mol);
}, function () {
document.title += ' (standalone)';
});
};
const buildInfo = {
version: '__VERSION__',
apiPath: '__API_PATH__',
buildDate: '__BUILD_DATE__',
buildNumber: '__BUILD_NUMBER__' || null,
buildOptions: '__BUILD_OPTIONS__',
miewPath: '__MIEW_PATH__' || null
};
const ketcher = module.exports = Object.assign({
getSmiles,
saveSmiles,
getMolfile,
setMolecule,
addFragment,
showMolfile
}, buildInfo);

View File

@ -0,0 +1,36 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
// Single entry point to Raphaël library
var Raphael = require('raphael');
var Vec2 = require('./util/vec2');
// TODO: refactor ugly prototype extensions to plain old functions
Raphael.el.translateAbs = function (x, y) {
this.delta = this.delta || new Vec2();
this.delta.x += x - 0;
this.delta.y += y - 0;
this.transform('t' + this.delta.x.toString() + ',' + this.delta.y.toString());
};
Raphael.st.translateAbs = function (x, y) {
this.forEach(function (el) {
el.translateAbs(x, y);
});
};
module.exports = Raphael;

View File

@ -0,0 +1,295 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var util = require('./util');
var Vec2 = require('../util/vec2');
var Raphael = require('../raphael-ext');
var tfx = util.tfx;
function arrow(paper, a, b, options) {
var width = 5,
length = 7;
return paper.path('M{0},{1}L{2},{3}L{4},{5}M{2},{3}L{4},{6}', tfx(a.x), tfx(a.y), tfx(b.x), tfx(b.y), tfx(b.x - length), tfx(b.y - width), tfx(b.y + width))
.attr(options.lineattr);
}
function plus(paper, c, options) {
var s = options.scale / 5;
return paper.path('M{0},{4}L{0},{5}M{2},{1}L{3},{1}', tfx(c.x), tfx(c.y), tfx(c.x - s), tfx(c.x + s), tfx(c.y - s), tfx(c.y + s))
.attr(options.lineattr);
}
function bondSingle(paper, hb1, hb2, options) {
var a = hb1.p,
b = hb2.p;
return paper.path(makeStroke(a, b))
.attr(options.lineattr);
}
function bondSingleUp(paper, a, b2, b3, options) { // eslint-disable-line max-params
return paper.path('M{0},{1}L{2},{3}L{4},{5}Z', tfx(a.x), tfx(a.y), tfx(b2.x), tfx(b2.y), tfx(b3.x), tfx(b3.y))
.attr(options.lineattr).attr({ fill: '#000' });
}
function bondSingleStereoBold(paper, a1, a2, a3, a4, options) { // eslint-disable-line max-params
return paper.path('M{0},{1}L{2},{3}L{4},{5}L{6},{7}Z',
tfx(a1.x), tfx(a1.y), tfx(a2.x), tfx(a2.y), tfx(a3.x), tfx(a3.y), tfx(a4.x), tfx(a4.y))
.attr(options.lineattr).attr({
stroke: '#000',
fill: '#000'
});
}
function bondDoubleStereoBold(paper, sgBondPath, b1, b2, options) { // eslint-disable-line max-params
return paper.set([sgBondPath, paper.path('M{0},{1}L{2},{3}', tfx(b1.x), tfx(b1.y), tfx(b2.x), tfx(b2.y))
.attr(options.lineattr)]);
}
function bondSingleDown(paper, hb1, d, nlines, step, options) { // eslint-disable-line max-params
var a = hb1.p,
n = hb1.norm;
var bsp = 0.7 * options.stereoBond;
var path = '',
p,
q,
r;
for (var i = 0; i < nlines; ++i) {
r = a.addScaled(d, step * i);
p = r.addScaled(n, bsp * (i + 0.5) / (nlines - 0.5));
q = r.addScaled(n, -bsp * (i + 0.5) / (nlines - 0.5));
path += makeStroke(p, q);
}
return paper.path(path).attr(options.lineattr);
}
function bondSingleEither(paper, hb1, d, nlines, step, options) { // eslint-disable-line max-params
var a = hb1.p,
n = hb1.norm;
var bsp = 0.7 * options.stereoBond;
var path = 'M' + tfx(a.x) + ',' + tfx(a.y),
r = a;
for (var i = 0; i < nlines; ++i) {
r = a.addScaled(d, step * (i + 0.5)).addScaled(n,
((i & 1) ? -1 : +1) * bsp * (i + 0.5) / (nlines - 0.5));
path += 'L' + tfx(r.x) + ',' + tfx(r.y);
}
return paper.path(path)
.attr(options.lineattr);
}
function bondDouble(paper, a1, a2, b1, b2, cisTrans, options) { // eslint-disable-line max-params
return paper.path(cisTrans ?
'M{0},{1}L{6},{7}M{4},{5}L{2},{3}' :
'M{0},{1}L{2},{3}M{4},{5}L{6},{7}',
tfx(a1.x), tfx(a1.y), tfx(b1.x), tfx(b1.y), tfx(a2.x), tfx(a2.y), tfx(b2.x), tfx(b2.y))
.attr(options.lineattr);
}
function bondSingleOrDouble(paper, hb1, hb2, nSect, options) { // eslint-disable-line max-statements, max-params
var a = hb1.p,
b = hb2.p,
n = hb1.norm;
var bsp = options.bondSpace / 2;
var path = '',
pi,
pp = a;
for (var i = 1; i <= nSect; ++i) {
pi = Vec2.lc2(a, (nSect - i) / nSect, b, i / nSect);
if (i & 1) {
path += makeStroke(pp, pi);
} else {
path += makeStroke(pp.addScaled(n, bsp), pi.addScaled(n, bsp));
path += makeStroke(pp.addScaled(n, -bsp), pi.addScaled(n, -bsp));
}
pp = pi;
}
return paper.path(path)
.attr(options.lineattr);
}
function bondTriple(paper, hb1, hb2, options) {
var a = hb1.p,
b = hb2.p,
n = hb1.norm;
var a2 = a.addScaled(n, options.bondSpace);
var b2 = b.addScaled(n, options.bondSpace);
var a3 = a.addScaled(n, -options.bondSpace);
var b3 = b.addScaled(n, -options.bondSpace);
return paper.path(makeStroke(a, b) + makeStroke(a2, b2) + makeStroke(a3, b3))
.attr(options.lineattr);
}
function bondAromatic(paper, paths, bondShift, options) {
var l1 = paper.path(paths[0]).attr(options.lineattr);
var l2 = paper.path(paths[1]).attr(options.lineattr);
if (bondShift !== undefined && bondShift !== null)
(bondShift > 0 ? l1 : l2).attr({ 'stroke-dasharray': '- ' });
return paper.set([l1, l2]);
}
function bondAny(paper, hb1, hb2, options) {
var a = hb1.p,
b = hb2.p;
return paper.path(makeStroke(a, b))
.attr(options.lineattr).attr({ 'stroke-dasharray': '- ' });
}
function reactingCenter(paper, p, options) {
var pathdesc = '';
for (var i = 0; i < p.length / 2; ++i)
pathdesc += makeStroke(p[2 * i], p[(2 * i) + 1]);
return paper.path(pathdesc).attr(options.lineattr);
}
function topologyMark(paper, p, mark, options) {
var path = paper.text(p.x, p.y, mark)
.attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': '#000'
});
var rbb = util.relBox(path.getBBox());
recenterText(path, rbb);
return path;
}
function radicalCap(paper, p, options) {
var s = options.lineWidth * 0.9;
var dw = s,
dh = 2 * s;
return paper.path('M{0},{1}L{2},{3}L{4},{5}',
tfx(p.x - dw), tfx(p.y + dh), tfx(p.x), tfx(p.y), tfx(p.x + dw), tfx(p.y + dh))
.attr({
'stroke': '#000',
'stroke-width': options.lineWidth * 0.7,
'stroke-linecap': 'square',
'stroke-linejoin': 'miter'
});
}
function radicalBullet(paper, p, options) {
return paper.circle(tfx(p.x), tfx(p.y), options.lineWidth)
.attr({
stroke: null,
fill: '#000'
});
}
function bracket(paper, d, n, c, bracketWidth, bracketHeight, options) { // eslint-disable-line max-params
bracketWidth = bracketWidth || 0.25;
bracketHeight = bracketHeight || 1.0;
var a0 = c.addScaled(n, -0.5 * bracketHeight);
var a1 = c.addScaled(n, 0.5 * bracketHeight);
var b0 = a0.addScaled(d, -bracketWidth);
var b1 = a1.addScaled(d, -bracketWidth);
return paper.path('M{0},{1}L{2},{3}L{4},{5}L{6},{7}',
tfx(b0.x), tfx(b0.y), tfx(a0.x), tfx(a0.y),
tfx(a1.x), tfx(a1.y), tfx(b1.x), tfx(b1.y))
.attr(options.sgroupBracketStyle);
}
function selectionRectangle(paper, p0, p1, options) {
return paper.rect(tfx(Math.min(p0.x, p1.x)),
tfx(Math.min(p0.y, p1.y)),
tfx(Math.abs(p1.x - p0.x)),
tfx(Math.abs(p1.y - p0.y))).attr(options.lassoStyle);
}
function selectionPolygon(paper, r, options) {
var v = r[r.length - 1];
var pstr = 'M' + tfx(v.x) + ',' + tfx(v.y);
for (var i = 0; i < r.length; ++i)
pstr += 'L' + tfx(r[i].x) + ',' + tfx(r[i].y);
return paper.path(pstr).attr(options.lassoStyle);
}
function selectionLine(paper, p0, p1, options) {
return paper.path(makeStroke(p0, p1)).attr(options.lassoStyle);
}
function makeStroke(a, b) {
return 'M' + tfx(a.x) + ',' + tfx(a.y) +
'L' + tfx(b.x) + ',' + tfx(b.y) + ' ';
}
function dashedPath(p0, p1, dash) {
var t0 = 0;
var t1 = Vec2.dist(p0, p1);
var d = Vec2.diff(p1, p0).normalized();
var black = true;
var path = '';
var i = 0;
while (t0 < t1) {
var len = dash[i % dash.length];
var t2 = t0 + Math.min(len, t1 - t0);
if (black)
path += 'M ' + p0.addScaled(d, t0).coordStr() + ' L ' + p0.addScaled(d, t2).coordStr();
t0 += len;
black = !black;
i++;
}
return path;
}
function aromaticBondPaths(a2, a3, b2, b3, mask, dash) { // eslint-disable-line max-params
var l1 = dash && (mask & 1) ? dashedPath(a2, b2, dash) : makeStroke(a2, b2);
var l2 = dash && (mask & 2) ? dashedPath(a3, b3, dash) : makeStroke(a3, b3);
return [l1, l2];
}
function recenterText(path, rbb) {
// TODO: find a better way
if (Raphael.vml) { // dirty hack
console.assert(null, "Souldn't go here!");
var gap = rbb.height * 0.16;
path.translateAbs(0, gap);
rbb.y += gap;
}
}
module.exports = {
recenterText: recenterText,
arrow: arrow,
plus: plus,
aromaticBondPaths: aromaticBondPaths,
bondSingle: bondSingle,
bondSingleUp: bondSingleUp,
bondSingleStereoBold: bondSingleStereoBold,
bondDoubleStereoBold: bondDoubleStereoBold,
bondSingleDown: bondSingleDown,
bondSingleEither: bondSingleEither,
bondDouble: bondDouble,
bondSingleOrDouble: bondSingleOrDouble,
bondTriple: bondTriple,
bondAromatic: bondAromatic,
bondAny: bondAny,
reactingCenter: reactingCenter,
topologyMark: topologyMark,
radicalCap: radicalCap,
radicalBullet: radicalBullet,
bracket: bracket,
selectionRectangle: selectionRectangle,
selectionPolygon: selectionPolygon,
selectionLine: selectionLine
};

View File

@ -0,0 +1,210 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Raphael = require('../raphael-ext');
var Box2Abs = require('../util/box2abs');
var Vec2 = require('../util/vec2');
var scale = require('../util/scale');
var Struct = require('../chem/struct');
var ReStruct = require('./restruct');
var defaultOptions = require('./options');
var DEBUG = { debug: false, logcnt: 0, logmouse: false, hl: false };
DEBUG.logMethod = function () { };
// DEBUG.logMethod = function (method) {addionalAtoms("METHOD: " + method);
function Render(clientArea, opt) {
this.userOpts = opt;
this.clientArea = clientArea;
this.paper = new Raphael(clientArea, 0, 0);
this.sz = Vec2.ZERO;
this.ctab = new ReStruct(new Struct(), this);
this.options = defaultOptions(this.userOpts);
}
Render.prototype.view2obj = function (p, isRelative) {
var scroll = this.scrollPos();
if (!this.useOldZoom) {
p = p.scaled(1 / this.options.zoom);
scroll = scroll.scaled(1 / this.options.zoom);
}
p = isRelative ? p : p.add(scroll).sub(this.options.offset);
return scale.scaled2obj(p, this.options);
};
Render.prototype.obj2view = function (v, isRelative) {
var p = scale.obj2scaled(v, this.options);
p = isRelative ? p : p.add(this.options.offset).sub(this.scrollPos().scaled(1 / this.options.zoom));
if (!this.useOldZoom)
p = p.scaled(this.options.zoom);
return p;
};
Render.prototype.scrollPos = function () {
return new Vec2(this.clientArea.scrollLeft, this.clientArea.scrollTop);
};
function cumulativeOffset(el) {
var curtop = 0;
var curleft = 0;
if (el.parentNode) {
do {
curtop += el.offsetTop || 0;
curleft += el.offsetLeft || 0;
el = el.offsetParent;
} while (el);
}
return { left: curleft, top: curtop };
}
Render.prototype.page2obj = function (pagePos) {
var offset = cumulativeOffset(this.clientArea);
var pp = new Vec2(pagePos.pageX - offset.left, pagePos.pageY - offset.top);
return this.view2obj(pp);
};
Render.prototype.setPaperSize = function (sz) {
DEBUG.logMethod('setPaperSize');
this.sz = sz;
this.paper.setSize(sz.x * this.options.zoom, sz.y * this.options.zoom);
this.setViewBox(this.options.zoom);
};
Render.prototype.setOffset = function (newoffset) {
DEBUG.logMethod('setOffset');
var delta = new Vec2(newoffset.x - this.options.offset.x, newoffset.y - this.options.offset.y);
this.clientArea.scrollLeft += delta.x;
this.clientArea.scrollTop += delta.y;
this.options.offset = newoffset;
};
Render.prototype.setZoom = function (zoom) {
// when scaling the canvas down it may happen that the scaled canvas is smaller than the view window
// don't forget to call setScrollOffset after zooming (or use extendCanvas directly)
console.info('set zoom', zoom);
this.options.zoom = zoom;
this.paper.setSize(this.sz.x * zoom, this.sz.y * zoom);
this.setViewBox(zoom);
};
function calcExtend(sSz, x0, y0, x1, y1) { // eslint-disable-line max-params
var ex = (x0 < 0) ? -x0 : 0;
var ey = (y0 < 0) ? -y0 : 0;
if (sSz.x < x1)
ex += x1 - sSz.x;
if (sSz.y < y1)
ey += y1 - sSz.y;
return new Vec2(ex, ey);
}
Render.prototype.setScrollOffset = function (x, y) {
var clientArea = this.clientArea;
var cx = clientArea.clientWidth;
var cy = clientArea.clientHeight;
var e = calcExtend(this.sz.scaled(this.options.zoom), x, y,
cx + x, cy + y).scaled(1 / this.options.zoom);
if (e.x > 0 || e.y > 0) {
this.setPaperSize(this.sz.add(e));
var d = new Vec2((x < 0) ? -x : 0,
(y < 0) ? -y : 0).scaled(1 / this.options.zoom);
if (d.x > 0 || d.y > 0) {
this.ctab.translate(d);
this.setOffset(this.options.offset.add(d));
}
}
clientArea.scrollLeft = x;
clientArea.scrollTop = y;
// TODO: store drag position in scaled systems
// scrollLeft = clientArea.scrollLeft;
// scrollTop = clientArea.scrollTop;
this.update(false);
};
Render.prototype.setScale = function (z) {
if (this.options.offset)
this.options.offset = this.options.offset.scaled(1 / z).scaled(z);
this.userOpts.scale *= z;
this.options = null;
this.update(true);
};
Render.prototype.setViewBox = function (z) {
if (!this.useOldZoom)
this.paper.canvas.setAttribute('viewBox', '0 0 ' + this.sz.x + ' ' + this.sz.y);
else
this.setScale(z);
};
Render.prototype.setMolecule = function (ctab) {
debugger;
DEBUG.logMethod('setMolecule');
this.paper.clear();
this.ctab = new ReStruct(ctab, this);
this.options.offset = new Vec2();
this.update(false);
};
Render.prototype.update = function (force, viewSz) { // eslint-disable-line max-statements
viewSz = viewSz || new Vec2(this.clientArea.clientWidth || 100,
this.clientArea.clientHeight || 100);
var changes = this.ctab.update(force);
this.ctab.setSelection(); // [MK] redraw the selection bits where necessary
if (changes) {
var sf = this.options.scale;
var bb = this.ctab.getVBoxObj().transform(scale.obj2scaled, this.options).translate(this.options.offset || new Vec2());
if (!this.options.autoScale) {
var ext = Vec2.UNIT.scaled(sf);
var eb = bb.sz().length() > 0 ? bb.extend(ext, ext) : bb;
var vb = new Box2Abs(this.scrollPos(), viewSz.scaled(1 / this.options.zoom).sub(Vec2.UNIT.scaled(20)));
var cb = Box2Abs.union(vb, eb);
if (!this.oldCb)
this.oldCb = new Box2Abs();
var sz = cb.sz().floor();
var delta = this.oldCb.p0.sub(cb.p0).ceil();
this.oldBb = bb;
if (!this.sz || sz.x != this.sz.x || sz.y != this.sz.y)
this.setPaperSize(sz);
this.options.offset = this.options.offset || new Vec2();
if (delta.x != 0 || delta.y != 0) {
this.setOffset(this.options.offset.add(delta));
this.ctab.translate(delta);
}
} else {
var sz1 = bb.sz();
var marg = this.options.autoScaleMargin;
var mv = new Vec2(marg, marg);
var csz = viewSz;
if (csz.x < (2 * marg) + 1 || csz.y < (2 * marg) + 1)
throw new Error('View box too small for the given margin');
var rescale = Math.max(sz1.x / (csz.x - (2 * marg)), sz1.y / (csz.y - (2 * marg)));
if (this.options.maxBondLength / rescale > 1.0)
rescale = 1.0;
var sz2 = sz1.add(mv.scaled(2 * rescale));
/* eslint-disable no-mixed-operators*/
this.paper.setViewBox(bb.pos().x - marg * rescale - (csz.x * rescale - sz2.x) / 2, bb.pos().y - marg * rescale - (csz.y * rescale - sz2.y) / 2, csz.x * rescale, csz.y * rescale);
/* eslint-enable no-mixed-operators*/
}
}
};
module.exports = Render;

View File

@ -0,0 +1,96 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var utils = require('../editor/tool/utils');
var Vec2 = require('../util/vec2');
function defaultOptions(opt) {
const scaleFactor = opt.scale || 100;
if (opt.rotationStep)
utils.setFracAngle(opt.rotationStep);
const labelFontSize = Math.ceil(1.9 * (scaleFactor / 6));
const subFontSize = Math.ceil(0.7 * labelFontSize);
const defaultOptions = {
// flags for debugging
showAtomIds: false,
showBondIds: false,
showHalfBondIds: false,
showLoopIds: false,
// rendering customization flags
hideChiralFlag: false,
showValenceWarnings: true,
autoScale: false, // scale structure to fit into the given view box, used in view mode
autoScaleMargin: 0,
maxBondLength: 0, // 0 stands for "not specified"
atomColoring: true,
hideImplicitHydrogen: false,
hideTerminalLabels: false,
// atoms
carbonExplicitly: false,
showCharge: true,
showHydrogenLabels: 'on',
showValence: true,
// bonds
aromaticCircle: true,
scale: scaleFactor,
zoom: 1.0,
offset: new Vec2(),
lineWidth: scaleFactor / 20,
bondSpace: opt.doubleBondWidth || scaleFactor / 7,
stereoBond: opt.stereoBondWidth || scaleFactor / 7,
subFontSize: subFontSize,
font: '30px Arial',
fontsz: labelFontSize,
fontszsub: subFontSize,
fontRLabel: labelFontSize * 1.2,
fontRLogic: labelFontSize * 0.7,
/* styles */
lineattr: {
'stroke': '#000',
'stroke-width': opt.bondThickness || scaleFactor / 20,
'stroke-linecap': 'round',
'stroke-linejoin': 'round'
},
/* eslint-enable quote-props */
selectionStyle: {
fill: '#7f7',
stroke: 'none'
},
highlightStyle: {
'stroke': '#0c0',
'stroke-width': 0.6 * scaleFactor / 20
},
sgroupBracketStyle: {
'stroke': 'darkgray',
'stroke-width': 0.5 * scaleFactor / 20
},
lassoStyle: {
'stroke': 'gray',
'stroke-width': '1px'
},
atomSelectionPlateRadius: labelFontSize * 1.2
};
return Object.assign({}, defaultOptions, opt);
}
module.exports = defaultOptions;

View File

@ -0,0 +1,642 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
// ReStruct is to store all the auxiliary information for
// Struct while rendering
var Box2Abs = require('../../util/box2abs');
var Map = require('../../util/map');
var Pool = require('../../util/pool');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var scale = require('../../util/scale');
var util = require('../util');
var Struct = require('../../chem/struct');
var ReAtom = require('./reatom');
var ReBond = require('./rebond');
var ReRxnPlus = require('./rerxnplus');
var ReRxnArrow = require('./rerxnarrow');
var ReFrag = require('./refrag');
var ReRGroup = require('./rergroup');
var ReDataSGroupData = require('./redatasgroupdata');
var ReChiralFlag = require('./rechiralflag');
var ReSGroup = require('./resgroup');
var ReLoop = require('./reloop');
var LAYER_MAP = {
background: 0,
selectionPlate: 1,
highlighting: 2,
warnings: 3,
data: 4,
indices: 5
};
function ReStruct(molecule, render) { // eslint-disable-line max-statements
this.render = render;
this.atoms = new Map();
this.bonds = new Map();
this.reloops = new Map();
this.rxnPluses = new Map();
this.rxnArrows = new Map();
this.frags = new Map();
this.rgroups = new Map();
this.sgroups = new Map();
this.sgroupData = new Map();
this.chiralFlags = new Map();
this.molecule = molecule || new Struct();
this.initialized = false;
this.layers = [];
this.initLayers();
this.connectedComponents = new Pool();
this.ccFragmentType = new Map();
for (var map in ReStruct.maps)
this[map + 'Changed'] = {};
this.structChanged = false;
// TODO: eachItem ?
molecule.atoms.each(function (aid, atom) {
this.atoms.set(aid, new ReAtom(atom));
}, this);
molecule.bonds.each(function (bid, bond) {
this.bonds.set(bid, new ReBond(bond));
}, this);
molecule.loops.each(function (lid, loop) {
this.reloops.set(lid, new ReLoop(loop));
}, this);
molecule.rxnPluses.each(function (id, item) {
this.rxnPluses.set(id, new ReRxnPlus(item));
}, this);
molecule.rxnArrows.each(function (id, item) {
this.rxnArrows.set(id, new ReRxnArrow(item));
}, this);
molecule.frags.each(function (id, item) {
this.frags.set(id, new ReFrag(item));
}, this);
molecule.rgroups.each(function (id, item) {
this.rgroups.set(id, new ReRGroup(item));
}, this);
molecule.sgroups.each(function (id, item) {
this.sgroups.set(id, new ReSGroup(item));
if (item.type === 'DAT' && !item.data.attached)
this.sgroupData.set(id, new ReDataSGroupData(item)); // [MK] sort of a hack, we use the SGroup id for the data field id
}, this);
if (molecule.isChiral) {
var bb = molecule.getCoordBoundingBox();
this.chiralFlags.set(0, new ReChiralFlag(new Vec2(bb.max.x, bb.min.y - 1)));
}
}
ReStruct.prototype.connectedComponentRemoveAtom = function (aid, atom) {
atom = atom || this.atoms.get(aid);
if (atom.component < 0)
return;
var cc = this.connectedComponents.get(atom.component);
Set.remove(cc, aid);
if (Set.size(cc) < 1)
this.connectedComponents.remove(atom.component);
atom.component = -1;
};
ReStruct.prototype.clearConnectedComponents = function () {
this.connectedComponents.clear();
this.atoms.each(function (aid, atom) {
atom.component = -1;
});
};
ReStruct.prototype.getConnectedComponent = function (aid, adjacentComponents) {
var list = (typeof (aid['length']) === 'number') ? [].slice.call(aid) : [aid];
var ids = Set.empty();
while (list.length > 0) {
(function () {
var aid = list.pop();
Set.add(ids, aid);
var atom = this.atoms.get(aid);
if (atom.component >= 0)
Set.add(adjacentComponents, atom.component);
for (var i = 0; i < atom.a.neighbors.length; ++i) {
var neiId = this.molecule.halfBonds.get(atom.a.neighbors[i]).end;
if (!Set.contains(ids, neiId))
list.push(neiId);
}
}).apply(this);
}
return ids;
};
ReStruct.prototype.addConnectedComponent = function (ids) {
var compId = this.connectedComponents.add(ids);
var adjacentComponents = Set.empty();
var atomIds = this.getConnectedComponent(Set.list(ids), adjacentComponents);
Set.remove(adjacentComponents, compId);
var type = -1;
Set.each(atomIds, function (aid) {
var atom = this.atoms.get(aid);
atom.component = compId;
if (atom.a.rxnFragmentType != -1) {
if (type != -1 && atom.a.rxnFragmentType != type)
throw new Error('reaction fragment type mismatch');
type = atom.a.rxnFragmentType;
}
}, this);
this.ccFragmentType.set(compId, type);
return compId;
};
ReStruct.prototype.removeConnectedComponent = function (ccid) {
Set.each(this.connectedComponents.get(ccid), function (aid) {
this.atoms.get(aid).component = -1;
}, this);
return this.connectedComponents.remove(ccid);
};
// TODO: remove? not used
ReStruct.prototype.connectedComponentMergeIn = function (ccid, set) {
Set.each(set, function (aid) {
this.atoms.get(aid).component = ccid;
}, this);
Set.mergeIn(this.connectedComponents.get(ccid), set);
};
ReStruct.prototype.assignConnectedComponents = function () {
this.atoms.each(function (aid, atom) {
if (atom.component >= 0)
return;
var adjacentComponents = Set.empty();
var ids = this.getConnectedComponent(aid, adjacentComponents);
Set.each(adjacentComponents, function (ccid) {
this.removeConnectedComponent(ccid);
}, this);
this.addConnectedComponent(ids);
}, this);
};
// TODO: remove? not used
ReStruct.prototype.connectedComponentGetBoundingBox = function (ccid, cc, bb) {
cc = cc || this.connectedComponents.get(ccid);
bb = bb || { min: null, max: null };
Set.each(cc, function (aid) {
var ps = scale.obj2scaled(this.atoms.get(aid).a.pp, this.render.options);
if (bb.min == null) {
bb.min = bb.max = ps;
} else {
bb.min = bb.min.min(ps);
bb.max = bb.max.max(ps);
}
}, this);
return bb;
};
ReStruct.prototype.initLayers = function () {
for (var group in LAYER_MAP) {
this.layers[LAYER_MAP[group]] =
this.render.paper.rect(0, 0, 10, 10)
.attr({
class: group + 'Layer',
fill: '#000',
opacity: '0.0'
}).toFront();
}
};
ReStruct.prototype.addReObjectPath = function (group, visel, path, pos, visible) { // eslint-disable-line max-params
if (!path || !this.layers[LAYER_MAP[group]].node.parentNode)
return;
var offset = this.render.options.offset;
var bb = visible ? Box2Abs.fromRelBox(util.relBox(path.getBBox())) : null;
var ext = pos && bb ? bb.translate(pos.negated()) : null;
if (offset !== null) {
path.translateAbs(offset.x, offset.y);
bb = bb ? bb.translate(offset) : null;
}
visel.add(path, bb, ext);
path.insertBefore(this.layers[LAYER_MAP[group]]);
};
ReStruct.prototype.clearMarks = function () {
for (var map in ReStruct.maps)
this[map + 'Changed'] = {};
this.structChanged = false;
};
ReStruct.prototype.markItemRemoved = function () {
this.structChanged = true;
};
ReStruct.prototype.markBond = function (bid, mark) {
this.markItem('bonds', bid, mark);
};
ReStruct.prototype.markAtom = function (aid, mark) {
this.markItem('atoms', aid, mark);
};
ReStruct.prototype.markItem = function (map, id, mark) {
var mapChanged = this[map + 'Changed'];
mapChanged[id] = (typeof (mapChanged[id]) !== 'undefined') ?
Math.max(mark, mapChanged[id]) : mark;
if (this[map].has(id))
this.clearVisel(this[map].get(id).visel);
};
ReStruct.prototype.clearVisel = function (visel) {
for (var i = 0; i < visel.paths.length; ++i)
visel.paths[i].remove();
visel.clear();
};
ReStruct.prototype.eachItem = function (func, context) {
for (var map in ReStruct.maps) {
this[map].each(function (id, item) {
func.call(context, item);
});
}
};
ReStruct.prototype.getVBoxObj = function (selection) {
selection = selection || {};
if (isSelectionEmpty(selection)) {
for (var map in ReStruct.maps)
if (ReStruct.maps.hasOwnProperty(map)) selection[map] = this[map].keys();
}
var vbox = null;
for (map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map) && selection[map]) {
selection[map].forEach(function (id) {
var box = this[map].get(id).getVBoxObj(this.render);
if (box)
vbox = vbox ? Box2Abs.union(vbox, box) : box.clone();
}, this);
}
}
vbox = vbox || new Box2Abs(0, 0, 0, 0);
return vbox;
};
function isSelectionEmpty(selection) {
if (selection) {
for (var map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map) && selection[map] && selection[map].length > 0)
return false;
}
}
return true;
}
ReStruct.prototype.translate = function (d) {
this.eachItem(function (item) {
item.visel.translate(d);
});
};
ReStruct.prototype.scale = function (s) {
// NOTE: bounding boxes are not valid after scaling
this.eachItem(function (item) {
scaleVisel(item.visel, s);
});
};
function scaleRPath(path, s) {
if (path.type == 'set') { // TODO: rework scaling
for (var i = 0; i < path.length; ++i)
scaleRPath(path[i], s);
} else {
if (!(typeof path.attrs === 'undefined')) {
if ('font-size' in path.attrs)
path.attr('font-size', path.attrs['font-size'] * s);
else if ('stroke-width' in path.attrs)
path.attr('stroke-width', path.attrs['stroke-width'] * s);
}
path.scale(s, s, 0, 0);
}
}
function scaleVisel(visel, s) {
for (var i = 0; i < visel.paths.length; ++i)
scaleRPath(visel.paths[i], s);
}
ReStruct.prototype.clearVisels = function () {
this.eachItem(function (item) {
this.clearVisel(item.visel);
}, this);
};
ReStruct.prototype.update = function (force) { // eslint-disable-line max-statements
force = force || !this.initialized;
// check items to update
var id, map, mapChanged;
if (force) {
for (map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map)) {
mapChanged = this[map + 'Changed'];
this[map].each(function (id) {
mapChanged[id] = 1;
}, this);
}
}
} else {
// check if some of the items marked are already gone
for (map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map)) {
mapChanged = this[map + 'Changed'];
for (id in mapChanged) {
if (!this[map].has(id)) // eslint-disable-line max-depth
delete mapChanged[id];
}
}
}
}
for (id in this.atomsChanged)
this.connectedComponentRemoveAtom(id);
// clean up empty fragments
// TODO: fragment removal should be triggered by the action responsible for the fragment contents removal and form an operation of its own
var emptyFrags = this.frags.findAll(function (fid, frag) {
return !frag.calcBBox(this.render.ctab, fid, this.render);
}, this);
for (var j = 0; j < emptyFrags.length; ++j) {
var fid = emptyFrags[j];
this.clearVisel(this.frags.get(fid).visel);
this.frags.unset(fid);
this.molecule.frags.remove(fid);
}
for (map in ReStruct.maps) {
mapChanged = this[map + 'Changed'];
for (id in mapChanged) {
this.clearVisel(this[map].get(id).visel);
this.structChanged |= mapChanged[id] > 0;
}
}
// TODO: when to update sgroup?
this.sgroups.each(function (sid, sgroup) {
this.clearVisel(sgroup.visel);
sgroup.highlighting = null;
sgroup.selectionPlate = null;
}, this);
// TODO [RB] need to implement update-on-demand for fragments and r-groups
this.frags.each(function (frid, frag) {
this.clearVisel(frag.visel);
}, this);
this.rgroups.each(function (rgid, rgroup) {
this.clearVisel(rgroup.visel);
}, this);
if (force) { // clear and recreate all half-bonds
this.clearConnectedComponents();
this.molecule.initHalfBonds();
this.molecule.initNeighbors();
}
// only update half-bonds adjacent to atoms that have moved
this.molecule.updateHalfBonds(new Map(this.atomsChanged).findAll(function (aid, status) {
return status >= 0;
}, this));
this.molecule.sortNeighbors(new Map(this.atomsChanged).findAll(function (aid, status) {
return status >= 1;
}, this));
this.assignConnectedComponents();
this.setImplicitHydrogen();
this.initialized = true;
this.verifyLoops();
var updLoops = force || this.structChanged;
if (updLoops)
this.updateLoops();
this.showLabels();
this.showBonds();
if (updLoops)
this.showLoops();
this.showReactionSymbols();
this.showSGroups();
this.showFragments();
this.showRGroups();
if (this.render.options.hideChiralFlag !== true) {
this.chiralFlags.each(function (id, item) {
item.show(this, id, this.render.options);
}, this);
}
this.clearMarks();
return true;
};
ReStruct.prototype.updateLoops = function () {
this.reloops.each(function (rlid, reloop) {
this.clearVisel(reloop.visel);
}, this);
var ret = this.molecule.findLoops();
ret.bondsToMark.forEach(function (bid) {
this.markBond(bid, 1);
}, this);
ret.newLoops.forEach(function (loopId) {
this.reloops.set(loopId, new ReLoop(this.molecule.loops.get(loopId)));
}, this);
};
ReStruct.prototype.showLoops = function () {
var options = this.render.options;
this.reloops.each(function (rlid, reloop) {
reloop.show(this, rlid, options);
}, this);
};
ReStruct.prototype.showReactionSymbols = function () {
var options = this.render.options;
var item;
var id;
for (id in this.rxnArrowsChanged) {
item = this.rxnArrows.get(id);
item.show(this, id, options);
}
for (id in this.rxnPlusesChanged) {
item = this.rxnPluses.get(id);
item.show(this, id, options);
}
};
ReStruct.prototype.showSGroups = function () {
var options = this.render.options;
this.molecule.sGroupForest.getSGroupsBFS().reverse().forEach(function (id) {
var resgroup = this.sgroups.get(id);
resgroup.show(this, id, options);
}, this);
};
ReStruct.prototype.showFragments = function () {
this.frags.each(function (id, frag) {
var path = frag.draw(this.render, id);
if (path) this.addReObjectPath('data', frag.visel, path, null, true);
// TODO fragment selection & highlighting
}, this);
};
ReStruct.prototype.showRGroups = function () {
var options = this.render.options;
this.rgroups.each(function (id, rgroup) {
rgroup.show(this, id, options);
}, this);
};
ReStruct.prototype.eachCC = function (func, type, context) {
this.connectedComponents.each(function (ccid, cc) {
if (!type || this.ccFragmentType.get(ccid) == type)
func.call(context || this, ccid, cc);
}, this);
};
// TODO: remove? not used
ReStruct.prototype.getGroupBB = function (type) {
var bb = { min: null, max: null };
this.eachCC(function (ccid, cc) {
bb = this.connectedComponentGetBoundingBox(ccid, cc, bb);
}, type, this);
return bb;
};
ReStruct.prototype.setImplicitHydrogen = function () {
// calculate implicit hydrogens for atoms that have been modified
this.molecule.setImplicitHydrogen(Object.keys(this.atomsChanged));
};
ReStruct.prototype.loopRemove = function (loopId) {
if (!this.reloops.has(loopId))
return;
var reloop = this.reloops.get(loopId);
this.clearVisel(reloop.visel);
var bondlist = [];
for (var i = 0; i < reloop.loop.hbs.length; ++i) {
var hbid = reloop.loop.hbs[i];
if (this.molecule.halfBonds.has(hbid)) {
var hb = this.molecule.halfBonds.get(hbid);
hb.loop = -1;
this.markBond(hb.bid, 1);
this.markAtom(hb.begin, 1);
bondlist.push(hb.bid);
}
}
this.reloops.unset(loopId);
this.molecule.loops.remove(loopId);
};
ReStruct.prototype.verifyLoops = function () {
var toRemove = [];
this.reloops.each(function (rlid, reloop) {
if (!reloop.isValid(this.molecule, rlid))
toRemove.push(rlid);
}, this);
for (var i = 0; i < toRemove.length; ++i)
this.loopRemove(toRemove[i]);
};
ReStruct.prototype.showLabels = function () { // eslint-disable-line max-statements
var options = this.render.options;
for (var aid in this.atomsChanged) {
var atom = this.atoms.get(aid);
atom.show(this, aid, options);
}
};
ReStruct.prototype.showBonds = function () { // eslint-disable-line max-statements
var options = this.render.options;
for (var bid in this.bondsChanged) {
var bond = this.bonds.get(bid);
bond.show(this, bid, options);
}
};
ReStruct.prototype.setSelection = function (selection) {
var redraw = (arguments.length === 0); // render.update only
for (var map in ReStruct.maps) {
if (ReStruct.maps.hasOwnProperty(map) && ReStruct.maps[map].isSelectable()) {
this[map].each(function (id, item) {
var selected = redraw ? item.selected :
selection && selection[map] && selection[map].indexOf(id) > -1;
this.showItemSelection(item, selected);
}, this);
}
}
};
ReStruct.prototype.showItemSelection = function (item, selected) {
var exists = item.selectionPlate != null && !item.selectionPlate.removed;
// TODO: simplify me, who sets `removed`?
item.selected = selected;
if (item instanceof ReDataSGroupData) item.sgroup.selected = selected;
if (selected) {
if (!exists) {
var render = this.render;
var options = render.options;
var paper = render.paper;
item.selectionPlate = item.makeSelectionPlate(this, paper, options);
this.addReObjectPath('selectionPlate', item.visel, item.selectionPlate);
}
if (item.selectionPlate)
item.selectionPlate.show(); // TODO [RB] review
} else
if (exists && item.selectionPlate) {
item.selectionPlate.hide(); // TODO [RB] review
}
};
ReStruct.maps = {
atoms: ReAtom,
bonds: ReBond,
rxnPluses: ReRxnPlus,
rxnArrows: ReRxnArrow,
frags: ReFrag,
rgroups: ReRGroup,
sgroupData: ReDataSGroupData,
chiralFlags: ReChiralFlag,
sgroups: ReSGroup,
reloops: ReLoop
};
module.exports = Object.assign(ReStruct, {
Atom: ReAtom,
Bond: ReBond,
RxnPlus: ReRxnPlus,
RxnArrow: ReRxnArrow,
Frag: ReFrag,
RGroup: ReRGroup,
ChiralFlag: ReChiralFlag,
SGroup: ReSGroup
});

View File

@ -0,0 +1,647 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Box2Abs = require('../../util/box2abs');
var ReObject = require('./reobject');
var scale = require('../../util/scale');
var element = require('../../chem/element');
var draw = require('../draw');
var util = require('../util');
var Vec2 = require('../../util/vec2');
var Struct = require('../../chem/struct');
function ReAtom(/* chem.Atom*/atom) {
this.init('atom');
this.a = atom; // TODO rename a to item
this.showLabel = false;
this.hydrogenOnTheLeft = false;
this.color = '#000000';
this.component = -1;
}
ReAtom.prototype = new ReObject();
ReAtom.isSelectable = function () {
return true;
};
ReAtom.prototype.getVBoxObj = function (render) {
if (this.visel.boundingBox)
return ReObject.prototype.getVBoxObj.call(this, render);
return new Box2Abs(this.a.pp, this.a.pp);
};
ReAtom.prototype.drawHighlight = function (render) {
var ret = this.makeHighlightPlate(render);
render.ctab.addReObjectPath('highlighting', this.visel, ret);
return ret;
};
ReAtom.prototype.makeHighlightPlate = function (render) {
var paper = render.paper;
var options = render.options;
var ps = scale.obj2scaled(this.a.pp, options);
return paper.circle(ps.x, ps.y, options.atomSelectionPlateRadius)
.attr(options.highlightStyle);
};
ReAtom.prototype.makeSelectionPlate = function (restruct, paper, styles) {
var ps = scale.obj2scaled(this.a.pp, restruct.render.options);
return paper.circle(ps.x, ps.y, styles.atomSelectionPlateRadius)
.attr(styles.selectionStyle);
};
ReAtom.prototype.show = function (restruct, aid, options) { // eslint-disable-line max-statements
var render = restruct.render;
var ps = scale.obj2scaled(this.a.pp, render.options);
this.hydrogenOnTheLeft = setHydrogenPos(restruct.molecule, this);
this.showLabel = labelIsVisible(restruct, render.options, this);
if (this.showLabel) {
var label = buildLabel(this, render.paper, ps, options);
var delta = 0.5 * options.lineWidth;
var rightMargin = label.rbb.width / 2;
var leftMargin = -label.rbb.width / 2;
var implh = Math.floor(this.a.implicitH);
var isHydrogen = label.text === 'H';
restruct.addReObjectPath('data', this.visel, label.path, ps, true);
var index = null;
if (options.showAtomIds) {
index = {};
index.text = aid.toString();
index.path = render.paper.text(ps.x, ps.y, index.text)
.attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': '#070'
});
index.rbb = util.relBox(index.path.getBBox());
draw.recenterText(index.path, index.rbb);
restruct.addReObjectPath('indices', this.visel, index.path, ps);
}
this.setHighlight(this.highlight, render);
if (this.a.alias || this.a.pseudo) return;
var hydroIndex = null;
if (isHydrogen && implh > 0) {
hydroIndex = showHydroIndex(this, render, implh, rightMargin);
rightMargin += hydroIndex.rbb.width + delta;
restruct.addReObjectPath('data', this.visel, hydroIndex.path, ps, true);
}
if (this.a.radical != 0) {
var radical = showRadical(this, render);
restruct.addReObjectPath('data', this.visel, radical.path, ps, true);
}
if (this.a.isotope != 0) {
var isotope = showIsotope(this, render, leftMargin);
leftMargin -= isotope.rbb.width + delta;
restruct.addReObjectPath('data', this.visel, isotope.path, ps, true);
}
if (!isHydrogen && implh > 0 && displayHydrogen(options.showHydrogenLabels, this)) {
var data = showHydrogen(this, render, implh, {
hydrogen: {},
hydroIndex: hydroIndex,
rightMargin: rightMargin,
leftMargin: leftMargin
});
var hydrogen = data.hydrogen;
hydroIndex = data.hydroIndex;
rightMargin = data.rightMargin;
leftMargin = data.leftMargin;
restruct.addReObjectPath('data', this.visel, hydrogen.path, ps, true);
if (hydroIndex != null)
restruct.addReObjectPath('data', this.visel, hydroIndex.path, ps, true);
}
if (this.a.charge != 0 && options.showCharge) {
var charge = showCharge(this, render, rightMargin);
rightMargin += charge.rbb.width + delta;
restruct.addReObjectPath('data', this.visel, charge.path, ps, true);
}
if (this.a.explicitValence >= 0 && options.showValence) {
var valence = showExplicitValence(this, render, rightMargin);
rightMargin += valence.rbb.width + delta;
restruct.addReObjectPath('data', this.visel, valence.path, ps, true);
}
if (this.a.badConn && options.showValenceWarnings) {
var warning = showWarning(this, render, leftMargin, rightMargin);
restruct.addReObjectPath('warnings', this.visel, warning.path, ps, true);
}
if (index) {
/* eslint-disable no-mixed-operators*/
pathAndRBoxTranslate(index.path, index.rbb,
-0.5 * label.rbb.width - 0.5 * index.rbb.width - delta,
0.3 * label.rbb.height);
/* eslint-enable no-mixed-operators*/
}
}
if (this.a.attpnt) {
var lsb = bisectLargestSector(this, restruct.molecule);
showAttpnt(this, render, lsb, restruct.addReObjectPath.bind(restruct));
}
var aamText = getAamText(this);
var queryAttrsText = getQueryAttrsText(this);
// this includes both aam flags, if any, and query features, if any
// we render them together to avoid possible collisions
aamText = (queryAttrsText.length > 0 ? queryAttrsText + '\n' : '') + (aamText.length > 0 ? '.' + aamText + '.' : '');
if (aamText.length > 0) {
var elem = element.map[this.a.label];
var aamPath = render.paper.text(ps.x, ps.y, aamText).attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': (options.atomColoring && elem && element[elem].color) ? element[elem].color : '#000'
});
var aamBox = util.relBox(aamPath.getBBox());
draw.recenterText(aamPath, aamBox);
var dir = bisectLargestSector(this, restruct.molecule);
var visel = this.visel;
var t = 3;
// estimate the shift to clear the atom label
for (var i = 0; i < visel.exts.length; ++i)
t = Math.max(t, Vec2.shiftRayBox(ps, dir, visel.exts[i].translate(ps)));
// estimate the shift backwards to account for the size of the aam/query text box itself
t += Vec2.shiftRayBox(ps, dir.negated(), Box2Abs.fromRelBox(aamBox));
dir = dir.scaled(8 + t);
pathAndRBoxTranslate(aamPath, aamBox, dir.x, dir.y);
restruct.addReObjectPath('data', this.visel, aamPath, ps, true);
}
};
function labelIsVisible(restruct, options, atom) {
var isVisibleTerminal = options.showHydrogenLabels !== 'off' &&
options.showHydrogenLabels !== 'Hetero';
if (atom.a.neighbors.length === 0 ||
(atom.a.neighbors.length < 2 && isVisibleTerminal) ||
(options.carbonExplicitly) ||
atom.a.label.toLowerCase() !== 'c' ||
(atom.a.badConn && options.showValenceWarnings) ||
atom.a.isotope != 0 ||
atom.a.radical != 0 ||
atom.a.charge != 0 ||
atom.a.explicitValence >= 0 ||
atom.a.atomList != null ||
atom.a.rglabel != null ||
atom.a.alias)
return true;
if (atom.a.neighbors.length == 2) {
var n1 = atom.a.neighbors[0];
var n2 = atom.a.neighbors[1];
var hb1 = restruct.molecule.halfBonds.get(n1);
var hb2 = restruct.molecule.halfBonds.get(n2);
var b1 = restruct.bonds.get(hb1.bid);
var b2 = restruct.bonds.get(hb2.bid);
if (b1.b.type == b2.b.type &&
b1.b.stereo == Struct.Bond.PATTERN.STEREO.NONE &&
b2.b.stereo == Struct.Bond.PATTERN.STEREO.NONE) {
if (Math.abs(Vec2.cross(hb1.dir, hb2.dir)) < 0.2)
return true;
}
}
return false;
}
function displayHydrogen(hydrogenLabels, atom) {
return ((hydrogenLabels === 'on') ||
(hydrogenLabels === 'Terminal' && atom.a.neighbors.length < 2) ||
(hydrogenLabels === 'Hetero' && atom.label.text.toLowerCase() !== 'c') ||
(hydrogenLabels === 'Terminal and Hetero' && (atom.a.neighbors.length < 2 || atom.label.text.toLowerCase() !== 'c')));
}
function setHydrogenPos(struct, atom) {
// check where should the hydrogen be put on the left of the label
if (atom.a.neighbors.length === 0) {
var elem = element.map[atom.a.label];
return !elem || !!element[elem].leftH;
}
var yl = 1,
yr = 1,
nl = 0,
nr = 0;
for (var i = 0; i < atom.a.neighbors.length; ++i) {
var d = struct.halfBonds.get(atom.a.neighbors[i]).dir;
if (d.x <= 0) {
yl = Math.min(yl, Math.abs(d.y));
nl++;
} else {
yr = Math.min(yr, Math.abs(d.y));
nr++;
}
}
return (yl < 0.51 || yr < 0.51) ? yr < yl : nr > nl;
}
function buildLabel(atom, paper, ps, options) { // eslint-disable-line max-statements
var label = {};
atom.color = 'black';
if (atom.a.atomList != null) {
label.text = atom.a.atomList.label();
} else if (atom.a.pseudo) {
label.text = atom.a.pseudo;
} else if (atom.a.alias) {
label.text = atom.a.alias;
} else if (atom.a.label === 'R#' && atom.a.rglabel != null) {
label.text = '';
for (var rgi = 0; rgi < 32; rgi++) {
if (atom.a.rglabel & (1 << rgi)) // eslint-disable-line max-depth
label.text += ('R' + (rgi + 1).toString());
}
if (label.text == '') label = 'R#'; // for structures that missed 'M RGP' tag in molfile
} else {
label.text = atom.a.label;
var elem = element.map[label.text];
if (options.atomColoring && elem)
atom.color = element[elem].color || '#000';
}
label.path = paper.text(ps.x, ps.y, label.text)
.attr({
'font': options.font,
'font-size': options.fontsz,
'fill': atom.color,
'font-style': atom.a.pseudo ? 'italic' : ''
});
label.rbb = util.relBox(label.path.getBBox());
draw.recenterText(label.path, label.rbb);
if (atom.a.atomList != null)
pathAndRBoxTranslate(label.path, label.rbb, (atom.hydrogenOnTheLeft ? -1 : 1) * (label.rbb.width - label.rbb.height) / 2, 0);
atom.label = label;
return label;
}
function showHydroIndex(atom, render, implh, rightMargin) {
var ps = scale.obj2scaled(atom.a.pp, render.options);
var options = render.options;
var delta = 0.5 * options.lineWidth;
var hydroIndex = {};
hydroIndex.text = (implh + 1).toString();
hydroIndex.path =
render.paper.text(ps.x, ps.y, hydroIndex.text)
.attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': atom.color
});
hydroIndex.rbb = util.relBox(hydroIndex.path.getBBox());
draw.recenterText(hydroIndex.path, hydroIndex.rbb);
/* eslint-disable no-mixed-operators*/
pathAndRBoxTranslate(hydroIndex.path, hydroIndex.rbb,
rightMargin + 0.5 * hydroIndex.rbb.width + delta,
0.2 * atom.label.rbb.height);
/* eslint-enable no-mixed-operators*/
return hydroIndex;
}
function showRadical(atom, render) {
var ps = scale.obj2scaled(atom.a.pp, render.options);
var options = render.options;
var paper = render.paper;
var radical = {};
var hshift;
switch (atom.a.radical) {
case 1:
radical.path = paper.set();
hshift = 1.6 * options.lineWidth;
radical.path.push(
draw.radicalBullet(paper, ps.add(new Vec2(-hshift, 0)), options),
draw.radicalBullet(paper, ps.add(new Vec2(hshift, 0)), options));
radical.path.attr('fill', atom.color);
break;
case 2:
radical.path = paper.set();
radical.path.push(
draw.radicalBullet(paper, ps, options));
radical.path.attr('fill', atom.color);
break;
case 3:
radical.path = paper.set();
hshift = 1.6 * options.lineWidth;
radical.path.push(
draw.radicalCap(paper, ps.add(new Vec2(-hshift, 0)), options),
draw.radicalCap(paper, ps.add(new Vec2(hshift, 0)), options));
radical.path.attr('stroke', atom.color);
break;
default:
break;
}
radical.rbb = util.relBox(radical.path.getBBox());
var vshift = -0.5 * (atom.label.rbb.height + radical.rbb.height);
if (atom.a.radical === 3)
vshift -= options.lineWidth / 2;
pathAndRBoxTranslate(radical.path, radical.rbb,
0, vshift);
return radical;
}
function showIsotope(atom, render, leftMargin) {
var ps = scale.obj2scaled(atom.a.pp, render.options);
var options = render.options;
var delta = 0.5 * options.lineWidth;
var isotope = {};
isotope.text = atom.a.isotope.toString();
isotope.path = render.paper.text(ps.x, ps.y, isotope.text)
.attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': atom.color
});
isotope.rbb = util.relBox(isotope.path.getBBox());
draw.recenterText(isotope.path, isotope.rbb);
/* eslint-disable no-mixed-operators*/
pathAndRBoxTranslate(isotope.path, isotope.rbb,
leftMargin - 0.5 * isotope.rbb.width - delta,
-0.3 * atom.label.rbb.height);
/* eslint-enable no-mixed-operators*/
return isotope;
}
function showCharge(atom, render, rightMargin) {
var ps = scale.obj2scaled(atom.a.pp, render.options);
var options = render.options;
var delta = 0.5 * options.lineWidth;
var charge = {};
charge.text = '';
var absCharge = Math.abs(atom.a.charge);
if (absCharge != 1)
charge.text = absCharge.toString();
if (atom.a.charge < 0)
charge.text += '\u2013';
else
charge.text += '+';
charge.path = render.paper.text(ps.x, ps.y, charge.text)
.attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': atom.color
});
charge.rbb = util.relBox(charge.path.getBBox());
draw.recenterText(charge.path, charge.rbb);
/* eslint-disable no-mixed-operators*/
pathAndRBoxTranslate(charge.path, charge.rbb,
rightMargin + 0.5 * charge.rbb.width + delta,
-0.3 * atom.label.rbb.height);
/* eslint-enable no-mixed-operators*/
return charge;
}
function showExplicitValence(atom, render, rightMargin) {
var mapValence = {
0: '0',
1: 'I',
2: 'II',
3: 'III',
4: 'IV',
5: 'V',
6: 'VI',
7: 'VII',
8: 'VIII',
9: 'IX',
10: 'X',
11: 'XI',
12: 'XII',
13: 'XIII',
14: 'XIV'
};
var ps = scale.obj2scaled(atom.a.pp, render.options);
var options = render.options;
var delta = 0.5 * options.lineWidth;
var valence = {};
valence.text = mapValence[atom.a.explicitValence];
if (!valence.text)
throw new Error('invalid valence ' + atom.a.explicitValence.toString());
valence.text = '(' + valence.text + ')';
valence.path = render.paper.text(ps.x, ps.y, valence.text)
.attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': atom.color
});
valence.rbb = util.relBox(valence.path.getBBox());
draw.recenterText(valence.path, valence.rbb);
/* eslint-disable no-mixed-operators*/
pathAndRBoxTranslate(valence.path, valence.rbb,
rightMargin + 0.5 * valence.rbb.width + delta,
-0.3 * atom.label.rbb.height);
/* eslint-enable no-mixed-operators*/
return valence;
}
function showHydrogen(atom, render, implh, data) { // eslint-disable-line max-statements
var hydroIndex = data.hydroIndex;
var hydrogenLeft = atom.hydrogenOnTheLeft;
var ps = scale.obj2scaled(atom.a.pp, render.options);
var options = render.options;
var delta = 0.5 * options.lineWidth;
var hydrogen = data.hydrogen;
hydrogen.text = 'H';
hydrogen.path = render.paper.text(ps.x, ps.y, hydrogen.text).attr({
'font': options.font,
'font-size': options.fontsz,
'fill': atom.color
});
hydrogen.rbb = util.relBox(hydrogen.path.getBBox());
draw.recenterText(hydrogen.path, hydrogen.rbb);
if (!hydrogenLeft) {
pathAndRBoxTranslate(hydrogen.path, hydrogen.rbb,
data.rightMargin + (0.5 * hydrogen.rbb.width) + delta, 0);
data.rightMargin += hydrogen.rbb.width + delta;
}
if (implh > 1) {
hydroIndex = {};
hydroIndex.text = implh.toString();
hydroIndex.path = render.paper.text(ps.x, ps.y, hydroIndex.text)
.attr({
'font': options.font,
'font-size': options.fontszsub,
'fill': atom.color
});
hydroIndex.rbb = util.relBox(hydroIndex.path.getBBox());
draw.recenterText(hydroIndex.path, hydroIndex.rbb);
if (!hydrogenLeft) {
pathAndRBoxTranslate(hydroIndex.path, hydroIndex.rbb,
data.rightMargin + (0.5 * hydroIndex.rbb.width) + delta,
0.2 * atom.label.rbb.height);
data.rightMargin += hydroIndex.rbb.width + delta;
}
}
if (hydrogenLeft) {
if (hydroIndex != null) {
pathAndRBoxTranslate(hydroIndex.path, hydroIndex.rbb,
data.leftMargin - (0.5 * hydroIndex.rbb.width) - delta,
0.2 * atom.label.rbb.height);
data.leftMargin -= hydroIndex.rbb.width + delta;
}
pathAndRBoxTranslate(hydrogen.path, hydrogen.rbb,
data.leftMargin - (0.5 * hydrogen.rbb.width) - delta, 0);
data.leftMargin -= hydrogen.rbb.width + delta;
}
return Object.assign(data, { hydrogen: hydrogen, hydroIndex: hydroIndex });
}
function showWarning(atom, render, leftMargin, rightMargin) {
var ps = scale.obj2scaled(atom.a.pp, render.options);
var delta = 0.5 * render.options.lineWidth;
var tfx = util.tfx;
var warning = {};
var y = ps.y + (atom.label.rbb.height / 2) + delta;
warning.path = render.paper.path('M{0},{1}L{2},{3}',
tfx(ps.x + leftMargin), tfx(y), tfx(ps.x + rightMargin), tfx(y))
.attr(render.options.lineattr).attr({ stroke: '#F00' });
warning.rbb = util.relBox(warning.path.getBBox());
return warning;
}
function showAttpnt(atom, render, lsb, addReObjectPath) { // eslint-disable-line max-statements
var asterisk = '';
var ps = scale.obj2scaled(atom.a.pp, render.options);
var options = render.options;
var tfx = util.tfx;
var i, c, j; // eslint-disable-line no-unused-vars
for (i = 0, c = 0; i < 4; ++i) {
var attpntText = '';
if (atom.a.attpnt & (1 << i)) {
if (attpntText.length > 0)
attpntText += ' ';
attpntText += asterisk;
for (j = 0; j < (i == 0 ? 0 : (i + 1)); ++j)
attpntText += '\'';
var pos0 = new Vec2(ps);
var pos1 = ps.addScaled(lsb, 0.7 * options.scale);
var attpntPath1 = render.paper.text(pos1.x, pos1.y, attpntText)
.attr({
'font': options.font,
'font-size': options.fontsz,
'fill': atom.color
});
var attpntRbb = util.relBox(attpntPath1.getBBox());
draw.recenterText(attpntPath1, attpntRbb);
var lsbn = lsb.negated();
/* eslint-disable no-mixed-operators*/
pos1 = pos1.addScaled(lsbn, Vec2.shiftRayBox(pos1, lsbn, Box2Abs.fromRelBox(attpntRbb)) + options.lineWidth / 2);
/* eslint-enable no-mixed-operators*/
pos0 = shiftBondEnd(atom, pos0, lsb, options.lineWidth);
var n = lsb.rotateSC(1, 0);
var arrowLeft = pos1.addScaled(n, 0.05 * options.scale).addScaled(lsbn, 0.09 * options.scale);
var arrowRight = pos1.addScaled(n, -0.05 * options.scale).addScaled(lsbn, 0.09 * options.scale);
var attpntPath = render.paper.set();
attpntPath.push(
attpntPath1,
render.paper.path('M{0},{1}L{2},{3}M{4},{5}L{2},{3}L{6},{7}', tfx(pos0.x), tfx(pos0.y), tfx(pos1.x), tfx(pos1.y), tfx(arrowLeft.x), tfx(arrowLeft.y), tfx(arrowRight.x), tfx(arrowRight.y))
.attr(render.options.lineattr).attr({ 'stroke-width': options.lineWidth / 2 })
);
addReObjectPath('indices', atom.visel, attpntPath, ps);
lsb = lsb.rotate(Math.PI / 6);
}
}
}
function getAamText(atom) {
var aamText = '';
if (atom.a.aam > 0) aamText += atom.a.aam;
if (atom.a.invRet > 0) {
if (aamText.length > 0) aamText += ',';
if (atom.a.invRet == 1) aamText += 'Inv';
else if (atom.a.invRet == 2) aamText += 'Ret';
else throw new Error('Invalid value for the invert/retain flag');
}
if (atom.a.exactChangeFlag > 0) {
if (aamText.length > 0) aamText += ',';
if (atom.a.exactChangeFlag == 1) aamText += 'ext';
else throw new Error('Invalid value for the exact change flag');
}
return aamText;
}
function getQueryAttrsText(atom) {
var queryAttrsText = '';
if (atom.a.ringBondCount != 0) {
if (atom.a.ringBondCount > 0) queryAttrsText += 'rb' + atom.a.ringBondCount.toString();
else if (atom.a.ringBondCount == -1) queryAttrsText += 'rb0';
else if (atom.a.ringBondCount == -2) queryAttrsText += 'rb*';
else throw new Error('Ring bond count invalid');
}
if (atom.a.substitutionCount != 0) {
if (queryAttrsText.length > 0) queryAttrsText += ',';
if (atom.a.substitutionCount > 0) queryAttrsText += 's' + atom.a.substitutionCount.toString();
else if (atom.a.substitutionCount == -1) queryAttrsText += 's0';
else if (atom.a.substitutionCount == -2) queryAttrsText += 's*';
else throw new Error('Substitution count invalid');
}
if (atom.a.unsaturatedAtom > 0) {
if (queryAttrsText.length > 0) queryAttrsText += ',';
if (atom.a.unsaturatedAtom == 1) queryAttrsText += 'u';
else throw new Error('Unsaturated atom invalid value');
}
if (atom.a.hCount > 0) {
if (queryAttrsText.length > 0) queryAttrsText += ',';
queryAttrsText += 'H' + (atom.a.hCount - 1).toString();
}
return queryAttrsText;
}
function pathAndRBoxTranslate(path, rbb, x, y) {
path.translateAbs(x, y);
rbb.x += x;
rbb.y += y;
}
function bisectLargestSector(atom, struct) {
var angles = [];
atom.a.neighbors.forEach(function (hbid) {
var hb = struct.halfBonds.get(hbid);
angles.push(hb.ang);
});
angles = angles.sort(function (a, b) {
return a - b;
});
var da = [];
for (var i = 0; i < angles.length - 1; ++i)
da.push(angles[(i + 1) % angles.length] - angles[i]);
da.push(angles[0] - angles[angles.length - 1] + (2 * Math.PI));
var daMax = 0;
var ang = -Math.PI / 2;
for (i = 0; i < angles.length; ++i) {
if (da[i] > daMax) {
daMax = da[i];
ang = angles[i] + (da[i] / 2);
}
}
return new Vec2(Math.cos(ang), Math.sin(ang));
}
function shiftBondEnd(atom, pos0, dir, margin) {
var t = 0;
var visel = atom.visel;
for (var k = 0; k < visel.exts.length; ++k) {
var box = visel.exts[k].translate(pos0);
t = Math.max(t, Vec2.shiftRayBox(pos0, dir, box));
}
if (t > 0)
pos0 = pos0.addScaled(dir, t + margin);
return pos0;
}
module.exports = ReAtom;

View File

@ -0,0 +1,587 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var ReObject = require('./reobject');
var Struct = require('../../chem/struct');
var draw = require('../draw');
var Vec2 = require('../../util/vec2');
var util = require('../util');
var scale = require('../../util/scale');
function ReBond(/* chem.Bond*/bond) {
this.init('bond');
this.b = bond; // TODO rename b to item
this.doubleBondShift = 0;
}
ReBond.prototype = new ReObject();
ReBond.isSelectable = function () {
return true;
};
ReBond.prototype.drawHighlight = function (render) {
var ret = this.makeHighlightPlate(render);
render.ctab.addReObjectPath('highlighting', this.visel, ret);
return ret;
};
ReBond.prototype.makeHighlightPlate = function (render) {
var options = render.options;
bondRecalc(this, render.ctab, options);
var c = scale.obj2scaled(this.b.center, options);
return render.paper.circle(c.x, c.y, 0.8 * options.atomSelectionPlateRadius)
.attr(options.highlightStyle);
};
ReBond.prototype.makeSelectionPlate = function (restruct, paper, options) {
bondRecalc(this, restruct, options);
var c = scale.obj2scaled(this.b.center, options);
return paper.circle(c.x, c.y, 0.8 * options.atomSelectionPlateRadius)
.attr(options.selectionStyle);
};
ReBond.prototype.show = function (restruct, bid, options) { // eslint-disable-line max-statements
var render = restruct.render;
var struct = restruct.molecule;
var paper = render.paper;
var hb1 = struct.halfBonds.get(this.b.hb1),
hb2 = struct.halfBonds.get(this.b.hb2);
checkStereoBold(bid, this, restruct);
bondRecalc(this, restruct, options);
setDoubleBondShift(this, struct);
this.path = getBondPath(restruct, this, hb1, hb2);
this.rbb = util.relBox(this.path.getBBox());
restruct.addReObjectPath('data', this.visel, this.path, null, true);
var reactingCenter = {};
reactingCenter.path = getReactingCenterPath(render, this, hb1, hb2);
if (reactingCenter.path) {
reactingCenter.rbb = util.relBox(reactingCenter.path.getBBox());
restruct.addReObjectPath('data', this.visel, reactingCenter.path, null, true);
}
var topology = {};
topology.path = getTopologyMark(render, this, hb1, hb2);
if (topology.path) {
topology.rbb = util.relBox(topology.path.getBBox());
restruct.addReObjectPath('data', this.visel, topology.path, null, true);
}
this.setHighlight(this.highlight, render);
var ipath = null;
var bondIdxOff = options.subFontSize * 0.6;
if (options.showBondIds) {
ipath = getIdsPath(bid, paper, hb1, hb2, bondIdxOff, 0.5, 0.5, hb1.norm);
restruct.addReObjectPath('indices', this.visel, ipath);
}
if (options.showHalfBondIds) {
ipath = getIdsPath(this.b.hb1, paper, hb1, hb2, bondIdxOff, 0.8, 0.2, hb1.norm);
restruct.addReObjectPath('indices', this.visel, ipath);
ipath = getIdsPath(this.b.hb2, paper, hb1, hb2, bondIdxOff, 0.2, 0.8, hb2.norm);
restruct.addReObjectPath('indices', this.visel, ipath);
}
if (options.showLoopIds && !options.showBondIds) {
ipath = getIdsPath(hb1.loop, paper, hb1, hb2, bondIdxOff, 0.5, 0.5, hb2.norm);
restruct.addReObjectPath('indices', this.visel, ipath);
ipath = getIdsPath(hb2.loop, paper, hb1, hb2, bondIdxOff, 0.5, 0.5, hb1.norm);
restruct.addReObjectPath('indices', this.visel, ipath);
}
};
function findIncomingStereoUpBond(atom, bid0, includeBoldStereoBond, restruct) {
return atom.neighbors.findIndex(function (hbid) {
var hb = restruct.molecule.halfBonds.get(hbid);
var bid = hb.bid;
if (bid === bid0)
return false;
var neibond = restruct.bonds.get(bid);
if (neibond.b.type === Struct.Bond.PATTERN.TYPE.SINGLE && neibond.b.stereo === Struct.Bond.PATTERN.STEREO.UP)
return neibond.b.end === hb.begin || (neibond.boldStereo && includeBoldStereoBond);
return !!(neibond.b.type === Struct.Bond.PATTERN.TYPE.DOUBLE && neibond.b.stereo === Struct.Bond.PATTERN.STEREO.NONE && includeBoldStereoBond && neibond.boldStereo);
});
}
function findIncomingUpBonds(bid0, bond, restruct) {
var halfbonds = [bond.b.begin, bond.b.end].map(function (aid) {
var atom = restruct.molecule.atoms.get(aid);
var pos = findIncomingStereoUpBond(atom, bid0, true, restruct);
return pos < 0 ? -1 : atom.neighbors[pos];
}, this);
console.assert(halfbonds.length === 2);
bond.neihbid1 = restruct.atoms.get(bond.b.begin).showLabel ? -1 : halfbonds[0];
bond.neihbid2 = restruct.atoms.get(bond.b.end).showLabel ? -1 : halfbonds[1];
}
function checkStereoBold(bid0, bond, restruct) {
var halfbonds = [bond.b.begin, bond.b.end].map(function (aid) {
var atom = restruct.molecule.atoms.get(aid);
var pos = findIncomingStereoUpBond(atom, bid0, false, restruct);
return pos < 0 ? -1 : atom.neighbors[pos];
}, restruct);
console.assert(halfbonds.length === 2);
bond.boldStereo = halfbonds[0] >= 0 && halfbonds[1] >= 0;
}
function getBondPath(restruct, bond, hb1, hb2) {
var path = null;
var render = restruct.render;
var struct = restruct.molecule;
var shiftA = !restruct.atoms.get(hb1.begin).showLabel;
var shiftB = !restruct.atoms.get(hb2.begin).showLabel;
switch (bond.b.type) {
case Struct.Bond.PATTERN.TYPE.SINGLE:
switch (bond.b.stereo) {
case Struct.Bond.PATTERN.STEREO.UP:
findIncomingUpBonds(hb1.bid, bond, restruct);
if (bond.boldStereo && bond.neihbid1 >= 0 && bond.neihbid2 >= 0)
path = getBondSingleStereoBoldPath(render, hb1, hb2, bond, struct);
else
path = getBondSingleUpPath(render, hb1, hb2, bond, struct);
break;
case Struct.Bond.PATTERN.STEREO.DOWN:
path = getBondSingleDownPath(render, hb1, hb2);
break;
case Struct.Bond.PATTERN.STEREO.EITHER:
path = getBondSingleEitherPath(render, hb1, hb2);
break;
default:
path = draw.bondSingle(render.paper, hb1, hb2, render.options);
break;
}
break;
case Struct.Bond.PATTERN.TYPE.DOUBLE:
findIncomingUpBonds(hb1.bid, bond, restruct);
if (bond.b.stereo === Struct.Bond.PATTERN.STEREO.NONE && bond.boldStereo &&
bond.neihbid1 >= 0 && bond.neihbid2 >= 0)
path = getBondDoubleStereoBoldPath(render, hb1, hb2, bond, struct, shiftA, shiftB);
else
path = getBondDoublePath(render, hb1, hb2, bond, shiftA, shiftB);
break;
case Struct.Bond.PATTERN.TYPE.TRIPLE:
path = draw.bondTriple(render.paper, hb1, hb2, render.options);
break;
case Struct.Bond.PATTERN.TYPE.AROMATIC:
var inAromaticLoop = (hb1.loop >= 0 && struct.loops.get(hb1.loop).aromatic) ||
(hb2.loop >= 0 && struct.loops.get(hb2.loop).aromatic);
path = inAromaticLoop ? draw.bondSingle(render.paper, hb1, hb2, render.options) :
getBondAromaticPath(render, hb1, hb2, bond, shiftA, shiftB);
break;
case Struct.Bond.PATTERN.TYPE.SINGLE_OR_DOUBLE:
path = getSingleOrDoublePath(render, hb1, hb2);
break;
case Struct.Bond.PATTERN.TYPE.SINGLE_OR_AROMATIC:
path = getBondAromaticPath(render, hb1, hb2, bond, shiftA, shiftB);
break;
case Struct.Bond.PATTERN.TYPE.DOUBLE_OR_AROMATIC:
path = getBondAromaticPath(render, hb1, hb2, bond, shiftA, shiftB);
break;
case Struct.Bond.PATTERN.TYPE.ANY:
path = draw.bondAny(render.paper, hb1, hb2, render.options);
break;
default:
throw new Error('Bond type ' + bond.b.type + ' not supported');
}
return path;
}
/* Get Path */
function getBondSingleUpPath(render, hb1, hb2, bond, struct) { // eslint-disable-line max-params
var a = hb1.p,
b = hb2.p,
n = hb1.norm;
var options = render.options;
var bsp = 0.7 * options.stereoBond;
var b2 = b.addScaled(n, bsp);
var b3 = b.addScaled(n, -bsp);
if (bond.neihbid2 >= 0) { // if the end is shared with another up-bond heading this way
var coords = stereoUpBondGetCoordinates(hb2, bond.neihbid2, options.stereoBond, struct);
b2 = coords[0];
b3 = coords[1];
}
return draw.bondSingleUp(render.paper, a, b2, b3, options);
}
function getBondSingleStereoBoldPath(render, hb1, hb2, bond, struct) { // eslint-disable-line max-params
var options = render.options;
var coords1 = stereoUpBondGetCoordinates(hb1, bond.neihbid1, options.stereoBond, struct);
var coords2 = stereoUpBondGetCoordinates(hb2, bond.neihbid2, options.stereoBond, struct);
var a1 = coords1[0];
var a2 = coords1[1];
var a3 = coords2[0];
var a4 = coords2[1];
return draw.bondSingleStereoBold(render.paper, a1, a2, a3, a4, options);
}
function getBondDoubleStereoBoldPath(render, hb1, hb2, bond, struct, shiftA, shiftB) { // eslint-disable-line max-params
var a = hb1.p,
b = hb2.p,
n = hb1.norm,
shift = bond.doubleBondShift;
var bsp = 1.5 * render.options.stereoBond;
var b1 = a.addScaled(n, bsp * shift);
var b2 = b.addScaled(n, bsp * shift);
if (shift > 0) {
if (shiftA)
b1 = b1.addScaled(hb1.dir, bsp * getBondLineShift(hb1.rightCos, hb1.rightSin));
if (shiftB)
b2 = b2.addScaled(hb1.dir, -bsp * getBondLineShift(hb2.leftCos, hb2.leftSin));
} else if (shift < 0) {
if (shiftA)
b1 = b1.addScaled(hb1.dir, bsp * getBondLineShift(hb1.leftCos, hb1.leftSin));
if (shiftB)
b2 = b2.addScaled(hb1.dir, -bsp * getBondLineShift(hb2.rightCos, hb2.rightSin));
}
var sgBondPath = getBondSingleStereoBoldPath(render, hb1, hb2, bond, struct);
return draw.bondDoubleStereoBold(render.paper, sgBondPath, b1, b2, render.options);
}
function getBondLineShift(cos, sin) {
if (sin < 0 || Math.abs(cos) > 0.9)
return 0;
return sin / (1 - cos);
}
function stereoUpBondGetCoordinates(hb, neihbid, bondSpace, struct) {
var neihb = struct.halfBonds.get(neihbid);
var cos = Vec2.dot(hb.dir, neihb.dir);
var sin = Vec2.cross(hb.dir, neihb.dir);
var cosHalf = Math.sqrt(0.5 * (1 - cos));
var biss = neihb.dir.rotateSC((sin >= 0 ? -1 : 1) * cosHalf, Math.sqrt(0.5 * (1 + cos)));
var denomAdd = 0.3;
var scale = 0.7;
var a1 = hb.p.addScaled(biss, scale * bondSpace / (cosHalf + denomAdd));
var a2 = hb.p.addScaled(biss.negated(), scale * bondSpace / (cosHalf + denomAdd));
return sin > 0 ? [a1, a2] : [a2, a1];
}
function getBondSingleDownPath(render, hb1, hb2) {
var a = hb1.p,
b = hb2.p;
var options = render.options;
var d = b.sub(a);
var len = d.length() + 0.2;
d = d.normalized();
var interval = 1.2 * options.lineWidth;
var nlines = Math.max(Math.floor((len - options.lineWidth) /
(options.lineWidth + interval)), 0) + 2;
var step = len / (nlines - 1);
return draw.bondSingleDown(render.paper, hb1, d, nlines, step, options);
}
function getBondSingleEitherPath(render, hb1, hb2) {
var a = hb1.p,
b = hb2.p;
var options = render.options;
var d = b.sub(a);
var len = d.length();
d = d.normalized();
var interval = 0.6 * options.lineWidth;
var nlines = Math.max(Math.floor((len - options.lineWidth) /
(options.lineWidth + interval)), 0) + 2;
var step = len / (nlines - 0.5);
return draw.bondSingleEither(render.paper, hb1, d, nlines, step, options);
}
function getBondDoublePath(render, hb1, hb2, bond, shiftA, shiftB) { // eslint-disable-line max-params, max-statements
var cisTrans = bond.b.stereo === Struct.Bond.PATTERN.STEREO.CIS_TRANS;
var a = hb1.p,
b = hb2.p,
n = hb1.norm,
shift = cisTrans ? 0 : bond.doubleBondShift;
var options = render.options;
var bsp = options.bondSpace / 2;
var s1 = bsp + (shift * bsp),
s2 = -bsp + (shift * bsp);
var a1 = a.addScaled(n, s1);
var b1 = b.addScaled(n, s1);
var a2 = a.addScaled(n, s2);
var b2 = b.addScaled(n, s2);
if (shift > 0) {
if (shiftA) {
a1 = a1.addScaled(hb1.dir, options.bondSpace *
getBondLineShift(hb1.rightCos, hb1.rightSin));
}
if (shiftB) {
b1 = b1.addScaled(hb1.dir, -options.bondSpace *
getBondLineShift(hb2.leftCos, hb2.leftSin));
}
} else if (shift < 0) {
if (shiftA) {
a2 = a2.addScaled(hb1.dir, options.bondSpace *
getBondLineShift(hb1.leftCos, hb1.leftSin));
}
if (shiftB) {
b2 = b2.addScaled(hb1.dir, -options.bondSpace *
getBondLineShift(hb2.rightCos, hb2.rightSin));
}
}
return draw.bondDouble(render.paper, a1, a2, b1, b2, cisTrans, options);
}
function getSingleOrDoublePath(render, hb1, hb2) {
var a = hb1.p,
b = hb2.p;
var options = render.options;
var nSect = (Vec2.dist(a, b) / (options.bondSpace + options.lineWidth)).toFixed() - 0;
if (!(nSect & 1))
nSect += 1;
return draw.bondSingleOrDouble(render.paper, hb1, hb2, nSect, options);
}
function getBondAromaticPath(render, hb1, hb2, bond, shiftA, shiftB) { // eslint-disable-line max-params
var dashdotPattern = [0.125, 0.125, 0.005, 0.125];
var mark = null,
dash = null;
var options = render.options;
var bondShift = bond.doubleBondShift;
if (bond.b.type == Struct.Bond.PATTERN.TYPE.SINGLE_OR_AROMATIC) {
mark = bondShift > 0 ? 1 : 2;
dash = dashdotPattern.map(function (v) {
return v * options.scale;
});
}
if (bond.b.type == Struct.Bond.PATTERN.TYPE.DOUBLE_OR_AROMATIC) {
mark = 3;
dash = dashdotPattern.map(function (v) {
return v * options.scale;
});
}
var paths = getAromaticBondPaths(hb1, hb2, bondShift, shiftA, shiftB, options.bondSpace, mark, dash);
return draw.bondAromatic(render.paper, paths, bondShift, options);
}
function getAromaticBondPaths(hb1, hb2, shift, shiftA, shiftB, bondSpace, mask, dash) { // eslint-disable-line max-params, max-statements
var a = hb1.p,
b = hb2.p,
n = hb1.norm;
var bsp = bondSpace / 2;
var s1 = bsp + (shift * bsp),
s2 = -bsp + (shift * bsp);
var a2 = a.addScaled(n, s1);
var b2 = b.addScaled(n, s1);
var a3 = a.addScaled(n, s2);
var b3 = b.addScaled(n, s2);
if (shift > 0) {
if (shiftA) {
a2 = a2.addScaled(hb1.dir, bondSpace *
getBondLineShift(hb1.rightCos, hb1.rightSin));
}
if (shiftB) {
b2 = b2.addScaled(hb1.dir, -bondSpace *
getBondLineShift(hb2.leftCos, hb2.leftSin));
}
} else if (shift < 0) {
if (shiftA) {
a3 = a3.addScaled(hb1.dir, bondSpace *
getBondLineShift(hb1.leftCos, hb1.leftSin));
}
if (shiftB) {
b3 = b3.addScaled(hb1.dir, -bondSpace *
getBondLineShift(hb2.rightCos, hb2.rightSin));
}
}
return draw.aromaticBondPaths(a2, a3, b2, b3, mask, dash);
}
function getReactingCenterPath(render, bond, hb1, hb2) { // eslint-disable-line max-statements
var a = hb1.p,
b = hb2.p;
var c = b.add(a).scaled(0.5);
var d = b.sub(a).normalized();
var n = d.rotateSC(1, 0);
var p = [];
var lw = render.options.lineWidth,
bs = render.options.bondSpace / 2;
var alongIntRc = lw, // half interval along for CENTER
alongIntMadeBroken = 2 * lw, // half interval between along for MADE_OR_BROKEN
alongSz = 1.5 * bs, // half size along for CENTER
acrossInt = 1.5 * bs, // half interval across for CENTER
acrossSz = 3.0 * bs, // half size across for all
tiltTan = 0.2; // tangent of the tilt angle
switch (bond.b.reactingCenterStatus) {
case Struct.Bond.PATTERN.REACTING_CENTER.NOT_CENTER: // X
p.push(c.addScaled(n, acrossSz).addScaled(d, tiltTan * acrossSz));
p.push(c.addScaled(n, -acrossSz).addScaled(d, -tiltTan * acrossSz));
p.push(c.addScaled(n, acrossSz).addScaled(d, -tiltTan * acrossSz));
p.push(c.addScaled(n, -acrossSz).addScaled(d, tiltTan * acrossSz));
break;
case Struct.Bond.PATTERN.REACTING_CENTER.CENTER: // #
p.push(c.addScaled(n, acrossSz).addScaled(d, tiltTan * acrossSz).addScaled(d, alongIntRc));
p.push(c.addScaled(n, -acrossSz).addScaled(d, -tiltTan * acrossSz).addScaled(d, alongIntRc));
p.push(c.addScaled(n, acrossSz).addScaled(d, tiltTan * acrossSz).addScaled(d, -alongIntRc));
p.push(c.addScaled(n, -acrossSz).addScaled(d, -tiltTan * acrossSz).addScaled(d, -alongIntRc));
p.push(c.addScaled(d, alongSz).addScaled(n, acrossInt));
p.push(c.addScaled(d, -alongSz).addScaled(n, acrossInt));
p.push(c.addScaled(d, alongSz).addScaled(n, -acrossInt));
p.push(c.addScaled(d, -alongSz).addScaled(n, -acrossInt));
break;
// case Bond.PATTERN.REACTING_CENTER.UNCHANGED: // o
// //draw a circle
// break;
case Struct.Bond.PATTERN.REACTING_CENTER.MADE_OR_BROKEN:
p.push(c.addScaled(n, acrossSz).addScaled(d, alongIntMadeBroken));
p.push(c.addScaled(n, -acrossSz).addScaled(d, alongIntMadeBroken));
p.push(c.addScaled(n, acrossSz).addScaled(d, -alongIntMadeBroken));
p.push(c.addScaled(n, -acrossSz).addScaled(d, -alongIntMadeBroken));
break;
case Struct.Bond.PATTERN.REACTING_CENTER.ORDER_CHANGED:
p.push(c.addScaled(n, acrossSz));
p.push(c.addScaled(n, -acrossSz));
break;
case Struct.Bond.PATTERN.REACTING_CENTER.MADE_OR_BROKEN_AND_CHANGED:
p.push(c.addScaled(n, acrossSz).addScaled(d, alongIntMadeBroken));
p.push(c.addScaled(n, -acrossSz).addScaled(d, alongIntMadeBroken));
p.push(c.addScaled(n, acrossSz).addScaled(d, -alongIntMadeBroken));
p.push(c.addScaled(n, -acrossSz).addScaled(d, -alongIntMadeBroken));
p.push(c.addScaled(n, acrossSz));
p.push(c.addScaled(n, -acrossSz));
break;
default:
return null;
}
return draw.reactingCenter(render.paper, p, render.options);
}
function getTopologyMark(render, bond, hb1, hb2) { // eslint-disable-line max-statements
var options = render.options;
var mark = null;
if (bond.b.topology == Struct.Bond.PATTERN.TOPOLOGY.RING)
mark = 'rng';
else if (bond.b.topology == Struct.Bond.PATTERN.TOPOLOGY.CHAIN)
mark = 'chn';
else
return null;
var a = hb1.p,
b = hb2.p;
var c = b.add(a).scaled(0.5);
var d = b.sub(a).normalized();
var n = d.rotateSC(1, 0);
var fixed = options.lineWidth;
if (bond.doubleBondShift > 0)
n = n.scaled(-bond.doubleBondShift);
else if (bond.doubleBondShift == 0)
fixed += options.bondSpace / 2;
var s = new Vec2(2, 1).scaled(options.bondSpace);
if (bond.b.type == Struct.Bond.PATTERN.TYPE.TRIPLE)
fixed += options.bondSpace;
var p = c.add(new Vec2(n.x * (s.x + fixed), n.y * (s.y + fixed)));
return draw.topologyMark(render.paper, p, mark, options);
}
function getIdsPath(bid, paper, hb1, hb2, bondIdxOff, param1, param2, norm) { // eslint-disable-line max-params
var pb = Vec2.lc(hb1.p, param1, hb2.p, param2, norm, bondIdxOff);
var ipath = paper.text(pb.x, pb.y, bid.toString());
var irbb = util.relBox(ipath.getBBox());
draw.recenterText(ipath, irbb);
return ipath;
}
/* ----- */
function setDoubleBondShift(bond, struct) {
var loop1, loop2;
loop1 = struct.halfBonds.get(bond.b.hb1).loop;
loop2 = struct.halfBonds.get(bond.b.hb2).loop;
if (loop1 >= 0 && loop2 >= 0) {
var d1 = struct.loops.get(loop1).dblBonds;
var d2 = struct.loops.get(loop2).dblBonds;
var n1 = struct.loops.get(loop1).hbs.length;
var n2 = struct.loops.get(loop2).hbs.length;
bond.doubleBondShift = selectDoubleBondShift(n1, n2, d1, d2);
} else if (loop1 >= 0) {
bond.doubleBondShift = -1;
} else if (loop2 >= 0) {
bond.doubleBondShift = 1;
} else {
bond.doubleBondShift = selectDoubleBondShiftChain(struct, bond);
}
}
function bondRecalc(bond, restruct, options) {
var render = restruct.render;
var atom1 = restruct.atoms.get(bond.b.begin);
var atom2 = restruct.atoms.get(bond.b.end);
var p1 = scale.obj2scaled(atom1.a.pp, render.options);
var p2 = scale.obj2scaled(atom2.a.pp, render.options);
var hb1 = restruct.molecule.halfBonds.get(bond.b.hb1);
var hb2 = restruct.molecule.halfBonds.get(bond.b.hb2);
hb1.p = shiftBondEnd(atom1, p1, hb1.dir, 2 * options.lineWidth);
hb2.p = shiftBondEnd(atom2, p2, hb2.dir, 2 * options.lineWidth);
bond.b.center = Vec2.lc2(atom1.a.pp, 0.5, atom2.a.pp, 0.5);
bond.b.len = Vec2.dist(p1, p2);
bond.b.sb = options.lineWidth * 5;
/* eslint-disable no-mixed-operators*/
bond.b.sa = Math.max(bond.b.sb, bond.b.len / 2 - options.lineWidth * 2);
/* eslint-enable no-mixed-operators*/
bond.b.angle = Math.atan2(hb1.dir.y, hb1.dir.x) * 180 / Math.PI;
}
function shiftBondEnd(atom, pos0, dir, margin) {
var t = 0;
var visel = atom.visel;
for (var k = 0; k < visel.exts.length; ++k) {
var box = visel.exts[k].translate(pos0);
t = Math.max(t, Vec2.shiftRayBox(pos0, dir, box));
}
if (t > 0)
pos0 = pos0.addScaled(dir, t + margin);
return pos0;
}
function selectDoubleBondShift(n1, n2, d1, d2) {
if (n1 == 6 && n2 != 6 && (d1 > 1 || d2 == 1))
return -1;
if (n2 == 6 && n1 != 6 && (d2 > 1 || d1 == 1))
return 1;
if (n2 * d1 > n1 * d2)
return -1;
if (n2 * d1 < n1 * d2)
return 1;
if (n2 > n1)
return -1;
return 1;
}
function selectDoubleBondShiftChain(struct, bond) {
var hb1 = struct.halfBonds.get(bond.b.hb1);
var hb2 = struct.halfBonds.get(bond.b.hb2);
var nLeft = (hb1.leftSin > 0.3 ? 1 : 0) + (hb2.rightSin > 0.3 ? 1 : 0);
var nRight = (hb2.leftSin > 0.3 ? 1 : 0) + (hb1.rightSin > 0.3 ? 1 : 0);
if (nLeft > nRight)
return -1;
if (nLeft < nRight)
return 1;
if ((hb1.leftSin > 0.3 ? 1 : 0) + (hb1.rightSin > 0.3 ? 1 : 0) == 1)
return 1;
return 0;
}
module.exports = ReBond;

View File

@ -0,0 +1,63 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Box2Abs = require('../../util/box2abs');
var scale = require('../../util/scale');
var ReObject = require('./reobject');
function ReChiralFlag(pos) {
this.init('chiralFlag');
this.pp = pos;
}
ReChiralFlag.prototype = new ReObject();
ReChiralFlag.isSelectable = function () {
return true;
};
ReChiralFlag.prototype.highlightPath = function (render) {
var box = Box2Abs.fromRelBox(this.path.getBBox());
var sz = box.p1.sub(box.p0);
var p0 = box.p0.sub(render.options.offset);
return render.paper.rect(p0.x, p0.y, sz.x, sz.y);
};
ReChiralFlag.prototype.drawHighlight = function (render) {
var ret = this.highlightPath(render).attr(render.options.highlightStyle);
render.ctab.addReObjectPath('highlighting', this.visel, ret);
return ret;
};
ReChiralFlag.prototype.makeSelectionPlate = function (restruct, paper, options) {
return this.highlightPath(restruct.render).attr(options.selectionStyle);
};
ReChiralFlag.prototype.show = function (restruct, id, options) {
var render = restruct.render;
if (restruct.chiralFlagsChanged[id] <= 0) return;
var paper = render.paper;
var ps = scale.obj2scaled(this.pp, options);
this.path = paper.text(ps.x, ps.y, 'Chiral')
.attr({
'font': options.font,
'font-size': options.fontsz,
'fill': '#000'
});
render.ctab.addReObjectPath('data', this.visel, this.path, null, true);
};
module.exports = ReChiralFlag;

View File

@ -0,0 +1,48 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var ReObject = require('./reobject');
var scale = require('../../util/scale');
function ReDataSGroupData(sgroup) {
this.init('sgroupData');
this.sgroup = sgroup;
}
ReDataSGroupData.prototype = new ReObject();
ReDataSGroupData.isSelectable = function () {
return true;
};
ReDataSGroupData.prototype.highlightPath = function (render) {
var box = this.sgroup.dataArea;
var p0 = scale.obj2scaled(box.p0, render.options);
var sz = scale.obj2scaled(box.p1, render.options).sub(p0);
return render.paper.rect(p0.x, p0.y, sz.x, sz.y);
};
ReDataSGroupData.prototype.drawHighlight = function (render) {
var ret = this.highlightPath(render).attr(render.options.highlightStyle);
render.ctab.addReObjectPath('highlighting', this.visel, ret);
return ret;
};
ReDataSGroupData.prototype.makeSelectionPlate = function (restruct, paper, styles) { // TODO [MK] review parameters
return this.highlightPath(restruct.render).attr(styles.selectionStyle);
};
module.exports = ReDataSGroupData;

View File

@ -0,0 +1,112 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Box2Abs = require('../../util/box2abs');
var Vec2 = require('../../util/vec2');
var ReObject = require('./reobject');
var scale = require('../../util/scale');
function ReFrag(/* Struct.Fragment = {}*/frag) {
this.init('frag');
this.item = frag;
}
ReFrag.prototype = new ReObject();
ReFrag.isSelectable = function () {
return false;
};
ReFrag.prototype.fragGetAtoms = function (render, fid) {
var ret = [];
render.ctab.atoms.each(function (aid, atom) {
if (atom.a.fragment == fid)
ret.push(aid);
}, this);
return ret;
};
ReFrag.prototype.fragGetBonds = function (render, fid) {
var ret = [];
render.ctab.bonds.each(function (bid, bond) {
if (render.ctab.atoms.get(bond.b.begin).a.fragment == fid &&
render.ctab.atoms.get(bond.b.end).a.fragment == fid)
ret.push(bid);
}, this);
return ret;
};
ReFrag.prototype.calcBBox = function (restruct, fid, render) { // TODO need to review parameter list
var ret;
restruct.atoms.each(function (aid, atom) {
if (atom.a.fragment == fid) {
// TODO ReObject.calcBBox to be used instead
var bba = atom.visel.boundingBox;
if (!bba) {
bba = new Box2Abs(atom.a.pp, atom.a.pp);
var ext = new Vec2(0.05 * 3, 0.05 * 3);
bba = bba.extend(ext, ext);
} else {
if (!render) {
console.warn('No boundingBox fragment precalc');
render = global._ui_editor.render; // eslint-disable-line
}
bba = bba.translate((render.options.offset || new Vec2()).negated()).transform(scale.scaled2obj, render.options);
}
ret = (ret ? Box2Abs.union(ret, bba) : bba);
}
});
return ret;
};
// TODO need to review parameter list
ReFrag.prototype._draw = function (render, fid, attrs) { // eslint-disable-line no-underscore-dangle
var bb = this.calcBBox(render.ctab, fid, render);
if (bb) {
var p0 = scale.obj2scaled(new Vec2(bb.p0.x, bb.p0.y), render.options);
var p1 = scale.obj2scaled(new Vec2(bb.p1.x, bb.p1.y), render.options);
return render.paper.rect(p0.x, p0.y, p1.x - p0.x, p1.y - p0.y, 0).attr(attrs);
} else { // eslint-disable-line no-else-return
// TODO abnormal situation, empty fragments must be destroyed by tools
console.assert(null, 'Empty fragment');
}
};
ReFrag.prototype.draw = function (render) { // eslint-disable-line no-unused-vars
return null;// this._draw(render, fid, { 'stroke' : 'lightgray' }); // [RB] for debugging only
};
ReFrag.prototype.drawHighlight = function (render) { // eslint-disable-line no-unused-vars
// Do nothing. This method shouldn't actually be called.
};
ReFrag.prototype.setHighlight = function (highLight, render) {
var fid = render.ctab.frags.keyOf(this);
if (!(typeof fid === "undefined")) {
render.ctab.atoms.each(function (aid, atom) {
if (atom.a.fragment == fid)
atom.setHighlight(highLight, render);
}, this);
render.ctab.bonds.each(function (bid, bond) {
if (render.ctab.atoms.get(bond.b.begin).a.fragment == fid)
bond.setHighlight(highLight, render);
}, this);
} else {
// TODO abnormal situation, fragment does not belong to the render
}
};
module.exports = ReFrag;

View File

@ -0,0 +1,124 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Vec2 = require('../../util/vec2');
var Visel = require('./visel');
var ReObject = require('./reobject');
var scale = require('../../util/scale');
var util = require('../util');
var Struct = require('../../chem/struct');
var tfx = util.tfx;
function ReLoop(loop) {
this.loop = loop;
this.visel = new Visel('loop');
this.centre = new Vec2();
this.radius = new Vec2();
}
ReLoop.prototype = new ReObject();
ReLoop.isSelectable = function () {
return false;
};
ReLoop.prototype.show = function (restruct, rlid, options) { // eslint-disable-line max-statements
var render = restruct.render;
var paper = render.paper;
var molecule = restruct.molecule;
var loop = this.loop;
this.centre = new Vec2();
loop.hbs.forEach(function (hbid) {
var hb = molecule.halfBonds.get(hbid);
var bond = restruct.bonds.get(hb.bid);
var apos = scale.obj2scaled(restruct.atoms.get(hb.begin).a.pp, options);
if (bond.b.type !== Struct.Bond.PATTERN.TYPE.AROMATIC)
loop.aromatic = false;
this.centre.add_(apos); // eslint-disable-line no-underscore-dangle
}, this);
loop.convex = true;
for (var k = 0; k < this.loop.hbs.length; ++k) {
var hba = molecule.halfBonds.get(loop.hbs[k]);
var hbb = molecule.halfBonds.get(loop.hbs[(k + 1) % loop.hbs.length]);
var angle = Math.atan2(
Vec2.cross(hba.dir, hbb.dir),
Vec2.dot(hba.dir, hbb.dir));
if (angle > 0)
loop.convex = false;
}
this.centre = this.centre.scaled(1.0 / loop.hbs.length);
this.radius = -1;
loop.hbs.forEach(function (hbid) {
var hb = molecule.halfBonds.get(hbid);
var apos = scale.obj2scaled(restruct.atoms.get(hb.begin).a.pp, options);
var bpos = scale.obj2scaled(restruct.atoms.get(hb.end).a.pp, options);
var n = Vec2.diff(bpos, apos).rotateSC(1, 0).normalized();
var dist = Vec2.dot(Vec2.diff(apos, this.centre), n);
this.radius = (this.radius < 0) ? dist : Math.min(this.radius, dist);
}, this);
this.radius *= 0.7;
if (!loop.aromatic)
return;
var path = null;
if (loop.convex && options.aromaticCircle) {
path = paper.circle(this.centre.x, this.centre.y, this.radius)
.attr({
'stroke': '#000',
'stroke-width': options.lineattr['stroke-width']
});
} else {
var pathStr = '';
for (k = 0; k < loop.hbs.length; ++k) {
hba = molecule.halfBonds.get(loop.hbs[k]);
hbb = molecule.halfBonds.get(loop.hbs[(k + 1) % loop.hbs.length]);
angle = Math.atan2(
Vec2.cross(hba.dir, hbb.dir),
Vec2.dot(hba.dir, hbb.dir));
var halfAngle = (Math.PI - angle) / 2;
var dir = hbb.dir.rotate(halfAngle);
var pi = scale.obj2scaled(restruct.atoms.get(hbb.begin).a.pp, options);
var sin = Math.sin(halfAngle);
var minSin = 0.1;
if (Math.abs(sin) < minSin)
sin = sin * minSin / Math.abs(sin);
var offset = options.bondSpace / sin;
var qi = pi.addScaled(dir, -offset);
pathStr += (k === 0 ? 'M' : 'L');
pathStr += tfx(qi.x) + ',' + tfx(qi.y);
}
pathStr += 'Z';
path = paper.path(pathStr)
.attr({
'stroke': '#000',
'stroke-width': options.lineattr['stroke-width'],
'stroke-dasharray': '- '
});
}
restruct.addReObjectPath('data', this.visel, path, null, true);
};
ReLoop.prototype.isValid = function (struct, rlid) {
var halfBonds = struct.halfBonds;
var loop = this.loop;
var bad = false;
loop.hbs.forEach(function (hbid) {
if (!halfBonds.has(hbid) || halfBonds.get(hbid).loop !== rlid)
bad = true;
}, this);
return !bad;
};
module.exports = ReLoop;

View File

@ -0,0 +1,75 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Visel = require('./visel');
var scale = require('../../util/scale');
function ReObject() {
}
ReObject.prototype.init = function (viselType) {
this.visel = new Visel(viselType);
this.highlight = false;
this.highlighting = null;
this.selected = false;
this.selectionPlate = null;
};
// returns the bounding box of a ReObject in the object coordinates
ReObject.prototype.getVBoxObj = function (render) {
var vbox = this.visel.boundingBox;
if (vbox === null)
return null;
if (render.options.offset)
vbox = vbox.translate(render.options.offset.negated());
return vbox.transform(scale.scaled2obj, render.options);
};
ReObject.prototype.setHighlight = function (highLight, render) { // TODO render should be field
if (highLight) {
var noredraw = 'highlighting' in this && this.highlighting != null;// && !this.highlighting.removed;
if (noredraw) {
if (this.highlighting.type == 'set')
noredraw = !this.highlighting[0].removed;
else
noredraw = !this.highlighting.removed;
}
if (noredraw) {
this.highlighting.show();
} else {
render.paper.setStart();
this.drawHighlight(render);
this.highlighting = render.paper.setFinish();
}
} else
if (this.highlighting) {
this.highlighting.hide();
}
this.highlight = highLight;
};
ReObject.prototype.drawHighlight = function () {
console.assert('ReObject.drawHighlight is not overridden'); // eslint-disable-line no-console
};
ReObject.prototype.makeSelectionPlate = function () {
console.assert(null, 'ReObject.makeSelectionPlate is not overridden'); // eslint-disable-line no-console
};
module.exports = ReObject;

View File

@ -0,0 +1,198 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Box2Abs = require('../../util/box2abs');
var Vec2 = require('../../util/vec2');
var util = require('../util');
var draw = require('../draw');
var scale = require('../../util/scale');
var ReObject = require('./reobject');
var BORDER_EXT = new Vec2(0.05 * 3, 0.05 * 3);
function ReRGroup(/* Struct.RGroup*/rgroup) {
this.init('rgroup');
this.labelBox = null;
this.item = rgroup;
}
ReRGroup.prototype = new ReObject();
ReRGroup.isSelectable = function () {
return false;
};
ReRGroup.prototype.getAtoms = function (render) {
var ret = [];
this.item.frags.each(function (fnum, fid) {
ret = ret.concat(render.ctab.frags.get(fid).fragGetAtoms(render, fid));
});
return ret;
};
ReRGroup.prototype.getBonds = function (render) {
var ret = [];
this.item.frags.each(function (fnum, fid) {
ret = ret.concat(render.ctab.frags.get(fid).fragGetBonds(render, fid));
});
return ret;
};
ReRGroup.prototype.calcBBox = function (render) {
var ret;
this.item.frags.each(function (fnum, fid) {
var bbf = render.ctab.frags.get(fid).calcBBox(render.ctab, fid, render);
if (bbf)
ret = (ret ? Box2Abs.union(ret, bbf) : bbf);
});
ret = ret.extend(BORDER_EXT, BORDER_EXT); // eslint-disable-line no-underscore-dangle
return ret;
};
function rGroupdrawBrackets(set, render, bb, d) {
d = scale.obj2scaled(d || new Vec2(1, 0), render.options);
var bracketWidth = Math.min(0.25, bb.sz().x * 0.3);
var bracketHeight = bb.p1.y - bb.p0.y;
var cy = 0.5 * (bb.p1.y + bb.p0.y);
var leftBracket = draw.bracket(render.paper, d.negated(),
d.negated().rotateSC(1, 0),
scale.obj2scaled(new Vec2(bb.p0.x, cy), render.options),
bracketWidth, bracketHeight, render.options);
var rightBracket = draw.bracket(render.paper, d, d.rotateSC(1, 0),
scale.obj2scaled(new Vec2(bb.p1.x, cy), render.options),
bracketWidth, bracketHeight, render.options);
return set.push(leftBracket, rightBracket);
}
// TODO need to review parameter list
ReRGroup.prototype.draw = function (render, options) { // eslint-disable-line max-statements
var bb = this.calcBBox(render);
if (bb) {
var ret = { data: [] };
var p0 = scale.obj2scaled(bb.p0, options);
var p1 = scale.obj2scaled(bb.p1, options);
var brackets = render.paper.set();
rGroupdrawBrackets(brackets, render, bb); // eslint-disable-line new-cap
ret.data.push(brackets);
var key = render.ctab.rgroups.keyOf(this);
var labelSet = render.paper.set();
var label = render.paper.text(p0.x, (p0.y + p1.y) / 2, 'R' + key + '=')
.attr({
'font': options.font,
'font-size': options.fontRLabel,
'fill': 'black'
});
var labelBox = util.relBox(label.getBBox());
/* eslint-disable no-mixed-operators*/
label.translateAbs(-labelBox.width / 2 - options.lineWidth, 0);
/* eslint-enable no-mixed-operators*/
labelSet.push(label);
var logicStyle = {
'font': options.font,
'font-size': options.fontRLogic,
'fill': 'black'
};
var logic = [];
// TODO [RB] temporary solution, need to review
// BEGIN
/*
if (this.item.range.length > 0)
logic.push(this.item.range);
if (this.item.resth)
logic.push("RestH");
if (this.item.ifthen > 0)
logic.push("IF R" + key.toString() + " THEN R" + this.item.ifthen.toString());
*/
logic.push(
(this.item.ifthen > 0 ? 'IF ' : '') +
'R' + key.toString() +
/* eslint-disable no-nested-ternary */
(this.item.range.length > 0 ?
this.item.range.startsWith('>') || this.item.range.startsWith('<') || this.item.range.startsWith('=') ?
this.item.range : '=' + this.item.range : '>0') +
(this.item.resth ? ' (RestH)' : '') +
(this.item.ifthen > 0 ? '\nTHEN R' + this.item.ifthen.toString() : '')
/* eslint-enable no-nested-ternary */
);
// END
/* eslint-disable no-mixed-operators*/
var shift = labelBox.height / 2 + options.lineWidth / 2;
/* eslint-enable no-mixed-operators*/
for (var i = 0; i < logic.length; ++i) {
var logicPath = render.paper.text(p0.x, (p0.y + p1.y) / 2, logic[i]).attr(logicStyle);
var logicBox = util.relBox(logicPath.getBBox());
shift += logicBox.height / 2;
/* eslint-disable no-mixed-operators*/
logicPath.translateAbs(-logicBox.width / 2 - 6 * options.lineWidth, shift);
shift += logicBox.height / 2 + options.lineWidth / 2;
/* eslint-enable no-mixed-operators*/
ret.data.push(logicPath);
labelSet.push(logicPath);
}
ret.data.push(label);
this.labelBox = Box2Abs.fromRelBox(labelSet.getBBox()).transform(scale.scaled2obj, render.options);
return ret;
} else { // eslint-disable-line no-else-return
// TODO abnormal situation, empty fragments must be destroyed by tools
return {};
}
};
// TODO need to review parameter list
ReRGroup.prototype._draw = function (render, rgid, attrs) { // eslint-disable-line no-underscore-dangle
var bb = this.getVBoxObj(render).extend(BORDER_EXT, BORDER_EXT); // eslint-disable-line no-underscore-dangle
if (bb) {
var p0 = scale.obj2scaled(bb.p0, render.options);
var p1 = scale.obj2scaled(bb.p1, render.options);
return render.paper.rect(p0.x, p0.y, p1.x - p0.x, p1.y - p0.y, 0).attr(attrs);
}
};
ReRGroup.prototype.drawHighlight = function (render) {
var rgid = render.ctab.rgroups.keyOf(this);
if (!(typeof rgid === 'undefined')) {
var ret = this._draw(render, rgid, render.options.highlightStyle/* { 'fill' : 'red' }*/); // eslint-disable-line no-underscore-dangle
render.ctab.addReObjectPath('highlighting', this.visel, ret);
/*
this.getAtoms(render).each(function(aid) {
render.ctab.atoms.get(aid).drawHighlight(render);
}, this);
*/
this.item.frags.each(function (fnum, fid) {
render.ctab.frags.get(fid).drawHighlight(render);
}, this);
return ret;
} else { // eslint-disable-line no-else-return
// TODO abnormal situation, fragment does not belong to the render
}
};
ReRGroup.prototype.show = function (restruct, id, options) {
var drawing = this.draw(restruct.render, options);
for (var group in drawing) {
if (drawing.hasOwnProperty(group)) {
while (drawing[group].length > 0)
restruct.addReObjectPath(group, this.visel, drawing[group].shift(), null, true);
}
}
// TODO rgroup selection & highlighting
};
module.exports = ReRGroup;

View File

@ -0,0 +1,63 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var ReObject = require('./reobject');
var Box2Abs = require('../../util/box2abs');
var Vec2 = require('../../util/vec2');
var draw = require('../draw');
var util = require('../util');
var scale = require('../../util/scale');
function ReRxnArrow(/* chem.RxnArrow*/arrow) {
this.init('rxnArrow');
this.item = arrow;
}
ReRxnArrow.prototype = new ReObject();
ReRxnArrow.isSelectable = function () {
return true;
};
ReRxnArrow.prototype.highlightPath = function (render) {
var p = scale.obj2scaled(this.item.pp, render.options);
var s = render.options.scale;
return render.paper.rect(p.x - s, p.y - s / 4, 2 * s, s / 2, s / 8); // eslint-disable-line no-mixed-operators
};
ReRxnArrow.prototype.drawHighlight = function (render) {
var ret = this.highlightPath(render).attr(render.options.highlightStyle);
render.ctab.addReObjectPath('highlighting', this.visel, ret);
return ret;
};
ReRxnArrow.prototype.makeSelectionPlate = function (restruct, paper, styles) {
return this.highlightPath(restruct.render).attr(styles.selectionStyle);
};
ReRxnArrow.prototype.show = function (restruct, id, options) {
var render = restruct.render;
var centre = scale.obj2scaled(this.item.pp, options);
var path = draw.arrow(render.paper,
new Vec2(centre.x - options.scale, centre.y),
new Vec2(centre.x + options.scale, centre.y),
options);
var offset = options.offset;
if (offset != null)
path.translateAbs(offset.x, offset.y);
this.visel.add(path, Box2Abs.fromRelBox(util.relBox(path.getBBox())));
};
module.exports = ReRxnArrow;

View File

@ -0,0 +1,61 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var ReObject = require('./reobject');
var Box2Abs = require('../../util/box2abs');
var draw = require('../draw');
var util = require('../util');
var scale = require('../../util/scale');
function ReRxnPlus(/* chem.RxnPlus*/plus) {
this.init('rxnPlus');
this.item = plus;
}
ReRxnPlus.prototype = new ReObject();
ReRxnPlus.isSelectable = function () {
return true;
};
ReRxnPlus.prototype.highlightPath = function (render) {
var p = scale.obj2scaled(this.item.pp, render.options);
var s = render.options.scale;
/* eslint-disable no-mixed-operators*/
return render.paper.rect(p.x - s / 4, p.y - s / 4, s / 2, s / 2, s / 8);
/* eslint-enable no-mixed-operators*/
};
ReRxnPlus.prototype.drawHighlight = function (render) {
var ret = this.highlightPath(render).attr(render.options.highlightStyle);
render.ctab.addReObjectPath('highlighting', this.visel, ret);
return ret;
};
ReRxnPlus.prototype.makeSelectionPlate = function (restruct, paper, styles) { // TODO [MK] review parameters
return this.highlightPath(restruct.render).attr(styles.selectionStyle);
};
ReRxnPlus.prototype.show = function (restruct, id, options) {
var render = restruct.render;
var centre = scale.obj2scaled(this.item.pp, options);
var path = draw.plus(render.paper, centre, options);
var offset = options.offset;
if (offset != null)
path.translateAbs(offset.x, offset.y);
this.visel.add(path, Box2Abs.fromRelBox(util.relBox(path.getBBox())));
};
module.exports = ReRxnPlus;

View File

@ -0,0 +1,378 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Box2Abs = require('../../util/box2abs');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var util = require('../util');
var scale = require('../../util/scale');
var Struct = require('../../chem/struct');
var draw = require('../draw');
var ReDataSGroupData = require('./redatasgroupdata');
var ReObject = require('./reobject');
var tfx = util.tfx;
function ReSGroup(sgroup) {
this.init('sgroup');
this.item = sgroup;
}
ReSGroup.prototype = new ReObject();
ReSGroup.isSelectable = function () {
return false;
};
ReSGroup.prototype.draw = function (remol, sgroup) {
var render = remol.render;
var set = render.paper.set();
var inBonds = [],
xBonds = [];
var atomSet = Set.fromList(sgroup.atoms);
Struct.SGroup.getCrossBonds(inBonds, xBonds, remol.molecule, atomSet);
bracketPos(sgroup, render, remol.molecule, xBonds);
var bb = sgroup.bracketBox;
var d = sgroup.bracketDir;
sgroup.areas = [bb];
switch (sgroup.type) {
case 'MUL':
new SGroupdrawBrackets(set, render, sgroup, xBonds, atomSet, bb, d, sgroup.data.mul);
break;
case 'SRU':
var connectivity = sgroup.data.connectivity || 'eu';
if (connectivity == 'ht')
connectivity = '';
var subscript = sgroup.data.subscript || 'n';
new SGroupdrawBrackets(set, render, sgroup, xBonds, atomSet, bb, d, subscript, connectivity);
break;
case 'SUP':
new SGroupdrawBrackets(set, render, sgroup, xBonds, atomSet, bb, d, sgroup.data.name, null, { 'font-style': 'italic' });
break;
case 'GEN':
new SGroupdrawBrackets(set, render, sgroup, xBonds, atomSet, bb, d);
break;
case 'DAT':
set = drawGroupDat(remol, sgroup);
break;
default: break;
}
return set;
};
function SGroupdrawBrackets(set, render, sg, xbonds, atomSet, bb, d, lowerIndexText, upperIndexText, indexAttribute) { // eslint-disable-line max-params
var brackets = getBracketParameters(render.ctab.molecule, xbonds, atomSet, bb, d, render, sg.id);
var ir = -1;
for (var i = 0; i < brackets.length; ++i) {
var bracket = brackets[i];
var path = draw.bracket(render.paper, scale.obj2scaled(bracket.d, render.options),
scale.obj2scaled(bracket.n, render.options),
scale.obj2scaled(bracket.c, render.options),
bracket.w, bracket.h, render.options);
set.push(path);
if (ir < 0 || brackets[ir].d.x < bracket.d.x || (brackets[ir].d.x == bracket.d.x && brackets[ir].d.y > bracket.d.y))
ir = i;
}
var bracketR = brackets[ir];
function renderIndex(text, shift) {
var indexPos = scale.obj2scaled(bracketR.c.addScaled(bracketR.n, shift * bracketR.h), render.options);
var indexPath = render.paper.text(indexPos.x, indexPos.y, text)
.attr({
'font': render.options.font,
'font-size': render.options.fontszsub
});
if (indexAttribute)
indexPath.attr(indexAttribute);
var indexBox = Box2Abs.fromRelBox(util.relBox(indexPath.getBBox()));
var t = Math.max(Vec2.shiftRayBox(indexPos, bracketR.d.negated(), indexBox), 3) + 2;
indexPath.translateAbs(t * bracketR.d.x, t * bracketR.d.y);
set.push(indexPath);
}
if (lowerIndexText)
renderIndex(lowerIndexText, 0.5);
if (upperIndexText)
renderIndex(upperIndexText, -0.5);
}
function showValue(paper, pos, sg, options) {
var text = paper.text(pos.x, pos.y, sg.data.fieldValue)
.attr({
'font': options.font,
'font-size': options.fontsz
});
var box = text.getBBox();
var rect = paper.rect(box.x - 1, box.y - 1, box.width + 2, box.height + 2, 3, 3);
rect = sg.selected ?
rect.attr(options.selectionStyle) :
rect.attr({ fill: '#fff', stroke: '#fff' });
var st = paper.set();
st.push(
rect,
text.toFront()
);
return st;
}
function drawGroupDat(restruct, sgroup) {
const render = restruct.render;
// NB: we did not pass xbonds parameter to the backetPos method above,
// so the result will be in the regular coordinate system
bracketPos(sgroup, render, restruct.molecule);
sgroup.areas = sgroup.bracketBox ? [sgroup.bracketBox] : [];
if (sgroup.pp === null)
sgroup.pp = definePP(restruct, sgroup);
return sgroup.data.attached ? drawAttachedDat(restruct, sgroup) : drawAbsoluteDat(restruct, sgroup);
}
function definePP(restruct, sgroup) {
let topLeftPoint = sgroup.bracketBox.p1.add(new Vec2(0.5, 0.5));
const sgroups = restruct.molecule.sgroups.values();
for (let i = 0; i < restruct.molecule.sgroups.count(); ++i) {
if (!descriptorIntersects(sgroups, topLeftPoint))
break;
topLeftPoint = topLeftPoint.add(new Vec2(0, 0.5));
}
return topLeftPoint;
}
function descriptorIntersects(sgroups, topLeftPoint) {
return sgroups.some(sg => {
if (!sg.pp)
return false;
const sgBottomRightPoint = sg.pp.add(new Vec2(0.5, 0.5));
const bottomRightPoint = topLeftPoint.add(new Vec2(0.5, 0.5));
return Vec2.segmentIntersection(sg.pp, sgBottomRightPoint, topLeftPoint, bottomRightPoint);
});
}
function drawAbsoluteDat(restruct, sgroup) {
const render = restruct.render;
const options = render.options;
const paper = render.paper;
const set = paper.set();
const ps = sgroup.pp.scaled(options.scale);
const name = showValue(paper, ps, sgroup, options);
const box = util.relBox(name.getBBox());
name.translateAbs(0.5 * box.width, -0.5 * box.height);
set.push(name);
const sbox = Box2Abs.fromRelBox(util.relBox(name.getBBox()));
sgroup.dataArea = sbox.transform(scale.scaled2obj, render.options);
if (!restruct.sgroupData.has(sgroup.id))
restruct.sgroupData.set(sgroup.id, new ReDataSGroupData(sgroup));
return set;
}
function drawAttachedDat(restruct, sgroup) {
const render = restruct.render;
const options = render.options;
const paper = render.paper;
const set = paper.set();
Struct.SGroup.getAtoms(restruct, sgroup).forEach(aid => {
const atom = restruct.atoms.get(aid);
const p = scale.obj2scaled(atom.a.pp, options);
const bb = atom.visel.boundingBox;
if (bb !== null)
p.x = Math.max(p.x, bb.p1.x);
p.x += options.lineWidth; // shift a bit to the right
const nameI = showValue(paper, p, sgroup, options);
const boxI = util.relBox(nameI.getBBox());
nameI.translateAbs(0.5 * boxI.width, -0.3 * boxI.height);
set.push(nameI);
let sboxI = Box2Abs.fromRelBox(util.relBox(nameI.getBBox()));
sboxI = sboxI.transform(scale.scaled2obj, render.options);
sgroup.areas.push(sboxI);
});
return set;
}
function bracketPos(sg, render, mol, xbonds) { // eslint-disable-line max-statements
var atoms = sg.atoms;
if (!xbonds || xbonds.length !== 2) {
sg.bracketDir = new Vec2(1, 0);
} else {
var p1 = mol.bonds.get(xbonds[0]).getCenter(mol);
var p2 = mol.bonds.get(xbonds[1]).getCenter(mol);
sg.bracketDir = Vec2.diff(p2, p1).normalized();
}
var d = sg.bracketDir;
var bb = null;
var contentBoxes = [];
atoms.forEach(function (aid) {
var atom = mol.atoms.get(aid);
var bba = render ? render.ctab.atoms.get(aid).visel.boundingBox : null;
if (!bba) {
var pos = new Vec2(atom.pp);
var ext = new Vec2(0.05 * 3, 0.05 * 3);
bba = new Box2Abs(pos, pos).extend(ext, ext);
} else {
bba = bba.translate((render.options.offset || new Vec2()).negated()).transform(scale.scaled2obj, render.options);
}
contentBoxes.push(bba);
}, this);
mol.sGroupForest.children.get(sg.id).forEach(function (sgid) {
var bba = render.ctab.sgroups.get(sgid).visel.boundingBox;
bba = bba.translate((render.options.offset || new Vec2()).negated()).transform(scale.scaled2obj, render.options);
contentBoxes.push(bba);
}, this);
contentBoxes.forEach(function (bba) {
var bbb = null;
[bba.p0.x, bba.p1.x].forEach(function (x) {
[bba.p0.y, bba.p1.y].forEach(function (y) {
var v = new Vec2(x, y);
var p = new Vec2(Vec2.dot(v, d), Vec2.dot(v, d.rotateSC(1, 0)));
bbb = (bbb === null) ? new Box2Abs(p, p) : bbb.include(p);
}, this);
}, this);
bb = (bb === null) ? bbb : Box2Abs.union(bb, bbb);
}, this);
var vext = new Vec2(0.2, 0.4);
if (bb !== null) bb = bb.extend(vext, vext);
sg.bracketBox = bb;
}
function getBracketParameters(mol, xbonds, atomSet, bb, d, render, id) { // eslint-disable-line max-params
function BracketParams(c, d, w, h) {
this.c = c;
this.d = d;
this.n = d.rotateSC(1, 0);
this.w = w;
this.h = h;
}
var brackets = [];
var n = d.rotateSC(1, 0);
if (xbonds.length < 2) {
(function () {
d = d || new Vec2(1, 0);
n = n || d.rotateSC(1, 0);
var bracketWidth = Math.min(0.25, bb.sz().x * 0.3);
var cl = Vec2.lc2(d, bb.p0.x, n, 0.5 * (bb.p0.y + bb.p1.y));
var cr = Vec2.lc2(d, bb.p1.x, n, 0.5 * (bb.p0.y + bb.p1.y));
var bracketHeight = bb.sz().y;
brackets.push(new BracketParams(cl, d.negated(), bracketWidth, bracketHeight), new BracketParams(cr, d, bracketWidth, bracketHeight));
})();
} else if (xbonds.length === 2) {
(function () { // eslint-disable-line max-statements
var b1 = mol.bonds.get(xbonds[0]);
var b2 = mol.bonds.get(xbonds[1]);
var cl0 = b1.getCenter(mol);
var cr0 = b2.getCenter(mol);
var tl = -1;
var tr = -1;
var tt = -1;
var tb = -1;
var cc = Vec2.centre(cl0, cr0);
var dr = Vec2.diff(cr0, cl0).normalized();
var dl = dr.negated();
var dt = dr.rotateSC(1, 0);
var db = dt.negated();
mol.sGroupForest.children.get(id).forEach(function (sgid) {
var bba = render.ctab.sgroups.get(sgid).visel.boundingBox;
bba = bba.translate((render.options.offset || new Vec2()).negated()).transform(scale.scaled2obj, render.options);
tl = Math.max(tl, Vec2.shiftRayBox(cl0, dl, bba));
tr = Math.max(tr, Vec2.shiftRayBox(cr0, dr, bba));
tt = Math.max(tt, Vec2.shiftRayBox(cc, dt, bba));
tb = Math.max(tb, Vec2.shiftRayBox(cc, db, bba));
}, this);
tl = Math.max(tl + 0.2, 0);
tr = Math.max(tr + 0.2, 0);
tt = Math.max(Math.max(tt, tb) + 0.1, 0);
var bracketWidth = 0.25;
var bracketHeight = 1.5 + tt;
brackets.push(new BracketParams(cl0.addScaled(dl, tl), dl, bracketWidth, bracketHeight),
new BracketParams(cr0.addScaled(dr, tr), dr, bracketWidth, bracketHeight));
})();
} else {
(function () {
for (var i = 0; i < xbonds.length; ++i) {
var b = mol.bonds.get(xbonds[i]);
var c = b.getCenter(mol);
var d = Set.contains(atomSet, b.begin) ? b.getDir(mol) : b.getDir(mol).negated();
brackets.push(new BracketParams(c, d, 0.2, 1.0));
}
})();
}
return brackets;
}
ReSGroup.prototype.drawHighlight = function (render) { // eslint-disable-line max-statements
var options = render.options;
var paper = render.paper;
var sg = this.item;
var bb = sg.bracketBox.transform(scale.obj2scaled, options);
var lw = options.lineWidth;
var vext = new Vec2(lw * 4, lw * 6);
bb = bb.extend(vext, vext);
var d = sg.bracketDir,
n = d.rotateSC(1, 0);
var a0 = Vec2.lc2(d, bb.p0.x, n, bb.p0.y);
var a1 = Vec2.lc2(d, bb.p0.x, n, bb.p1.y);
var b0 = Vec2.lc2(d, bb.p1.x, n, bb.p0.y);
var b1 = Vec2.lc2(d, bb.p1.x, n, bb.p1.y);
var set = paper.set();
sg.highlighting = paper
.path('M{0},{1}L{2},{3}L{4},{5}L{6},{7}L{0},{1}', tfx(a0.x), tfx(a0.y), tfx(a1.x), tfx(a1.y), tfx(b1.x), tfx(b1.y), tfx(b0.x), tfx(b0.y))
.attr(options.highlightStyle);
set.push(sg.highlighting);
Struct.SGroup.getAtoms(render.ctab.molecule, sg).forEach(function (aid) {
set.push(render.ctab.atoms.get(aid).makeHighlightPlate(render));
}, this);
Struct.SGroup.getBonds(render.ctab.molecule, sg).forEach(function (bid) {
set.push(render.ctab.bonds.get(bid).makeHighlightPlate(render));
}, this);
render.ctab.addReObjectPath('highlighting', this.visel, set);
};
ReSGroup.prototype.show = function (restruct) {
var render = restruct.render;
var sgroup = this.item;
if (sgroup.data.fieldName !== "MRV_IMPLICIT_H") {
var remol = render.ctab;
var path = this.draw(remol, sgroup);
restruct.addReObjectPath('data', this.visel, path, null, true);
this.setHighlight(this.highlight, render); // TODO: fix this
}
};
module.exports = ReSGroup;

View File

@ -0,0 +1,62 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
// Visel is a shorthand for VISual ELement
// It corresponds to a visualization (i.e. set of paths) of an atom or a bond.
var Box2Abs = require('../../util/box2abs');
var Vec2 = require('../../util/vec2');
function Visel(type) {
this.type = type;
this.paths = [];
this.boxes = [];
this.boundingBox = null;
}
Visel.prototype.add = function (path, bb, ext) {
this.paths.push(path);
if (bb) {
this.boxes.push(bb);
this.boundingBox = this.boundingBox == null ? bb : Box2Abs.union(this.boundingBox, bb);
}
if (ext)
this.exts.push(ext);
};
Visel.prototype.clear = function () {
this.paths = [];
this.boxes = [];
this.exts = [];
this.boundingBox = null;
};
Visel.prototype.translate = function (x, y) {
if (arguments.length > 2) // TODO: replace to debug time assert
throw new Error('One vector or two scalar arguments expected');
if (y === undefined) {
this.translate(x.x, x.y);
} else {
var delta = new Vec2(x, y);
for (var i = 0; i < this.paths.length; ++i)
this.paths[i].translateAbs(x, y);
for (var j = 0; j < this.boxes.length; ++j)
this.boxes[j] = this.boxes[j].translate(delta);
if (this.boundingBox !== null)
this.boundingBox = this.boundingBox.translate(delta);
}
};
module.exports = Visel;

View File

@ -0,0 +1,33 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
function tfx(v) {
return (v - 0).toFixed(8);
}
function relBox(box) {
return {
x: box.x,
y: box.y,
width: box.width,
height: box.height
};
}
module.exports = {
tfx: tfx,
relBox: relBox
};

View File

@ -0,0 +1,44 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
export const basic = ['H', 'C', 'N', 'O', 'S', 'P',
'F', 'Cl', 'Br', 'I'];
export const atomCuts = {
"H": "h",
"C": "c",
"N": "n",
"O": "o",
"S": "s",
"P": "p",
"F": "f",
"Cl": "Shift+c",
"Br": "Shift+b",
"I": "i",
"A": "a"
};
export default Object.keys(atomCuts).reduce((res, label) => {
res[`atom-${label.toLowerCase()}`] = {
title: `Atom ${label}`,
shortcut: atomCuts[label],
action: {
tool: 'atom',
opts: { label }
}
};
return res;
}, {});

View File

@ -0,0 +1,38 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 molfile from '../../chem/molfile';
export default {
// original: for dev purposes
"force-update": {
shortcut: "Ctrl+Shift+r",
action: editor => {
editor.update(true);
}
},
"qs-serialize": {
shortcut: "Alt+Shift+r",
action: editor => {
const molStr = molfile.stringify(editor.struct());
const molQs = 'mol=' + encodeURIComponent(molStr).replace(/%20/g, '+');
const qs = document.location.search;
document.location.search = !qs ? '?' + molQs :
qs.search('mol=') === -1 ? qs + '&' + molQs :
qs.replace(/mol=[^&$]*/, molQs);
}
}
}

View File

@ -0,0 +1,184 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 tools from './tools';
import atoms from './atoms';
import zoom from './zoom';
import server from './server';
import debug from './debug';
import templates from './templates';
import { exec } from '../component/cliparea';
import { miewAction } from '../state/miew';
export default {
"new": {
shortcut: "Mod+Delete",
title: "Clear Canvas",
action: {
thunk: (dispatch, getState) => {
let editor = getState().editor;
if (!editor.struct().isBlank())
editor.struct(null);
dispatch({ type: 'ACTION', action: tools['select-lasso'].action });
}
}
},
"open": {
shortcut: "Mod+o",
title: "Open…",
action: { dialog: 'open' }
},
"save": {
shortcut: "Mod+s",
title: "Save As…",
action: { dialog: 'save' }
},
"undo": {
shortcut: "Mod+z",
title: "Undo",
action: editor => {
editor.undo();
},
disabled: editor => (
editor.historySize().undo === 0
)
},
"redo": {
shortcut: ["Mod+Shift+z", "Mod+y"],
title: "Redo",
action: editor => {
editor.redo();
},
disabled: editor => (
editor.historySize().redo === 0
)
},
"cut": {
shortcut: "Mod+x",
title: "Cut",
action: () => {
exec('cut') || dontClipMessage('Cut');
},
disabled: editor => !hasSelection(editor)
},
"copy": {
shortcut: "Mod+c",
title: "Copy",
action: () => {
exec('copy') || dontClipMessage('Copy');
},
disabled: editor => !hasSelection(editor)
},
"paste": {
shortcut: "Mod+v",
title: "Paste",
action: () => {
exec('paste') || dontClipMessage('Paste')
},
selected: ({ actions }) => (
actions && // TMP
actions.active && actions.active.tool === 'paste'
)
},
"check": {
title: "Check Structure",
action: { dialog: 'check' },
disabled: (editor, server, options) => !options.app.server
},
"analyse": {
title: "Calculated Values",
action: { dialog: 'analyse' },
disabled: (editor, server, options) => !options.app.server
},
"recognize": {
title: "Recognize Molecule",
action: { dialog: 'recognize' },
disabled: (editor, server, options) => !options.app.server
},
"miew": {
title: "3D Viewer",
action: { thunk: miewAction },
disabled: (editor, server, options) => !options.app.server || !options.app.miewPath
},
"settings": {
title: "Settings",
action: { dialog: 'settings' }
},
"help": {
shortcut: ["?", "Shift+/"],
title: "Help",
action: { dialog: 'help' }
},
"about": {
title: "About",
action: { dialog: 'about' }
},
"reaction-automap": {
title: "Reaction Auto-Mapping Tool",
action: { dialog: 'automap' },
disabled: (editor, server, options) => !options.app.server || !editor.struct().hasRxnArrow()
},
"period-table": {
title: "Periodic Table",
action: { dialog: 'period-table' }
},
"select-all": {
title: "Select All",
shortcut: "Mod+a",
action: {
thunk: (dispatch, getState) => {
getState().editor.selection('all');
dispatch({ type: 'ACTION', action: tools['select-lasso'].action });
}
}
},
"deselect-all": {
title: "Deselect All",
shortcut: "Mod+Shift+a",
action: editor => {
editor.selection(null);
}
},
"select-descriptors": {
title: "Select descriptors",
shortcut: "Mod+d",
action: {
thunk: (dispatch, getState) => {
const editor = getState().editor;
editor.alignDescriptors();
editor.selection('descriptors');
dispatch({ type: 'ACTION', action: tools['select-lasso'].action });
}
}
},
...server,
...debug,
...tools,
...atoms,
...zoom,
...templates
};
function hasSelection(editor) {
let selection = editor.selection();
return selection && // if not only sgroupData selected
(Object.keys(selection).length > 1 || !selection.sgroupData);
}
function dontClipMessage(title) {
alert('These action is unavailble via menu.\n' +
'Instead, use shortcut to ' + title + '.');
}

View File

@ -0,0 +1,58 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { serverTransform } from '../state/server';
export default {
"layout": {
shortcut: "Mod+l",
title: "Layout",
action: {
thunk: serverTransform('layout')
},
disabled: (editor, server, options) => !options.app.server
},
"clean": {
shortcut: "Mod+Shift+l",
title: "Clean Up",
action: {
thunk: serverTransform('clean')
},
disabled: (editor, server, options) => !options.app.server
},
"arom": {
title: "Aromatize",
action: {
thunk: serverTransform('aromatize')
},
disabled: (editor, server, options) => !options.app.server
},
"dearom": {
title: "Dearomatize",
action: {
thunk: serverTransform('dearomatize')
},
disabled: (editor, server, options) => !options.app.server
},
"cip": {
shortcut: "Mod+p",
title: "Calculate CIP",
action: {
thunk: serverTransform('calculateCip')
},
disabled: (editor, server, options) => !options.app.server
}
};

View File

@ -0,0 +1,39 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 templates from '../data/templates';
const templateLib = {
"template-lib": {
shortcut: "Shift+t",
title: "Custom Templates",
action: { dialog: 'templates' },
disabled: (editor, server, options) => !options.app.templates
}
};
export default templates.reduce((res, struct, i) => {
res[`template-${i}`] = {
title: `${struct.name}`,
shortcut: 't',
action: {
tool: 'template',
opts: { struct }
}
};
return res;
}, templateLib);

View File

@ -0,0 +1,142 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { bond as bondSchema } from '../structschema';
import { toBondType } from '../structconv';
const toolActions = {
"select-lasso": {
title: "Lasso Selection",
shortcut: "Escape",
action: { tool: 'select', opts: 'lasso' }
},
"select-rectangle": {
title: "Rectangle Selection",
shortcut: "Escape",
action: { tool: 'select', opts: 'rectangle' }
},
"select-fragment": {
title: "Fragment Selection",
shortcut: "Escape",
action: { tool: 'select', opts: 'fragment' }
},
"erase": {
title: "Erase",
shortcut: ["Delete", "Backspace"],
action: { tool: 'eraser', opts: 1 } // TODO last selector mode is better
},
"chain": {
title: "Chain",
action: { tool: 'chain' }
},
"chiral-flag": {
title: "Chiral Flag",
action: { tool: 'chiralFlag' },
selected: editor => editor.struct().isChiral
},
"charge-plus": {
shortcut: "5",
title: "Charge Plus",
action: { tool: 'charge', opts: 1 }
},
"charge-minus": {
shortcut: "5",
title: "Charge Minus",
action: { tool: 'charge', opts: -1 }
},
"transform-rotate": {
shortcut: "Alt+r",
title: "Rotate Tool",
action: { tool: 'rotate' }
},
"transform-flip-h": {
shortcut: "Alt+h",
title: "Horizontal Flip",
action: { tool: 'rotate', opts: 'horizontal' }
},
"transform-flip-v": {
shortcut: "Alt+v",
title: "Vertical Flip",
action: { tool: 'rotate', opts: 'vertical' }
},
"sgroup": {
shortcut: "Mod+g",
title: "S-Group",
action: { tool: 'sgroup' }
},
"sgroup-data": {
shortcut: "Mod+g",
title: "Data S-Group",
action: { tool: 'sgroup', opts: 'DAT' }
},
"reaction-arrow": {
title: "Reaction Arrow Tool",
action: { tool: 'reactionarrow' }
},
"reaction-plus": {
title: "Reaction Plus Tool",
action: { tool: 'reactionplus' }
},
"reaction-map": {
title: "Reaction Mapping Tool",
action: { tool: 'reactionmap' }
},
"reaction-unmap": {
title: "Reaction Unmapping Tool",
action: { tool: 'reactionunmap' }
},
"rgroup-label": {
shortcut: "Mod+r",
title: "R-Group Label Tool",
action: { tool: 'rgroupatom' }
},
"rgroup-fragment": {
shortcut: ["Mod+Shift+r", "Mod+r"],
title: "R-Group Fragment Tool",
action: { tool: 'rgroupfragment' }
},
"rgroup-attpoints": {
shortcut: "Mod+r",
title: "Attachment Point Tool",
action: { tool: 'apoint' }
},
};
const bondCuts = {
"single": "1",
"double": "2",
"triple": "3",
"up": "1",
"down": "1",
"updown": "1",
"crossed": "2",
"any": "0",
"aromatic": "4",
};
const typeSchema = bondSchema.properties.type;
export default typeSchema.enum.reduce((res, type, i) => {
res[`bond-${type}`] = {
title: `${typeSchema.enumNames[i]} Bond`,
shortcut: bondCuts[type],
action: {
tool: 'bond',
opts: toBondType(type)
}
};
return res;
}, toolActions);

View File

@ -0,0 +1,56 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { findIndex, findLastIndex } from 'lodash/fp';
export const zoomList = [
0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1,
1.1, 1.2, 1.3, 1.4, 1.5, 1.7, 2, 2.5, 3, 3.5, 4
];
export default {
"zoom": {
selected: editor => editor.zoom()
},
"zoom-out": {
shortcut: ["-", "_", "Shift+-"],
title: "Zoom Out",
disabled: editor => (
editor.zoom() <= zoomList[0] // unsave
),
action: editor => {
let zoom = editor.zoom();
let i = findIndex(z => z >= zoom, zoomList);
editor.zoom(
zoomList[(zoomList[i] === zoom && i > 0) ? i - 1 : i]
);
}
},
"zoom-in": {
shortcut: ["+", "=", "Shift+="],
title: "Zoom In",
disabled: editor => (
zoomList[zoomList.length - 1] <= editor.zoom()
),
action: editor => {
let zoom = editor.zoom();
let i = findLastIndex(z => z <= zoom, zoomList);
editor.zoom(
zoomList[(zoomList[i] === zoom && i < zoomList.length - 1) ? i + 1 : i]
);
}
}
}

View File

@ -0,0 +1,138 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { Provider, connect } from 'preact-redux';
import { omit } from 'lodash/fp';
import state, { onAction, load } from './state';
import { initTmplLib } from './state/templates';
import { initEditor } from './state/editor';
import { checkServer } from './state/server';
import { initKeydownListener, initClipboard } from './state/hotkeys';
import { initResize } from './state/toolbar';
import { h, Component, render } from 'preact';
/** @jsx h */
import Toolbar from './toolbar';
import StructEditor from './component/structeditor';
import ClipArea from './component/cliparea';
import modals from './dialog';
const AppEditor = connect(
state => ({
options: state.options.settings
}),
dispatch => dispatch(initEditor)
)(StructEditor);
const AppModal = connect(
state => ({
modal: state.modal
}),
dispatch => ({
onOk: function (res) {
console.info('Output:', res);
dispatch({ type: 'MODAL_CLOSE' });
},
onCancel: function () {
dispatch({ type: 'MODAL_CLOSE' });
}
}),
(stateProps, dispatchProps) => {
let prop = stateProps.modal && stateProps.modal.prop;
let initProps = prop ? omit(['onResult', 'onCancel'], prop) : {};
return {
modal: stateProps.modal,
...initProps,
onOk: function (res) {
if (prop && prop.onResult) prop.onResult(res);
dispatchProps.onOk(res);
},
onCancel: function () {
if (prop && prop.onCancel) prop.onCancel();
dispatchProps.onCancel();
}
};
}
)(({modal, ...props}) => {
if (!modal)
return null;
let Modal = modals[modal.name];
if (!Modal)
throw new Error(`There is no modal window named ${modal.name}`);
return (
<div className="overlay">
<Modal {...props}/>
</div>
);
});
const AppTemplates = connect(
null,
dispatch => ({
onInitTmpls: (cacheEl) => initTmplLib(dispatch, '', cacheEl)
})
)(class extends Component {
componentDidMount() {
this.props.onInitTmpls(this.cacheEl);
}
render = () => (<div className="cellar" ref={c => this.cacheEl = c} />)
});
const AppCliparea = connect(
null,
dispatch => (dispatch(initClipboard))
)(ClipArea);
const App = connect(
null,
{ onAction, checkServer }
)(class extends Component {
componentDidMount() {
this.props.checkServer();
}
render = props => (
<main role="application">
<AppEditor id="canvas" />
<Toolbar {...props}/>
<AppCliparea/>
<AppModal/>
<AppTemplates/>
</main>
)
});
function init(el, options, server) {
const store = state(options, server);
store.dispatch(initKeydownListener(el));
store.dispatch(initResize());
render((
<Provider store={store}>
<App/>
</Provider>
), el);
return {
load: (structStr, options) => store.dispatch(load(structStr, options))
}
}
export default init;

View File

@ -0,0 +1,50 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 {h, Component} from 'preact';
/** @jsx h */
class Accordion extends Component {
constructor(props) {
super(props);
this.state.active = props.active ? props.active : {};
}
onActive(index) {
let newActive = {};
newActive[index] = !this.state.active[index];
this.setState({ active: Object.assign(this.state.active, newActive)});
if (this.props.onActive) this.props.onActive();
}
render() {
let {children, captions, ...props} = this.props;
return (
<ul {...props}>
{ captions.map((caption, index) => (
<li className="tab">
<a className={this.state.active[index] ? 'active' : ''}
onClick={() => this.onActive(index)}>
{caption}
</a>
{this.state.active[index] ? children[index] : null }
</li>
)) }
</ul>
);
}
}
export default Accordion;

View File

@ -0,0 +1,99 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h } from 'preact';
/** @jsx h */
import classNames from 'classnames';
import action from '../action';
import { hiddenAncestor } from '../state/toolbar';
const isMac = /Mac/.test(navigator.platform);
const shortcutAliasMap = {
'Escape': 'Esc',
'Delete': 'Del',
'Mod': isMac ? '⌘' : 'Ctrl'
};
export function shortcutStr(shortcut) {
const key = Array.isArray(shortcut) ? shortcut[0] : shortcut;
return key.replace(/(\b[a-z]\b$|Mod|Escape|Delete)/g, function (key) {
return shortcutAliasMap[key] || key.toUpperCase();
});
}
function ActionButton({action, status={}, onAction, ...props}) {
let shortcut = action.shortcut && shortcutStr(action.shortcut);
return (
<button disabled={status.disabled}
onClick={(ev) => {
if (!status.selected || action.action.tool === 'chiralFlag') {
onAction(action.action);
ev.stopPropagation();
}
} }
title={shortcut ? `${action.title} (${shortcut})` : action.title}>
{action.title}
</button>
)
}
function ActionMenu({name, menu, className, role, ...props}) {
return (
<menu className={className} role={role}
style={toolMargin(name, menu, props.visibleTools)}>
{
menu.map(item => (
<li id={item.id || item}
className={classNames(props.status[item]) + ` ${item.id === props.opened ? 'opened' : ''}`}
onClick={(ev) => openHandle(ev, props.onOpen) }>
{ typeof item !== 'object' ?
( <ActionButton {...props} action={action[item]}
status={props.status[item]} /> ) :
item.menu ?
( <ActionMenu {...props} name={item.id} menu={item.menu} /> ) :
item.component(props)
}
</li>
))
}
</menu>
);
}
function toolMargin(menuName, menu, visibleTools) {
if (!visibleTools[menuName]) return {};
let iconHeight = (window.innerHeight < 600 || window.innerWidth < 1040) ? 32 : 40;
// now not found better way
let index = menu.indexOf(visibleTools[menuName]); // first level
if (index === -1) {
let tools = [];
menu.forEach(item => tools = tools.concat(item.menu));
index = tools.indexOf(visibleTools[menuName]); // second level. example: `bond: bond-any`
}
return (index !== -1) ? { marginTop: -(iconHeight * index) + 'px' } : {};
}
function openHandle(event, onOpen) {
let hiddenEl = hiddenAncestor(event.currentTarget);
if (hiddenEl) onOpen(hiddenEl.id);
event.stopPropagation();
}
export default ActionMenu;

View File

@ -0,0 +1,41 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h } from 'preact';
/** @jsx h */
import element from '../../chem/element';
const metPrefix = ['alkali', 'alkaline-earth', 'transition',
'post-transition']; // 'lanthanide', 'actinide'
function atomClass(el) {
let own = `atom-${el.label.toLowerCase()}`;
let type = metPrefix.indexOf(el.type) >= 0 ? `${el.type} metal` :
(el.type || 'unknown-props');
return [own, type, el.state || 'unknown-state', el.origin];
}
function Atom({el, shortcut, className, ...props}) {
return (
<button title={shortcut ? `${el.title} (${shortcut})` : el.title}
className={[...atomClass(el), className].join(' ')}
value={element.map[el.label]} {...props}>
{el.label}
</button>
);
}
export default Atom;

View File

@ -0,0 +1,136 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
const ieCb = window.clipboardData;
class ClipArea extends Component {
shouldComponentUpdate() {
return false;
}
componentDidMount() {
const el = this.refs ? this.refs.base : this.base;
this.target = this.props.target || el.parentNode;
this.listeners = {
'mouseup': event => {
if (this.props.focused() && !isFormElement(event.target))
autofocus(el);
},
'copy': event => {
if (this.props.focused() && this.props.onCopy) {
const data = this.props.onCopy();
if (data)
copy(event.clipboardData, data);
event.preventDefault();
}
},
'cut': event => {
if (this.props.focused() && this.props.onCut) {
const data = this.props.onCut();
if (data)
copy(event.clipboardData, data);
event.preventDefault();
}
},
'paste': event => {
if (this.props.focused() && this.props.onPaste) {
const data = paste(event.clipboardData, this.props.formats);
if (data)
this.props.onPaste(data);
event.preventDefault();
}
}
};
Object.keys(this.listeners).forEach(en => {
this.target.addEventListener(en, this.listeners[en]);
});
}
componentWillUnmount() {
Object.keys(this.listeners).forEach(en => {
this.target.removeEventListener(en, this.listeners[en]);
});
}
render() {
return (
<textarea className="cliparea" contentEditable={true}
autoFocus={true}/>
);
}
}
function isFormElement(el) {
if (el.tagName === 'INPUT' && el.type === 'button') return false;
return ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(el.tagName) > -1;
}
function autofocus(cliparea) {
cliparea.value = ' ';
cliparea.focus();
cliparea.select();
}
function copy(cb, data) {
if (!cb && ieCb) {
ieCb.setData('text', data['text/plain']);
} else {
cb.setData('text/plain', data['text/plain']);
try {
Object.keys(data).forEach(function (fmt) {
cb.setData(fmt, data[fmt]);
});
} catch (ex) {
console.info('Could not write exact type', ex);
}
}
}
function paste(cb, formats) {
let data = {};
if (!cb && ieCb) {
data['text/plain'] = ieCb.getData('text');
} else {
data['text/plain'] = cb.getData('text/plain');
data = formats.reduce(function (data, fmt) {
const d = cb.getData(fmt);
if (d)
data[fmt] = d;
return data;
}, data);
}
return data;
}
export const actions = ['cut', 'copy', 'paste'];
export function exec(action) {
let enabled = document.queryCommandSupported(action);
if (enabled) try {
enabled = document.execCommand(action) || ieCb;
} catch (ex) {
// FF < 41
enabled = false;
}
return enabled;
}
export default ClipArea;

View File

@ -0,0 +1,75 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
class ComboBox extends Component {
constructor(props) {
super(props);
this.state = {
suggestsHidden: true
};
this.click = this.click.bind(this);
this.blur = this.blur.bind(this);
this.updateInput = this.updateInput.bind(this);
}
updateInput(event) {
const value = (event.target.value || event.target.textContent);
this.setState({ suggestsHidden: true });
this.props.onChange(value);
}
click() {
this.setState({ suggestsHidden: false });
}
blur() {
this.setState({ suggestsHidden: true });
}
render(props) {
const { value, type = 'text', schema } = props;
const suggestList = schema.enumNames
.filter(item => item !== value)
.map(item => <li onMouseDown={this.updateInput}>{item}</li>);
return (
<div>
<input type={type} value={value} onClick={this.click}
onBlur={this.blur} onInput={this.updateInput} autocomplete="off"
/>
{
suggestList.length !== 0 ?
(
<ui className='suggestList'
style={`display: ${this.state.suggestsHidden ? 'none' : 'block'}`}
>
{
suggestList
}
</ui>
) : ''
}
</div>
);
}
}
export default ComboBox;

View File

@ -0,0 +1,82 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
import keyName from 'w3c-keyname';
class Dialog extends Component {
exit(mode) {
let { params, result=() => null,
valid=() => !!result() } = this.props;
let key = (mode === 'OK') ? 'onOk' : 'onCancel';
if (params && key in params && (key !== 'onOk' || valid()) )
params[key](result());
}
keyDown(ev) {
let key = keyName(ev);
let active = document.activeElement;
let activeTextarea = active && active.tagName === 'TEXTAREA';
if (key === 'Escape' || key === 'Enter' && !activeTextarea) {
this.exit(key === 'Enter' ? 'OK': 'Cancel');
ev.preventDefault();
}
ev.stopPropagation();
}
componentDidMount() {
const fe = this.base.querySelector(['input:not([type=checkbox]):not([type=button])', 'textarea',
'[contenteditable]','select'].join(',')) ||
this.base.querySelector(['button.close'].join(','));
console.assert(fe, 'No active buttons');
if (fe.focus) fe.focus();
}
componentWillUnmount() {
(document.querySelector('.cliparea') || document.body).focus();
}
render() {
let {
children, title, params = {},
result = () => null, valid = () => !!result(), // Hmm, dublicate.. No simple default props
buttons = ["Cancel", "OK"], ...props
} = this.props; // see: https://git.io/v1KR6
return (
<form role="dialog" onSubmit={ev => ev.preventDefault()}
onKeyDown={ev => this.keyDown(ev)} tabIndex="-1" {...props}>
<header>{title}
{params.onCancel && title && (
<button className="close"
onClick={() => this.exit('Cancel')}>×
</button> )
}
</header>
{children}
<footer>{
buttons.map(b => (
typeof b !== 'string' ? b :
<input type="button" value={b}
disabled={b === 'OK' && !valid()}
onClick={() => this.exit(b)}/>
))
}</footer>
</form>
);
}
}
export default Dialog;

View File

@ -0,0 +1,196 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 jsonschema from 'jsonschema';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import Input from './input';
import { updateFormState } from '../state/form';
class Form extends Component {
constructor({ onUpdate, schema, init, ...props }) {
super();
this.schema = propSchema(schema, props);
if (init) {
let { valid, errors } = this.schema.serialize(init);
const errs = getErrorsObj(errors);
init = Object.assign({}, init, { init: true });
onUpdate(init, valid, errs);
}
}
updateState(newstate) {
const { instance, valid, errors } = this.schema.serialize(newstate);
const errs = getErrorsObj(errors);
this.props.onUpdate(instance, valid, errs);
}
getChildContext() {
const { schema } = this.props;
return { schema, stateStore: this };
}
field(name, onChange) {
const { result, errors } = this.props;
const value = result[name];
const self = this;
return {
dataError: errors && errors[name] || false,
value: value,
onChange(value) {
const newstate = Object.assign({}, self.props.result, { [name]: value });
self.updateState(newstate);
if (onChange) onChange(value);
}
};
}
render(props) {
const { result, children, schema, ...prop } = props;
if (schema.key && schema.key !== this.schema.key) {
this.schema = propSchema(schema, prop);
this.schema.serialize(result); // hack: valid first state
this.updateState(result);
}
return (
<form {...prop}>
{children}
</form>
);
}
}
Form = connect(
null,
dispatch => ({
onUpdate: function (result, valid, errors) {
dispatch(updateFormState({ result, valid, errors }));
}
})
)(Form);
function Label({ labelPos, title, children, ...props }) {
return (
<label {...props}>{ title && labelPos !== 'after' ? `${title}:` : '' }
{children}
{ title && labelPos === 'after' ? title : '' }
</label>
);
}
class Field extends Component {
render(props) {
const { name, onChange, className, component, ...prop } = props;
const { schema, stateStore } = this.context;
const desc = prop.schema || schema.properties[name];
const { dataError, ...fieldOpts } = stateStore.field(name, onChange);
return (
<Label className={className} data-error={dataError} title={prop.title || desc.title} >
{
component ?
h(component, { ...fieldOpts, ...prop }) :
<Input name={name} schema={desc}
{...fieldOpts} {...prop}/>
}
</Label>
);
}
}
const SelectOneOf = (props) => {
const { title, name, schema, ...prop } = props;
const selectDesc = {
title: title,
enum: [],
enumNames: []
};
Object.keys(schema).forEach(item => {
selectDesc.enum.push(item);
selectDesc.enumNames.push(schema[item].title || item);
});
return <Field name={name} schema={selectDesc} {...prop}/>;
};
////
function propSchema(schema, { customValid, serialize = {}, deserialize = {} }) {
const v = new jsonschema.Validator();
if (customValid) {
schema = Object.assign({}, schema); // copy
schema.properties = Object.keys(customValid).reduce((res, prop) => {
v.customFormats[prop] = customValid[prop];
res[prop] = { format: prop, ...res[prop] };
return res;
}, schema.properties);
}
return {
key: schema.key || '',
serialize: inst => v.validate(inst, schema, {
rewrite: serializeRewrite.bind(null, serialize)
}),
deserialize: inst => v.validate(inst, schema, {
rewrite: deserializeRewrite.bind(null, deserialize)
})
};
}
function serializeRewrite(serializeMap, instance, schema) {
const res = {};
if (typeof instance !== 'object' || !schema.properties) {
return instance !== undefined ? instance :
schema.default;
}
for (let p in schema.properties) {
if (schema.properties.hasOwnProperty(p) && (p in instance)) {
res[p] = instance[serializeMap[p]] || instance[p];
}
}
return res;
}
function deserializeRewrite(deserializeMap, instance, schema) {
return instance;
}
function getErrorsObj(errors) {
let errs = {};
let field;
errors.forEach(item => {
field = item.property.split('.')[1];
if (!errs[field])
errs[field] = item.schema.invalidMessage || item.message;
});
return errs;
}
export { Form, Field, SelectOneOf };

View File

@ -0,0 +1,232 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
function GenericInput({ value, onChange, type = "text", ...props }) {
return (
<input type={type} value={value} onInput={onChange} {...props} />
);
}
GenericInput.val = function (ev, schema) {
const input = ev.target;
const isNumber = (input.type === 'number' || input.type === 'range') ||
(schema && (schema.type === 'number' || schema.type === 'integer'));
const value = isNumber ? input.value.replace(/,/g, '.') : input.value;
return (isNumber && !isNaN(value - 0)) ? value - 0 : value;
};
function TextArea({ value, onChange, ...props }) {
return (
<textarea value={value} onInput={onChange} {...props}/>
);
}
TextArea.val = (ev) => ev.target.value;
function CheckBox({ value, onChange, ...props }) {
return (
<input type="checkbox" checked={value} onClick={onChange} {...props} />
);
}
CheckBox.val = function (ev) {
ev.stopPropagation();
return !!ev.target.checked;
};
function Select({ schema, value, selected, onSelect, ...props }) {
return (
<select onChange={onSelect} {...props}>
{
enumSchema(schema, (title, val) => (
<option selected={selected(val, value)}
value={typeof val !== 'object' && val}>
{title}
</option>
))
}
</select>
);
}
Select.val = function (ev, schema) {
const select = ev.target;
if (!select.multiple)
return enumSchema(schema, select.selectedIndex);
return [].reduce.call(select.options, function (res, o, i) {
return !o.selected ? res :
[enumSchema(schema, i), ...res];
}, []);
};
function FieldSet({ schema, value, selected, onSelect, type = "radio", ...props }) {
return (
<fieldset onClick={onSelect} className="radio">
{
enumSchema(schema, (title, val) => (
<label>
<input type={type} checked={selected(val, value)}
value={typeof val !== 'object' && val}
{...props}/>
{title}
</label>
))
}
</fieldset>
);
}
FieldSet.val = function (ev, schema) {
const input = ev.target;
if (ev.target.tagName !== 'INPUT') {
ev.stopPropagation();
return undefined;
}
// Hm.. looks like premature optimization
// should we inline this?
const fieldset = input.parentNode.parentNode;
const res = [].reduce.call(fieldset.querySelectorAll('input'),
function (res, inp, i) {
return !inp.checked ? res :
[enumSchema(schema, i), ...res];
}, []);
return input.type === 'radio' ? res[0] : res;
};
function enumSchema(schema, cbOrIndex) {
const isTypeValue = Array.isArray(schema);
if (!isTypeValue && schema.items)
schema = schema.items;
if (typeof cbOrIndex === 'function') {
return (isTypeValue ? schema : schema.enum).map((item, i) => {
const title = isTypeValue ? item.title :
schema.enumNames && schema.enumNames[i];
return cbOrIndex(title !== undefined ? title : item,
item.value !== undefined ? item.value : item);
});
}
if (!isTypeValue)
return schema.enum[cbOrIndex];
const res = schema[cbOrIndex];
return res.value !== undefined ? res.value : res;
}
function inputCtrl(component, schema, onChange) {
let props = {};
if (schema) {
// TODO: infer maxLength, min, max, step, etc
if (schema.type === 'number' || schema.type === 'integer')
props = { type: 'text' };
}
return {
onChange: function (ev) {
const val = !component.val ? ev :
component.val(ev, schema);
onChange(val);
},
...props
};
}
function singleSelectCtrl(component, schema, onChange) {
return {
selected: (testVal, value) => (value === testVal),
onSelect: function (ev, value) {
const val = !component.val ? ev :
component.val(ev, schema);
if (val !== undefined)
onChange(val);
}
};
}
function multipleSelectCtrl(component, schema, onChange) {
return {
multiple: true,
selected: (testVal, values) =>
(values && values.indexOf(testVal) >= 0),
onSelect: function (ev, values) {
if (component.val) {
let val = component.val(ev, schema);
if (val !== undefined)
onChange(val);
} else {
const i = values ? values.indexOf(ev) : -1;
if (i < 0)
onChange(values ? [ev, ...values] : [ev]);
else
onChange([...values.slice(0, i),
...values.slice(i + 1)]);
}
}
};
}
function ctrlMap(component, { schema, multiple, onChange }) {
if (!schema || !schema.enum && !schema.items && !Array.isArray(schema) || schema.type === 'string')
return inputCtrl(component, schema, onChange);
if (multiple || schema.type === 'array')
return multipleSelectCtrl(component, schema, onChange);
return singleSelectCtrl(component, schema, onChange);
}
function componentMap({ schema, type, multiple }) {
if (!schema || !schema.enum && !schema.items && !Array.isArray(schema)) {
if (type === 'checkbox' || schema && schema.type === 'boolean')
return CheckBox;
return (type === 'textarea') ? TextArea : GenericInput;
}
if (multiple || schema.type === 'array')
return (type === 'checkbox') ? FieldSet : Select;
return (type === 'radio') ? FieldSet : Select;
}
function shallowCompare(a, b) {
for (let i in a) if (!(i in b)) return true;
for (let i in b) if (a[i] !== b[i]) { return true; }
return false;
}
export default class Input extends Component {
constructor({ component, ...props }) {
super(props);
this.component = component || componentMap(props);
this.ctrl = ctrlMap(this.component, props);
}
shouldComponentUpdate({ children, onChange, ...nextProps }) {
var { children, onChange, ...oldProps } = this.props;
return shallowCompare(oldProps, nextProps);
}
render() {
var { children, onChange, ...props } = this.props;
return h(this.component, { ...this.ctrl, ...props });
}
}

View File

@ -0,0 +1,71 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
import Input from './input';
class MeasureInput extends Component {
constructor(props) {
super(props);
this.state = { meas: 'px' };
}
handleChange(value, onChange) {
const convValue = convertValue(value, this.state.meas, 'px');
this.state.cust = value;
onChange(convValue);
}
render() {
const { meas, cust } = this.state;
const { schema, value, onChange, ...props } = this.props;
if (convertValue(cust, meas, 'px') !== value)
this.setState({ meas: 'px', cust: value }); // Hack: New store (RESET)
return (
<div style="display: inline-flex;" {...props}>
<Input schema={schema} step={meas === 'px' || meas === 'pt' ? '1' : '0.001'} style="width: 75%;"
value={cust} onChange={(v) => this.handleChange(v, onChange)} />
<Input schema={{ enum: ['cm', 'px', 'pt', 'inch'] }} style="width: 25%;"
value={meas}
onChange={(m) => this.setState({
meas: m,
cust: convertValue(this.state.cust, this.state.meas, m)
})} />
</div>
);
}
}
const measureMap = {
'px': 1,
'cm': 37.795278,
'pt': 1.333333,
'inch': 96,
};
function convertValue(value, measureFrom, measureTo) {
if (!value && value !== 0 || isNaN(value)) return null;
return (measureTo === 'px' || measureTo === 'pt')
? (value * measureMap[measureFrom] / measureMap[measureTo]).toFixed( ) - 0
: (value * measureMap[measureFrom] / measureMap[measureTo]).toFixed(3) - 0;
}
export default MeasureInput;

View File

@ -0,0 +1,109 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
class OpenButton extends Component {
constructor(props) {
super(props);
if (props.server) {
fileOpener(props.server).then(opener => {
this.setState({opener});
});
}
}
open(ev) {
const files = ev.target.files;
const noop = () => null;
const { onLoad = noop, onError = noop } = this.props;
if (this.state.opener && files.length) {
this.state.opener(files[0]).then(onLoad, onError);
} else if (files.length)
onLoad(files[0]);
ev.target.value = null;
ev.preventDefault();
}
render() {
const { children, type, ...props } = this.props;
return (
<div { ...props }>
<input id="input-file" onChange={ ev => this.open(ev) }
accept={ type } type="file"/>
<label for="input-file">
{ children }
</label>
</div>
);
}
}
function fileOpener (server) {
return new Promise((resolve, reject) => {
// TODO: refactor return
if (global.FileReader)
resolve(throughFileReader);
else if (global.ActiveXObject) {
try {
const fso = new ActiveXObject('Scripting.FileSystemObject');
resolve(file => Promise.resolve(throughFileSystemObject(fso, file)));
} catch (e) {
reject(e);
}
} else if (server) {
resolve(server.then(() => {
throw "Server doesn't still support echo method";
//return resolve(throughForm2IframePosting);
}));
} else
reject(new Error("Your browser does not support " +
"opening files locally"));
});
}
function throughFileReader(file) {
return new Promise((resolve, reject) => {
const rd = new FileReader();
rd.onload = () => {
const content = rd.result;
if (file.msClose)
file.msClose();
resolve(content);
};
rd.onerror = event => {
reject(event);
};
rd.readAsText(file, 'UTF-8');
});
}
function throughFileSystemObject(fso, file) {
// IE9 and below
const fd = fso.OpenTextFile(file.name, 1),
content = fd.ReadAll();
fd.Close();
return content;
}
export default OpenButton;

View File

@ -0,0 +1,77 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
import fs from 'filesaver.js';
class SaveButton extends Component {
constructor({filename="unnamed", type="text/plain", className='', ...props}) {
super({filename, type, className, ...props});
fileSaver(props.server).then(saver => {
this.setState({saver});
});
}
save(ev) {
const noop = () => null;
const { filename, data, type, onSave = noop, onError = noop } = this.props;
if (this.state.saver && data)
try {
this.state.saver(data, filename, type);
onSave();
}
catch(e) {
onError(e);
}
ev.preventDefault();
}
render() {
let { children, filename, data, className, ...props } = this.props;
if (!this.state.saver || !data)
className = `disabled ${className}`;
return (
<a download={filename} onClick={ev => this.save(ev)}
className={className} {...props}>
{ children }
</a>
);
}
}
function fileSaver(server) {
return new Promise((resolve, reject) => {
if (global.Blob && fs.saveAs) {
resolve((data, fn, type) => {
const blob = new Blob([data], { type });
fs.saveAs(blob, fn);
});
} else if (server) {
resolve(server.then(() => {
throw "Server doesn't still support echo method";
}));
} else
reject(new Error("Your browser does not support " +
"opening files locally"));
});
}
export default SaveButton;

View File

@ -0,0 +1,40 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h } from 'preact';
/** @jsx h */
function SelectList({ schema, value, onSelect, splitIndexes, ...props }) {
return (
<ul {...props}>{
schema.enum.map((opt, index) => (
<li onClick={() => onSelect(opt, index) }
className={
(opt === value ? 'selected ' : '') +
(isSplitIndex(index, splitIndexes) ? 'split' : '')
}>
{schema.enumNames ? schema.enumNames[index] : opt}
</li>
))
}</ul>
);
}
function isSplitIndex(index, splitIndexes) {
return index > 0 && splitIndexes && splitIndexes.includes(index);
}
export default SelectList;

View File

@ -0,0 +1,26 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h } from 'preact';
/** @jsx h */
function Spin({...props}) {
return (
<div className="spinner" {...props}></div>
);
}
export default Spin;

View File

@ -0,0 +1,78 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { upperFirst } from 'lodash/fp';
import { h, Component } from 'preact';
/** @jsx h */
import Editor from '../../editor'
function setupEditor(editor, props, oldProps = {}) {
const { struct, tool, toolOpts, options } = props;
if (struct !== oldProps.struct)
editor.struct(struct);
if (tool !== oldProps.tool || toolOpts !== oldProps.toolOpts)
editor.tool(tool, toolOpts);
if (oldProps.options && options !== oldProps.options)
editor.options(options);
// update handlers
for (let name in editor.event) {
if (!editor.event.hasOwnProperty(name))
continue;
let eventName = `on${upperFirst(name)}`;
if (props[eventName] !== oldProps[eventName]) {
console.info('update editor handler', eventName);
if (oldProps[eventName])
editor.event[name].remove(oldProps[eventName]);
if (props[eventName])
editor.event[name].add(props[eventName]);
}
}
}
class StructEditor extends Component {
shouldComponentUpdate() {
return false;
}
componentWillReceiveProps(props) {
setupEditor(this.instance, props, this.props);
}
componentDidMount() {
console.assert(this.base, "No backing element");
this.instance = new Editor(this.base, { ...this.props.options });
setupEditor(this.instance, this.props);
if (this.props.onInit)
this.props.onInit(this.instance);
}
render () {
let { Tag="div", struct, tool, toolOpts, options, ...props } = this.props;
return (
<Tag onMouseDown={ev => ev.preventDefault()} {...props} />
);
}
}
export default StructEditor;

View File

@ -0,0 +1,71 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
import Struct from '../../chem/struct';
import molfile from '../../chem/molfile';
import Render from '../../render';
function renderStruct(el, struct, options={}) {
if (el) {
if (struct.prerender) // Should it sit here?
el.innerHTML = struct.prerender;
else {
console.info('render!', el.clientWidth, el.clientWidth);
const rnd = new Render(el, {
autoScale: true,
...options
});
rnd.setMolecule(struct);
rnd.update();
// console.info('render!');//, el.innerHTML);
// struct.prerender = el.innerHTML;
}
}
}
class StructRender extends Component {
constructor(props) {
super(props);
if (!(props.struct instanceof Struct)) try {
this.props.struct = molfile.parse(props.struct);
} catch (e) {
alert("Could not parse structure\n" + e);
this.props.struct = null;
}
}
shouldComponentUpdate() {
return false;
}
componentDidMount() {
const el = this.refs ? this.refs.base : this.base;
const { struct, options } = this.props;
renderStruct(el, struct, options);
}
render () {
let { struct, Tag="div", ...props } = this.props;
return (
<Tag /*ref="el"*/ {...props}>{ struct ? null : 'No molecule' }</Tag>
);
}
}
export default StructRender;

View File

@ -0,0 +1,88 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
import FontFaceObserver from "font-face-observer";
import Input from './input';
/** @jsx h */
const commonFonts = [
"Arial",
"Arial Black",
"Comic Sans MS",
"Courier New",
"Georgia",
"Impact", "Charcoal",
"Lucida Console", "Monaco",
"Palatino Linotype", "Book Antiqua", "Palatino",
"Tahoma", "Geneva",
"Times New Roman", "Times",
"Verdana",
"Symbol",
"MS Serif", "MS Sans Serif", "New York",
"Droid Sans", "Droid Serif", "Droid Sans Mono", "Roboto"
];
function checkInSystem() {
const availableFontsPromises = commonFonts.map((fontName) => {
const observer = new FontFaceObserver(fontName);
return observer.check().then(() => fontName, () => null);
});
return Promise.all(availableFontsPromises);
}
let cache = null;
class SystemFonts extends Component {
constructor(props) {
super(props);
this.state = { availableFonts: [subfontname(props.value)] };
this.setAvailableFonts();
}
setAvailableFonts() {
cache ? this.setState({ availableFonts: cache }) :
checkInSystem().then((results) => {
cache = results.filter((i) => i !== null);
this.setState({ availableFonts: cache });
});
}
render() {
const {...props} = this.props;
const desc = {
enum: [],
enumNames: []
};
this.state.availableFonts.forEach((font) => {
desc.enum.push(`30px ${font}`);
desc.enumNames.push(font);
});
return desc.enum.length !== 1
? <Input schema={desc} {...props} />
: <select><option>{desc.enumNames[0]}</option></select>;
}
}
function subfontname(name) {
return name.substring(name.indexOf('px ') + 3);
}
export default SystemFonts;

View File

@ -0,0 +1,53 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 {h, Component} from 'preact';
/** @jsx h */
class Tabs extends Component {
constructor(props) {
super(props);
this.state.tabIndex = props.tabIndex || 0;
this.props.changeTab(this.state.tabIndex);
}
changeTab(ev, index) {
this.setState({ tabIndex: index });
if (this.props.changeTab)
this.props.changeTab(index);
}
render() {
const {children, captions, ...props} = this.props;
return (
<ul {...props}>
<li className="tabs">
{ captions.map((caption, index) => (
<a className={this.state.tabIndex === index ? 'active' : ''}
onClick={ ev => this.changeTab(ev, index)}>
{caption}
</a>
)) }
</li>
<li className="tabs-content">
{ children[this.state.tabIndex] }
</li>
</ul>
);
}
}
export default Tabs;

View File

@ -0,0 +1,101 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h, Component } from 'preact';
/** @jsx h */
const STYLE_INNER = 'position:relative; overflow:hidden; width:100%; min-height:100%;';
const STYLE_CONTENT = 'position:absolute; top:0; left:0; height:100%; width:100%; overflow:visible;';
export default class VirtualList extends Component {
constructor(props) {
super(props);
this.state = {
offset: 0,
height: 0
};
}
resize = (ev, reset) => {
const height = this.base.offsetHeight;
if (this.state.height !== height) {
this.setState({ height });
}
if (reset) {
this.setState({offset: 0});
this.base.scrollTop = 0;
}
};
handleScroll = () => {
this.setState({ offset: this.base.scrollTop });
if (this.props.sync) this.forceUpdate();
};
componentDidUpdate({data}) {
const equal = (data.length === this.props.data.length &&
this.props.data.every((v, i)=> v === data[i]));
this.resize(null, !equal);
}
componentDidMount() {
this.resize();
addEventListener('resize', this.resize);
}
componentWillUnmount() {
removeEventListener('resize', this.resize);
}
render() {
const { data, rowHeight, children, Tag="div", overscanCount=1, sync, ...props } = this.props;
const { offset, height } = this.state;
// first visible row index
let start = (offset / rowHeight) || 0;
const renderRow = children[0];
// actual number of visible rows (without overscan)
let visibleRowCount = (height / rowHeight) || 0;
// Overscan: render blocks of rows modulo an overscan row count
// This dramatically reduces DOM writes during scrolling
if (overscanCount) {
start = Math.max(0, start - (start % overscanCount));
visibleRowCount += overscanCount;
}
// last visible + overscan row index
const end = start + 1 + visibleRowCount;
// data slice currently in viewport plus overscan items
let selection = data.slice(start, end);
return (
<div onScroll={this.handleScroll} {...props}>
<div style={`${STYLE_INNER} height:${data.length*rowHeight}px;`}>
<Tag style={`${STYLE_CONTENT} top:${start*rowHeight}px;`}>
{ selection.map((d, i) => renderRow(d, start + i)) }
</Tag>
</div>
</div>
);
}
}

View File

@ -0,0 +1,223 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 jsonschema from 'jsonschema';
const editor = {
resetToSelect: {
title: "Reset to Select Tool",
enum: [true, 'paste', false],
enumNames: ['on', 'After Paste', 'off'],
default: 'paste'
},
rotationStep: {
title: "Rotation Step, º",
type: "integer",
minimum: 1,
maximum: 90,
default: 15
},
};
const miew = {
miewMode: {
title: "Display mode",
enum: ['lines', 'balls and sticks', 'licorice'],
enumNames: ['Lines', 'Balls and Sticks', 'Licorice'],
default: 'lines'
},
miewTheme: {
title: "Background color",
enum: ['light', 'dark'],
enumNames: ['Light', 'Dark'],
default: 'light'
},
miewAtomLabel: {
title: "Label coloring",
enum: ['no', 'bright', 'inverse', 'black and white', 'black'],
enumNames: ['No', 'Bright', 'Inverse', 'Black and White', 'Black'],
default: 'bright'
},
};
const render = {
showValenceWarnings: {
title: "Show valence warnings",
type: "boolean",
default: true
},
atomColoring: {
title: "Atom coloring",
type: "boolean",
default: true
},
hideChiralFlag: {
title: "Do not show the Chiral flag",
type: "boolean",
default: false
},
font: {
title: "Font",
type: "string",
default: '30px Arial'
},
fontsz: {
title: "Font size",
type: "integer",
default: 13,
minimum: 1,
maximum: 96
},
fontszsub: {
title: "Sub font size",
type: "integer",
default: 13,
minimum: 1,
maximum: 96
},
// Atom
carbonExplicitly: {
title: "Display carbon explicitly",
type: "boolean",
default: false
},
showCharge: {
title: "Display charge",
type: "boolean",
default: true
},
showValence: {
title: "Display valence",
type: "boolean",
default: true
},
showHydrogenLabels: {
title: "Show hydrogen labels",
enum: ['off', 'Hetero', 'Terminal', 'Terminal and Hetero', 'on'],
default: 'on',
},
// Bonds
aromaticCircle: {
title: "Aromatic Bonds as circle",
type: "boolean",
default: true
},
doubleBondWidth: {
title: "Double bond width",
type: "integer",
default: 6,
minimum: 1,
maximum: 96
},
bondThickness: {
title: "Bond thickness",
type: "integer",
default: 2,
minimum: 1,
maximum: 96
},
stereoBondWidth: {
title: "Stereo (Wedge) bond width",
type: "integer",
default: 6,
minimum: 1,
maximum: 96
}
};
const server = {
'smart-layout': {
title: "Smart-layout",
type: "boolean",
default: true
},
'ignore-stereochemistry-errors': {
title: "Ignore stereochemistry errors",
type: "boolean",
default: true
},
'mass-skip-error-on-pseudoatoms': {
title: "Ignore pseudoatoms at mass",
type: "boolean",
default: false
},
'gross-formula-add-rsites': {
title: "Add Rsites at mass calculation",
type: "boolean",
default: true
}
};
export const SERVER_OPTIONS = Object.keys(server);
const debug = {
showAtomIds: {
title: "Show atom Ids",
type: "boolean",
default: false
},
showBondIds: {
title: "Show bonds Ids",
type: "boolean",
default: false
},
showHalfBondIds: {
title: "Show half bonds Ids",
type: "boolean",
default: false
},
showLoopIds: {
title: "Show loop Ids",
type: "boolean",
default: false
}
};
const optionsSchema = {
title: "Settings",
type: "object",
required: [],
properties: {
...editor,
...render,
...miew,
...server,
...debug
}
};
export default optionsSchema;
export function getDefaultOptions() {
return Object.keys(optionsSchema.properties).reduce((res, prop) => {
res[prop] = optionsSchema.properties[prop].default;
return res;
}, {});
}
export function validation(settings) {
if (typeof settings !== 'object' || settings === null) return null;
const v = new jsonschema.Validator();
const { errors } = v.validate(settings, optionsSchema);
const errProps = errors.map(err => err.property.split('.')[1]);
return Object.keys(settings).reduce((res, prop) => {
if (optionsSchema.properties[prop] && errProps.indexOf(prop) === -1)
res[prop] = settings[prop];
return res;
}, {});
}

View File

@ -0,0 +1,402 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { mapOf } from '../utils';
const radioButtonsSchema = {
enum: [
"Absolute",
"Relative",
"Attached"
],
default: "Absolute"
};
const contextSchema = {
title: 'Context',
enum: [
'Fragment',
'Multifragment',
'Bond',
'Atom',
'Group'
],
default: 'Fragment'
};
const sData = {
Fragment: {
title: 'Fragment',
type: 'Object',
oneOf: [
{
key: 'FRG_STR',
title: 'MDLBG_FRAGMENT_STEREO',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: 'Field name',
enum: ["MDLBG_FRAGMENT_STEREO"],
default: "MDLBG_FRAGMENT_STEREO"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"abs",
"(+)-enantiomer",
"(-)-enantiomer",
"racemate",
"steric",
"rel",
"R(a)",
"S(a)",
"R(p)",
"S(p)"
]
},
default: ["abs"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'FRG_COEFF',
title: 'MDLBG_FRAGMENT_COEFFICIENT',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_FRAGMENT_COEFFICIENT"],
default: "MDLBG_FRAGMENT_COEFFICIENT"
},
fieldValue: {
title: "Field value",
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'FRG_CHRG',
title: 'MDLBG_FRAGMENT_CHARGE',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: 'Field name',
enum: ["MDLBG_FRAGMENT_CHARGE"],
default: "MDLBG_FRAGMENT_CHARGE"
},
fieldValue: {
title: "Field value",
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'FRG_RAD',
title: 'MDLBG_FRAGMENT_RADICALS',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_FRAGMENT_RADICALS"],
default: "MDLBG_FRAGMENT_RADICALS"
},
fieldValue: {
title: "Field value",
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
]
},
Multifragment: {
title: 'Multifragment',
type: 'Object',
oneOf: [
{
key: 'MLT_FRG',
title: 'KETCHER_MULTIPLE_FRAGMENT',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: 'Field name',
enum: ["KETCHER_MULTIPLE_FRAGMENT"],
default: "KETCHER_MULTIPLE_FRAGMENT"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"aerosol",
"alloy",
"catenane",
"complex",
"composite",
"co-polymer",
"emulsion",
"host-guest complex",
"mixture",
"rotaxane",
"suspension"
]
},
default: ["aerosol"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
},
Bond: {
title: 'Bond',
type: 'Object',
oneOf: [
{
key: 'SB_STR',
title: 'MDLBG_STEREO_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_STEREO_KEY"],
default: "MDLBG_STEREO_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"erythro",
"threo",
"alpha",
"beta",
"endo",
"exo",
"anti",
"syn",
"ECL",
"STG"
]
},
default: ["erythro"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'SB_BND',
title: 'MDLBG_BOND_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_BOND_KEY"],
default: "MDLBG_BOND_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"Value=4"
]
},
default: ["Value=4"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
},
Atom: {
title: 'Atom',
type: 'Object',
oneOf: [
{
key: 'AT_STR',
title: 'MDLBG_STEREO_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_STEREO_KEY"],
default: "MDLBG_STEREO_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"RS",
"SR",
"P-3",
"P-3-PI",
"SP-4",
"SP-4-PI",
"T-4",
"T-4-PI",
"SP-5",
"SP-5-PI",
"TB-5",
"TB-5-PI",
"OC-6",
"TP-6",
"PB-7",
"CU-8",
"SA-8",
"DD-8",
"HB-9",
"TPS-9"
],
},
default: ["RS"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
},
Group: {
title: 'Group',
type: 'Object',
oneOf: [
{
key: 'GRP_STR',
title: 'MDLBG_STEREO_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_STEREO_KEY"],
default: "MDLBG_STEREO_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"cis",
"trans"
]
},
default: ["cis"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
}
};
export const sdataCustomSchema = {
key: 'Custom',
properties: {
type: { enum: ["DAT"] },
context: {
title: 'Context',
enum: [
'Fragment',
'Multifragment',
'Bond',
'Atom',
'Group'
],
default: 'Fragment'
},
fieldName: {
title: 'Field name',
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
fieldValue: {
title: 'Field value',
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field value"
},
radiobuttons: {
enum: [
"Absolute",
"Relative",
"Attached"
],
default: "Absolute"
}
},
required: ["context", "fieldName", "fieldValue", "radiobuttons"]
};
export const sdataSchema = Object.keys(sData).reduce((acc, title) => {
acc[title] = mapOf(sData[title], 'fieldName');
Object.keys(acc[title]).forEach(fieldName => acc[title][fieldName].properties.context = contextSchema);
return acc;
}, {});
/**
* Returns first key of passed object
* @param obj { object }
*/
function firstKeyOf(obj) {
return Object.keys(obj)[0];
}
/**
* Returns schema default values. Depends on passed arguments:
* pass schema only -> returns default context
* pass schema & context -> returns default fieldName
* pass schema & context & fieldName -> returns default fieldValue
* @param context? { string }
* @param fieldName? { string }
* @returns { string }
*/
export function getSdataDefault(context, fieldName) {
if (!context && !fieldName)
return firstKeyOf(sdataSchema);
if (!fieldName)
return firstKeyOf(sdataSchema[context]);
return sdataSchema[context][fieldName] ?
sdataSchema[context][fieldName].properties.fieldValue.default :
'';
}

View File

@ -0,0 +1,154 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 molfile from '../../chem/molfile';
export default [
'Benzene\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 6 6 0 0 0 999 V2000\n' +
' 0.8660 2.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.8660 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 2 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 2 0 0 0\n' +
' 5 6 1 0 0 0\n' +
' 6 1 2 0 0 0\n' +
'M END\n',
'Cyclopentadiene\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 5 5 0 0 0 999 V2000\n' +
' 0.0000 1.4257 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.8090 0.8379 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.5000 -0.1132 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -0.5000 -0.1132 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -0.8090 0.8379 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 2 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 2 0 0 0\n' +
' 5 1 1 0 0 0\n' +
'M END\n',
'Cyclohexane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 6 6 0 0 0 999 V2000\n' +
' 0.8660 2.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.8660 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 1 0 0 0\n' +
' 5 6 1 0 0 0\n' +
' 6 1 1 0 0 0\n' +
'M END\n',
'Cyclopentane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 5 5 0 0 0 999 V2000\n' +
' 0.8090 1.5389 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.6180 0.9511 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.3090 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.3090 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.9511 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 1 0 0 0\n' +
' 5 1 1 0 0 0\n' +
'M END\n',
'Cyclopropane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 3 3 0 0 0 999 V2000\n' +
' -3.2250 -0.2750 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.2250 -0.2750 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.7250 0.5910 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 1 3 1 0 0 0\n' +
'M END\n',
'Cyclobutane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 4 4 0 0 0 999 V2000\n' +
' -3.8250 1.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -3.8250 0.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.8250 1.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.8250 0.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 1 3 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 2 1 0 0 0\n' +
'M END\n',
'Cycloheptane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 7 7 0 0 0 999 V2000\n' +
' 0.0000 1.6293 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7835 2.2465 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7559 2.0242 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 2.1897 1.1289 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.6228 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7566 0.2224 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7835 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 6 7 1 0 0 0\n' +
' 5 7 1 0 0 0\n' +
' 1 5 1 0 0 0\n' +
' 4 6 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 1 2 1 0 0 0\n' +
'M END\n',
'Cyclooctane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 8 8 0 0 0 999 V2000\n' +
' 0.0000 0.7053 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 1.7078 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7053 2.4131 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7056 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7079 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 2.4133 0.7053 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 2.4133 1.7078 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7079 2.4131 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 8 3 1 0 0 0\n' +
' 7 8 1 0 0 0\n' +
' 6 7 1 0 0 0\n' +
' 5 6 1 0 0 0\n' +
' 4 5 1 0 0 0\n' +
' 1 4 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 1 2 1 0 0 0\n' +
'M END\n'
].map(structStr => molfile.parse(structStr));

View File

@ -0,0 +1,70 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import Dialog from '../component/dialog';
function About(props) {
return (
<Dialog title="About"
className="about" params={props}
buttons={["Close"]}>
<a href="http://lifescience.opensource.epam.com/ketcher/" target="_blank">
<img src="images/ketcher-logo.svg"/>
</a>
<dl>
<dt>
<a href="http://lifescience.opensource.epam.com/ketcher/help.html" target="_blank">Ketcher</a>
</dt>
<dd>
version <var>{props.version}</var>
</dd>
{
props.buildNumber ? (
<dd>
build #<var>{props.buildNumber}</var>
{" at "}
<time>{props.buildDate}</time>
</dd> ) : null
}
{
props.indigoVersion ? (
<div>
<dt>
<a href="http://lifescience.opensource.epam.com/indigo/" target="_blank">Indigo
Toolkit</a>
</dt>
<dd>version <var>{props.indigoVersion}</var></dd>
</div>
) : ( <dd>standalone</dd> )
}
<dt>
<a href="http://lifescience.opensource.epam.com/" target="_blank">EPAM Life Sciences</a>
</dt>
<dd>
<a href="http://lifescience.opensource.epam.com/ketcher/#feedback" target="_blank">Feedback</a>
</dd>
</dl>
</Dialog>
);
}
export default connect(
store => ({ ...store.options.app })
)(About);

View File

@ -0,0 +1,136 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { range } from 'lodash/fp';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import keyName from 'w3c-keyname';
import Dialog from '../component/dialog';
import Input from '../component/input';
import { changeRound } from '../state/options';
import { analyse } from '../state/server';
function FrozenInput({value}) {
return (
<input type="text" spellCheck={false} value={value}
onKeyDown={ev => allowMovement(ev)}/>
);
}
const formulaRegexp = /\b([A-Z][a-z]{0,3})(\d*)\s*\b/g;
const errorRegexp = /error:.*/g;
function formulaInputMarkdown(value) {
return (
<div className="chem-input" spellCheck={false} contentEditable={true}
onKeyDown={ev => allowMovement(ev)}>{value}</div>
);
}
function FormulaInput({value}) {
if (errorRegexp.test(value)) {
return formulaInputMarkdown(value);
}
const content = [];
var cnd;
var pos = 0;
while (cnd = formulaRegexp.exec(value)) {
content.push(value.substring(pos, cnd.index) + cnd[1]);
if (cnd[2].length > 0) content.push(<sub>{cnd[2]}</sub>);
pos = cnd.index + cnd[0].length;
}
if (pos === 0) content.push(value);
else content.push(value.substring(pos, value.length));
return formulaInputMarkdown(content);
}
class Analyse extends Component {
constructor(props) {
super(props);
props.onAnalyse();
}
render() {
const { values, round, onAnalyse, onChangeRound, ...props } = this.props;
return (
<Dialog title="Calculated Values" className="analyse"
buttons={["Close"]} params={props}>
<ul>{[
{ name: 'Chemical Formula', key: 'gross' },
{ name: 'Molecular Weight', key: 'molecular-weight', round: 'roundWeight' },
{ name: 'Exact Mass', key: 'monoisotopic-mass', round: 'roundMass' },
{ name: 'Elemental Analysis', key: 'mass-composition' }
].map(item => (
<li>
<label>{item.name}:</label>
{item.key === 'gross'
? <FormulaInput value={values ? values[item.key] : 0}/>
: <FrozenInput value={values ? roundOff(values[item.key], round[item.round]) : 0}/>
}
{item.round
? <Input schema={{
enum: range(0, 8),
enumNames: range(0, 8).map(i => `${i} decimal places`)
}} value={round[item.round]} onChange={val => onChangeRound(item.round, val)}/>
: null
}
</li>
))
}</ul>
</Dialog>
);
}
}
function allowMovement(event) {
const movementKeys = ['Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'];
const key = keyName(event);
if (movementKeys.indexOf(key) === -1)
event.preventDefault();
}
function roundOff(value, round) {
if (typeof value === 'number')
return value.toFixed(round);
return value.replace(/[0-9]*\.[0-9]+/g, (str) => (
(+str).toFixed(round)
));
}
export default connect(
store => ({
values: store.options.analyse.values,
round: {
roundWeight: store.options.analyse.roundWeight,
roundMass: store.options.analyse.roundMass
}
}),
dispatch => ({
onAnalyse: () => dispatch(analyse()),
onChangeRound: (roundName, val) => dispatch(changeRound(roundName, val))
})
)(Analyse);

View File

@ -0,0 +1,78 @@
/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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 { capitalize } from 'lodash/fp';
import { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { atom as atomSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
import element from '../../chem/element';
function ElementNumber(props, {stateStore}) {
let { result } = stateStore.props;
return (
<label>Number:
<input className="number" type="text" readOnly={true}
value={element.map[capitalize(result.label)] || ''}/>
</label>
);
}
function Atom(props) {
let { formState, ...prop } = props;
return (
<Dialog title="Atom Properties" className="atom-props"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={atomSchema} customValid={{ label: l => atomValid(l) }}
init={prop} {...formState}>
<fieldset className="main">
<Field name="label"/>
<Field name="alias"/>
<ElementNumber/>
<Field name="charge" maxlength="5"/>
<Field name="explicitValence"/>
<Field name="isotope"/>
<Field name="radical"/>
</fieldset>
<fieldset className="query">
<legend>Query specific</legend>
<Field name="ringBondCount"/>
<Field name="hCount"/>
<Field name="substitutionCount"/>
<Field name="unsaturatedAtom"/>
</fieldset>
<fieldset className="reaction">
<legend>Reaction flags</legend>
<Field name="invRet"/>
<Field name="exactChangeFlag"/>
</fieldset>
</Form>
</Dialog>
);
}
function atomValid(label) {
return label && !!element.map[capitalize(label)];
}
export default connect(
(store) => ({ formState: store.modal.form })
)(Atom);

Some files were not shown because too many files have changed in this diff Show More