diff --git a/package.json b/package.json index ecdd2c6f7f75598a35e945491e0a491b2c30d118..d4fcf13c35c5ee3bc9e55ee3981e100b3a3b5934 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,6 @@ "compression": "^1.7.3", "express": "^4.16.4", "graphql": "^14.0.2", - "graphql-request": "^1.8.2", "immutable": "^3.8.2", "node-fetch": "^2.3.0", "react": "^16.6.3", diff --git a/src/mol-model-props/rcsb/symmetry.ts b/src/mol-model-props/rcsb/assembly-symmetry.ts similarity index 92% rename from src/mol-model-props/rcsb/symmetry.ts rename to src/mol-model-props/rcsb/assembly-symmetry.ts index 11795b06b3f6de0f4e5e3e6d48940fa855cc9179..29209bd0ace506d634e07250398b2be6c74e2215 100644 --- a/src/mol-model-props/rcsb/symmetry.ts +++ b/src/mol-model-props/rcsb/assembly-symmetry.ts @@ -4,8 +4,6 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { GraphQLClient } from 'graphql-request' - import { AssemblySymmetry as AssemblySymmetryGraphQL } from './graphql/types'; import query from './graphql/symmetry.gql'; @@ -18,6 +16,9 @@ import { CifExportContext } from 'mol-model/structure/export/mmcif'; import { toTable } from 'mol-io/reader/cif/schema'; import { CifCategory } from 'mol-io/reader/cif'; import { PropertyWrapper } from 'mol-model-props/common/wrapper'; +import { Task, RuntimeContext } from 'mol-task'; +import { GraphQLClient } from 'mol-util/graphql-client'; +import { ajaxGet } from 'mol-util/data-source'; const { str, int, float, Aliased, Vector, List } = Column.Schema; @@ -152,8 +153,6 @@ const _Descriptor: ModelPropertyDescriptor = { } } -const client = new GraphQLClient('http://rest-experimental.rcsb.org/graphql') - export interface AssemblySymmetry { db: AssemblySymmetry.Database getSymmetries(assemblyId: string): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry']> @@ -177,7 +176,10 @@ export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetr } } +const Client = new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, (url: string, type: 'string' | 'binary', body?: string) => ajaxGet({ url, type, body }) ) + export namespace AssemblySymmetry { + export const GraphQLEndpointURL = 'http://rest-experimental.rcsb.org/graphql' export const Schema = { rcsb_assembly_symmetry_info: { updated_datetime_utc: Column.Schema.str @@ -247,7 +249,7 @@ export namespace AssemblySymmetry { export const Descriptor = _Descriptor; - export async function attachFromCifOrAPI(model: Model) { + export async function attachFromCifOrAPI(model: Model, client: GraphQLClient = Client, ctx?: RuntimeContext) { if (model.customProperties.has(Descriptor)) return true; let db: Database @@ -258,7 +260,7 @@ export namespace AssemblySymmetry { let result: AssemblySymmetryGraphQL.Query const variables: AssemblySymmetryGraphQL.Variables = { pdbId: model.label.toLowerCase() }; try { - result = await client.request<AssemblySymmetryGraphQL.Query>(query, variables); + result = await client.request<AssemblySymmetryGraphQL.Query>(ctx || RuntimeContext.Synchronous, query, variables); } catch (e) { console.error(e) return false; @@ -273,6 +275,14 @@ export namespace AssemblySymmetry { return true; } + export function createAttachTask(fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) { + return (model: Model) => Task.create('RCSB Assembly Symmetry', async ctx => { + if (get(model)) return true; + + return await attachFromCifOrAPI(model, new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, fetch), ctx) + }); + } + export function get(model: Model): AssemblySymmetry | undefined { return model._staticPropertyData.__RCSBAssemblySymmetry__; } diff --git a/src/mol-model-props/rcsb/themes/assembly-symmetry.ts b/src/mol-model-props/rcsb/themes/assembly-symmetry.ts new file mode 100644 index 0000000000000000000000000000000000000000..41f03e6135f94e22fd83cd9cc9d41b67012b1961 --- /dev/null +++ b/src/mol-model-props/rcsb/themes/assembly-symmetry.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ThemeDataContext } from 'mol-theme/theme'; +import { ColorTheme, LocationColor } from 'mol-theme/color'; +import { ParamDefinition as PD } from 'mol-util/param-definition' +import { Table } from 'mol-data/db'; +import { AssemblySymmetry } from '../assembly-symmetry'; +import { ColorScale, Color } from 'mol-util/color'; +import { Unit, StructureElement, StructureProperties } from 'mol-model/structure'; +import { Location } from 'mol-model/location'; +import { ColorListName, ColorListOptions } from 'mol-util/color/scale'; + +const DefaultColor = Color(0xCCCCCC) + +function getAsymId(unit: Unit): StructureElement.Property<string> { + switch (unit.kind) { + case Unit.Kind.Atomic: + return StructureProperties.chain.label_asym_id + case Unit.Kind.Spheres: + case Unit.Kind.Gaussians: + return StructureProperties.coarse.asym_id + } +} + +function clusterMemberKey (asym_id: string, oper_list_ids: string[]) { + return `${asym_id}-${oper_list_ids.join('x')}` +} + +export const AssemblySymmetryClusterColorThemeParams = { + list: PD.Select<ColorListName>('Viridis', ColorListOptions), + symmetryId: PD.Select<number>(0, []), +} +export type AssemblySymmetryClusterColorThemeParams = typeof AssemblySymmetryClusterColorThemeParams +export function getAssemblySymmetryClusterColorThemeParams(ctx: ThemeDataContext) { + const params = PD.clone(AssemblySymmetryClusterColorThemeParams) + + if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) { + const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])! + const assemblyName = ctx.structure.assemblyName + const s = assemblySymmetry.db.rcsb_assembly_symmetry + if (s._rowCount) { + params.symmetryId.options = [] + for (let i = 0, il = s._rowCount; i < il; ++i) { + if (s.assembly_id.value(i) === assemblyName) { + params.symmetryId.options.push([ + s.id.value(i), `${s.symbol.value(i)} ${s.kind.value(i)}` + ]) + } + } + params.symmetryId.defaultValue = s.id.value(0) + } + } + + return params +} + +export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props: PD.Values<AssemblySymmetryClusterColorThemeParams>): ColorTheme<AssemblySymmetryClusterColorThemeParams> { + let color: LocationColor = () => DefaultColor + + const { symmetryId } = props + + if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) { + const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])! + + const s = assemblySymmetry.db.rcsb_assembly_symmetry + const symmetry = Table.pickRow(s, i => s.id.value(i) === symmetryId) + if (symmetry) { + + const clusters = assemblySymmetry.getClusters(symmetryId) + if (clusters._rowCount) { + + const clusterByMember = new Map<string, number>() + for (let i = 0, il = clusters._rowCount; i < il; ++i) { + const clusterMembers = assemblySymmetry.getClusterMembers(clusters.id.value(i)) + for (let j = 0, jl = clusterMembers._rowCount; j < jl; ++j) { + const asym_id = clusterMembers.asym_id.value(j) + const oper_list_ids = clusterMembers.pdbx_struct_oper_list_ids.value(j) + if (oper_list_ids.length === 0) oper_list_ids.push('1') // TODO hack assuming '1' is the id of the identity operator + clusterByMember.set(clusterMemberKey(asym_id, oper_list_ids), i) + } + } + + const scale = ColorScale.create({ listOrName: props.list, domain: [ 0, clusters._rowCount - 1 ] }) + + color = (location: Location): Color => { + if (StructureElement.isLocation(location)) { + const asym_id = getAsymId(location.unit) + const ns = location.unit.conformation.operator.name.split('-') + const oper_list_ids = ns.length === 2 ? ns[1].split('x') : [] + const cluster = clusterByMember.get(clusterMemberKey(asym_id(location), oper_list_ids)) + return cluster !== undefined ? scale.color(cluster) : DefaultColor + } + return DefaultColor + } + } + } + } + + return { + factory: AssemblySymmetryClusterColorTheme, + granularity: 'instance', + color, + props, + description: 'Assigns chain colors according to assembly symmetry cluster membership.', + } +} + +export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams> = { + label: 'RCSB Assembly Symmetry Cluster', + factory: AssemblySymmetryClusterColorTheme, + getParams: getAssemblySymmetryClusterColorThemeParams, + defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams) +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props.ts b/src/mol-plugin/behavior/dynamic/custom-props.ts index 0c4680938fdb6087a9e1d41e648beb3276b35fad..caba1bc138bf5927c0b6572a6ef1bf3c95568560 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props.ts @@ -2,78 +2,8 @@ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { OrderedSet } from 'mol-data/int'; -import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report'; -import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report'; -import { Loci } from 'mol-model/loci'; -import { StructureElement } from 'mol-model/structure'; -import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry'; -import { ParamDefinition as PD } from 'mol-util/param-definition'; -import { PluginBehavior } from '../behavior'; - -export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({ - name: 'pdbe-structure-quality-report-prop', - display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' }, - ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { - private attach = StructureQualityReport.createAttachTask( - m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`, - this.ctx.fetch - ); - - private provider: CustomPropertyRegistry.Provider = { - option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'], - descriptor: StructureQualityReport.Descriptor, - defaultSelected: this.params.autoAttach, - attachableTo: () => true, - attach: this.attach - } - - register(): void { - this.ctx.customModelProperties.register(this.provider); - this.ctx.lociLabels.addProvider(labelPDBeValidation); - - // TODO: support filtering of themes based on the input structure - // in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor) - this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', { - label: 'PDBe Structure Quality Report', - factory: StructureQualityReportColorTheme, - getParams: () => ({}), - defaultValues: {} - }) - } - - update(p: { autoAttach: boolean }) { - let updated = this.params.autoAttach !== p.autoAttach - this.params.autoAttach = p.autoAttach; - this.provider.defaultSelected = p.autoAttach; - return updated; - } - - unregister() { - this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name); - this.ctx.lociLabels.removeProvider(labelPDBeValidation); - this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report') - } - }, - params: () => ({ - autoAttach: PD.Boolean(false) - }) -}); - -function labelPDBeValidation(loci: Loci): string | undefined { - switch (loci.kind) { - case 'element-loci': - const e = loci.elements[0]; - const u = e.unit; - if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0; - - const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]); - const issues = StructureQualityReport.getIssues(se); - if (issues.length === 0) return 'PDBe Validation: No Issues'; - return `PDBe Validation: ${issues.join(', ')}`; - - default: return void 0; - } -} \ No newline at end of file +export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report' +export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry' \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts new file mode 100644 index 0000000000000000000000000000000000000000..948edad6cfe81e0a08819b3b10cef2c7b2be568b --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { OrderedSet } from 'mol-data/int'; +import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report'; +import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report'; +import { Loci } from 'mol-model/loci'; +import { StructureElement } from 'mol-model/structure'; +import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginBehavior } from '../../../behavior'; + +export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({ + name: 'pdbe-structure-quality-report-prop', + display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' }, + ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { + private attach = StructureQualityReport.createAttachTask( + m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`, + this.ctx.fetch + ); + + private provider: CustomPropertyRegistry.Provider = { + option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'], + descriptor: StructureQualityReport.Descriptor, + defaultSelected: this.params.autoAttach, + attachableTo: () => true, + attach: this.attach + } + + register(): void { + this.ctx.customModelProperties.register(this.provider); + this.ctx.lociLabels.addProvider(labelPDBeValidation); + + // TODO: support filtering of themes based on the input structure + // in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor) + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', { + label: 'PDBe Structure Quality Report', + factory: StructureQualityReportColorTheme, + getParams: () => ({}), + defaultValues: {} + }) + } + + update(p: { autoAttach: boolean }) { + let updated = this.params.autoAttach !== p.autoAttach + this.params.autoAttach = p.autoAttach; + this.provider.defaultSelected = p.autoAttach; + return updated; + } + + unregister() { + this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name); + this.ctx.lociLabels.removeProvider(labelPDBeValidation); + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report') + } + }, + params: () => ({ + autoAttach: PD.Boolean(false) + }) +}); + +function labelPDBeValidation(loci: Loci): string | undefined { + switch (loci.kind) { + case 'element-loci': + const e = loci.elements[0]; + const u = e.unit; + if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0; + + const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]); + const issues = StructureQualityReport.getIssues(se); + if (issues.length === 0) return 'PDBe Validation: No Issues'; + return `PDBe Validation: ${issues.join(', ')}`; + + default: return void 0; + } +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts new file mode 100644 index 0000000000000000000000000000000000000000..3337e6b60d49058f6b4b8a917c92d434735eec49 --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginBehavior } from 'mol-plugin/behavior'; +import { ParamDefinition as PD } from 'mol-util/param-definition' +import { AssemblySymmetry } from 'mol-model-props/rcsb/assembly-symmetry'; +import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry'; +import { AssemblySymmetryClusterColorThemeProvider } from 'mol-model-props/rcsb/themes/assembly-symmetry'; + +export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({ + name: 'rcsb-assembly-symmetry-prop', + display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' }, + ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { + private attach = AssemblySymmetry.createAttachTask(this.ctx.fetch); + + private provider: CustomPropertyRegistry.Provider = { + option: [AssemblySymmetry.Descriptor.name, 'RCSB Assembly Symmetry'], + descriptor: AssemblySymmetry.Descriptor, + defaultSelected: this.params.autoAttach, + attachableTo: () => true, + attach: this.attach + } + + register(): void { + this.ctx.customModelProperties.register(this.provider); + + // TODO: support filtering of themes based on the input structure + // in this case, it would check structure.models[0].customProperties.has(AssemblySymmetry.Descriptor) + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('rcsb-assembly-symmetry-cluster', AssemblySymmetryClusterColorThemeProvider) + } + + update(p: { autoAttach: boolean }) { + let updated = this.params.autoAttach !== p.autoAttach + this.params.autoAttach = p.autoAttach; + this.provider.defaultSelected = p.autoAttach; + return updated; + } + + unregister() { + this.ctx.customModelProperties.unregister(AssemblySymmetry.Descriptor.name); + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('rcsb-assembly-symmetry-cluster') + } + }, + params: () => ({ + autoAttach: PD.Boolean(false) + }) +}); \ No newline at end of file diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 6ed4f0f7a06172e3e92d45a126910048785db2ef..838935a85296c4ca41ec10382ef96be395f98da7 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -36,7 +36,8 @@ const DefaultSpec: PluginSpec = { PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci), PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }), - PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }) + PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }), + PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }), ] } diff --git a/src/mol-util/graphql-client.ts b/src/mol-util/graphql-client.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6f8e5c719766380f22675f260e5f910425a12f9 --- /dev/null +++ b/src/mol-util/graphql-client.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * + * Adapted from https://github.com/prisma/graphql-request, Copyright (c) 2017 Graphcool, MIT + */ + +import { Task, RuntimeContext } from 'mol-task'; + +type Variables = { [key: string]: any } + +interface GraphQLError { + message: string + locations: { line: number, column: number }[] + path: string[] +} + +interface GraphQLResponse { + data?: any + errors?: GraphQLError[] + extensions?: any + status: number + [key: string]: any +} + +interface GraphQLRequestContext { + query: string + variables?: Variables +} + +export class ClientError extends Error { + response: GraphQLResponse + request: GraphQLRequestContext + + constructor (response: GraphQLResponse, request: GraphQLRequestContext) { + const message = `${ClientError.extractMessage(response)}: ${JSON.stringify({ response, request })}` + + super(message) + + this.response = response + this.request = request + + // this is needed as Safari doesn't support .captureStackTrace + /* tslint:disable-next-line */ + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, ClientError) + } + } + + private static extractMessage (response: GraphQLResponse): string { + try { + return response.errors![0].message + } catch (e) { + return `GraphQL Error (Code: ${response.status})` + } + } +} + +export class GraphQLClient { + constructor(private url: string, private fetch: (url: string, type: 'string' | 'binary', body?: string) => Task<string | Uint8Array>) { + this.url = url + } + + async request<T extends any>(ctx: RuntimeContext, query: string, variables?: Variables): Promise<T> { + + const body = JSON.stringify({ + query, + variables: variables ? variables : undefined, + }) + + const resultStr = await this.fetch(this.url, 'string', body).runInContext(ctx) as string + const result = JSON.parse(resultStr) + + if (!result.errors && result.data) { + return result.data + } else { + const errorResult = typeof result === 'string' ? { error: result } : result + throw new ClientError( + { ...errorResult }, + { query, variables }, + ) + } + } +} \ No newline at end of file diff --git a/src/servers/model/properties/providers/rcsb.ts b/src/servers/model/properties/providers/rcsb.ts index 06c9da06c80d0b232eaa178bfbe2f9b399ea3c48..001bf56b81b8da5e45968eb41d42e475b65e0062 100644 --- a/src/servers/model/properties/providers/rcsb.ts +++ b/src/servers/model/properties/providers/rcsb.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry'; +import { AssemblySymmetry } from 'mol-model-props/rcsb/assembly-symmetry'; import { AttachModelProperty } from '../../property-provider'; export const RCSB_assemblySymmetry: AttachModelProperty = ({ model }) => {