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,70 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import Dialog from '../component/dialog';
function About(props) {
return (
<Dialog title="About"
className="about" params={props}
buttons={["Close"]}>
<a href="http://lifescience.opensource.epam.com/ketcher/" target="_blank">
<img src="images/ketcher-logo.svg"/>
</a>
<dl>
<dt>
<a href="http://lifescience.opensource.epam.com/ketcher/help.html" target="_blank">Ketcher</a>
</dt>
<dd>
version <var>{props.version}</var>
</dd>
{
props.buildNumber ? (
<dd>
build #<var>{props.buildNumber}</var>
{" at "}
<time>{props.buildDate}</time>
</dd> ) : null
}
{
props.indigoVersion ? (
<div>
<dt>
<a href="http://lifescience.opensource.epam.com/indigo/" target="_blank">Indigo
Toolkit</a>
</dt>
<dd>version <var>{props.indigoVersion}</var></dd>
</div>
) : ( <dd>standalone</dd> )
}
<dt>
<a href="http://lifescience.opensource.epam.com/" target="_blank">EPAM Life Sciences</a>
</dt>
<dd>
<a href="http://lifescience.opensource.epam.com/ketcher/#feedback" target="_blank">Feedback</a>
</dd>
</dl>
</Dialog>
);
}
export default connect(
store => ({ ...store.options.app })
)(About);

View File

@ -0,0 +1,136 @@
/****************************************************************************
* 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 { range } from 'lodash/fp';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import keyName from 'w3c-keyname';
import Dialog from '../component/dialog';
import Input from '../component/input';
import { changeRound } from '../state/options';
import { analyse } from '../state/server';
function FrozenInput({value}) {
return (
<input type="text" spellCheck={false} value={value}
onKeyDown={ev => allowMovement(ev)}/>
);
}
const formulaRegexp = /\b([A-Z][a-z]{0,3})(\d*)\s*\b/g;
const errorRegexp = /error:.*/g;
function formulaInputMarkdown(value) {
return (
<div className="chem-input" spellCheck={false} contentEditable={true}
onKeyDown={ev => allowMovement(ev)}>{value}</div>
);
}
function FormulaInput({value}) {
if (errorRegexp.test(value)) {
return formulaInputMarkdown(value);
}
const content = [];
var cnd;
var pos = 0;
while (cnd = formulaRegexp.exec(value)) {
content.push(value.substring(pos, cnd.index) + cnd[1]);
if (cnd[2].length > 0) content.push(<sub>{cnd[2]}</sub>);
pos = cnd.index + cnd[0].length;
}
if (pos === 0) content.push(value);
else content.push(value.substring(pos, value.length));
return formulaInputMarkdown(content);
}
class Analyse extends Component {
constructor(props) {
super(props);
props.onAnalyse();
}
render() {
const { values, round, onAnalyse, onChangeRound, ...props } = this.props;
return (
<Dialog title="Calculated Values" className="analyse"
buttons={["Close"]} params={props}>
<ul>{[
{ name: 'Chemical Formula', key: 'gross' },
{ name: 'Molecular Weight', key: 'molecular-weight', round: 'roundWeight' },
{ name: 'Exact Mass', key: 'monoisotopic-mass', round: 'roundMass' },
{ name: 'Elemental Analysis', key: 'mass-composition' }
].map(item => (
<li>
<label>{item.name}:</label>
{item.key === 'gross'
? <FormulaInput value={values ? values[item.key] : 0}/>
: <FrozenInput value={values ? roundOff(values[item.key], round[item.round]) : 0}/>
}
{item.round
? <Input schema={{
enum: range(0, 8),
enumNames: range(0, 8).map(i => `${i} decimal places`)
}} value={round[item.round]} onChange={val => onChangeRound(item.round, val)}/>
: null
}
</li>
))
}</ul>
</Dialog>
);
}
}
function allowMovement(event) {
const movementKeys = ['Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'];
const key = keyName(event);
if (movementKeys.indexOf(key) === -1)
event.preventDefault();
}
function roundOff(value, round) {
if (typeof value === 'number')
return value.toFixed(round);
return value.replace(/[0-9]*\.[0-9]+/g, (str) => (
(+str).toFixed(round)
));
}
export default connect(
store => ({
values: store.options.analyse.values,
round: {
roundWeight: store.options.analyse.roundWeight,
roundMass: store.options.analyse.roundMass
}
}),
dispatch => ({
onAnalyse: () => dispatch(analyse()),
onChangeRound: (roundName, val) => dispatch(changeRound(roundName, val))
})
)(Analyse);

View File

@ -0,0 +1,78 @@
/****************************************************************************
* 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 } from 'lodash/fp';
import { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { atom as atomSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
import element from '../../chem/element';
function ElementNumber(props, {stateStore}) {
let { result } = stateStore.props;
return (
<label>Number:
<input className="number" type="text" readOnly={true}
value={element.map[capitalize(result.label)] || ''}/>
</label>
);
}
function Atom(props) {
let { formState, ...prop } = props;
return (
<Dialog title="Atom Properties" className="atom-props"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={atomSchema} customValid={{ label: l => atomValid(l) }}
init={prop} {...formState}>
<fieldset className="main">
<Field name="label"/>
<Field name="alias"/>
<ElementNumber/>
<Field name="charge" maxlength="5"/>
<Field name="explicitValence"/>
<Field name="isotope"/>
<Field name="radical"/>
</fieldset>
<fieldset className="query">
<legend>Query specific</legend>
<Field name="ringBondCount"/>
<Field name="hCount"/>
<Field name="substitutionCount"/>
<Field name="unsaturatedAtom"/>
</fieldset>
<fieldset className="reaction">
<legend>Reaction flags</legend>
<Field name="invRet"/>
<Field name="exactChangeFlag"/>
</fieldset>
</Form>
</Dialog>
);
}
function atomValid(label) {
return label && !!element.map[capitalize(label)];
}
export default connect(
(store) => ({ formState: store.modal.form })
)(Atom);

View File

@ -0,0 +1,40 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { attachmentPoints as attachmentPointsSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
function AttachmentPoints (props) {
let { formState, ...prop} = props;
return (
<Dialog title="Attachment Points" className="attach-points"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={attachmentPointsSchema} init={prop} {...formState}>
<Field name="primary"/>
<Field name="secondary"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form })
)(AttachmentPoints);

View File

@ -0,0 +1,59 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
import { automap } from '../state/server';
export const automapSchema = {
title: "Reaction Auto-Mapping",
type: "object",
required: ["mode"],
properties: {
mode: {
title: "Mode",
enum: ["discard", "keep", "alter", "clear"],
enumNames: ["Discard", "Keep", "Alter", "Clear"],
default: "discard"
}
}
};
function Automap (props) {
let { formState, ...prop} = props;
return (
<Dialog title="Reaction Auto-Mapping" className="automap"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={automapSchema} {...formState}>
<Field name="mode"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form }),
(dispatch, props) => ({
onOk: (res) => {
dispatch(automap(res));
props.onOk(res);
}
})
)(Automap);

View File

@ -0,0 +1,41 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { bond as bondSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
function Bond(props) {
let { formState, ...prop} = props;
return (
<Dialog title="Bond Properties" className="bond"
result={() => formState.result} valid={() => formState.valid} params={prop} >
<Form schema={bondSchema} init={prop} {...formState}>
<Field name="type"/>
<Field name="topology"/>
<Field name="center"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form })
)(Bond);

View File

@ -0,0 +1,89 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import Dialog from '../component/dialog';
import Tabs from '../component/tabs';
import { Form, Field } from '../component/form';
import { check } from '../state/server';
const checkSchema = {
title: 'Check',
type: 'object',
properties: {
checkOptions: {
type: 'array',
items: {
type: "string",
enum: ['valence', 'radicals', 'pseudoatoms', 'stereo', 'query', 'overlapping_atoms',
'overlapping_bonds', 'rgroups', 'chiral', '3d'],
enumNames: ['Valence', 'Radical', 'Pseudoatom', 'Stereochemistry', 'Query', 'Overlapping Atoms',
'Overlapping Bonds', 'R-Groups', 'Chirality', '3D Structure']
}
}
}
};
function getOptionName(opt) {
const d = checkSchema.properties.checkOptions.items;
return d.enumNames[d.enum.indexOf(opt)];
}
function Check(props) {
const tabs = ['Check', 'Settings'];
const { formState, onCheck, ...prop } = props;
const { result, moleculeErrors } = formState;
return (
<Dialog title="Structure Check" className="check"
result={() => result} params={prop}>
<Form schema={checkSchema} {...formState}>
<Tabs className="tabs" captions={tabs}
changeTab={(i) => i === 0 ? onCheck(result.checkOptions) : null}>
<ErrorsCheck moleculeErrors={moleculeErrors}/>
<Field name="checkOptions" multiple={true} type="checkbox"/>
</Tabs>
</Form>
</Dialog>
);
}
function ErrorsCheck(props) {
const { moleculeErrors } = props;
const moleculeErrorsTypes = Object.keys(moleculeErrors);
return (
<fieldset {...props}>
{moleculeErrorsTypes.length === 0 ?
<dt>No errors found</dt> :
moleculeErrorsTypes.map(type => (
<div>
<dt>{getOptionName(type)} error :</dt>
<dd>{moleculeErrors[type]}</dd>
</div>
))}
</fieldset>
);
}
export default connect(
store => ({ formState: store.modal.form }),
dispatch => ({
onCheck: (opts) => dispatch(check(opts))
})
)(Check);

View File

@ -0,0 +1,134 @@
/****************************************************************************
* 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 { h } from 'preact';
/** @jsx h */
import generics from '../../chem/generics';
const viewSchema = {
'atom': {
caption: 'Atom Generics',
order: ['any', 'no-carbon', 'metal', 'halogen']
},
'group': {
caption: 'Group Generics',
order: ['acyclic', 'cyclic']
},
'special': {
caption: 'Special Nodes',
order: []
},
'group/acyclic': {
caption: 'Acyclic',
order: ['carbo', 'hetero']
},
'group/cyclic': {
caption: 'Cyclic',
order: ['no-carbon', 'carbo', 'hetero']
},
'group/acyclic/carbo': {
caption: 'Carbo',
order: ['alkynyl', 'alkyl', 'alkenyl']
},
'group/acyclic/hetero': {
caption: 'Hetero',
order: ['alkoxy']
},
'group/cyclic/carbo': {
caption: 'Carbo',
order: ['aryl', 'cycloalkyl', 'cycloalkenyl']
},
'group/cyclic/hetero': {
caption: 'Hetero',
order: ['aryl']
},
'atom/any': 'any atom',
'atom/no-carbon': 'except C or H',
'atom/metal': 'any metal',
'atom/halogen': 'any halogen',
'group/cyclic/no-carbon': 'no carbon',
'group/cyclic/hetero/aryl': 'hetero aryl'
};
function GenSet({labels, caption='', selected, onSelect, ...props}) {
return (
<fieldset {...props}>
{
labels.map(label => (
<button onClick={e => onSelect(label)}
className={selected(label) ? 'selected' : ''}>
{label}</button>
))
}
{
caption ? (
<legend>{caption}</legend>
) : null
}
</fieldset>
);
}
function GenGroup({gen, name, path, selected, onSelect}) {
const group = gen[name];
const pk = path ? `${path}/${name}` : name;
const schema = viewSchema[pk];
return (schema && schema.caption) ? (
<fieldset className={name}>
<legend>{schema.caption}</legend>
{
group.labels ? (
<GenSet labels={group.labels}
selected={selected} onSelect={onSelect} />
) : null
}
{
schema.order.map(child => ( // TODO:order = Object.keys ifndef
<GenGroup gen={group} name={child} path={pk}
selected={selected} onSelect={onSelect}/>
))
}
</fieldset>
) : (
<GenSet labels={group.labels}
caption={schema || name} className={name}
selected={selected} onSelect={onSelect} />
);
}
function GenericGroups({ selected, onSelect, ...props }) {
return (
<div summary="Generic Groups" {...props}>
<div className="col">
<GenGroup gen={generics} name='atom'
selected={l => selected(l)}
onSelect={l => onSelect(l)}/>
<GenGroup gen={generics} name='special'
selected={l => selected(l)}
onSelect={l => onSelect(l)}/>
</div>
<div className="col">
<GenGroup gen={generics} name='group'
selected={l => selected(l)}
onSelect={l => onSelect(l)}/>
</div>
</div>
);
}
export default GenericGroups;

View File

@ -0,0 +1,32 @@
/****************************************************************************
* 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 { h } from 'preact';
/** @jsx h */
import Dialog from '../component/dialog';
function Help(props) {
return (
<Dialog title="Help"
className="help" params={props}
buttons={["Close"]}>
<iframe className="help" src="doc/help.html"></iframe>
</Dialog>
);
}
export default Help;

View File

@ -0,0 +1,64 @@
/****************************************************************************
* 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 Open from './open';
import Save from './save';
import Analyse from './analyse';
import Recognize from './recognize';
import PeriodTable from './period-table';
import Rgroup from './rgroup';
import TemplateAttach from './template-attach';
import TemplatesLib from './template-lib';
import About from './about';
import Help from './help';
import Miew from './miew';
// schemify dialogs
import Atom from './atom';
import AttachPoints from './attach';
import Automap from './automap';
import Bond from './bond';
import Check from './check';
import LabelEdit from './labeledit';
import RgroupLogic from './rgroup-logic';
import Settings from './options';
import Sgroup from './sgroup';
import Sdata from './sdata';
export default {
open: Open,
save: Save,
analyse: Analyse,
recognize: Recognize,
'period-table': PeriodTable,
rgroup: Rgroup,
attach: TemplateAttach,
templates: TemplatesLib,
about: About,
help: Help,
miew: Miew,
atomProps: Atom,
attachmentPoints: AttachPoints,
automap: Automap,
bondProps: Bond,
check: Check,
labelEdit: LabelEdit,
rgroupLogic: RgroupLogic,
settings: Settings,
sgroup: Sgroup,
sdata: Sdata
};

View File

@ -0,0 +1,96 @@
/****************************************************************************
* 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 } from 'lodash/fp';
import { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import element from '../../chem/element';
import Dialog from '../component/dialog';
import { Form, Field } from '../component/form';
export const labelEditSchema = {
title: "Label Edit",
type: "object",
required: ["label"],
properties: {
label: {
title: "Atom",
default: ''
}
}
};
function serialize(lc) {
const charge = Math.abs(lc.charge);
const radical = ['', ':', '.', '^^'][lc.radical] || '';
let sign = '';
if (charge)
sign = lc.charge < 0 ? '-' : '+';
return (lc.isotope || '') + lc.label + radical +
(charge > 1 ? charge: '') + sign;
}
function deserialize(value) {
const match = value.match(/^(\d+)?([a-z*]{1,3})(\.|:|\^\^)?(\d+[-+]|[-+])?$/i); // TODO: radical on last place
if (match) {
const label = match[2] === '*' ? 'A' : capitalize(match[2]);
let charge = 0;
let isotope = 0;
let radical = 0;
if (match[1])
isotope = parseInt(match[1]);
if (match[3])
radical = { ':': 1, '.': 2, '^^': 3 }[match[3]];
if (match[4]) {
charge = parseInt(match[4]);
if (isNaN(charge)) // NaN => [-+]
charge = 1;
if (match[4].endsWith('-'))
charge = -charge;
}
// Not consistant
if (label === 'A' || label === 'Q' || label === 'X' || label === 'M' || element.map[label])
return { label, charge, isotope, radical };
}
return null;
}
function LabelEdit(props) {
const init = { label: props.letter || serialize(props) };
const { formState, ...prop} = props;
const { result, valid } = formState;
return (
<Dialog title="Label Edit" className="labeledit" valid={() => valid}
result={() => deserialize(result.label)} params={prop}>
<Form schema={labelEditSchema} customValid={{label: l => deserialize(l)}}
init={init} {...formState}>
<Field name="label" maxlength="20" size="10"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form })
)(LabelEdit);

View File

@ -0,0 +1,223 @@
/****************************************************************************
* 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 { camelCase } from 'lodash/fp';
import { h, Component } from 'preact';
/** @jsx h */
import Dialog from '../component/dialog';
import { storage } from '../utils';
const MIEW_PATH = '__MIEW_PATH__';
const MIEW_OPTIONS = {
preset: 'small',
settings: {
theme: 'light',
atomLabel: 'bright',
autoPreset: false,
inversePanning: true
},
reps: [{
mode: 'LN',
colorer: 'AT',
selector: 'all'
}]
};
const MIEW_WINDOW = {
location: 'no',
menubar: 'no',
toolbar: 'no',
directories: 'no',
modal: 'yes',
alwaysRaised: 'yes'
};
const MIEW_MODES = {
'lines': 'LN',
'ballsAndSticks': 'BS',
'licorice': 'LC'
};
function getLocalMiewOpts() {
let userOpts = storage.getItem("ketcher-opts");
if (!userOpts)
return MIEW_OPTIONS;
const opts = MIEW_OPTIONS;
if (userOpts.miewTheme)
opts.settings.theme = camelCase(userOpts.miewTheme);
if (userOpts.miewAtomLabel)
opts.settings.atomLabel = camelCase(userOpts.miewAtomLabel);
if (userOpts.miewMode)
opts.reps[0].mode = MIEW_MODES[camelCase(userOpts.miewMode)];
return opts;
}
function origin (url) {
let loc = url;
if (!loc.href) {
loc = document.createElement('a');
loc.href = url;
}
if (loc.origin)
return loc.origin;
if (!loc.hostname) // relative url, IE
loc = document.location;
return loc.protocol + '//' + loc.hostname +
(!loc.port ? '' : ':' + loc.port);
}
function queryOptions(options, sep='&') {
if (Array.isArray(options)) {
return options.reduce((res, item) => {
let value = queryOptions(item);
if (value !== null)
res.push(value);
return res;
}, []).join(sep);
} else if (typeof options === 'object') {
return Object.keys(options).reduce((res, item) => {
let value = options[item];
res.push(typeof value === 'object' ?
queryOptions(value) :
encodeURIComponent(item) + '=' +
encodeURIComponent(value));
return res;
}, []).join(sep);
} else {
return null;
}
}
function miewLoad(wnd, url, options={}) { // TODO: timeout
return new Promise(function (resolve, reject) {
addEventListener('message', function onload(event) {
if (event.origin === origin(url) && event.data === 'miewLoadComplete') {
window.removeEventListener('message', onload);
let miew = wnd.MIEWS[0];
miew._opts.load = false; // setOptions({ load: '' })
miew._menuDisabled = true; // no way to disable menu after constructor return
if (miew.init()) {
miew.setOptions(options);
miew.benchmarkGfx().then(() => {
miew.run();
setTimeout(() => resolve(miew), 10);
// see setOptions message handler
});
}
}
});
});
}
function miewSave(miew, url) {
miew.saveData();
return new Promise(function (resolve, reject) {
addEventListener('message', function onsave(event) {
if (event.origin === origin(url) && event.data.startsWith('CML:')) {
window.removeEventListener('message', onsave);
resolve(atob(event.data.slice(4)));
}
});
});
}
class Miew extends Component {
constructor(props) {
console.info('init');
super(props);
this.opts = getLocalMiewOpts();
}
load(ev) {
let miew = miewLoad(ev.target.contentWindow,
MIEW_PATH, this.opts);
this.setState({ miew });
this.state.miew.then(miew => {
miew.parse(this.props.structStr, {
fileType: 'cml',
loaded: true
});
this.setState({ miew });
});
}
save(ev) {
if (this.props.onOk) {
let structStr = miewSave(this.state.miew, MIEW_PATH);
this.setState({ structStr });
this.state.structStr.then(structStr => {
this.props.onOk({ structStr });
});
}
}
window() {
let opts = {
...this.opts,
load: `CML:${btoa(this.props.structStr)}`,
sourceType: 'message'
};
let br = this.base.getBoundingClientRect(); // Preact specifiec
// see: epa.ms/1NAYWp
let wndProps = {
...MIEW_WINDOW,
top: Math.round(br.top),
left: Math.round(br.left),
width: Math.round(br.width),
height: Math.round(br.height)
};
let wnd = window.open(`${MIEW_PATH}?${queryOptions(opts)}`,
'miew', queryOptions(wndProps, ','));
if (wnd) {
this.props.onCancel && this.props.onCancel();
wnd.onload = function () {
console.info('windowed');
};
}
}
render(props) {
let {miew, structStr} = this.state;
return (
<Dialog title="3D View"
className="miew" params={props}
buttons={[
"Close",
<button disabled={miew instanceof Promise || structStr instanceof Promise}
onClick={ ev => this.save(ev) }>
Apply
</button>,
<button className="window"
disabled={/MSIE|rv:11/i.test(navigator.userAgent)}
onClick={ ev => this.window() }>
Detach to new window
</button>
]}>
<iframe id="miew-iframe"
src={MIEW_PATH}
onLoad={ev => this.load(ev) }></iframe>
</Dialog>
);
}
}
export default Miew;

View File

@ -0,0 +1,95 @@
/****************************************************************************
* 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 { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { map as formatMap } from '../structformat';
import Dialog from '../component/dialog';
import OpenButton from '../component/openbutton';
import ClipArea, { exec } from '../component/cliparea';
import { load } from '../state';
class Open extends Component {
constructor(props) {
super(props);
this.state = {
structStr: '',
fragment: false
};
}
result() {
let { structStr, fragment } = this.state;
return structStr ? { structStr, fragment } : null;
}
changeStructStr(structStr) {
this.setState({ structStr });
}
changeFragment(target) {
this.setState({
fragment: target.checked
});
}
render () {
let { structStr, fragment } = this.state;
return (
<Dialog title="Open Structure"
className="open" result={() => this.result()}
params={this.props}
buttons={[(
<OpenButton className="open" server={this.props.server}
type={structAcceptMimes()}
onLoad={s => this.changeStructStr(s)}>
Open From File
</OpenButton>
), "Cancel", "OK"]}>
<textarea value={structStr}
onInput={ev => this.changeStructStr(ev.target.value)}/>
<label>
<input type="checkbox" checked={fragment}
onClick={ev => this.changeFragment(ev.target)}/>
Load as a fragment and copy to the Clipboard
</label>
<ClipArea focused={() => true}
onCopy={() => ({ 'text/plain': structStr })}/>
</Dialog>
);
}
}
function structAcceptMimes() {
return Object.keys(formatMap).reduce((res, key) => (
res.concat(formatMap[key].mime, ...formatMap[key].ext)
), []).join(',');
}
export default connect(
store => ({ server: store.server }),
(dispatch, props) => ({
onOk: (res) => {
if (res.fragment) exec('copy');
dispatch(
load(res.structStr, {
badHeaderRecover: true,
fragment: res.fragment
})
);
props.onOk(res);
}
})
)(Open);

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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { updateFormState, setDefaultSettings } from '../state/form';
import { saveSettings } from '../state/options';
import settingsSchema from '../data/options-schema';
import { Form, Field } from '../component/form';
import { storage } from '../utils';
import Dialog from '../component/dialog';
import Accordion from '../component/accordion';
import SystemFonts from '../component/systemfonts';
import SaveButton from '../component/savebutton';
import OpenButton from '../component/openbutton';
import MeasureInput from '../component/measure-input';
function Settings(props) {
const { initState, formState, server, onOpenFile, onReset, appOpts, ...prop } = props;
const tabs = ['Rendering customization options', 'Atoms', 'Bonds', 'Server', '3D Viewer', 'Options for debugging'];
const activeTabs = { 0: true, 1: false, 2: false, 3: false, 4: false, 5: false };
return (
<Dialog title="Settings" className="settings"
result={() => formState.result} valid={() => formState.valid} params={prop}
buttons={[
<OpenButton className="open" server={ server } onLoad={ onOpenFile }>
Open From File
</OpenButton>,
<SaveButton className="save" data={JSON.stringify(formState.result)} filename={'ketcher-settings'}>
Save To File
</SaveButton>,
<button onClick={ onReset }>Reset</button>,
"OK", "Cancel"]} >
<Form schema={settingsSchema} init={initState} {...formState}>
<Accordion className="accordion" captions={tabs} active={activeTabs}>
<fieldset className="render">
<Field name="resetToSelect"/>
<Field name="rotationStep"/>
<SelectCheckbox name="showValenceWarnings"/>
<SelectCheckbox name="atomColoring"/>
<SelectCheckbox name="hideChiralFlag"/>
<Field name="font" component={SystemFonts}/>
<FieldMeasure name="fontsz"/>
<FieldMeasure name="fontszsub"/>
</fieldset>
<fieldset className="atoms">
<SelectCheckbox name="carbonExplicitly"/>
<SelectCheckbox name="showCharge"/>
<SelectCheckbox name="showValence"/>
<Field name="showHydrogenLabels"/>
</fieldset>
<fieldset className="bonds">
<SelectCheckbox name="aromaticCircle"/>
<FieldMeasure name="doubleBondWidth"/>
<FieldMeasure name="bondThickness"/>
<FieldMeasure name="stereoBondWidth"/>
</fieldset>
<fieldset className="server" disabled={!appOpts.server}>
<SelectCheckbox name="smart-layout"/>
<SelectCheckbox name="ignore-stereochemistry-errors"/>
<SelectCheckbox name="mass-skip-error-on-pseudoatoms"/>
<SelectCheckbox name="gross-formula-add-rsites"/>
</fieldset>
<fieldset className="3dView" disabled={!appOpts.miewPath}>
<Field name="miewMode"/>
<Field name="miewTheme"/>
<Field name="miewAtomLabel"/>
</fieldset>
<fieldset className="debug">
<SelectCheckbox name="showAtomIds"/>
<SelectCheckbox name="showBondIds"/>
<SelectCheckbox name="showHalfBondIds"/>
<SelectCheckbox name="showLoopIds"/>
</fieldset>
</Accordion>
{ !storage.isAvailable() ? <div className="warning">{storage.warningMessage}</div> : null }
</Form>
</Dialog>
);
}
function SelectCheckbox(props, {schema}) {
const desc = {
title: schema.properties[props.name].title,
enum: [true, false],
enumNames: ['on', 'off'],
};
return <Field schema={desc} {...props}/>;
}
function FieldMeasure(props, {schema}) {
return <Field schema={schema.properties[props.name]} component={MeasureInput} {...props}/>
}
export default connect(store => ({
appOpts: store.options.app,
initState: store.options.settings,
formState: store.modal.form
}), (dispatch, props) => ({
onOpenFile: newOpts => {
try {
dispatch(updateFormState({ result: JSON.parse(newOpts) }));
} catch (ex) {
console.info('Bad file');
}
},
onReset: () => dispatch(setDefaultSettings()),
onOk: (res) => {
dispatch(saveSettings(res));
props.onOk(res);
}
}))(Settings);

View File

@ -0,0 +1,272 @@
/****************************************************************************
* 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 { range } from 'lodash/fp';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import element from '../../chem/element';
import Dialog from '../component/dialog';
import Atom from '../component/atom';
import Tabs from '../component/tabs';
import GenericGroups from './generic-groups';
import { fromElement, toElement } from '../structconv';
import { onAction } from '../state';
import { addAtoms } from '../state/toolbar';
const typeSchema = [
{ title: 'Single', value: 'atom' },
{ title: 'List', value: 'list'},
{ title: 'Not List', value: 'not-list'}
];
const beforeSpan = {
'He': 16,
'B': 10,
'Al': 10,
'Hf': 1,
'Rf': 1
};
const main = rowPartition(element.filter(el => el && el.type !== 'actinide' &&
el.type !== 'lanthanide'));
const lanthanides = element.filter(el => el && el.type === 'lanthanide');
const actinides = element.filter(el => el && el.type === 'actinide');
function Header() {
return (
<tr>
{
range(0, 19).map(i => (
<th>{i || ''}</th>
))
}
</tr>
);
}
function TypeChoise({value, onChange, ...props}) {
return (
<fieldset>
{
typeSchema.map(sc => (
<label>
<input type="radio" value={sc.value}
checked={sc.value === value}
onClick={ev => onChange(sc.value) } {...props}/>
{sc.title}
</label>
))
}
</fieldset>
);
}
function MainRow({row, caption, refer, selected, onSelect, curEvents}) {
return (
<tr>
<th>{caption}</th>
{
row.map(el => (typeof el !== 'number') ? (
<td>
<Atom el={el}
className={selected(el.label) ? 'selected' : ''}
onClick={ev => onSelect(el.label)} {...curEvents(el)}/>
</td>
) : (
refer(el) ? ( <td className="ref">{refer(el)}</td> ) :
( <td colspan={el}/> )
))
}
</tr>
);
}
function OutinerRow({row, caption, selected, onSelect, curEvents}) {
return (
<tr>
<th colspan="3" className="ref">{caption}</th>
{
row.map(el => (
<td>
<Atom el={el}
className={selected(el.label) ? 'selected' : ''}
onClick={ev => onSelect(el.label)} {...curEvents(el)}/>
</td>
))
}
<td></td>
</tr>
);
}
function AtomInfo({el, isInfo}) {
const numberStyle = { color: el.color || 'black', 'font-size': '1.2em' };
const elemStyle = { color: el.color || 'black', 'font-weight': 'bold', 'font-size': '2em' };
return (
<div className={`atom-info ${isInfo ? '' : 'none'}`}>
<div style={numberStyle}>{element.map[el.label]}</div>
<span style={elemStyle}>{el.label}</span><br/>
{el.title}<br/>
{el.atomic_mass}
</div>
);
}
class PeriodTable extends Component {
constructor(props) {
super(props);
let genType = !!this.props.pseudo ? 'gen' : null;
this.state = {
type: props.type || genType || 'atom',
value: props.values || props.label || null,
cur: element[2],
isInfo: false
};
this.firstType = true;
}
changeType(type) {
if (this.firstType)
return this.firstType = false;
let pl = this.state.type === 'list' || this.state.type === 'not-list';
let l = type === 'list' || type === 'not-list';
if (l && pl)
this.setState({type});
else
this.setState({
type,
value: type === 'atom' || type === 'gen' ? null : []
});
}
selected(label) {
let {type, value} = this.state;
return (type === 'atom' || type === 'gen') ? value === label :
value.includes(label);
}
onSelect(label) {
let {type, value} = this.state;
if (type === 'atom' || type === 'gen')
this.setState({ value: label });
else {
let i = value.indexOf(label);
if (i < 0)
value.push(label);
else
value.splice(i, 1);
this.setState({ value });
}
}
result() {
let {type, value} = this.state;
if (type === 'atom')
return value ? { label: value, pseudo: null } : null;
else if (type === 'gen')
return value ? { type, label: value, pseudo: value} : null;
else
return value.length ? { type, values: value } : null;
}
curEvents = (el) => {
return {
onMouseEnter: () => this.setState({ cur: el, isInfo: true }),
onMouseLeave: () => this.setState({ isInfo: false })
};
};
render () {
const tabs = ['Table', 'Extended'];
let { type } = this.state;
return (
<Dialog title="Periodic table" className="elements-table"
params={this.props} result={() => this.result()}>
<Tabs className="tabs" captions={tabs} tabIndex={type !== 'gen' ? 0 : 1}
changeTab={(i) => this.changeType(i === 0 ? 'atom' : 'gen')}>
<div className="period-table">
<table summary="Periodic table of the chemical elements">
<Header/>
<AtomInfo el={this.state.cur} isInfo={this.state.isInfo}/>
{
main.map((row, i) => (
<MainRow row={row} caption={i + 1}
refer={o => o === 1 && (i === 5 ? '*' : '**')}
curEvents={this.curEvents}
selected={l => this.selected(l)}
onSelect={l => this.onSelect(l)}/>
))
}
<OutinerRow row={lanthanides} caption="*"
curEvents={this.curEvents}
selected={l => this.selected(l)}
onSelect={l => this.onSelect(l)}/>
<OutinerRow row={actinides} caption="**"
curEvents={this.curEvents}
selected={l => this.selected(l)}
onSelect={l => this.onSelect(l)}/>
</table>
<TypeChoise value={type}
onChange={t => this.changeType(t) }/>
</div>
<GenericGroups className="generic-groups"
selected={this.selected.bind(this)}
onSelect={this.onSelect.bind(this)}/>
</Tabs>
</Dialog>
);
}
}
function rowPartition(elements) {
return elements.reduce(function (res, el) {
let row = res[el.period - 1];
if (!row)
res.push([el]);
else {
if (beforeSpan[el.label])
row.push(beforeSpan[el.label]);
row.push(el);
}
return res;
}, []);
}
function mapSelectionToProps(editor) {
const selection = editor.selection();
if (selection && Object.keys(selection).length === 1 &&
selection.atoms && Object.keys(selection.atoms).length === 1) {
let struct = editor.struct();
let atom = struct.atoms.get(selection.atoms[0]);
return { ...fromElement(atom) }
}
return {};
}
export default connect(
(store, props) => {
if (props.values || props.label) return {};
return mapSelectionToProps(store.editor);
},
(dispatch, props) => ({
onOk: (res) => {
if (!res.type || res.type === 'atom') dispatch(addAtoms(res.label));
dispatch(onAction({ tool: 'atom', opts: toElement(res) }));
props.onOk(res);
}
})
)(PeriodTable);

View File

@ -0,0 +1,104 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { changeImage, shouldFragment } from '../state/options';
import { load } from '../state';
import { recognize } from '../state/server';
import Dialog from '../component/dialog';
import Input from '../component/input';
import StructRender from '../component/structrender';
import OpenButton from '../component/openbutton';
import Spin from '../component/spin';
function Recognize(prop) {
const {file, structStr, fragment, onRecognize, isFragment, onImage, ...props} = prop;
const result = () =>
structStr && !(structStr instanceof Promise) ? {structStr, fragment} : null;
return (
<Dialog title="Import From Image" className="recognize"
params={props} result={() => result(structStr, fragment) }
buttons={[
<OpenButton className="open" onLoad={onImage} type="image/*">
Choose file
</OpenButton>,
<span className="open-filename">{file ? file.name : null}</span>,
file && !structStr ? (
<button onClick={() => onRecognize(file) }>Recognize</button>
) : null,
"Cancel",
"OK"
]}>
<div className="picture">
{
file ? (
<img id="pic" src={url(file) || ""}
onError={() => {
onImage(null);
alert("Error, it isn't a picture");
}}/>
) : null
}
</div>
<div className="output">
{
structStr ? (
structStr instanceof Promise || typeof structStr !== 'string' ? // in Edge 38:
( <Spin/> ) : // instanceof Promise always `false`
( <StructRender className="struct" struct={structStr}/> )
) : null
}
</div>
<label>
<Input type="checkbox" value={fragment} onChange={v => isFragment(v)}/>
Load as a fragment
</label>
</Dialog>
);
}
function url(file) {
if (!file) return null;
const URL = window.URL || window.webkitURL;
return URL ? URL.createObjectURL(file) : "No preview";
}
export default connect(
store => ({
file: store.options.recognize.file,
structStr: store.options.recognize.structStr,
fragment: store.options.recognize.fragment
}),
(dispatch, props) => ({
isFragment: (v) => dispatch(shouldFragment(v)),
onImage: (file) => dispatch(changeImage(file)),
onRecognize: (file) => dispatch(recognize(file)),
onOk: (res) => {
dispatch(
load(res.structStr, {
rescale: true,
fragment: res.fragment
})
);
props.onOk(res);
}
})
)(Recognize);

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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { rgroup as rgroupSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
function IfThenSelect(props, { schema }) {
const { name, rgids } = props;
const desc = {
title: schema.properties[name].title,
enum: [0],
enumNames: ['Always']
};
rgids.forEach(label => {
if (props.label !== label) {
desc.enum.push(label);
desc.enumNames.push(`IF R${props.label} THEN R${label}`);
}
});
return <Field name={name} schema={desc} {...props}/>;
}
function RgroupLogic (props) {
const { formState, label, rgroupLabels, ...prop } = props;
return (
<Dialog title="R-Group Logic" className="rgroup-logic"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={rgroupSchema}
customValid={{range: r => rangeConv(r)}} init={prop} {...formState}>
<Field name="range"/>
<Field name="resth"/>
<IfThenSelect name="ifthen" className="cond" label={label} rgids={rgroupLabels}/>
</Form>
</Dialog>
);
}
function rangeConv(range) { // structConv
const res = range.replace(/\s*/g, '').replace(/,+/g, ',')
.replace(/^,/, '').replace(/,$/, '');
return res.split(',').every(function (s) {
return s.match(/^[>,<=]?[0-9]+$/g) ||
s.match(/^[0-9]+-[0-9]+$/g);
});
}
export default connect(
store => ({ formState: store.modal.form })
)(RgroupLogic);

View File

@ -0,0 +1,101 @@
/****************************************************************************
* 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 { range } from 'lodash/fp';
import { h, Component } from 'preact';
/** @jsx h */
import Dialog from '../component/dialog';
function RGroup({ selected, onSelect, result, ...props }) {
return (
<Dialog title="R-Group"
className="rgroup" params={props}
result={() => result()}>
<ul>
{ range(1, 33).map(i => (
<li>
<button
className={ selected(i) ? 'selected' : ''}
onClick={ev => onSelect(i)}>
{`R${i}`}
</button>
</li>
)) }
</ul>
</Dialog>
);
}
class RGroupFragment extends Component {
constructor({label}) {
super();
this.state.label = label || null;
}
onSelect(label) {
this.setState({
label: label !== this.state.label ? label : null
});
}
selected(label) {
return label === this.state.label;
}
result() {
return { label: this.state.label };
}
render() {
return (
<RGroup selected={i => this.selected(i)}
onSelect={i => this.onSelect(i)}
result={() => this.result()} {...this.props}/>
);
}
}
class RGroupAtom extends Component {
constructor({values}) {
super();
this.state.values = values || [];
}
onSelect(index) {
const {values} = this.state;
const i = values.indexOf(index);
if (i < 0)
values.push(index);
else
values.splice(i, 1);
this.setState({ values });
}
selected(index) {
return this.state.values.includes(index);
}
result() {
return {
type: 'rlabel',
values: this.state.values
};
}
render() {
return (
<RGroup selected={i => this.selected(i)}
onSelect={i => this.onSelect(i)}
result={() => this.result() } {...this.props}/>
);
}
}
export default params => params.type === 'rlabel' ? (<RGroupAtom {...params}/>) : (<RGroupFragment {...params}/>);

View File

@ -0,0 +1,90 @@
/****************************************************************************
* 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 { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import * as structFormat from '../structformat';
import { saveUserTmpl } from '../state/templates';
import Dialog from '../component/dialog';
import SaveButton from '../component/savebutton';
class Save extends Component {
constructor(props) {
super(props);
this.state = { type: props.struct.hasRxnArrow() ? 'rxn' : 'mol' };
this.changeType().catch(props.onCancel);
}
changeType(ev) {
let { type } = this.state;
if (ev) {
type = ev.target.value;
ev.preventDefault();
}
let converted = structFormat.toString(this.props.struct, type, this.props.server, this.props.options);
return converted.then(structStr => this.setState({ type, structStr }),
e => { alert(e); });
}
render () {
// $('[value=inchi]').disabled = ui.standalone;
let { type, structStr } = this.state;
let format = structFormat.map[type];
console.assert(format, "Unknown chemical file type");
return (
<Dialog title="Save Structure"
className="save" params={this.props}
buttons={[(
<SaveButton className="save"
data={structStr}
filename={'ketcher' + format.ext[0]}
type={format.mime}
server={this.props.server}
onSave={ () => this.props.onOk() }>
Save To File
</SaveButton>
), (
<button className="save-tmpl"
onClick={ () => this.props.onTmplSave(structStr) }>
Save to Templates</button>
), "Close"]}>
<label>Format:
<select value={type} onChange={ev => this.changeType(ev)}>{
[this.props.struct.hasRxnArrow() ? 'rxn' : 'mol', 'smiles', 'smarts', 'cml', 'inchi'].map(type => (
<option value={type}>{structFormat.map[type].name}</option>
))
}</select>
</label>
<textarea className={type} value={structStr} readonly
ref={ el => el && setTimeout(() => el.select(), 10) }/>
</Dialog>
);
}
}
export default connect(
store => ({
server: store.server,
struct: store.editor.struct(),
options: store.options.getServerSettings()
}),
dispatch => ({
onTmplSave: struct => dispatch(saveUserTmpl(struct))
})
)(Save);

View File

@ -0,0 +1,89 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
import { Form, Field, SelectOneOf } from '../component/form';
import Dialog from '../component/dialog';
import ComboBox from '../component/combobox';
import { sdataSchema, sdataCustomSchema, getSdataDefault } from '../data/sdata-schema'
/** @jsx h */
function SelectInput({ title, name, schema, ...prop }) {
const inputSelect = Object.keys(schema).reduce((acc, item) => {
acc.enum.push(item);
acc.enumNames.push(schema[item].title || item);
return acc;
},
{
title: title,
type: 'string',
default: '',
minLength: 1,
enum: [],
enumNames: []
}
);
return <Field name={name} schema={inputSelect} component={ComboBox} {...prop} />
}
function SData({ context, fieldName, fieldValue, type, radiobuttons, formState, ...prop }) {
const { result, valid } = formState;
const init = {
context,
fieldName: fieldName || getSdataDefault(context),
type,
radiobuttons
};
init.fieldValue = fieldValue || getSdataDefault(context, init.fieldName);
const formSchema = sdataSchema[result.context][result.fieldName] || sdataCustomSchema;
const serialize = {
context: result.context.trim(),
fieldName: result.fieldName.trim(),
fieldValue: typeof (result.fieldValue) === 'string' ? result.fieldValue.trim() : result.fieldValue
};
return (
<Dialog title={"S-Group Properties"} className="sgroup"
result={() => result} valid={() => valid} params={prop}>
<Form serialize={serialize} schema={formSchema} init={init} {...formState}>
<SelectOneOf title="Context" name="context" schema={sdataSchema}/>
<fieldset className={"data"}>
<SelectInput title="Field name" name="fieldName" schema={sdataSchema[result.context]}/>
{
content(formSchema, result.context, result.fieldName)
}
</fieldset>
</Form>
</Dialog>
);
}
const content = (schema, context, fieldName) => Object.keys(schema.properties)
.filter(prop => prop !== "type" && prop !== "context" && prop !== "fieldName")
.map(prop => prop === "radiobuttons" ?
<Field name={prop} type="radio" key={`${context}-${fieldName}-${prop}-radio`}/> :
<Field name={prop} type="textarea" multiple={true} size="10" key={`${context}-${fieldName}-${prop}-select`}/>
);
export default connect(
store => ({ formState: store.modal.form })
)(SData);

View File

@ -0,0 +1,61 @@
/****************************************************************************
* 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 { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { sgroup as sgroupSchema } from '../structschema';
import { Form, Field, SelectOneOf } from '../component/form';
import { mapOf } from '../utils';
import Dialog from '../component/dialog';
const schemes = mapOf(sgroupSchema, 'type');
function Sgroup({ formState, ...prop }) {
const { result, valid } = formState;
const type = result.type;
return (
<Dialog title="S-Group Properties" className="sgroup"
result={() => result} valid={() => valid} params={prop}>
<Form schema={schemes[type]} init={prop} {...formState}>
<SelectOneOf title="Type" name="type" schema={schemes}/>
<fieldset className={type === 'DAT' ? 'data' : 'base'}>
{ content(type) }
</fieldset>
</Form>
</Dialog>
);
}
const content = type => Object.keys(schemes[type].properties)
.filter(prop => prop !== 'type')
.map(prop => {
let props = {};
if (prop === 'name') props.maxlength = 15;
if (prop === 'fieldName') props.maxlength = 30;
if (prop === 'fieldValue') props.type = 'textarea';
if (prop === 'radiobuttons') props.type = 'radio';
return <Field name={prop} key={`${type}-${prop}`} {...props}/>;
}
);
export default connect(
(store) => ({ formState: store.modal.form })
)(Sgroup);

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 { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import Dialog from '../component/dialog';
import Input from '../component/input';
import StructEditor from '../component/structeditor';
import { storage } from '../utils';
import { initAttach, setAttachPoints, setTmplName } from '../state/templates';
const EDITOR_STYLES = {
selectionStyle: { fill: '#47b3ec', stroke: 'none' },
highlightStyle: { stroke: '#1a7090', 'stroke-width': 1.2 }
};
class Attach extends Component {
constructor({ onInit, ...props }) {
super();
this.tmpl = initTmpl(props.tmpl);
onInit(this.tmpl.struct.name, this.tmpl.props);
this.onResult = this.onResult.bind(this);
}
onResult() {
const { name, atomid, bondid } = this.props;
return name && (
name !== this.tmpl.struct.name ||
atomid !== this.tmpl.props.atomid ||
bondid !== this.tmpl.props.bondid
) ? { name, attach: { atomid, bondid } } : null;
}
render() {
const {
name, atomid, bondid,
onNameEdit, onAttachEdit, ...prop
} = this.props;
const struct = this.tmpl.struct;
const options = Object.assign(EDITOR_STYLES, { scale: getScale(struct) });
return (
<Dialog title="Template Edit" className="attach"
result={this.onResult} params={prop}>
<label>Template name:
<Input value={name} onChange={onNameEdit}/>
</label>
<label>Choose attachment atom and bond:</label>
<StructEditor className="editor"
struct={struct}
onAttachEdit={onAttachEdit}
tool="attach" toolOpts={{ atomid, bondid }}
options={options}/>
{!storage.isAvailable() ? <div className="warning">{storage.warningMessage}</div> : null}
</Dialog>
);
}
}
export default connect(
store => ({ ...store.templates.attach }),
dispatch => ({
onInit: (name, ap) => dispatch(initAttach(name, ap)),
onAttachEdit: ap => dispatch(setAttachPoints(ap)),
onNameEdit: name => dispatch(setTmplName(name))
})
)(Attach);
function initTmpl(tmpl) {
const normTmpl = {
struct: structNormalization(tmpl.struct),
props: {
atomid: +tmpl.props.atomid || 0,
bondid: +tmpl.props.bondid || 0
}
};
normTmpl.struct.name = tmpl.struct.name;
return normTmpl;
}
function structNormalization(struct) {
const normStruct = struct.clone();
const cbb = normStruct.getCoordBoundingBox();
normStruct.atoms.each(function (aid, atom) { // only atoms ?? mb arrow etc ...
atom.pp = atom.pp.sub(cbb.min);
});
return normStruct;
}
function getScale(struct) {
const cbb = struct.getCoordBoundingBox();
const VIEW_SIZE = 200;
let scale = VIEW_SIZE / Math.max(cbb.max.y - cbb.min.y, cbb.max.x - cbb.min.x);
if (scale < 35) scale = 35;
if (scale > 75) scale = 75;
return scale;
}

View File

@ -0,0 +1,174 @@
/****************************************************************************
* 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 { escapeRegExp, chunk, flow, filter as _filter, reduce, omit } from 'lodash/fp';
import { createSelector } from 'reselect';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import sdf from '../../chem/sdf';
import VisibleView from '../component/visibleview';
import StructRender from '../component/structrender';
import Dialog from '../component/dialog';
import SaveButton from '../component/savebutton';
import Input from '../component/input';
import SelectList from '../component/select';
import { changeFilter, changeGroup, selectTmpl, editTmpl } from '../state/templates';
import { onAction } from "../state/";
const GREEK_SIMBOLS = {
'Alpha': 'A', 'alpha': 'α',
'Beta': 'B', 'beta': 'β',
'Gamma': 'Г', 'gamma': 'γ'
};
function tmplName(tmpl, i) {
console.assert(tmpl.props && tmpl.props.group, "No group");
return tmpl.struct.name || `${tmpl.props.group} template ${i + 1}`;
}
function partition(n, array) {
console.warn('partition', n);
return chunk(n)(array);
}
const greekRe = new RegExp('\\b' + Object.keys(GREEK_SIMBOLS).join('\\b|\\b') + '\\b', 'g');
function greekify(str) {
return str.replace(greekRe, sym => GREEK_SIMBOLS[sym]);
}
const filterLibSelector = createSelector(
(props) => props.lib,
(props) => props.filter,
filterLib
);
function filterLib(lib, filter) {
console.warn('Filter', filter);
let re = new RegExp(escapeRegExp(greekify(filter)), 'i');
return flow(
_filter(item => !filter || re.test(greekify(item.struct.name)) || re.test(greekify(item.props.group))),
reduce((res, item) => {
!res[item.props.group] ? res[item.props.group] = [item] : res[item.props.group].push(item);
return res;
}, {})
)(lib)
}
const libRowsSelector = createSelector(
(props) => props.lib,
(props) => props.group,
(props) => props.COLS,
libRows
);
function libRows(lib, group, COLS) {
console.warn("Group", group);
return partition(COLS, lib[group])
}
function RenderTmpl({ tmpl, ...props }) {
return tmpl.props && tmpl.props.prerender ?
( <svg {...props}><use xlinkHref={tmpl.props.prerender}/></svg> ) :
( <StructRender struct={tmpl.struct} options={{ autoScaleMargin: 15 }} {...props}/> );
}
class TemplateLib extends Component {
select(tmpl) {
if (tmpl === this.props.selected)
this.props.onOk(this.result());
else
this.props.onSelect(tmpl);
}
result() {
const tmpl = this.props.selected;
console.assert(!tmpl || tmpl.props, 'Incorrect SDF parse');
return tmpl ? {
struct: tmpl.struct,
aid: parseInt(tmpl.props.atomid) || null,
bid: parseInt(tmpl.props.bondid) || null
} : null;
}
renderRow(row, index, COLS) {
return (
<div className="tr" key={index}>{ row.map((tmpl, i) => (
<div className={tmpl === this.props.selected ? 'td selected' : 'td'}
title={greekify(tmplName(tmpl, index * COLS + i))}>
<RenderTmpl tmpl={tmpl} className="struct" onClick={() => this.select(tmpl)}/>
<button className="attach-button" onClick={() => this.props.onAttach(tmpl)}>
Edit
</button>
</div>
))}</div>
);
}
render() {
const COLS = 3;
let { group, filter, onFilter, onChangeGroup, ...props } = this.props;
const lib = filterLibSelector(this.props);
group = lib[group] ? group : Object.keys(lib)[0];
return (
<Dialog title="Template Library"
className="template-lib" params={props}
result={() => this.result()}
buttons={[
<SaveButton className="save"
data={ sdf.stringify(this.props.lib) }
filename={'ketcher-tmpls.sdf'}>
Save To SDF
</SaveButton>,
"OK", "Cancel"]}>
<label>
<Input type="search" placeholder="Filter"
value={ filter } onChange={value => onFilter(value)}/>
</label>
<Input className="groups" component={SelectList}
splitIndexes={[Object.keys(lib).indexOf('User Templates')]}
value={ group } onChange={g => onChangeGroup(g)}
schema={{
enum: Object.keys(lib),
enumNames: Object.keys(lib).map(g => greekify(g))
}}/>
<VisibleView data={libRowsSelector({ lib, group, COLS })}
rowHeight={120} className="table">
{ (row, i) => this.renderRow(row, i, COLS) }
</VisibleView>
</Dialog>
);
}
}
export default connect(
store => ({ ...omit(['attach'], store.templates) }),
(dispatch, props) => ({
onFilter: filter => dispatch(changeFilter(filter)),
onSelect: tmpl => dispatch(selectTmpl(tmpl)),
onChangeGroup: group => dispatch(changeGroup(group)),
onAttach: tmpl => dispatch(editTmpl(tmpl)),
onOk: res => {
dispatch(onAction({ tool: 'template', opts: res }));
props.onOk(res);
}
})
)(TemplateLib);