diff --git a/package.json b/package.json index d114fb55c4e7fd6f88bbcd99f6ba5450dd07b5c0..5d21cb7dd83fe100854134ca400c72d678804124 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "watch-viewer": "webpack build/node_modules/apps/viewer/index.js -w --mode development -o build/viewer/index.js", "build-canvas": "webpack build/node_modules/apps/canvas/index.js --mode development -o build/canvas/index.js", "watch-canvas": "webpack build/node_modules/apps/canvas/index.js -w --mode development -o build/canvas/index.js", + "build-ms-query": "webpack build/node_modules/apps/model-server-query/index.js --mode development -o build/model-server-query/index.js", + "watch-ms-query": "webpack build/node_modules/apps/model-server-query/index.js -w --mode development -o build/model-server-query/index.js", "model-server": "node build/node_modules/servers/model/server.js", "model-server-watch": "nodemon --watch build/node_modules build/node_modules/servers/model/server.js" }, diff --git a/src/apps/model-server-query/index.html b/src/apps/model-server-query/index.html new file mode 100644 index 0000000000000000000000000000000000000000..4a77ef8a04b5c276c2bf0012e3628bd11a49daa7 --- /dev/null +++ b/src/apps/model-server-query/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> + <title>Mol* ModelServer Query Builder</title> + </head> + <body> + <div id="app"></div> + <script type="text/javascript" src="./index.js"></script> + </body> +</html> \ No newline at end of file diff --git a/src/apps/model-server-query/index.tsx b/src/apps/model-server-query/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5df577f40cd62875738e4c136687279d2b835949 --- /dev/null +++ b/src/apps/model-server-query/index.tsx @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import * as Rx from 'rxjs' + +import { QueryDefinition, QueryList } from '../../servers/model/server/api' + +import './index.html' + +interface State { + query: Rx.BehaviorSubject<QueryDefinition>, + id: Rx.BehaviorSubject<string>, + params: Rx.BehaviorSubject<any>, + isBinary: Rx.BehaviorSubject<boolean>, + models: Rx.BehaviorSubject<number[]>, + url: Rx.Subject<string> +} + +class Root extends React.Component<{ state: State }, { }> { + parseParams(str: string) { + try { + const params = JSON.parse(str); + this.props.state.params.next(params); + } catch { + this.props.state.params.next({}); + } + } + + render() { + return <div> + <div> + Query: <QuerySelect state={this.props.state} /> + </div> + <div> + ID: <input type='text' onChange={t => this.props.state.id.next(t.currentTarget.value)} /> + </div> + <div> + Params:<br/> + <textarea style={{height: '300px'}} cols={80} onChange={t => this.parseParams(t.currentTarget.value)} /> + </div> + <div> + Model numbers (empty for all): <ModelNums state={this.props.state} /> + </div> + <div> + <input type='checkbox' onChange={t => this.props.state.isBinary.next(!!t.currentTarget.checked)} /> Binary + </div> + <div> + Query string: + <QueryUrl state={this.props.state} /> + </div> + </div> + } +} + +class QuerySelect extends React.Component<{ state: State }> { + render() { + return <select onChange={s => this.props.state.query.next(QueryList[+s.currentTarget.value].definition)}> + { QueryList.map((q, i) => <option value={i} key={i} selected={i === 1}>{q.definition.niceName}</option>) } + </select> + } +} + +class QueryUrl extends React.Component<{ state: State }, { queryString: string }> { + state = { queryString: '' }; + + componentDidMount() { + this.props.state.url.subscribe(url => this.setState({ queryString: url })) + } + + render() { + return <input type='text' value={this.state.queryString} style={{ width: '800px' }} /> + } +} + +class ModelNums extends React.Component<{ state: State }> { + render() { + return <input type='text' defaultValue='1' style={{ width: '300px' }} onChange={t => + this.props.state.models.next(t.currentTarget.value.split(',') + .map(v => v.trim()) + .filter(v => !!v) + .map(v => +v) + )} /> + } +} + +const state: State = { + query: new Rx.BehaviorSubject(QueryList[1].definition), + id: new Rx.BehaviorSubject('1cbs'), + params: new Rx.BehaviorSubject({ }), + isBinary: new Rx.BehaviorSubject(false), + models: new Rx.BehaviorSubject<number[]>([]), + url: new Rx.Subject() +} + +function formatUrl() { + const json = JSON.stringify({ + name: state.query.value.name, + id: state.id.value, + modelNums: state.models.value.length ? state.models.value : void 0, + binary: state.isBinary.value, + params: state.params.value + }); + state.url.next(encodeURIComponent(json)); +} + +Rx.merge(state.query, state.id, state.params, state.isBinary, state.models).subscribe(s => formatUrl()); + +ReactDOM.render(<Root state={state} />, document.getElementById('app')); diff --git a/src/servers/model/server/api-local.ts b/src/servers/model/server/api-local.ts index be895be820fb3e555e2ce36e9c84364c663403d8..27c9f4250a5ce18d38391889cfe8034dbe259b2b 100644 --- a/src/servers/model/server/api-local.ts +++ b/src/servers/model/server/api-local.ts @@ -17,6 +17,7 @@ export type LocalInput = { input: string, output: string, query: string, + modelNums?: number[], params?: any }[]; @@ -27,7 +28,7 @@ export async function runLocal(input: LocalInput) { } for (const job of input) { - JobManager.add('_local_', job.input, job.query, job.params || { }, job.output); + JobManager.add('_local_', job.input, job.query, job.params || { }, job.modelNums, job.output); } JobManager.sort(); diff --git a/src/servers/model/server/api-web.ts b/src/servers/model/server/api-web.ts index 93332c55f5dbc1d579a8b00ab28b4201044337e0..2e80b9eb09d31abf6d5bff986fbf449445229279 100644 --- a/src/servers/model/server/api-web.ts +++ b/src/servers/model/server/api-web.ts @@ -103,7 +103,7 @@ export function initWebApi(app: express.Express) { const name = args.name; const entryId = args.id; const params = args.params || { }; - const jobId = JobManager.add('pdb', entryId, name, params); + const jobId = JobManager.add('pdb', entryId, name, params, args.modelNums); responseMap.set(jobId, res); if (JobManager.size === 1) processNextJob(); }); diff --git a/src/servers/model/server/api.ts b/src/servers/model/server/api.ts index 93504ed29051832428fcb743039640d958577003..3416436db0a5aaa26452114383fdbc13c86ed700 100644 --- a/src/servers/model/server/api.ts +++ b/src/servers/model/server/api.ts @@ -88,7 +88,7 @@ const QueryMap: { [id: string]: Partial<QueryDefinition> } = { description: 'Computes structural assembly.', query: () => Queries.generators.all, structureTransform(p, s) { - return StructureSymmetry.buildAssembly(s, '' + p.name).run(); + return StructureSymmetry.buildAssembly(s, '' + (p.name || '1')).run(); }, params: [{ name: 'name', diff --git a/src/servers/model/server/jobs.ts b/src/servers/model/server/jobs.ts index 8791d26b101074bf32ded4a5e837eff91e954af1..f2c77d77dfb86f923ca9080c8c53af7921970d06 100644 --- a/src/servers/model/server/jobs.ts +++ b/src/servers/model/server/jobs.ts @@ -23,11 +23,12 @@ export interface Job { queryDefinition: QueryDefinition, normalizedParams: any, responseFormat: ResponseFormat, + modelNums?: number[], outputFilename?: string } -export function createJob(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, outputFilename?: string): Job { +export function createJob(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, modelNums?: number[], outputFilename?: string): Job { const queryDefinition = getQueryByName(queryName); if (!queryDefinition) throw new Error(`Query '${queryName}' is not supported.`); @@ -42,6 +43,7 @@ export function createJob(sourceId: '_local_' | string, entryId: string, queryNa queryDefinition, normalizedParams, responseFormat: { isBinary: !!params.binary }, + modelNums, outputFilename }; } @@ -53,8 +55,8 @@ class _JobQueue { return this.list.count; } - add(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, outputFilename?: string) { - const job = createJob(sourceId, entryId, queryName, params, outputFilename); + add(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, modelNums?: number[], outputFilename?: string) { + const job = createJob(sourceId, entryId, queryName, params, modelNums, outputFilename); this.list.addLast(job); return job.id; } diff --git a/src/servers/model/server/query.ts b/src/servers/model/server/query.ts index ee5391e0c513558ba3c37c98335aae02c7c902e1..09603e183fe2dc7a19481a70f01b61749e69e5cf 100644 --- a/src/servers/model/server/query.ts +++ b/src/servers/model/server/query.ts @@ -40,7 +40,7 @@ export async function resolveJob(job: Job): Promise<CifWriter.Encoder<any>> { try { perf.start('query'); - const sourceStructures = await resolveStructures(wrappedStructure); + const sourceStructures = await resolveStructures(wrappedStructure, job.modelNums); if (!sourceStructures.length) throw new Error('Model not available'); let structures: Structure[] = sourceStructures;