partridgejiang / Kekule-React

React component wrapper for Kekule.js widgets.
MIT License
3 stars 2 forks source link

loading smiles string #2

Open spencertr opened 1 year ago

spencertr commented 1 year ago

hi Partridge,

How do I load a smiles string? I followed this git hub issue: link

Kekule.OpenBabel.enable(() => {
        smilesString = this.props.smilesString
        mol = Kekule.IO.loadFormatData(smilesString, 'smi')
        let generator = new Kekule.Calculator.ObStructure2DGenerator()
        generator.setSourceMol(mol)
        generator.executeSync(() => {
          let newMol = generator.getGeneratedMol()
          console.log(newMol)
          this.setState({ chemObj: newMol })
        })
      })

I'm currently getting an error in the console:

Uncaught SyntaxError: Unexpected token '<' (at openbabel.js:1:1)
react_devtools_backend.js:4012 TypeError: Cannot read properties of undefined (reading 'value')
    at bundle.js:1:1
    at __webpack_modules__../node_modules/kekule/dist/mins/root.min.js.Array.map.Array.map (root.min.js:1:1)
    at e.value (bundle.js:1:1)
    at e.value (bundle.js:1:1)
    at App.fetchCalcProps (App.js:115:1)

UPDATE: i just realised I need to deploy openbabel.js link, i downloaded the three files and placed the <script> tag in my index.html as described in the website, but now I am getting:

GET http://localhost:8080/extra/openbabel.js net::ERR_ABORTED 404 (Not Found)

Why is it trying to import from /extra? I don't have this directory, do I need to make an extra folder? The docs don't mention a specific directory name. I suppose I am missing another step.

partridgejiang commented 1 year ago

Hi @spencertr, the OpenBabel module is compiled to an individual .wasm file (not in js format), so it usually can not be automatically wrapped togather with js bundle by your pack tool. To use this module, you may have to manually copy the essential files from kekule/dist/extra/ directory (including openbabel.* and kekule.worker.obStructureGenerator.js to your dest path, then manually set this path in Kekule.js:

  Kekule.environment.setEnvVar('openbabel.path', 'Your/Custom/Path/To/OpenBabel/Files/');  // need only to call this once
  Kekule.OpenBabel.enable(() => {
    smilesString = this.props.smilesString;
    mol = Kekule.IO.loadFormatData(smilesString, 'smi');
    // ....
  }
spencertr commented 1 year ago

I tried what you suggested in two different scenarios, in an attempt to troubleshoot: 1) in create-react-app:

.
├── App.js
├── App.test.js
├── components
│   ├── ComposerViewer.js
├── index.js
├── kekule
│   ├── kekule.react.base.js
│   ├── kekule.react.components.js
│   ├── kekule.react.css
│   ├── kekule.react.js
│   ├── kekule.react.wrap.js
│   ├── kekule.worker.obStructureGenerator.js
│   ├── openbabel.data
│   ├── openbabel.js
│   └── openbabel.wasm

and ...

Kekule.environment.setEnvVar('openbabel.path', '../kekule/') // need only to call this once
Kekule.OpenBabel.enable(() => {
    smilesString = this.props.smilesString;
    mol = Kekule.IO.loadFormatData(smilesString, 'smi');
    // ....
  }

I tried setting the path with ./kekule and with absolute path as well. I am currently getting this error: Uncaught SyntaxError: Unexpected token '<' (at openbabel.js:1:1) Tried two additional things:

  componentDidMount() {
    const script = document.createElement('script')
    script.src = './src/kekule/openbabel.js' // tried ./kekule and ../kekule
    script.async = true

    document.body.appendChild(script)
  }

and

  <script type="text/javascript" src="./src/kekule/openbabel.js"></script> // tried ./kekule and ../kekule

2) vanilla js:

.
├── package-lock.json
├── package.json
├── public
│   ├── bundle.js
│   ├── index.html
│   ├── index.js
│   ├── kekule.worker.obStructureGenerator.js
│   ├── openbabel.data
│   ├── openbabel.js
│   └── openbabel.wasm
└── webpack.config.js

and ...

Kekule.environment.setEnvVar('openbabel.path', './') // need only to call this once
Kekule.OpenBabel.enable(() => {
    smilesString = this.props.smilesString;
    mol = Kekule.IO.loadFormatData(smilesString, 'smi');
    // ....
  }

I don't get the error like above in the react app, however it doesn't run the block of code to load the smilesString. I instead just wrote it like this:

Kekule.environment.setEnvVar('openbabel.path', './') // need only to call this once
Kekule.OpenBabel.enable()
console.log('test')
mol = Kekule.IO.loadFormatData(smiles, 'smi')
console.log(mol)

But it gives me the error:

Uncaught Kekule.Exception
name: undefined
"Can not read data of format: smi"
"Error
    at eval (webpack:///./node_modules/kekule/dist/mins/common.min.js?:1:54842)
    at ./node_modules/kekule/dist/mins/common.min.js (http://localhost:8080/bundle.js:114:1)
    at __webpack_require__ (http://localhost:8080/bundle.js:2191:42)
    at eval (webpack:///./node_modules/kekule/dist/kekule.esm.mjs?:14:77)
    at ./node_modules/kekule/dist/kekule.esm.mjs (http://localhost:8080/bundle.js:2156:1)
    at __webpack_require__ (http://localhost:8080/bundle.js:2191:42)
    at eval (webpack:///./public/index.js?:2:64)
    at ./public/index.js (http://localhost:8080/bundle.js:19:1)
    at __webpack_require__ (http://localhost:8080/bundle.js:2191:42)
    at http://localhost:8080/bundle.js:2301:37"

Not sure what else to try, appreciate some help. Thanks

spencertr commented 1 year ago

I also tried this, which didn't work:

Kekule.Indigo.enable(() => {
  let smiles = 'C1CCCCC1'
  let mol = Kekule.IO.loadFormatData(smiles, 'smi')
  let generator = new Kekule.Calculator.ObStructure2DGenerator()
  generator.setSourceMol(mol)
  console.log('test')
  console.log(smiles)
  generator.execute((err) => {
    if (!err) {
      let newMol = generator.getGeneratedMol()
      composer.setChemObj(newMol)
    }
  })
})

but this seemed to work ...

    const { Molecule } = require('openchemlib')
    let smiles = 'C1CCCCC1'
    const molfile = Molecule.fromSmiles(smiles).toMolfile()
    let mol = Kekule.IO.loadFormatData(molfile, 'mol')
    let generator = new Kekule.Calculator.ObStructure2DGenerator()
    generator.setSourceMol(mol)
    console.log(smiles)
    generator.execute((err) => {
      if (!err) {
        let newMol = generator.getGeneratedMol()
        composer.setChemObj(newMol)
      } else {
        console.error(err)
      }
})

But I see an error in the console: FS.trackingDelegate error on read file: /dev/stdin - openbabel.js :8

Finally, got the react app to work, i moved the kekule files to be in public. The only solution I found to work so far was:

          // ...
          this.composer.current.getWidget().setChemObj(newMol)
          // this.setState({ chemObj: newMol }) // I couldn't get this to work
          // ...

It would be nice to get the openbabel to work as originally planned, rather than install another similar package. Also, do you know why I can't setState the newMol object?

partridgejiang commented 1 year ago

Hi @spencertr, thanks for the detailed feedback. With further investigation, I have found and solved some bugs preventing the proper loading of OpenBabel module in React app. Please check the latest dist files in Kekule.js repo (https://github.com/partridgejiang/Kekule.js/tree/master/dist) and use them to overwrite the ones in your /node_modules/kekule/dist (installed from npm) .

A simple demo is also attached here. The demo utilize vite to create the React App. The openbabel related files locates in /public/openbabel/ directory, and will be published to /dist/openbabel/ in building. So in the components/composerAndViewer.jsx, this path is manually setted:

Kekule.environment.setEnvVar('openbabel.path', '/openbabel/');

Kekule.OpenBabel.enable(error => {
    if (!error)
    {       
        this.composer.current.widget.loadFromData('c1ccccc1', null, null, Kekule.IO.DataFormat.SMILES);
        // Almost equal to Kekule.IO.loadFormatData but do automatical coordinate generation in composer.
        // You previous code should also be workable here:
        // let smiles = 'C1CCCCC1';
        // let mol = Kekule.IO.loadFormatData(smiles, 'smi');
        // let generator = new Kekule.Calculator.ObStructure2DGenerator();
        // ...
        // this.setState({ chemObj: newMol });
    }
});

By the way, the chemObj prop should be set to <Composer>, otherwise this.setState({ chemObj: newMol }) should not work.

render()
{
    return (<Composer className="ComposerWidget" ref={this.composer} chemObj={this.state.chemObj} ></Composer>);
}

kekule-react-vite.zip

spencertr commented 1 year ago

HI @partridgejiang , thanks for providing the information. I tried to run the attached kekule-react-vite.zip app but I don't think it works either. I did not make any changes to any of the .jsx files and ran npm run dev opened the webpage and do see the Chem editor (no errors in console), however there is no console.log('OpenBabel loaded!');. I adapted your code to my current application and it still gives me: Uncaught Kekule.Exception {message: 'Can not read data of format 'smi'. I tried following your instructions by copying all the new dist files into my node_modules/kekule/dist too. Did I miss another step? Thanks for help.

partridgejiang commented 1 year ago

Hi @spencertr, after running npm install for kekule-react-vite demo, the node_modules/kekule/dist should also be updated. Anyway, the demo with all node_module files included is attached here. You may run the npm run dev directly after decompression. Please have a try, :). kekule-react-vite.zip

spencertr commented 1 year ago

hi @partridgejiang , that .zip file you attached does indeed work. What did you do to get it working? I would like to dockerise the app eventually and I would like to know the steps to get it consistently working. I tried to get the original kekule-react-vite.zip you sent last week to work but have failed. I don't get any errors, but openbabel does not get enabled. Likewise, when I tried what I believe are all the steps you mentioned for my app, the same, lack of enablement happens, the Kekule.OpenBabel.enable() line does not seem to work. I don't see any differences in the two .zip files you sent. I did indeed update the node_modules/kekule/dist directory; I even copied from the newest .zip file you sent and that OpenBabel.enable() line still does not work. Appreciate your responses. Cheers.

partridgejiang commented 1 year ago

Hi @spencertr, my approximate steps to create the kekule-react-vite:

If possible, could you please attach your unworkable demo here (better to include files in node_modules directory) ? So that I may do a further investigation?

spencertr commented 1 year ago

hi @partridgejiang , thanks for the step by step instructions. I did exactly that besides copying /node_modules/kekule/dist/extra into /public/openbabel, I did have 'those' files in the correct directory with the correct names, however I created those files by copying and pasting from this repo: https://github.com/partridgejiang/cheminfo-to-web. I guess those are not up-to-date? Anyways, I got the smiles string to load using openbabel.js, however I am running into another error. Here is the Composer code:

import React from "react";
import { Kekule } from "kekule";
import { KekuleReact, Components } from "kekule-react";
import "kekule/theme";
Kekule.environment.setEnvVar("openbabel.path", "/openbabel/");

let Composer = Components.Composer;

class Chemcomposer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      composerPredefinedSetting: "fullFunc",
      viewerPredefinedSetting: "basic",
      chemObj: null,
      selectedObjs: undefined,
    };
    this.composer = React.createRef();

    this.onPredefineSettingChange = this.onPredefineSettingChange.bind(this);
    this.onComposerUserModificationDone = this.onComposerUserModificationDone.bind(
      this
    );
    this.onComposerSelectionChange = this.onComposerSelectionChange.bind(this);
  }
  componentDidMount() {
    this.composer.current.widget.newDoc();
    Kekule.OpenBabel.enable((error) => {
      if (!error) {
        //let mol = Kekule.IO.loadFormatData('c1ccccc1', Kekule.IO.DataFormat.SMILES);
        //this.composer.current.widget.setChemObj(mol);
        //this.setState({'chemObj': mol});
        this.composer.current.widget.loadFromData(
          "c1ccccc1",
          null,
          null,
          Kekule.IO.DataFormat.SMILES
        );
      }
    });
  }
  getComposerSelectedAtomsAndBonds(selection) {
    let result = { atoms: [], bonds: [] };
    (selection || []).forEach((obj) => {
      if (obj instanceof Kekule.Atom) result.atoms.push(obj);
      else if (obj instanceof Kekule.Bond) result.bonds.push(obj);
    });
    return result;
  }

  onPredefineSettingChange(e) {
    this.setState({ composerPredefinedSetting: e.target.value });
  }

  onComposerUserModificationDone(e) {
    this.setState({ chemObj: this.composer.current.getWidget().getChemObj() });
  }
  onComposerSelectionChange(e) {
    this.setState({
      selectedObjs: this.composer.current.getWidget().getSelection(),
    });
  }
  render() {
    let selectionInfoElem;
    if (this.state.selectedObjs && this.state.selectedObjs.length) {
      let selDetails = this.getComposerSelectedAtomsAndBonds(
        this.state.selectedObjs
      );
      selectionInfoElem = (
        <span>
          You have selected {this.state.selectedObjs.length} object(s),
          including {selDetails.atoms.length} atom(s) and{" "}
          {selDetails.bonds.length} bond(s).
        </span>
      );
    } else
      selectionInfoElem = (
        <span>
          Please edit and select objects in the composer to see the changes.
        </span>
      );

    return (
      <div>
        <div className="InfoPanel">
          <label>{selectionInfoElem}</label>
        </div>
        <div>
          <Composer
            className="SubWidget"
            ref={this.composer}
            predefinedSetting={this.state.composerPredefinedSetting}
            onUserModificationDone={this.onComposerUserModificationDone}
            onSelectionChange={this.onComposerSelectionChange}
            chemObj={this.state.chemObj}
          ></Composer>
        </div>
      </div>
    );
  }
}

export default Chemcomposer;

if I remove onSelectionChange={this.onComposerSelectionChange}, it works. Otherwise the error is Uncaught TypeError: this.composer.current is null. The code I am using for my app is almost verbatim copy from your Kekule-React repo, and it has that method in it; I didn't touch it. It seems to be working as is, without the openbabel.js.

partridgejiang commented 1 year ago

The code in componentDidMount will invoke a selectionChange event (selection to empty), but this.composer.current may not be initialized until the React component rendering process done. So an error is raised. It is quite easy to fix that, just add a condition check in the onComposerSelectionChange method:

onComposerSelectionChange(e) {
  if (this.composer.current)
    this.setState({
      selectedObjs: this.composer.current.getWidget().getSelection(),
    });
  }