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