Skip to content

Commit

Permalink
#3231 - Support serializing canvas to KET format for macromolecules o…
Browse files Browse the repository at this point in the history
…n ketcher side

- added ket macromolecules serializer for monomers and bonds
  • Loading branch information
rrodionov91 committed Sep 15, 2023
1 parent d6b4f58 commit ff01c78
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Sugar } from 'domain/entities/Sugar';
import { Phosphate } from 'domain/entities/Phosphate';
import { RNABase } from 'domain/entities/RNABase';
import { BaseMonomer } from 'domain/entities/BaseMonomer';
import { monomerClass } from 'application/formatters/types/ket';

type DerivedClass<T> = new (...args: unknown[]) => T;
const MONOMER_CONST = {
Expand All @@ -21,35 +22,45 @@ const MONOMER_CONST = {
RNA: 'RNA',
R: 'R', // states for Ribose
P: 'P', // states for Phosphate
SUGAR: 'SUGAR',
BASE: 'BASE',
PHOSPHATE: 'PHOSPHATE',
};

export const monomerFactory = (
monomer: MonomerItemType,
): [
Monomer: typeof BaseMonomer,
MonomerRenderer: DerivedClass<BaseMonomerRenderer>,
monomerClass: monomerClass,
] => {
let Monomer;
let MonomerRenderer;
let monomerClass: monomerClass;

if (monomer.props.MonomerType === MONOMER_CONST.CHEM) {
Monomer = Chem;
MonomerRenderer = ChemRenderer;
monomerClass = 'CHEM';
} else if (monomer.props.MonomerType === MONOMER_CONST.PEPTIDE) {
Monomer = Peptide;
MonomerRenderer = PeptideRenderer;
monomerClass = 'PEPTIDE';
} else {
if (monomer.props.MonomerNaturalAnalogCode === MONOMER_CONST.R) {
Monomer = Sugar;
MonomerRenderer = SugarRenderer;
monomerClass = 'RNA';
} else if (monomer.props.MonomerNaturalAnalogCode === MONOMER_CONST.P) {
Monomer = Phosphate;
MonomerRenderer = PhosphateRenderer;
monomerClass = 'RNA';
} else {
Monomer = RNABase;
MonomerRenderer = RNABaseRenderer;
monomerClass = 'RNA';
}
}

return [Monomer, MonomerRenderer];
return [Monomer, MonomerRenderer, monomerClass];
};
2 changes: 2 additions & 0 deletions packages/ketcher-core/src/application/editor/tools/Monomer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class MonomerTool implements Tool {
const editorSettings = provideEditorSettings();
const modelChanges = this.editor.drawingEntitiesManager.addMonomer(
this.monomer,
// We convert monomer coordinates from pixels to angstroms
// because the model layer (like BaseMonomer) should not work with pixels
Scale.scaled2obj(
new Vec2(
this.editor.lastCursorPosition.x -
Expand Down
13 changes: 9 additions & 4 deletions packages/ketcher-core/src/application/formatters/types/ket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export interface IKetGroupNode {
export type KetNode = IKetMonomerNode | IKetGroupNode;

export interface IKetConnectionEndPoint {
monomerId: string;
attachmentPointId: string;
groupId: string;
monomerId?: string;
attachmentPointId?: string;
groupId?: string;
}

export interface IKetConnection {
Expand All @@ -29,9 +29,11 @@ export interface IKetConnection {
endPoint2: IKetConnectionEndPoint;
}

export type monomerClass = 'RNA' | 'PEPTIDE' | 'CHEM' | 'UNKNOWN';

export interface IKetMonomerTemplate {
type: 'monomerTemplate';
monomerClass?: 'RNA' | 'PEPTIDE' | 'CHEM' | 'UNKNOWN';
monomerClass?: monomerClass;
monomerSubClass?:
| 'AminoAcid'
| 'Sugar'
Expand All @@ -47,6 +49,9 @@ export interface IKetMonomerTemplate {
alias?: string;
naturalAnalog?: string;
attachmentPoints?;
root: {
nodes;
};
}

export interface IKetMacromoleculesContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ export abstract class BaseMonomerRenderer extends BaseRenderer {
}

private get scaledMonomerPosition() {
// we need to convert monomer coordinates(stored in angstroms) to pixels.
// it needs to be done in view layer of application (like renderers)
return Scale.obj2scaled(this.monomer.position, this.editorSettings);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/ketcher-core/src/domain/helpers/monomers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MonomerItemType } from 'domain/types';

export function getMonomerUniqueKey(monomer: MonomerItemType) {
return `${monomer.props.MonomerName}___${monomer.props.Name}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { CoreEditor } from 'application/editor';
import { monomerToDrawingEntity } from 'domain/serializers/ket/fromKet/monomerToDrawingEntity';
import assert from 'assert';
import { polymerBondToDrawingEntity } from 'domain/serializers/ket/fromKet/polymerBondToDrawingEntity';
import { getMonomerUniqueKey } from 'domain/helpers/monomers';
import { monomerFactory } from 'application/editor/operations/monomer/monomerFactory';

function parseNode(node: any, struct: any) {
const type = node.type;
Expand Down Expand Up @@ -229,8 +231,8 @@ export class KetSerializer implements Serializer<Struct> {
const template = parsedFileContent.root.templates.find(
(template) => template.id === node.templateId,
);
const struct = this.fillStruct(template);
assert(template);
const struct = this.fillStruct(template);
command.merge(monomerToDrawingEntity(node, template, struct));
break;
}
Expand All @@ -253,4 +255,62 @@ export class KetSerializer implements Serializer<Struct> {
});
editor.renderersContainer.update(command);
}

serializeMacromolecules() {
const editor = CoreEditor.provideEditorInstance();
const fileContent: IKetMacromoleculesContent = {
root: {
nodes: [],
connections: [],
templates: [],
},
};
editor.drawingEntitiesManager.monomers.forEach((monomer) => {
const templateId = getMonomerUniqueKey(monomer.monomerItem);
fileContent.root.nodes.push({
type: 'monomer',
id: monomer.id.toString(),
position: {
x: monomer.position.x,
y: monomer.position.y,
},
alias: monomer.label,
templateId,
});
if (
!fileContent.root.templates.find(
(template) => template.id === templateId,
)
) {
const [, , monomerClass] = monomerFactory(monomer.monomerItem);
fileContent.root.templates.push({
type: 'monomerTemplate',
monomerClass,
naturalAnalogShort:
monomer.monomerItem.props.MonomerNaturalAnalogCode,
id: templateId,
fullName: monomer.monomerItem.props.MonomerName,
alias: monomer.monomerItem.label,
...JSON.parse(this.serialize(monomer.monomerItem.struct)),
});
}
});

editor.drawingEntitiesManager.polymerBonds.forEach((polymerBond) => {
fileContent.root.connections.push({
connectionType: 'single',
endPoint1: {
monomerId: polymerBond.firstMonomer.id.toString(),
attachmentPointId:
polymerBond.firstMonomer.getAttachmentPointByBond(polymerBond),
},
endPoint2: {
monomerId: polymerBond.secondMonomer?.id.toString(),
attachmentPointId:
polymerBond.secondMonomer?.getAttachmentPointByBond(polymerBond),
},
});
});
return fileContent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class Molfile {
ret.initHalfBonds();
ret.initNeighbors();
ret.bindSGroupsToFunctionalGroups();
ret.markFragments();

return ret;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export interface Serializer<T> {
deserialize: (content: string) => T;
serialize: (struct: T) => string;
deserializeMacromolecule?: (content: string) => void;
serializeMacromolecules?: (content: string) => void;
}
1 change: 0 additions & 1 deletion packages/ketcher-polymer-editor-react/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ function Editor({ theme }: EditorProps) {
id="polymer-editor-canvas"
data-testid="ketcher-canvas"
ref={canvasRef}
data-testid="ketcher-canvas"
width="100%"
height="100%"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ import { SaveButton } from 'components/modal/save/saveButton';
import { getPropertiesByFormat, SupportedFormats } from 'helpers/formats';
import { ActionButton } from 'components/shared/actionButton';
import { Icon } from 'ketcher-react';
import { KetSerializer } from 'ketcher-core';
import { saveAs } from 'file-saver';

interface Props {
onClose: () => void;
isModalOpen: boolean;
}

const options: Array<Option> = [
{ id: 'ket', label: 'Ket' },
{ id: 'mol', label: 'MDL Molfile V3000' },
{ id: 'helm', label: 'HELM' },
];

const Form = styled.form({
Expand Down Expand Up @@ -87,49 +89,11 @@ const ErrorsButton = styled(ActionButton)(({ theme }) => ({
},
}));

const structExample = {
mol: `
-INDIGO-02102212382D
0 0 0 0 0 0 0 0 0 0 0 V3000
M V30 BEGIN CTAB
M V30 COUNTS 11 10 0 0 0
M V30 BEGIN ATOM
M V30 1 C 2.75 -5.225 0.0 0
M V30 2 C 3.61603 -5.725 0.0 0
M V30 3 C 4.48205 -5.225 0.0 0
M V30 4 C 5.34808 -5.725 0.0 0
M V30 5 C 6.2141 -5.225 0.0 0
M V30 6 C 7.08013 -5.725 0.0 0
M V30 7 C 7.94615 -5.225 0.0 0
M V30 8 C 8.81218 -5.725 0.0 0
M V30 9 C 9.6782 -5.225 0.0 0
M V30 10 C 10.5442 -5.725 0.0 0
M V30 11 C 11.4103 -5.225 0.0 0
M V30 END ATOM
M V30 BEGIN BOND
M V30 1 1 1 2
M V30 2 1 2 3
M V30 3 1 3 4
M V30 4 1 4 5
M V30 5 1 5 6
M V30 6 1 6 7
M V30 7 1 7 8
M V30 8 1 8 9
M V30 9 1 9 10
M V30 10 1 10 11
M V30 END BOND
M V30 END CTAB
M END
`,
helm: `PEPTIDE1{A.C.D.F.G.H.K.A.C.D}$PEPTIDE1,PEPTIDE1,3:R3-7:R3`,
}; // TODO remove when canvas and get struct method are ready

export const Save = ({ onClose, isModalOpen }: Props): JSX.Element => {
const [currentFileFormat, setCurrentFileFormat] =
useState<SupportedFormats>('mol');
useState<SupportedFormats>('ket');
const [currentFileName, setCurrentFileName] = useState('ketcher');
const [struct, setStruct] = useState('');
const [struct] = useState('');
const [errors, setErrors] = useState('');

const handleSelectChange = (value) => {
Expand All @@ -141,18 +105,20 @@ export const Save = ({ onClose, isModalOpen }: Props): JSX.Element => {
};

const handleSave = () => {
console.log('Saved', structExample);
const ketSerializer = new KetSerializer();
const serializedKet = ketSerializer.serializeMacromolecules();
const blob = new Blob([JSON.stringify(serializedKet)], {
type: getPropertiesByFormat(currentFileFormat).mime,
});
saveAs(blob, getPropertiesByFormat(currentFileFormat).name);
};

const handleErrorsClick = () => {
console.log('errors...');
};

useEffect(() => {
console.log('Getting and setting struct here...'); // get / convert struct and errors
setStruct(structExample[currentFileFormat]);

if (currentFileFormat === 'mol') {
if (currentFileFormat === 'ket') {
// just an example
setErrors('some error');
} else {
Expand Down Expand Up @@ -197,13 +163,7 @@ export const Save = ({ onClose, isModalOpen }: Props): JSX.Element => {

<SaveButton
label="Save as file"
data={struct}
type={getPropertiesByFormat(currentFileFormat).mime}
onSave={handleSave}
filename={
currentFileName +
getPropertiesByFormat(currentFileFormat).extensions[0]
}
disabled={!currentFileName}
/>
</Modal.Footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,16 @@
* limitations under the License.
***************************************************************************/

import { saveAs } from 'file-saver';

import { ActionButton } from 'components/shared/actionButton';
import { ChemicalMimeType } from 'helpers/formats';

type Props = {
label: string;
data: string;
filename: string;
type: ChemicalMimeType;
onSave: () => void;
disabled?: boolean;
};

export const SaveButton = ({
label,
data,
filename,
type,
onSave,
disabled = false,
}: Props) => {
export const SaveButton = ({ label, onSave, disabled = false }: Props) => {
const handleSave = () => {
const blob = new Blob([data], { type });
saveAs(blob, filename);
console.log('saved');
onSave();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,27 @@ import {
SupportedFormatProperties,
} from './supportedFormatProperties';

export type SupportedFormats = 'mol' | 'helm';
export type SupportedFormats = 'mol' | 'ket';

type FormatProperties = {
[key in SupportedFormats]: SupportedFormatProperties;
};

const formatProperties: FormatProperties = {
ket: new SupportedFormatProperties(
'Ket file',
ChemicalMimeType.Ket,
['.ket'],
true,
{},
),
mol: new SupportedFormatProperties(
'MDL Molfile V3000',
ChemicalMimeType.Mol,
['.mol'],
true,
{ 'molfile-saving-mode': '3000' },
),
helm: new SupportedFormatProperties('HELM', ChemicalMimeType.Helm, ['.helm']),
};

export const getPropertiesByFormat = (format: SupportedFormats) => {
Expand Down
Loading

0 comments on commit ff01c78

Please sign in to comment.