Files
enviPy-bayer/static/js/ketcher2/script/editor/tool/sgroup.js
2025-06-23 20:13:54 +02:00

347 lines
9.6 KiB
JavaScript

/****************************************************************************
* 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
});