Skip to content

Commit

Permalink
#3338 - New S-Group type Query component level grouping (#3356)
Browse files Browse the repository at this point in the history
* #3338 - New S-Group type Query component level grouping

* #3338 - New S-Group type Query component level grouping
 - Fix e2e tests

* #3338 - New S-Group type Query component level grouping
 - Fix pr comments
  • Loading branch information
AKZhuk authored Sep 28, 2023
1 parent 90f63cd commit 8660fdc
Show file tree
Hide file tree
Showing 18 changed files with 197 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ test.describe('S-Group Properties', () => {
test('Checking S-Group drop-down types', async ({ page }) => {
/*
Test case: EPMLSOPKET-1502
Description: Checking S-Group drop-down types 'Type' drop-down list with Data,
Multiple group, SRU polymer and Superatom items. Data item is selected by default;
Description: Checking S-Group drop-down types 'Type' drop-down list with Data,
Multiple group, SRU polymer, Superatom and Query Component items. Data item is selected by default;
*/
await selectRingButton(RingButton.Benzene, page);
await clickInTheMiddleOfTheScreen(page);
Expand Down Expand Up @@ -77,4 +77,18 @@ test.describe('S-Group Properties', () => {
await takeEditorScreenshot(page);
await page.getByRole('button', { name: 'Apply' }).click();
});

test('A query component is created', async ({ page }) => {
await selectRingButton(RingButton.Benzene, page);
await clickInTheMiddleOfTheScreen(page);

await selectLeftPanelButton(LeftPanelButton.S_Group, page);
const { x, y } = await getCoordinatesTopAtomOfBenzeneRing(page);
await page.mouse.click(x, y);
await page.getByRole('button', { name: 'Data' }).click();
await page.getByRole('option', { name: 'Query component' }).click();

await takeEditorScreenshot(page);
await page.getByRole('button', { name: 'Apply' }).click();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions packages/ketcher-core/src/application/editor/actions/sgroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@ export function fromSgroupAction(
return fromAtomAction(restruct, newSg, newSourceAtoms);
}

if (SGroup.isQuerySGroup(newSg)) {
return fromQueryComponentSGroupAction(
restruct,
newSg,
newSourceAtoms as number[],
Array.from(restruct.atoms.keys()),
);
}

return {
action: fromSeveralSgroupAddition(
restruct,
Expand Down Expand Up @@ -300,6 +309,52 @@ function fromAtomAction(restruct, newSg, sourceAtoms) {
);
}

function fromQueryComponentSGroupAction(
restruct: Restruct,
newSg: {
type: string;
attrs: object;
},
sourceAtoms: number[],
targetAtoms: number[],
) {
const selection: {
atoms: number[];
bonds: number[];
} = {
atoms: [],
bonds: [],
};

const allFragments = new Pile(
sourceAtoms.map((aid) => restruct.atoms.get(aid)?.a.fragment),
);

Array.from(allFragments).forEach((fragId) => {
const atoms = targetAtoms.reduce((res: number[], aid: number) => {
const atom = restruct.atoms.get(aid)?.a;
if (fragId === atom?.fragment) res.push(aid);

return res;
}, []);

const bonds = getAtomsBondIds(restruct.molecule, atoms) as number[];

selection.atoms = selection.atoms.concat(atoms);
selection.bonds = selection.bonds.concat(bonds);
});

return {
action: fromSeveralSgroupAddition(
restruct,
newSg.type,
selection.atoms,
newSg.attrs,
),
selection,
};
}

function fromGroupAction(restruct, newSg, sourceAtoms, targetAtoms) {
const allFragments = new Pile(
sourceAtoms.map((aid) => restruct.atoms.get(aid).a.fragment),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class ReSGroup extends ReObject {
SGroupdrawBracketsOptions.lowerIndexText = sgroup.data.mul;
break;
}
case 'queryComponent': {
break;
}
case 'SRU': {
let connectivity: string = sgroup.data.connectivity || 'eu';
if (connectivity === 'ht') connectivity = '';
Expand All @@ -118,7 +121,13 @@ class ReSGroup extends ReObject {
}

// DAT S-Groups do not have brackets
const sgroupTypesWithBrackets = ['MUL', 'SRU', 'SUP', 'GEN'];
const sgroupTypesWithBrackets = [
'MUL',
'SRU',
'SUP',
'GEN',
'queryComponent',
];
if (sgroupTypesWithBrackets.includes(sgroup.type)) {
SGroupdrawBrackets(SGroupdrawBracketsOptions);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/ketcher-core/src/domain/entities/sgroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class SGroup {
DAT: 'DAT',
ANY: 'ANY',
GEN: 'GEN',
queryComponent: 'queryComponent',
};

type: string;
Expand Down Expand Up @@ -758,6 +759,10 @@ export class SGroup {
return sGroup.type === SGroup.TYPES.DAT;
}

static isQuerySGroup(sGroup: SGroup): boolean {
return sGroup.type === SGroup.TYPES.queryComponent;
}

static isSRUSGroup(sGroup: SGroup): boolean {
return sGroup.type === SGroup.TYPES.SRU;
}
Expand Down
37 changes: 26 additions & 11 deletions packages/ketcher-core/src/domain/entities/sgroupForest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { Pile } from './pile';
import { SGroup } from './sgroup';
import assert from 'assert';
import { Struct } from './struct';

export class SGroupForest {
/** node id -> parent id */
Expand Down Expand Up @@ -168,19 +169,33 @@ export class SGroupForest {
}
}

export function checkOverlapping(struct, atoms) {
export function checkOverlapping(
struct: Struct,
atoms: number[] = [],
sGroupType: 'queryComponent' | 'common',
) {
const searchFunction = {
common: (sid: number) => {
const sg = struct.sgroups.get(sid);
if (sg?.type === 'DAT') return false;
const sgAtoms = SGroup.getAtoms(struct, sg);

return sgAtoms.length < atoms.length
? sgAtoms.findIndex((aid) => atoms.indexOf(aid) === -1) >= 0
: atoms.findIndex((aid) => sgAtoms.indexOf(aid) === -1) >= 0;
},
queryComponent: (sid: number) => {
const sg = struct.sgroups.get(sid);
if (sg?.type !== 'queryComponent') return false;
const sgAtoms = SGroup.getAtoms(struct, sg);

return atoms.some((aid) => sgAtoms.includes(aid));
},
};
const sgroups = atoms.reduce((res, aid) => {
const atom = struct.atoms.get(aid);
return res.union(atom.sgs);
return atom ? res.union(atom.sgs) : res;
}, new Pile());

return Array.from(sgroups).some((sid) => {
const sg = struct.sgroups.get(sid);
if (sg.type === 'DAT') return false;
const sgAtoms = SGroup.getAtoms(struct, sg);

return sgAtoms.length < atoms.length
? sgAtoms.findIndex((aid) => atoms.indexOf(aid) === -1) >= 0
: atoms.findIndex((aid) => sgAtoms.indexOf(aid) === -1) >= 0;
});
return Array.from(sgroups).some(searchFunction[sGroupType]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@
},
"type": {
"type": "string",
"enum": ["GEN", "MUL", "SRU", "SUP", "DAT"]
"enum": ["GEN", "MUL", "SRU", "SUP", "DAT", "queryComponent"]
}
},
"if": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ function bondToKet(source) {
return result;
}

function sgroupToKet(struct, source: SGroup) {
function sgroupToKet(struct: Struct, source: SGroup) {
const result = {};

ifDef(result, 'type', source.type);
Expand All @@ -161,6 +161,9 @@ function sgroupToKet(struct, source: SGroup) {
ifDef(result, 'mul', source.data.mul || 1);
break;
}
case 'queryComponent': {
break;
}
case 'SRU': {
ifDef(result, 'subscript', source.data.subscript || 'n');
ifDef(
Expand Down
5 changes: 5 additions & 0 deletions packages/ketcher-core/src/domain/serializers/mol/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const prepareForSaving = {
SUP: prepareSupForSaving,
DAT: prepareDatForSaving,
GEN: prepareGenForSaving,
queryComponent: prepareQueryComponentForSaving,
};

function prepareSruForSaving(sgroup, mol) {
Expand Down Expand Up @@ -138,6 +139,10 @@ function prepareGenForSaving(_sgroup, _mol) {
// eslint-disable-line no-unused-vars
}

function prepareQueryComponentForSaving(_sgroup, _mol) {
// eslint-disable-line no-unused-vars
}

function prepareDatForSaving(sgroup, mol) {
sgroup.atoms = SGroup.getAtoms(mol, sgroup);
}
Expand Down
14 changes: 12 additions & 2 deletions packages/ketcher-core/src/domain/serializers/mol/molfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
* limitations under the License.
***************************************************************************/

import { StereoFlag, Struct, SGroupAttachmentPoint } from 'domain/entities';
import {
StereoFlag,
Struct,
SGroupAttachmentPoint,
SGroup,
} from 'domain/entities';

import { Elements } from 'domain/constants';
import common from './common';
Expand Down Expand Up @@ -437,6 +442,10 @@ export class Molfile {
// each group on its own
const id = sgmapback[sGroupIdInCTab];
const sgroup = this.molecule!.sgroups.get(id)!;
if (SGroup.isQuerySGroup(sgroup)) {
console.warn('Query group does not support in mol format');
continue;
}
this.write('M STY');
this.writePaddedNumber(1, 3);
this.writeWhiteSpace(1);
Expand Down Expand Up @@ -507,7 +516,8 @@ export class Molfile {

const expandedGroups: number[] = [];
this.molecule!.sgroups.forEach((sg) => {
if (sg.isExpanded()) expandedGroups.push(sg.id + 1);
if (sg.isExpanded() && !SGroup.isQuerySGroup(sg))
expandedGroups.push(sg.id + 1);
});

if (expandedGroups.length) {
Expand Down
52 changes: 43 additions & 9 deletions packages/ketcher-react/src/script/editor/tool/sgroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
import LassoHelper from './helper/lasso';
import { isEqual } from 'lodash/fp';
import { selMerge } from './select';
import Editor from '../Editor';
import Editor, { Selection } from '../Editor';
import { Tool } from './Tool';

const searchMaps = [
Expand Down Expand Up @@ -465,7 +465,7 @@ class SGroupTool implements Tool {
this.editor.selection(null);
}

static sgroupDialog(editor, id) {
static sgroupDialog(editor: Editor, id: number | null) {
const restruct = editor.render.ctab;
const struct = restruct.molecule;
const selection = editor.selection() || {};
Expand All @@ -490,22 +490,30 @@ class SGroupTool implements Tool {
Promise.resolve(res)
.then((newSg) => {
// TODO: check before signal
const isQuerySGroup = SGroup.isQuerySGroup(newSg);
const isDataSGroup = SGroup.isDataSGroup(newSg);
if (
newSg.type !== 'DAT' && // when data s-group separates
checkOverlapping(struct, selection.atoms || [])
!isDataSGroup && // when data s-group separates
!isQuerySGroup &&
checkOverlapping(struct, selection.atoms, 'common')
) {
editor.event.message.dispatch({
error: 'Partial S-group overlapping is not allowed.',
});
} else {
if (
!sg &&
newSg.type !== 'DAT' &&
!isDataSGroup &&
!isQuerySGroup &&
(!selection.atoms || selection.atoms.length === 0)
)
) {
return;
}

const isDataSg = sg && sg.getAttrs().context === newSg.attrs.context;
const isDataSg =
sg &&
sg.getAttrs().context === newSg.attrs.context &&
!isQuerySGroup;

if (isDataSg) {
const action = fromSeveralSgroupAddition(
Expand All @@ -521,9 +529,11 @@ class SGroupTool implements Tool {
editor.selection(selection);
return;
}
const result = isQuerySGroup
? createQueryComponentSGroup(id, editor, newSg, selection, sg)
: fromContextType(id, editor, newSg, selection);

const result = fromContextType(id, editor, newSg, selection);
editor.update(result.action);
result && editor.update(result.action);
editor.selection(null);
}
})
Expand All @@ -533,6 +543,30 @@ class SGroupTool implements Tool {
}
}

function createQueryComponentSGroup(
id: number | null,
editor: Editor,
newSg,
selection: Selection,
sg: SGroup | null | undefined,
) {
const struct = editor.render.ctab.molecule;
if (!selection.atoms && sg) {
if (!sg.atoms) {
editor.errorHandler?.('Cannot convert to a query component');
return;
}
selection = { atoms: sg.atoms || [] };
}
if (checkOverlapping(struct, selection.atoms, 'queryComponent')) {
editor.errorHandler?.(
'Cannot create a query component: one fragment can only be part of one query component',
);
} else {
return fromContextType(id, editor, newSg, selection);
}
}

function getContextBySgroup(restruct, sgAtoms) {
const struct = restruct.molecule;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ export function setFunctionalGroupsTooltip({
?.sgroups.get(closestCollapsibleStructures.id);
const isSGroupPresent = sGroup?.hovering;
const isShowingTooltip =
!sGroup?.data.expanded || SGroup.isDataSGroup(sGroup);
!sGroup?.data.expanded ||
SGroup.isDataSGroup(sGroup) ||
SGroup.isQuerySGroup(sGroup);
if (isSGroupPresent && isShowingTooltip) {
const groupName = sGroup.data.name;
const groupStruct = makeStruct(editor, sGroup);
Expand Down
Loading

0 comments on commit 8660fdc

Please sign in to comment.