forked from enviPath/enviPy
Current Dev State
This commit is contained in:
115
static/js/ketcher2/script/api.js
Normal file
115
static/js/ketcher2/script/api.js
Normal 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;
|
||||
15
static/js/ketcher2/script/banner.js
Normal file
15
static/js/ketcher2/script/banner.js
Normal 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.
|
||||
***************************************************************************/
|
||||
1294
static/js/ketcher2/script/chem/element.js
Normal file
1294
static/js/ketcher2/script/chem/element.js
Normal file
File diff suppressed because it is too large
Load Diff
110
static/js/ketcher2/script/chem/generics.js
Normal file
110
static/js/ketcher2/script/chem/generics.js
Normal 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;
|
||||
269
static/js/ketcher2/script/chem/molfile/common.js
Normal file
269
static/js/ketcher2/script/chem/molfile/common.js
Normal 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
|
||||
};
|
||||
54
static/js/ketcher2/script/chem/molfile/index.js
Normal file
54
static/js/ketcher2/script/chem/molfile/index.js
Normal 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 || {});
|
||||
}
|
||||
};
|
||||
488
static/js/ketcher2/script/chem/molfile/molfile.js
Normal file
488
static/js/ketcher2/script/chem/molfile/molfile.js
Normal 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;
|
||||
301
static/js/ketcher2/script/chem/molfile/parseSGroup.js
Normal file
301
static/js/ketcher2/script/chem/molfile/parseSGroup.js
Normal 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
|
||||
};
|
||||
274
static/js/ketcher2/script/chem/molfile/utils.js
Normal file
274
static/js/ketcher2/script/chem/molfile/utils.js
Normal 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
|
||||
};
|
||||
426
static/js/ketcher2/script/chem/molfile/v2000.js
Normal file
426
static/js/ketcher2/script/chem/molfile/v2000.js
Normal 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
|
||||
};
|
||||
482
static/js/ketcher2/script/chem/molfile/v3000.js
Normal file
482
static/js/ketcher2/script/chem/molfile/v3000.js
Normal 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
|
||||
};
|
||||
64
static/js/ketcher2/script/chem/sdf.js
Normal file
64
static/js/ketcher2/script/chem/sdf.js
Normal 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
|
||||
};
|
||||
212
static/js/ketcher2/script/chem/smiles/cis_trans.js
Normal file
212
static/js/ketcher2/script/chem/smiles/cis_trans.js
Normal 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;
|
||||
177
static/js/ketcher2/script/chem/smiles/dfs.js
Normal file
177
static/js/ketcher2/script/chem/smiles/dfs.js
Normal 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;
|
||||
735
static/js/ketcher2/script/chem/smiles/index.js
Normal file
735
static/js/ketcher2/script/chem/smiles/index.js
Normal 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);
|
||||
}
|
||||
};
|
||||
519
static/js/ketcher2/script/chem/smiles/stereocenters.js
Normal file
519
static/js/ketcher2/script/chem/smiles/stereocenters.js
Normal 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;
|
||||
435
static/js/ketcher2/script/chem/struct/atom.js
Normal file
435
static/js/ketcher2/script/chem/struct/atom.js
Normal 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;
|
||||
44
static/js/ketcher2/script/chem/struct/atomlist.js
Normal file
44
static/js/ketcher2/script/chem/struct/atomlist.js
Normal 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;
|
||||
145
static/js/ketcher2/script/chem/struct/bond.js
Normal file
145
static/js/ketcher2/script/chem/struct/bond.js
Normal 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;
|
||||
975
static/js/ketcher2/script/chem/struct/index.js
Normal file
975
static/js/ketcher2/script/chem/struct/index.js
Normal 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
|
||||
});
|
||||
51
static/js/ketcher2/script/chem/struct/rgroup.js
Normal file
51
static/js/ketcher2/script/chem/struct/rgroup.js
Normal 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;
|
||||
164
static/js/ketcher2/script/chem/struct/sgforest.js
Normal file
164
static/js/ketcher2/script/chem/struct/sgforest.js
Normal 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;
|
||||
383
static/js/ketcher2/script/chem/struct/sgroup.js
Normal file
383
static/js/ketcher2/script/chem/struct/sgroup.js
Normal 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;
|
||||
1560
static/js/ketcher2/script/editor/action.js
Normal file
1560
static/js/ketcher2/script/editor/action.js
Normal file
File diff suppressed because it is too large
Load Diff
253
static/js/ketcher2/script/editor/closest.js
Normal file
253
static/js/ketcher2/script/editor/closest.js
Normal 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
|
||||
};
|
||||
364
static/js/ketcher2/script/editor/index.js
Normal file
364
static/js/ketcher2/script/editor/index.js
Normal 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;
|
||||
980
static/js/ketcher2/script/editor/op.js
Normal file
980
static/js/ketcher2/script/editor/op.js
Normal 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
|
||||
};
|
||||
52
static/js/ketcher2/script/editor/tool/apoint.js
Normal file
52
static/js/ketcher2/script/editor/tool/apoint.js
Normal 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;
|
||||
119
static/js/ketcher2/script/editor/tool/atom.js
Normal file
119
static/js/ketcher2/script/editor/tool/atom.js
Normal 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
|
||||
});
|
||||
65
static/js/ketcher2/script/editor/tool/attach.js
Normal file
65
static/js/ketcher2/script/editor/tool/attach.js
Normal 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;
|
||||
171
static/js/ketcher2/script/editor/tool/bond.js
Normal file
171
static/js/ketcher2/script/editor/tool/bond.js
Normal 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
|
||||
});
|
||||
111
static/js/ketcher2/script/editor/tool/chain.js
Normal file
111
static/js/ketcher2/script/editor/tool/chain.js
Normal 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;
|
||||
53
static/js/ketcher2/script/editor/tool/charge.js
Normal file
53
static/js/ketcher2/script/editor/tool/charge.js
Normal 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;
|
||||
34
static/js/ketcher2/script/editor/tool/chiral-flag.js
Normal file
34
static/js/ketcher2/script/editor/tool/chiral-flag.js
Normal 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;
|
||||
86
static/js/ketcher2/script/editor/tool/eraser.js
Normal file
86
static/js/ketcher2/script/editor/tool/eraser.js
Normal 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;
|
||||
83
static/js/ketcher2/script/editor/tool/helper/lasso.js
Normal file
83
static/js/ketcher2/script/editor/tool/helper/lasso.js
Normal 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;
|
||||
160
static/js/ketcher2/script/editor/tool/helper/locate.js
Normal file
160
static/js/ketcher2/script/editor/tool/helper/locate.js
Normal 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
|
||||
};
|
||||
59
static/js/ketcher2/script/editor/tool/paste.js
Normal file
59
static/js/ketcher2/script/editor/tool/paste.js
Normal 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;
|
||||
65
static/js/ketcher2/script/editor/tool/reactionarrow.js
Normal file
65
static/js/ketcher2/script/editor/tool/reactionarrow.js
Normal 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;
|
||||
129
static/js/ketcher2/script/editor/tool/reactionmap.js
Normal file
129
static/js/ketcher2/script/editor/tool/reactionmap.js
Normal 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;
|
||||
60
static/js/ketcher2/script/editor/tool/reactionplus.js
Normal file
60
static/js/ketcher2/script/editor/tool/reactionplus.js
Normal 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;
|
||||
51
static/js/ketcher2/script/editor/tool/reactionunmap.js
Normal file
51
static/js/ketcher2/script/editor/tool/reactionunmap.js
Normal 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;
|
||||
69
static/js/ketcher2/script/editor/tool/rgroupatom.js
Normal file
69
static/js/ketcher2/script/editor/tool/rgroupatom.js
Normal 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;
|
||||
71
static/js/ketcher2/script/editor/tool/rgroupfragment.js
Normal file
71
static/js/ketcher2/script/editor/tool/rgroupfragment.js
Normal 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;
|
||||
154
static/js/ketcher2/script/editor/tool/rotate.js
Normal file
154
static/js/ketcher2/script/editor/tool/rotate.js
Normal 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;
|
||||
254
static/js/ketcher2/script/editor/tool/select.js
Normal file
254
static/js/ketcher2/script/editor/tool/select.js
Normal 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;
|
||||
346
static/js/ketcher2/script/editor/tool/sgroup.js
Normal file
346
static/js/ketcher2/script/editor/tool/sgroup.js
Normal 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
|
||||
});
|
||||
258
static/js/ketcher2/script/editor/tool/template.js
Normal file
258
static/js/ketcher2/script/editor/tool/template.js
Normal 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;
|
||||
52
static/js/ketcher2/script/editor/tool/utils.js
Normal file
52
static/js/ketcher2/script/editor/tool/utils.js
Normal 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
|
||||
};
|
||||
128
static/js/ketcher2/script/index.js
Normal file
128
static/js/ketcher2/script/index.js
Normal 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);
|
||||
36
static/js/ketcher2/script/raphael-ext.js
Normal file
36
static/js/ketcher2/script/raphael-ext.js
Normal 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;
|
||||
295
static/js/ketcher2/script/render/draw.js
Normal file
295
static/js/ketcher2/script/render/draw.js
Normal 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
|
||||
};
|
||||
210
static/js/ketcher2/script/render/index.js
Normal file
210
static/js/ketcher2/script/render/index.js
Normal 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;
|
||||
96
static/js/ketcher2/script/render/options.js
Normal file
96
static/js/ketcher2/script/render/options.js
Normal 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;
|
||||
642
static/js/ketcher2/script/render/restruct/index.js
Normal file
642
static/js/ketcher2/script/render/restruct/index.js
Normal 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
|
||||
});
|
||||
647
static/js/ketcher2/script/render/restruct/reatom.js
Normal file
647
static/js/ketcher2/script/render/restruct/reatom.js
Normal 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;
|
||||
587
static/js/ketcher2/script/render/restruct/rebond.js
Normal file
587
static/js/ketcher2/script/render/restruct/rebond.js
Normal 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;
|
||||
63
static/js/ketcher2/script/render/restruct/rechiralflag.js
Normal file
63
static/js/ketcher2/script/render/restruct/rechiralflag.js
Normal 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;
|
||||
@ -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;
|
||||
112
static/js/ketcher2/script/render/restruct/refrag.js
Normal file
112
static/js/ketcher2/script/render/restruct/refrag.js
Normal 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;
|
||||
124
static/js/ketcher2/script/render/restruct/reloop.js
Normal file
124
static/js/ketcher2/script/render/restruct/reloop.js
Normal 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;
|
||||
75
static/js/ketcher2/script/render/restruct/reobject.js
Normal file
75
static/js/ketcher2/script/render/restruct/reobject.js
Normal 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;
|
||||
198
static/js/ketcher2/script/render/restruct/rergroup.js
Normal file
198
static/js/ketcher2/script/render/restruct/rergroup.js
Normal 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;
|
||||
63
static/js/ketcher2/script/render/restruct/rerxnarrow.js
Normal file
63
static/js/ketcher2/script/render/restruct/rerxnarrow.js
Normal 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;
|
||||
61
static/js/ketcher2/script/render/restruct/rerxnplus.js
Normal file
61
static/js/ketcher2/script/render/restruct/rerxnplus.js
Normal 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;
|
||||
378
static/js/ketcher2/script/render/restruct/resgroup.js
Normal file
378
static/js/ketcher2/script/render/restruct/resgroup.js
Normal 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;
|
||||
62
static/js/ketcher2/script/render/restruct/visel.js
Normal file
62
static/js/ketcher2/script/render/restruct/visel.js
Normal 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;
|
||||
33
static/js/ketcher2/script/render/util.js
Normal file
33
static/js/ketcher2/script/render/util.js
Normal 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
|
||||
};
|
||||
44
static/js/ketcher2/script/ui/action/atoms.js
Normal file
44
static/js/ketcher2/script/ui/action/atoms.js
Normal 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;
|
||||
}, {});
|
||||
38
static/js/ketcher2/script/ui/action/debug.js
Normal file
38
static/js/ketcher2/script/ui/action/debug.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
184
static/js/ketcher2/script/ui/action/index.js
Normal file
184
static/js/ketcher2/script/ui/action/index.js
Normal 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 + '.');
|
||||
}
|
||||
58
static/js/ketcher2/script/ui/action/server.js
Normal file
58
static/js/ketcher2/script/ui/action/server.js
Normal 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
|
||||
}
|
||||
};
|
||||
39
static/js/ketcher2/script/ui/action/templates.js
Normal file
39
static/js/ketcher2/script/ui/action/templates.js
Normal 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);
|
||||
|
||||
142
static/js/ketcher2/script/ui/action/tools.js
Normal file
142
static/js/ketcher2/script/ui/action/tools.js
Normal 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);
|
||||
56
static/js/ketcher2/script/ui/action/zoom.js
Normal file
56
static/js/ketcher2/script/ui/action/zoom.js
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
138
static/js/ketcher2/script/ui/app.jsx
Normal file
138
static/js/ketcher2/script/ui/app.jsx
Normal 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;
|
||||
50
static/js/ketcher2/script/ui/component/accordion.jsx
Normal file
50
static/js/ketcher2/script/ui/component/accordion.jsx
Normal 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;
|
||||
99
static/js/ketcher2/script/ui/component/actionmenu.jsx
Normal file
99
static/js/ketcher2/script/ui/component/actionmenu.jsx
Normal 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;
|
||||
41
static/js/ketcher2/script/ui/component/atom.jsx
Normal file
41
static/js/ketcher2/script/ui/component/atom.jsx
Normal 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;
|
||||
136
static/js/ketcher2/script/ui/component/cliparea.jsx
Normal file
136
static/js/ketcher2/script/ui/component/cliparea.jsx
Normal 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;
|
||||
75
static/js/ketcher2/script/ui/component/combobox.jsx
Normal file
75
static/js/ketcher2/script/ui/component/combobox.jsx
Normal 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;
|
||||
82
static/js/ketcher2/script/ui/component/dialog.jsx
Normal file
82
static/js/ketcher2/script/ui/component/dialog.jsx
Normal 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;
|
||||
196
static/js/ketcher2/script/ui/component/form.jsx
Normal file
196
static/js/ketcher2/script/ui/component/form.jsx
Normal 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 };
|
||||
232
static/js/ketcher2/script/ui/component/input.jsx
Normal file
232
static/js/ketcher2/script/ui/component/input.jsx
Normal 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 });
|
||||
}
|
||||
}
|
||||
71
static/js/ketcher2/script/ui/component/measure-input.jsx
Normal file
71
static/js/ketcher2/script/ui/component/measure-input.jsx
Normal 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;
|
||||
|
||||
109
static/js/ketcher2/script/ui/component/openbutton.jsx
Normal file
109
static/js/ketcher2/script/ui/component/openbutton.jsx
Normal 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;
|
||||
77
static/js/ketcher2/script/ui/component/savebutton.jsx
Normal file
77
static/js/ketcher2/script/ui/component/savebutton.jsx
Normal 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;
|
||||
40
static/js/ketcher2/script/ui/component/select.jsx
Normal file
40
static/js/ketcher2/script/ui/component/select.jsx
Normal 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;
|
||||
26
static/js/ketcher2/script/ui/component/spin.jsx
Normal file
26
static/js/ketcher2/script/ui/component/spin.jsx
Normal 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;
|
||||
78
static/js/ketcher2/script/ui/component/structeditor.jsx
Normal file
78
static/js/ketcher2/script/ui/component/structeditor.jsx
Normal 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;
|
||||
71
static/js/ketcher2/script/ui/component/structrender.jsx
Normal file
71
static/js/ketcher2/script/ui/component/structrender.jsx
Normal 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;
|
||||
88
static/js/ketcher2/script/ui/component/systemfonts.jsx
Normal file
88
static/js/ketcher2/script/ui/component/systemfonts.jsx
Normal 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;
|
||||
53
static/js/ketcher2/script/ui/component/tabs.jsx
Normal file
53
static/js/ketcher2/script/ui/component/tabs.jsx
Normal 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;
|
||||
101
static/js/ketcher2/script/ui/component/visibleview.jsx
Normal file
101
static/js/ketcher2/script/ui/component/visibleview.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
223
static/js/ketcher2/script/ui/data/options-schema.js
Normal file
223
static/js/ketcher2/script/ui/data/options-schema.js
Normal 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;
|
||||
}, {});
|
||||
}
|
||||
402
static/js/ketcher2/script/ui/data/sdata-schema.js
Normal file
402
static/js/ketcher2/script/ui/data/sdata-schema.js
Normal 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 :
|
||||
'';
|
||||
}
|
||||
154
static/js/ketcher2/script/ui/data/templates.js
Normal file
154
static/js/ketcher2/script/ui/data/templates.js
Normal 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));
|
||||
70
static/js/ketcher2/script/ui/dialog/about.jsx
Normal file
70
static/js/ketcher2/script/ui/dialog/about.jsx
Normal 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);
|
||||
136
static/js/ketcher2/script/ui/dialog/analyse.jsx
Normal file
136
static/js/ketcher2/script/ui/dialog/analyse.jsx
Normal 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);
|
||||
78
static/js/ketcher2/script/ui/dialog/atom.jsx
Normal file
78
static/js/ketcher2/script/ui/dialog/atom.jsx
Normal 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
Reference in New Issue
Block a user