Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,72 @@
/****************************************************************************
* 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 acts from '../action';
import { isEqual, isEmpty, pickBy } from 'lodash/fp';
function execute(activeTool, { action, editor, server, options }) {
if (action.tool) {
if (editor.tool(action.tool, action.opts))
return action;
}
else if (typeof action === 'function')
action(editor, server, options);
else
console.info('no action');
return activeTool;
}
function selected(actObj, activeTool, { editor, server }) {
if (typeof actObj.selected === 'function')
return actObj.selected(editor, server);
else if (actObj.action && actObj.action.tool)
return isEqual(activeTool, actObj.action);
return false;
}
function disabled(actObj, { editor, server, options }) {
if (typeof actObj.disabled === 'function')
return actObj.disabled(editor, server, options);
return false;
}
function status(key, activeTool, params) {
let actObj = acts[key];
return pickBy(x => x, {
selected: selected(actObj, activeTool, params),
disabled: disabled(actObj, params)
});
}
export default function (state=null, { type, action, ...params }) {
switch(type) {
case 'INIT':
action = acts['select-lasso'].action;
case 'ACTION':
const activeTool = execute(state && state.activeTool, {
...params, action
});
case 'UPDATE':
return Object.keys(acts).reduce((res, key) => {
const value = status(key, res.activeTool, params);
if (!isEmpty(value))
res[key] = value;
return res;
}, { activeTool: activeTool || state.activeTool });
default:
return state;
}
}

View File

@ -0,0 +1,106 @@
/****************************************************************************
* 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 { debounce } from 'lodash/fp';
import element from '../../chem/element';
import acts from '../action';
import { openDialog } from './';
import { fromBond, toBond, fromSgroup, toSgroup, fromElement, toElement } from '../structconv';
export function initEditor(dispatch, getState) {
const updateAction = debounce(100, () => dispatch({ type: 'UPDATE' }));
const sleep = (time) => new Promise(resolve => setTimeout(resolve, time));
function resetToSelect(dispatch, getState) {
const resetToSelect = getState().options.settings.resetToSelect;
const activeTool = getState().actionState.activeTool.tool;
if (resetToSelect === true || resetToSelect === activeTool) // example: 'paste'
dispatch({ type: 'ACTION', action: acts['select-lasso'].action });
else
updateAction();
}
return {
onInit: editor => {
dispatch({ type: 'INIT', editor });
},
onChange: () => {
dispatch(resetToSelect);
},
onSelectionChange: () => {
updateAction();
},
onElementEdit: selem => {
const elem = fromElement(selem);
let dlg = null;
if (element.map[elem.label]) {
dlg = openDialog(dispatch, 'atomProps', elem);
} else if (Object.keys(elem).length === 1 && 'ap' in elem) {
dlg = openDialog(dispatch, 'attachmentPoints', elem.ap)
.then((res) => ({ ap: res }));
} else if (elem.type === 'list' || elem.type === 'not-list') {
dlg = openDialog(dispatch, 'period-table', elem);
} else if (elem.type === 'rlabel') {
dlg = openDialog(dispatch, 'rgroup', elem);
} else {
dlg = openDialog(dispatch, 'period-table', elem);
}
return dlg.then(toElement);
},
onQuickEdit: atom => {
return openDialog(dispatch, 'labelEdit', atom)
},
onBondEdit: bond => {
return openDialog(dispatch, 'bondProps', fromBond(bond))
.then(toBond);
},
onRgroupEdit: rgroup => {
if (Object.keys(rgroup).length > 1) {
const rgids = [];
getState().editor.struct().rgroups.each(rgid => rgids.push(rgid));
if (!rgroup.range) rgroup.range = '>0';
return openDialog(dispatch, 'rgroupLogic',
Object.assign({ rgroupLabels: rgids }, rgroup));
}
return openDialog(dispatch, 'rgroup', rgroup);
},
onSgroupEdit: sgroup => {
return sleep(0) // huck to open dialog after dispatch sgroup tool action
.then(() => openDialog(dispatch, 'sgroup', fromSgroup(sgroup)))
.then(toSgroup);
},
onSdataEdit: sgroup => {
return sleep(0)
.then(() => openDialog(dispatch, sgroup.type === 'DAT' ? 'sdata' : 'sgroup', fromSgroup(sgroup)))
.then(toSgroup);
},
onMessage: msg => {
if (msg.error)
alert(msg.error);
else {
let act = Object.keys(msg)[0];
console[act](msg[act]);
}
},
onMouseDown: event => {}
};
}

View 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.
***************************************************************************/
import { getDefaultOptions } from '../data/options-schema';
import { initSdata, sdataReducer } from './sdata';
export const formsState = {
atomProps: {
errors: {},
valid: true,
result: {
label: '',
charge: 0,
explicitValence: -1,
hCount: 0,
invRet: 0,
isotope: 0,
radical: 0,
ringBondCount: 0,
substitutionCount: 0
}
} ,
attachmentPoints: {
errors: {},
valid: true,
result: {
primary: false,
secondary: false
}
},
automap: {
errors: {},
valid: true,
result: {
mode: "discard"
}
},
bondProps: {
errors: {},
valid: true,
result: {
type: 'single',
topology: 0,
center: 0
}
},
check: {
errors: {},
moleculeErrors: {},
result: {
checkOptions: ['valence', 'radicals', 'pseudoatoms', 'stereo', 'query', 'overlapping_atoms',
'overlapping_bonds', 'rgroups', 'chiral', '3d']
}
},
labelEdit: {
errors: {},
valid: true,
result: {
label: '',
}
},
rgroupLogic: {
errors: {},
valid: true,
result: {
ifthen: 0,
range: '>0',
resth: false
}
},
settings: {
errors: {},
valid: true,
result: getDefaultOptions()
},
sgroup: {
errors: {},
valid: true,
result: {
type: 'GEN'
}
},
sdata: initSdata()
};
export function updateFormState(data) {
return {
type: 'UPDATE_FORM',
data: data
};
}
export function checkErrors(errors) {
return {
type: 'UPDATE_FORM',
data: { moleculeErrors: errors }
};
}
export function setDefaultSettings() {
return {
type: 'UPDATE_FORM',
data: {
result: getDefaultOptions(),
valid: true,
errors: {}
}
};
}
export function formReducer(state, action, formName) {
if (formName === 'sdata')
return sdataReducer(state, action);
return Object.assign({}, state, action.data);
}

View File

@ -0,0 +1,156 @@
/****************************************************************************
* 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 { isEqual, debounce } from 'lodash/fp';
import molfile from '../../chem/molfile';
import keyNorm from '../keynorm';
import actions from '../action';
import * as clipArea from '../component/cliparea';
import * as structFormat from '../structformat';
import { onAction, openDialog, load } from './';
export function initKeydownListener(element) {
return function (dispatch, getState) {
const hotKeys = initHotKeys();
element.addEventListener('keydown', (event) => keyHandle(dispatch, getState, hotKeys, event));
}
}
/* HotKeys */
function keyHandle(dispatch, getState, hotKeys, event) {
const state = getState();
if (state.modal) return;
const editor = state.editor;
const actionState = state.actionState;
const actionTool = actionState.activeTool;
const key = keyNorm(event);
const atomsSelected = editor.selection() && editor.selection().atoms;
let group = null;
if (key && key.length === 1 && atomsSelected && key.match(/\w/)) {
console.assert(atomsSelected.length > 0);
openDialog(dispatch, 'labelEdit', { letter: key }).then(res => {
dispatch(onAction({ tool: 'atom', opts: res }));
});
event.preventDefault();
} else if (group = keyNorm.lookup(hotKeys, event)) {
let index = checkGroupOnTool(group, actionTool); // index currentTool in group || -1
index = (index + 1) % group.length;
let actName = group[index];
if (actionState[actName] && actionState[actName].disabled === true)
return event.preventDefault();
if (clipArea.actions.indexOf(actName) === -1) {
let newAction = actions[actName].action;
dispatch(onAction(newAction));
event.preventDefault();
} else if (window.clipboardData) // IE support
clipArea.exec(event);
}
}
function setHotKey(key, actName, hotKeys) {
if (Array.isArray(hotKeys[key]))
hotKeys[key].push(actName);
else
hotKeys[key] = [actName];
}
function initHotKeys() {
const hotKeys = {};
let act;
for (let actName in actions) {
act = actions[actName];
if (!act.shortcut) continue;
if (Array.isArray(act.shortcut))
act.shortcut.forEach(key => setHotKey(key, actName, hotKeys));
else
setHotKey(act.shortcut, actName, hotKeys);
}
return keyNorm(hotKeys);
}
function checkGroupOnTool(group, actionTool) {
let index = group.indexOf(actionTool.tool);
group.forEach((actName, i) => {
if (isEqual(actions[actName].action, actionTool))
index = i;
});
return index;
}
/* ClipArea */
export function initClipboard(dispatch, getState) {
const formats = Object.keys(structFormat.map).map(function (fmt) {
return structFormat.map[fmt].mime;
});
const debAction = debounce(0, (action) => dispatch( onAction(action) ));
const loadStruct = debounce(0, (structStr, opts) => dispatch( load(structStr, opts) ));
return {
formats: formats,
focused: function () {
return !getState().modal;
},
onCut: function () {
let data = clipData(getState().editor);
debAction({ tool: 'eraser', opts: 1 });
return data;
},
onCopy: function () {
let editor = getState().editor;
let data = clipData(editor);
editor.selection(null);
return data;
},
onPaste: function (data) {
const structStr = data['chemical/x-mdl-molfile'] ||
data['chemical/x-mdl-rxnfile'] ||
data['text/plain'];
if (structStr)
loadStruct(structStr, { fragment: true });
}
};
}
function clipData(editor) {
const res = {};
const struct = editor.structSelected();
if (struct.isBlank())
return null;
const type = struct.isReaction ?
'chemical/x-mdl-molfile' : 'chemical/x-mdl-rxnfile';
res['text/plain'] = res[type] = molfile.stringify(struct);
// res['chemical/x-daylight-smiles'] =
// smiles.stringify(struct);
return res;
}

View File

@ -0,0 +1,167 @@
/****************************************************************************
* 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 { pick } from 'lodash/fp';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import { logger } from 'redux-logger';
import * as structFormat from '../structformat';
import { formsState, formReducer } from './form';
import { optionsState, optionsReducer } from './options';
import { initTmplState, templatesReducer } from './templates';
import action from './action';
import toolbar from './toolbar';
function modal(state = null, action) {
const { type, data } = action;
if (type === 'UPDATE_FORM') {
const formState = formReducer(state.form, action, state.name);
return { ...state, form: formState }
}
switch (type) {
case 'MODAL_CLOSE':
return null;
case 'MODAL_OPEN':
return {
name: data.name,
form: formsState[data.name] || null,
prop: data.prop || null
};
default:
return state;
}
}
const shared = combineReducers({
actionState: action,
toolbar,
modal,
server: (store=null) => store,
editor: (store=null) => store,
options: optionsReducer,
templates: templatesReducer
});
export function onAction(action) {
if (action && action.dialog)
return {
type: 'MODAL_OPEN',
data: { name: action.dialog }
};
if (action && action.thunk)
return action.thunk;
return {
type: 'ACTION',
action
};
}
export function openDialog(dispatch, dialogName, props) {
return new Promise((resolve, reject) => {
dispatch({
type: 'MODAL_OPEN',
data: {
name: dialogName,
prop: {
...props,
onResult: resolve,
onCancel: reject
}
}
})
});
}
export function load(structStr, options) {
return (dispatch, getState) => {
const state = getState();
const editor = state.editor;
const server = state.server;
options = options || {};
// TODO: check if structStr is parsed already
//utils.loading('show');
const parsed = structFormat.fromString(structStr,
options, server);
parsed.catch(function (err) {
//utils.loading('hide');
alert("Can't parse molecule!");
});
return parsed.then(function (struct) {
//utils.loading('hide');
console.assert(struct, 'No molecule to update');
if (options.rescale)
struct.rescale(); // TODO: move out parsing?
if (options.fragment && !struct.isBlank())
dispatch(onAction({ tool: 'paste', opts: struct }));
else
editor.struct(struct);
return struct;
}, function (err) {
alert(err);
});
}
}
function root(state, action) {
switch (action.type) {
case 'INIT':
global._ui_editor = action.editor;
case 'UPDATE':
let {type, ...data} = action;
if (data)
state = { ...state, ...data };
}
const sh = shared(state, {
...action,
...pick(['editor', 'server', 'options'], state)
});
return (sh === state.shared) ? state : {
...state, ...sh
};
}
export default function(options, server) {
// TODO: redux localStorage here
const initState = {
actionState: null,
options: Object.assign(optionsState, { app: options }),
server: server || Promise.reject("Standalone mode!"),
editor: null,
modal: null,
templates: initTmplState
};
const middleware = [ thunk ];
if (process.env.NODE_ENV !== 'production')
middleware.push(logger);
return createStore(root, initState, applyMiddleware(...middleware));
};

View 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.
***************************************************************************/
import { openDialog, load } from './';
import * as structFormat from '../structformat';
export function miewAction(dispatch, getState) {
const editor = getState().editor;
const server = getState().server;
let convert = structFormat.toString(editor.struct(),
'cml', server);
convert.then(function (cml) {
openDialog(dispatch, 'miew', {
structStr: cml
}).then(function (res) {
if (res.structStr)
dispatch(load(res.structStr));
});
});
}

View File

@ -0,0 +1,114 @@
/****************************************************************************
* 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 { pick } from 'lodash/fp';
import { SERVER_OPTIONS, getDefaultOptions, validation } from '../data/options-schema';
import { storage } from '../utils';
export const optionsState = {
app: {
server: false,
templates: false
},
analyse: {
values: null,
roundWeight: 3,
roundMass: 3
},
recognize: {
file: null,
structStr: null,
fragment: false
},
settings: Object.assign(getDefaultOptions(), validation(storage.getItem("ketcher-opts"))),
getServerSettings: function() {
return pick(SERVER_OPTIONS, this.settings);
}
};
export function appUpdate(data) {
return dispatch => {
dispatch({ type: 'APP_OPTIONS', data });
dispatch({ type: 'UPDATE' })
}
}
/* SETTINGS */
export function saveSettings(newSettings) {
storage.setItem("ketcher-opts", newSettings);
return {
type: 'SAVE_SETTINGS',
data: newSettings
};
}
/* ANALYZE */
export function changeRound(roundName, value) {
return {
type: 'CHANGE_ANALYSE',
data: { [roundName]: value }
};
}
/* RECOGNIZE */
const recognizeActions = [
'SET_RECOGNIZE_STRUCT',
'CHANGE_RECOGNIZE_FILE',
'IS_FRAGMENT_RECOGNIZE'
];
export function setStruct(str) {
return {
type: 'SET_RECOGNIZE_STRUCT',
data: { structStr: str }
};
}
export function changeImage(file) {
return {
type: 'CHANGE_RECOGNIZE_FILE',
data: {
file: file,
structStr: null
}
};
}
export function shouldFragment(isFrag) {
return {
type: 'IS_FRAGMENT_RECOGNIZE',
data: { fragment: isFrag }
};
}
export function optionsReducer(state = {}, action) {
let { type, data } = action;
if (type === 'APP_OPTIONS')
return {...state, app: { ...state.app, ...data }};
if (type === 'SAVE_SETTINGS')
return {...state, settings: data};
if (type === 'CHANGE_ANALYSE')
return {...state, analyse: { ...state.analyse, ...data }};
if (recognizeActions.includes(type)) {
return {...state, recognize: { ...state.recognize, ...data }}
}
return state;
}

View File

@ -0,0 +1,114 @@
/****************************************************************************
* 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 { sdataSchema, getSdataDefault } from '../data/sdata-schema'
export const initSdata = () => {
const context = getSdataDefault();
const fieldName = getSdataDefault(context);
const fieldValue = getSdataDefault(context, fieldName);
const radiobuttons = 'Absolute';
return {
errors: {},
valid: true,
result: {
context,
fieldName,
fieldValue,
radiobuttons,
type: 'DAT'
}
}
};
export function sdataReducer(state, action) {
if (action.data.result.init)
return correctErrors({
...state,
result: Object.assign({}, state.result, action.data.result)
}, action.data);
const actionContext = action.data.result.context;
const actionFieldName = action.data.result.fieldName;
let newstate = null;
if (actionContext !== state.result.context)
newstate = onContextChange(state, action.data.result);
else if (actionFieldName !== state.result.fieldName)
newstate = onFieldNameChange(state, action.data.result);
newstate = newstate || {
...state,
result: Object.assign({}, state.result, action.data.result)
};
return correctErrors(newstate, action.data);
}
const correctErrors = (state, payload) => {
const { valid, errors } = payload;
const { fieldName, fieldValue } = state.result;
return {
result: state.result,
valid: valid && !!fieldName && !!fieldValue,
errors: errors,
};
};
const onContextChange = (state, payload) => {
const { context, fieldValue } = payload;
const fieldName = getSdataDefault(context);
let fValue = fieldValue;
if (fValue === state.result.fieldValue)
fValue = getSdataDefault(context, fieldName);
return {
result: {
...payload,
context,
fieldName,
fieldValue: fValue
}
};
};
const onFieldNameChange = (state, payload) => {
let { fieldName } = payload;
const context = state.result.context;
let fieldValue = payload.fieldValue;
if (sdataSchema[context][fieldName]) {
fieldValue = getSdataDefault(context, fieldName);
}
if (fieldValue === state.result.fieldValue && sdataSchema[context][state.result.fieldName])
fieldValue = '';
return {
result: {
...payload,
fieldName,
fieldValue,
}
};
};

View File

@ -0,0 +1,155 @@
/****************************************************************************
* 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 { pick, omit } from 'lodash/fp';
import molfile from '../../chem/molfile';
import { setStruct, appUpdate } from './options';
import { checkErrors } from './form';
import { load } from './';
export function checkServer() {
return (dispatch, getState) => {
const server = getState().server;
server.then(
(res) => dispatch(appUpdate({
indigoVersion: res.indigoVersion,
server: true
})),
(err) => console.info(err)
);
}
}
export function recognize(file) {
return (dispatch, getState) => {
const recognize = getState().server.recognize;
let process = recognize(file).then(res => {
dispatch(setStruct(res.struct));
}, err => {
dispatch(setStruct(null));
setTimeout(() => alert("Error! The picture isn't recognized."), 200); // TODO: remove me...
});
dispatch(setStruct(process));
};
}
export function check(optsTypes) {
return (dispatch, getState) => {
const { editor, server } = getState();
const options = getState().options.getServerSettings();
options.data = { 'types': optsTypes };
serverCall(editor, server, 'check', options)
.then(res => dispatch(checkErrors(res)))
.catch(console.error);
}
}
export function automap(res) {
return serverTransform('automap', res);
}
export function analyse() {
return (dispatch, getState) => {
const { editor, server } = getState();
const options = getState().options.getServerSettings();
options.data = {
properties: ['molecular-weight', 'most-abundant-mass',
'monoisotopic-mass', 'gross', 'mass-composition']
};
serverCall(editor, server, 'calculate', options).then(function (values) {
dispatch({
type: 'CHANGE_ANALYSE',
data: { values }
});
});
}
}
export function serverTransform(method, data, struct) {
return (dispatch, getState) => {
const state = getState();
let opts = state.options.getServerSettings();
opts.data = data;
serverCall(state.editor, state.server, method, opts, struct).then(function (res) {
dispatch( load(res.struct, { rescale: method === 'layout' }) );
});
};
}
function serverCall(editor, server, method, options, struct) {
const selection = editor.selection();
let selectedAtoms = [];
if (selection)
selectedAtoms = selection.atoms ? selection.atoms : editor.explicitSelected().atoms;
if (!struct) {
const aidMap = {};
struct = editor.struct().clone(null, null, false, aidMap);
const reindexMap = getReindexMap(struct.getComponents());
selectedAtoms = selectedAtoms.map(function (aid) {
return reindexMap[aidMap[aid]];
});
}
let request = server.then(function () {
return server[method](Object.assign({
struct: molfile.stringify(struct, { ignoreErrors: true })
}, selectedAtoms && selectedAtoms.length > 0 ? {
selected: selectedAtoms
} : null, options.data), omit('data', options));
});
//utils.loading('show');
request.catch(function (err) {
alert(err);
}).then(function (er) {
//utils.loading('hide');
});
return request;
}
function getReindexMap(components) {
return flatten(components.reactants)
.concat(flatten(components.products))
.reduce(function (acc, item, index) {
acc[item] = index;
return acc;
}, {});
}
/**
* Flats passed object
* Ex: [ [1, 2], [3, [4, 5] ] ] -> [1, 2, 3, 4, 5]
* { a: 1, b: { c: 2, d: 3 } } -> [1, 2, 3]
* @param source { object }
*/
function flatten(source) {
if (typeof source !== 'object')
return source;
return Object.keys(source).reduce(function (acc, key) {
const item = source[key];
return acc.concat(flatten(item));
}, []);
}

View File

@ -0,0 +1,250 @@
/****************************************************************************
* 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 { omit } from 'lodash/fp';
import sdf from '../../chem/sdf';
import molfile from '../../chem/molfile';
import { appUpdate } from './options';
import { openDialog } from './';
import { storage } from '../utils';
/* TEMPLATES */
export function selectTmpl(tmpl) {
return {
type: 'TMPL_SELECT',
data: { selected: tmpl }
}
}
export function changeGroup(group) {
return {
type: 'TMPL_CHANGE_GROUP',
data: { group: group, selected: null }
}
}
export function changeFilter(filter) {
return {
type: 'TMPL_CHANGE_FILTER',
data: { filter: filter.trim(), selected: null } // TODO: change this
}
}
/* TEMPLATE-ATTACH-EDIT */
export function initAttach(name, attach) {
return {
type: 'INIT_ATTACH',
data: {
name,
atomid: attach.atomid,
bondid: attach.bondid
}
};
}
export function setAttachPoints(attach) {
return {
type: 'SET_ATTACH_POINTS',
data: {
atomid: attach.atomid,
bondid: attach.bondid
}
};
}
export function setTmplName(name) {
return {
type: 'SET_TMPL_NAME',
data: { name }
};
}
export function editTmpl(tmpl) {
return (dispatch, getState) => {
openDialog(dispatch, 'attach', { tmpl }).then(
({ name, attach }) => {
tmpl.struct.name = name;
tmpl.props = Object.assign({}, tmpl.props, attach);
if (tmpl.props.group === 'User Templates')
updateLocalStore(getState().templates.lib);
openDialog(dispatch, 'templates');
}, () => {
openDialog(dispatch, 'templates');
}
);
}
}
/* SAVE */
export function saveUserTmpl(structStr) {
const tmpl = { struct: molfile.parse(structStr), props: {} };
return (dispatch, getState) => {
openDialog(dispatch, 'attach', { tmpl }).then(
({ name, attach }) => {
tmpl.struct.name = name;
tmpl.props = { ...attach, group: 'User Templates' };
let lib = getState().templates.lib.concat(tmpl);
dispatch(initLib(lib));
updateLocalStore(lib);
}
);
}
}
function updateLocalStore(lib) {
const userLib = lib
.filter(item => item.props.group === 'User Templates')
.map(item => {
return {
struct: molfile.stringify(item.struct),
props: Object.assign({}, omit(['group'], item.props))
};
});
storage.setItem("ketcher-tmpls", userLib);
}
/* REDUCER */
export const initTmplState = {
lib: [],
selected: null,
filter: '',
group: null,
attach: {}
};
const tmplActions = [
'TMPL_INIT',
'TMPL_SELECT',
'TMPL_CHANGE_GROUP',
'TMPL_CHANGE_FILTER'
];
const attachActions = [
'INIT_ATTACH',
'SET_ATTACH_POINTS',
'SET_TMPL_NAME'
];
export function templatesReducer(state = initTmplState, action) {
if (tmplActions.includes(action.type)) {
return Object.assign({}, state, action.data);
}
if (attachActions.includes(action.type)) {
const attach = Object.assign({}, state.attach, action.data);
return { ...state, attach };
}
return state;
}
/* INIT TEMPLATES LIBRARY */
function initLib(lib) {
return {
type: 'TMPL_INIT',
data: { lib: lib }
}
}
export function initTmplLib(dispatch, baseUrl, cacheEl) {
prefetchStatic(baseUrl + 'library.sdf').then(text => {
const tmpls = sdf.parse(text);
const prefetch = prefetchRender(tmpls, baseUrl, cacheEl);
return prefetch.then(cachedFiles => (
tmpls.map(tmpl => {
const pr = prefetchSplit(tmpl);
if (pr.file)
tmpl.props.prerender = cachedFiles.indexOf(pr.file) !== -1 ? `#${pr.id}` : '';
return tmpl;
})
));
}).then(res => {
const lib = res.concat(userTmpls());
dispatch(initLib(lib));
dispatch(appUpdate({ templates: true }));
});
}
function userTmpls() {
const userLib = storage.getItem("ketcher-tmpls");
if (!Array.isArray(userLib) || userLib.length === 0) return [];
return userLib
.map(tmpl => {
try {
if (tmpl.props === '') tmpl.props = {};
tmpl.props.group = 'User Templates';
return {
struct: molfile.parse(tmpl.struct),
props: tmpl.props
};
} catch (ex) {
return null;
}
})
.filter(tmpl => tmpl !== null);
}
function prefetchStatic(url) {
return fetch(url, { credentials: 'same-origin' }).then(function (resp) {
if (resp.ok)
return resp.text();
throw "Could not fetch " + url;
});
}
function prefetchSplit(tmpl) {
const pr = tmpl.props.prerender;
const res = pr && pr.split('#', 2);
return {
file: pr && res[0],
id: pr && res[1]
};
}
function prefetchRender(tmpls, baseUrl, cacheEl) {
const files = tmpls.reduce((res, tmpl) => {
const file = prefetchSplit(tmpl).file;
if (file && res.indexOf(file) === -1)
res.push(file);
return res;
}, []);
const fetch = Promise.all(files.map(fn => (
prefetchStatic(baseUrl + fn).catch(() => null)
)));
return fetch.then(svgs => {
svgs.forEach(svgContent => {
if (svgContent)
cacheEl.innerHTML += svgContent;
});
return files.filter((file, i) => (
!!svgs[i]
));
});
}

View 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.
***************************************************************************/
import { capitalize, debounce, isEqual } from 'lodash/fp';
import { basic as basicAtoms } from '../action/atoms';
import tools from '../action/tools';
const initial = {
freqAtoms: [],
currentAtom: 0,
opened: null,
visibleTools: {}
};
const MAX_ATOMS = 7;
export function initResize() {
return function (dispatch, getState) {
const onResize = debounce(100, () => {
getState().editor.render.update();
dispatch({ type: 'CLEAR_VISIBLE' })
});
addEventListener('resize', onResize);
}
}
export default function (state=initial, action) {
let { type, data } = action;
switch (type) {
case 'ACTION':
let visibleTool = toolInMenu(action.action);
return visibleTool
? { ...state, opened: null, visibleTools: { ...state.visibleTools, ...visibleTool } }
: state;
case 'ADD_ATOMS':
const newState = addFreqAtom(data, state.freqAtoms, state.currentAtom);
return { ...state, ...newState };
case 'CLEAR_VISIBLE':
return { ...state, opened: null, visibleTools: {} };
case 'OPENED':
return { ...state, opened: data };
case 'UPDATE':
return { ...state, opened: null };
default:
return state;
}
}
function addFreqAtom(label, freqAtoms, index) {
label = capitalize(label);
if (basicAtoms.indexOf(label) > -1 || freqAtoms.indexOf(label) !== -1) return { freqAtoms };
freqAtoms[index] = label;
index = (index + 1) % MAX_ATOMS;
return { freqAtoms, currentAtom: index };
}
export function addAtoms(atomLabel) {
return {
type: 'ADD_ATOMS',
data: atomLabel
};
}
function getToolFromAction(action) {
let tool = null;
for (let toolName in tools) {
if (tools.hasOwnProperty(toolName) && isEqual(action, tools[toolName].action))
tool = toolName;
}
return tool;
}
function toolInMenu(action) {
let tool = getToolFromAction(action);
let sel = document.getElementById(tool);
let dropdown = sel && hiddenAncestor(sel);
return dropdown ? { [dropdown.id]: sel.id } : null;
}
export function hiddenAncestor(el, base) {
base = base || document.body;
let findEl = el;
while (window.getComputedStyle(findEl).overflow !== 'hidden' && !findEl.classList.contains('opened')) {
if (findEl === base) return null;
findEl = findEl.parentNode;
}
return findEl;
}