Bladeren bron

Merge branch 'master' into graphics

# Conflicts:
#	src/mol-theme/color.ts
Alexander Rose 5 jaren geleden
bovenliggende
commit
7912e4894d

+ 1 - 1
src/mol-canvas3d/helper/postprocessing.ts

@@ -33,7 +33,7 @@ const PostprocessingSchema = {
 
 export const PostprocessingParams = {
     occlusionEnable: PD.Boolean(false),
-    occlusionKernelSize: PD.Numeric(4, { min: 1, max: 100, step: 1 }),
+    occlusionKernelSize: PD.Numeric(4, { min: 1, max: 32, step: 1 }),
     occlusionBias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
     occlusionRadius: PD.Numeric(64, { min: 0, max: 256, step: 1 }),
 

+ 2 - 2
src/mol-math/linear-algebra/3d/mat4.ts

@@ -1021,8 +1021,8 @@ namespace Mat4 {
     }
 
     const xAxis = Vec3.create(1, 0, 0)
-    const yAxis = Vec3.create(1, 0, 0)
-    const zAxis = Vec3.create(1, 0, 0)
+    const yAxis = Vec3.create(0, 1, 0)
+    const zAxis = Vec3.create(0, 0, 1)
 
     /** Rotation matrix for 90deg around x-axis */
     export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), xAxis)

+ 1 - 0
src/mol-model-formats/structure/mmcif/ihm.ts

@@ -19,6 +19,7 @@ import { FormatData } from './parser';
 export interface IHMData {
     model_id: number,
     model_name: string,
+    model_group_name: string,
     entities: Entities,
     atom_site: mmCIF['atom_site'],
     atom_site_sourceIndex: Column<number>,

+ 15 - 8
src/mol-model-formats/structure/mmcif/parser.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 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>
@@ -190,6 +190,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, sourceIn
     return {
         id: UUID.create22(),
         label,
+        entry: label,
         sourceData: format,
         modelNum: atom_site.pdbx_PDB_model_num.value(0),
         entities,
@@ -212,10 +213,15 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, sourceIn
 function createModelIHM(format: mmCIF_Format, data: IHMData, formatData: FormatData): Model {
     const atomic = getAtomicHierarchyAndConformation(data.atom_site, data.atom_site_sourceIndex, data.entities, formatData);
     const coarse = getIHMCoarse(data, formatData);
+    const entry = format.data.entry.id.valueKind(0) === Column.ValueKind.Present
+        ? format.data.entry.id.value(0)
+        : format.data._name;
+    const label = data.model_group_name ? `${data.model_name}: ${data.model_group_name}` : data.model_name
 
     return {
         id: UUID.create22(),
-        label: data.model_name,
+        label,
+        entry,
         sourceData: format,
         modelNum: data.model_id,
         entities: data.entities,
@@ -324,14 +330,13 @@ function splitTable<T extends Table<any>>(table: T, col: Column<number>) {
 }
 
 async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: FormatData) {
-    if (format.data.atom_site._rowCount && !format.data.atom_site.ihm_model_id.isDefined) {
-        throw new Error('expected _atom_site.ihm_model_id to be defined')
-    }
+    // when `atom_site.ihm_model_id` is undefined fall back to `atom_site.pdbx_PDB_model_num`
+    const atom_sites_modelColumn = format.data.atom_site.ihm_model_id.isDefined ? format.data.atom_site.ihm_model_id : format.data.atom_site.pdbx_PDB_model_num
 
     const { ihm_model_list } = format.data;
     const entities = getEntities(format)
 
-    const atom_sites = splitTable(format.data.atom_site, format.data.atom_site.ihm_model_id);
+    const atom_sites = splitTable(format.data.atom_site, atom_sites_modelColumn);
     // TODO: will coarse IHM records require sorting or will we trust it?
     // ==> Probably implement a sort as as well and store the sourceIndex same as with atomSite
     // If the sorting is implemented, updated mol-model/structure/properties: atom.sourceIndex
@@ -340,14 +345,15 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo
 
     const models: Model[] = [];
 
-    const { model_id, model_name } = ihm_model_list;
+    const { model_id, model_name, model_group_name } = ihm_model_list;
     for (let i = 0; i < ihm_model_list._rowCount; i++) {
         const id = model_id.value(i);
 
         let atom_site, atom_site_sourceIndex;
         if (atom_sites.has(id)) {
             const e = atom_sites.get(id)!;
-            const { atom_site: sorted, sourceIndex } = await sortAtomSite(ctx, e.table, e.start, e.end);
+            // need to sort `format.data.atom_site` as `e.start` and `e.end` are indices into that
+            const { atom_site: sorted, sourceIndex } = await sortAtomSite(ctx, format.data.atom_site, e.start, e.end);
             atom_site = sorted;
             atom_site_sourceIndex = sourceIndex;
         } else {
@@ -358,6 +364,7 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo
         const data: IHMData = {
             model_id: id,
             model_name: model_name.value(i),
+            model_group_name: model_group_name.value(i),
             entities: entities,
             atom_site,
             atom_site_sourceIndex,

+ 6 - 2
src/mol-model/structure/model/model.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 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 UUID from 'mol-util/uuid';
@@ -25,7 +26,10 @@ export interface Model extends Readonly<{
     id: UUID,
     label: string,
 
-    // for IHM, corresponds to ihm_model_list.model_id
+    /** the name of the entry/file/collection the model is part of */
+    entry: string,
+
+    /** for IHM, corresponds to ihm_model_list.model_id */
     modelNum: number,
 
     sourceData: ModelFormat,

+ 35 - 1
src/mol-model/structure/model/types.ts

@@ -576,4 +576,38 @@ export namespace LinkType {
     export function isCovalent(flags: LinkType.Flag) {
         return (flags & LinkType.Flag.Covalent) !== 0;
     }
-}
+}
+
+/**
+ * "Experimentally determined hydrophobicity scale for proteins at membrane interfaces"
+ * by Wimely and White (doi:10.1038/nsb1096-842)
+ * http://blanco.biomol.uci.edu/Whole_residue_HFscales.txt
+ * https://www.nature.com/articles/nsb1096-842
+ */
+export const ResidueHydrophobicity = {
+    // AA  DGwif   DGwoct  Oct-IF
+    'ALA': [ 0.17, 0.50, 0.33 ],
+    'ARG': [ 0.81, 1.81, 1.00 ],
+    'ASN': [ 0.42, 0.85, 0.43 ],
+    'ASP': [ 1.23, 3.64, 2.41 ],
+    'ASH': [ -0.07, 0.43, 0.50 ],
+    'CYS': [ -0.24, -0.02, 0.22 ],
+    'GLN': [ 0.58, 0.77, 0.19 ],
+    'GLU': [ 2.02, 3.63, 1.61 ],
+    'GLH': [ -0.01, 0.11, 0.12 ],
+    'GLY': [ 0.01, 1.15, 1.14 ],
+    // "His+": [  0.96,  2.33,  1.37 ],
+    'HIS': [ 0.17, 0.11, -0.06 ],
+    'ILE': [ -0.31, -1.12, -0.81 ],
+    'LEU': [ -0.56, -1.25, -0.69 ],
+    'LYS': [ 0.99, 2.80, 1.81 ],
+    'MET': [ -0.23, -0.67, -0.44 ],
+    'PHE': [ -1.13, -1.71, -0.58 ],
+    'PRO': [ 0.45, 0.14, -0.31 ],
+    'SER': [ 0.13, 0.46, 0.33 ],
+    'THR': [ 0.14, 0.25, 0.11 ],
+    'TRP': [ -1.85, -2.09, -0.24 ],
+    'TYR': [ -0.94, -0.71, 0.23 ],
+    'VAL': [ 0.07, -0.46, -0.53 ]
+  }
+  export const DefaultResidueHydrophobicity = [ 0.00, 0.00, 0.00 ]

+ 11 - 5
src/mol-model/structure/structure/carbohydrates/compute.ts

@@ -421,16 +421,22 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[
         return `${unit.id}|${residueIndex}`
     }
 
-    const anomericCarbonMap = new Map<string, ElementIndex>()
+    const anomericCarbonMap = new Map<string, ElementIndex[]>()
     for (let i = 0, il = elements.length; i < il; ++i) {
         const { unit, anomericCarbon } = elements[i]
         const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[anomericCarbon]
-        anomericCarbonMap.set(anomericCarbonKey(unit, residueIndex), anomericCarbon)
+        const k = anomericCarbonKey(unit, residueIndex)
+        if (anomericCarbonMap.has(k)) {
+            anomericCarbonMap.get(k)!.push(anomericCarbon)
+        } else {
+            anomericCarbonMap.set(k, [anomericCarbon])
+        }
     }
 
-    function getAnomericCarbon(unit: Unit, residueIndex: ResidueIndex) {
-        return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex))
+    const EmptyArray: ReadonlyArray<any> = []
+    function getAnomericCarbons(unit: Unit, residueIndex: ResidueIndex) {
+        return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex)) || EmptyArray
     }
 
-    return { getElementIndex, getLinkIndex, getLinkIndices, getTerminalLinkIndex, getTerminalLinkIndices, getAnomericCarbon }
+    return { getElementIndex, getLinkIndex, getLinkIndices, getTerminalLinkIndex, getTerminalLinkIndices, getAnomericCarbons }
 }

+ 2 - 2
src/mol-model/structure/structure/carbohydrates/data.ts

@@ -49,7 +49,7 @@ export interface Carbohydrates {
     getLinkIndices: (unit: Unit, anomericCarbon: ElementIndex) => ReadonlyArray<number>
     getTerminalLinkIndex: (unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) => number | undefined
     getTerminalLinkIndices: (unit: Unit, element: ElementIndex) => ReadonlyArray<number>
-    getAnomericCarbon: (unit: Unit, residueIndex: ResidueIndex) => ElementIndex | undefined
+    getAnomericCarbons: (unit: Unit, residueIndex: ResidueIndex) => ReadonlyArray<ElementIndex>
 }
 
 const EmptyArray: ReadonlyArray<any> = []
@@ -63,5 +63,5 @@ export const EmptyCarbohydrates: Carbohydrates = {
     getLinkIndices: () => EmptyArray,
     getTerminalLinkIndex: () => undefined,
     getTerminalLinkIndices: () => EmptyArray,
-    getAnomericCarbon: () => undefined,
+    getAnomericCarbons: () => [],
 }

+ 1 - 1
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -99,7 +99,7 @@ export namespace VolumeStreaming {
         channels: Channels = {}
 
         private async queryData(box?: Box3D) {
-            let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId}`);
+            let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId.toLowerCase()}`);
 
             if (box) {
                 const { min: a, max: b } = box;

+ 1 - 1
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -115,7 +115,7 @@ const CreateVolumeStreamingInfo = PluginStateTransform.BuiltIn({
         const dataId = params.dataId;
         const emDefaultContourLevel = params.source.name === 'em' ? params.source.params.isoValue : VolumeIsoValue.relative(1);
         await taskCtx.update('Getting server header...');
-        const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${params.source.name}/${dataId}`), type: 'json' }).runInContext(taskCtx);
+        const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${params.source.name}/${dataId.toLocaleLowerCase()}`), type: 'json' }).runInContext(taskCtx);
         const data: VolumeServerInfo.Data = {
             serverUrl: params.serverUrl,
             dataId,

+ 2 - 0
src/mol-plugin/state/actions/data-format.ts

@@ -150,6 +150,8 @@ type cifVariants = 'dscif' | -1
 export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
     if (info.ext === 'bcif') {
         try {
+            // TODO find a way to run msgpackDecode only once
+            //      now it is run twice, here and during file parsing
             if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif'
         } catch { }
     } else if (info.ext === 'cif') {

+ 19 - 2
src/mol-plugin/state/actions/structure.ts

@@ -91,6 +91,10 @@ const DownloadStructure = StateAction.build({
                 id: PD.Text('1tqn', { label: 'Id' }),
                 options: DownloadStructurePdbIdSourceOptions
             }, { isFlat: true }),
+            'pdb-dev': PD.Group({
+                id: PD.Text('PDBDEV_00000001', { label: 'Id' }),
+                options: DownloadStructurePdbIdSourceOptions
+            }, { isFlat: true }),
             'bcif-static': PD.Group({
                 id: PD.Text('1tqn', { label: 'Id' }),
                 options: DownloadStructurePdbIdSourceOptions
@@ -107,6 +111,7 @@ const DownloadStructure = StateAction.build({
                 options: [
                     ['pdbe-updated', 'PDBe Updated'],
                     ['rcsb', 'RCSB'],
+                    ['pdb-dev', 'PDBDEV'],
                     ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
                     ['url', 'URL']
                 ]
@@ -133,6 +138,18 @@ const DownloadStructure = StateAction.build({
             supportProps = !!src.params.options.supportProps;
             asTrajectory = !!src.params.options.asTrajectory;
             break;
+        case 'pdb-dev':
+            downloadParams = getDownloadParams(src.params.id,
+                id => {
+                    const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`
+                    return `https://pdb-dev.wwpdb.org/static/cif/${nId.toUpperCase()}.cif`
+                },
+                id => id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`,
+                false
+            );
+            supportProps = !!src.params.options.supportProps;
+            asTrajectory = !!src.params.options.asTrajectory;
+            break;
         case 'bcif-static':
             downloadParams = getDownloadParams(src.params.id, id => `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${id.toLowerCase()}`, id => `BinaryCIF: ${id}`, true);
             supportProps = !!src.params.options.supportProps;
@@ -155,7 +172,7 @@ const DownloadStructure = StateAction.build({
 });
 
 function getDownloadParams(src: string, url: (id: string) => string, label: (id: string) => string, isBinary: boolean): StateTransformer.Params<Download>[] {
-    const ids = src.split(',').map(id => id.trim()).filter(id => !!id && id.length >= 4);
+    const ids = src.split(',').map(id => id.trim()).filter(id => !!id && (id.length >= 4 || /^[1-9][0-9]*$/.test(id)));
     const ret: StateTransformer.Params<Download>[] = [];
     for (const id of ids) {
         ret.push({ url: url(id), isBinary, label: label(id) })
@@ -228,7 +245,7 @@ export function complexRepresentation(
     if (!params || !params.hideCoarse) {
         root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
             .apply(StateTransforms.Representation.StructureRepresentation3D,
-                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill', {}, 'polymer-id'));
     }
 }
 

+ 1 - 1
src/mol-plugin/state/transforms/helpers.ts

@@ -42,7 +42,7 @@ export function getStructureTransparency(structure: Structure, script: Script, v
  * Attaches ComputedSecondaryStructure property when unavailable in sourceData
  */
 export async function ensureSecondaryStructure(s: Structure) {
-    if (s.model.sourceData.kind === 'mmCIF') {
+    if (s.model && s.model.sourceData.kind === 'mmCIF') {
         if (!s.model.sourceData.data.struct_conf.id.isDefined && !s.model.sourceData.data.struct_sheet_range.id.isDefined) {
             await ComputedSecondaryStructure.attachFromCifOrCompute(s)
         }

+ 3 - 4
src/mol-plugin/state/transforms/model.ts

@@ -153,10 +153,9 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     apply({ a, params }) {
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
         const model = a.data[params.modelIndex];
-        const props = a.data.length === 1
-            ? { label: `${model.label}` }
-            : { label: `${model.label}:${model.modelNum}`, description: `Model ${params.modelIndex + 1} of ${a.data.length}` };
-        return new SO.Molecule.Model(model, props);
+        const label = a.data.length === 1 ? model.entry : `${model.entry}: ${model.modelNum}`
+        const description = a.data.length === 1 ? undefined : `Model ${params.modelIndex + 1} of ${a.data.length}`
+        return new SO.Molecule.Model(model, { label, description });
     }
 });
 

+ 4 - 3
src/mol-plugin/state/transforms/representation.ts

@@ -108,13 +108,14 @@ namespace StructureRepresentation3DHelpers {
         })
     }
 
-    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> {
+    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>, colorName?: BuiltInColorThemeName): StateTransformer.Params<StructureRepresentation3D> {
         const type = ctx.structureRepresentation.registry.get(name);
-        const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues;
+        const color = colorName || type.defaultColorTheme;
+        const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(color).defaultValues;
         const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues
         return ({
             type: { name, params: structureParams ? { ...type.defaultValues, ...structureParams } : type.defaultValues },
-            colorTheme: { name: type.defaultColorTheme, params: colorParams },
+            colorTheme: { name: color, params: colorParams },
             sizeTheme: { name: type.defaultSizeTheme, params: sizeParams }
         })
     }

+ 13 - 13
src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts

@@ -25,7 +25,7 @@ import { OrderedSet, Interval } from 'mol-data/int';
 import { EmptyLoci, Loci } from 'mol-model/loci';
 import { VisualContext } from 'mol-repr/visual';
 import { Theme } from 'mol-theme/theme';
-import { getResidueLoci } from './util/common';
+import { getAltResidueLoci } from './util/common';
 
 const t = Mat4.identity()
 const sVec = Vec3.zero()
@@ -82,7 +82,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
                 Mat4.scaleUniformly(t, t, side)
                 MeshBuilder.addPrimitive(builderState, t, perforatedBox)
                 Mat4.mul(t, t, Mat4.rotZ90X180)
-                builderState.currentGroup = i * 2 + 1
+                builderState.currentGroup += 1
                 MeshBuilder.addPrimitive(builderState, t, perforatedBox)
                 break;
             case SaccharideShapes.FilledCone:
@@ -93,7 +93,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
                 Mat4.scaleUniformly(t, t, side * 1.2)
                 MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid)
                 Mat4.mul(t, t, Mat4.rotZ90)
-                builderState.currentGroup = i * 2 + 1
+                builderState.currentGroup += 1
                 MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid)
                 break
             case SaccharideShapes.FlatBox:
@@ -116,7 +116,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
                 Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
                 MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron)
                 Mat4.mul(t, t, Mat4.rotY90)
-                builderState.currentGroup = i * 2 + 1
+                builderState.currentGroup += 1
                 MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron)
                 break
             case SaccharideShapes.FlatDiamond:
@@ -186,28 +186,28 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
     const { objectId, groupId } = pickingId
     if (id === objectId) {
         const carb = structure.carbohydrates.elements[Math.floor(groupId / 2)]
-        return getResidueLoci(structure, carb.unit, carb.anomericCarbon)
+        return getAltResidueLoci(structure, carb.unit, carb.anomericCarbon)
     }
     return EmptyLoci
 }
 
 /** For each carbohydrate (usually a monosaccharide) when all its residue's elements are in a loci. */
 function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
-    const { getElementIndex, getAnomericCarbon } = structure.carbohydrates
+    const { getElementIndex, getAnomericCarbons } = structure.carbohydrates
     let changed = false
     if (!StructureElement.isLoci(loci)) return false
     if (!Structure.areEquivalent(loci.structure, structure)) return false
     for (const e of loci.elements) {
+        // TODO make more efficient by handling/grouping `e.indices` by residue index
+        // TODO only call apply when the full alt-residue of the unit is part of `e`
         OrderedSet.forEach(e.indices, v => {
             const { model, elements } = e.unit
-            const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+            const { index } = model.atomicHierarchy.residueAtomSegments
             const rI = index[elements[v]]
-            const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
-            const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
-            const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-            if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
-            const eI = getAnomericCarbon(e.unit, rI)
-            if (eI !== undefined) {
+            const eIndices = getAnomericCarbons(e.unit, rI)
+            for (let i = 0, il = eIndices.length; i < il; ++i) {
+                const eI = eIndices[i]
+                if (!OrderedSet.has(e.indices, OrderedSet.indexOf(elements, eI))) continue
                 const idx = getElementIndex(e.unit, eI)
                 if (idx !== undefined) {
                     if (apply(Interval.ofBounds(idx * 2, idx * 2 + 2))) changed = true

+ 27 - 0
src/mol-repr/structure/visual/util/common.ts

@@ -28,6 +28,33 @@ export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementI
     return EmptyLoci
 }
 
+/**
+ * Return a Loci for the elements of a whole residue the elementIndex belongs to but
+ * restrict to elements that have the same label_alt_id or none
+ */
+export function getAltResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci {
+    const { elements, model } = unit
+    const { label_alt_id } = model.atomicHierarchy.atoms
+    const elementAltId = label_alt_id.value(elementIndex)
+    if (OrderedSet.indexOf(elements, elementIndex) !== -1) {
+        const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+        const rI = index[elementIndex]
+        const _indices: number[] = []
+        for (let i = offsets[rI], il = offsets[rI + 1]; i < il; ++i) {
+            const unitIndex = OrderedSet.indexOf(elements, i)
+            if (unitIndex !== -1) {
+                const altId = label_alt_id.value(i)
+                if (elementAltId === altId || altId === '') {
+                    _indices.push(unitIndex)
+                }
+            }
+        }
+        const indices = OrderedSet.ofSortedArray<StructureElement.UnitIndex>(SortedArray.ofSortedArray(_indices))
+        return StructureElement.Loci(structure, [{ unit, indices }])
+    }
+    return EmptyLoci
+}
+
 //
 
 export function createUnitsTransform({ units }: Unit.SymmetryGroup, transformData?: TransformData) {

+ 18 - 0
src/mol-script/runtime/query/table.ts

@@ -187,7 +187,25 @@ const symbols = [
     // C(MolScript.structureQuery.slot.elementSetReduce, (ctx, _) => ctx.element),
 
     // ============= FILTERS ================
+    D(MolScript.structureQuery.filter.pick, (ctx, xs) => Queries.filters.pick(xs[0] as any, xs['test'])(ctx)),
     D(MolScript.structureQuery.filter.first, (ctx, xs) => Queries.filters.first(xs[0] as any)(ctx)),
+    D(MolScript.structureQuery.filter.withSameAtomProperties, (ctx, xs) => Queries.filters.withSameAtomProperties(xs[0] as any, xs['source'] as any, xs['property'] as any)(ctx)),
+    D(MolScript.structureQuery.filter.intersectedBy, (ctx, xs) => Queries.filters.areIntersectedBy(xs[0] as any, xs['by'] as any)(ctx)),
+    D(MolScript.structureQuery.filter.within, (ctx, xs) => Queries.filters.within({
+        query: xs[0] as any,
+        target: xs['target'] as any,
+        minRadius: xs['min-radius'] as any,
+        maxRadius: xs['max-radius'] as any,
+        elementRadius: xs['atom-radius'] as any,
+        invert: xs['invert'] as any
+    })(ctx)),
+    D(MolScript.structureQuery.filter.isConnectedTo, (ctx, xs) => Queries.filters.isConnectedTo({
+        query: xs[0] as any,
+        target: xs['target'] as any,
+        disjunct: xs['disjunct'] as any,
+        invert: xs['invert'] as any,
+        bondTest: xs['bond-test']
+    })(ctx)),
 
     // ============= GENERATORS ================
     D(MolScript.structureQuery.generator.atomGroups, (ctx, xs) => Queries.generators.atoms({

+ 4 - 0
src/mol-theme/color.ts

@@ -28,6 +28,8 @@ import { ScaleLegend } from 'mol-util/color/scale';
 import { TableLegend } from 'mol-util/color/tables';
 import { UncertaintyColorThemeProvider } from './color/uncertainty';
 import { GeneColorThemeProvider } from './color/gene';
+import { IllustrativeColorThemeProvider } from './color/illustrative';
+import { HydrophobicityColorThemeProvider } from './color/hydrophobicity';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
@@ -75,6 +77,8 @@ export const BuiltInColorThemes = {
     'element-index': ElementIndexColorThemeProvider,
     'element-symbol': ElementSymbolColorThemeProvider,
     'gene': GeneColorThemeProvider,
+    'hydrophobicity': HydrophobicityColorThemeProvider,
+    'illustrative': IllustrativeColorThemeProvider,
     'molecule-type': MoleculeTypeColorThemeProvider,
     'polymer-id': PolymerIdColorThemeProvider,
     'polymer-index': PolymerIndexColorThemeProvider,

+ 4 - 4
src/mol-theme/color/carbohydrate-symbol.ts

@@ -27,13 +27,13 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
     let color: LocationColor
 
     if (ctx.structure) {
-        const { elements, getElementIndex, getAnomericCarbon } = ctx.structure.carbohydrates
+        const { elements, getElementIndex, getAnomericCarbons } = ctx.structure.carbohydrates
 
         const getColor = (unit: Unit, index: ElementIndex) => {
             const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[index]
-            const anomericCarbon = getAnomericCarbon(unit, residueIndex)
-            if (anomericCarbon !== undefined) {
-                const idx = getElementIndex(unit, anomericCarbon)
+            const anomericCarbons = getAnomericCarbons(unit, residueIndex)
+            if (anomericCarbons.length > 0) {
+                const idx = getElementIndex(unit, anomericCarbons[0])
                 if (idx !== undefined) return elements[idx].component.color
             }
             return DefaultColor

+ 107 - 0
src/mol-theme/color/hydrophobicity.ts

@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color, ColorScale } from 'mol-util/color';
+import { StructureElement, Unit, Link, ElementIndex } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { ColorTheme } from '../color';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { ThemeDataContext } from '../theme';
+import { ResidueHydrophobicity } from 'mol-model/structure/model/types';
+import { ColorListName, ColorListOptions } from 'mol-util/color/scale';
+
+const Description = 'Assigns a color to every amino acid according to the "Experimentally determined hydrophobicity scale for proteins at membrane interfaces" by Wimely and White (doi:10.1038/nsb1096-842).'
+
+export const HydrophobicityColorThemeParams = {
+    list: PD.ColorScale<ColorListName>('RedYellowGreen', ColorListOptions),
+    scale: PD.Select('DGwif', [['DGwif', 'DG water-membrane'], ['DGwoct', 'DG water-octanol'], ['Oct-IF', 'DG difference']])
+}
+export type HydrophobicityColorThemeParams = typeof HydrophobicityColorThemeParams
+export function getHydrophobicityColorThemeParams(ctx: ThemeDataContext) {
+    return HydrophobicityColorThemeParams // TODO return copy
+}
+
+const scaleIndexMap = { 'DGwif': 0, 'DGwoct': 1, 'Oct-IF': 2 }
+
+export function hydrophobicity(compId: string, scaleIndex: number): number {
+    const c = (ResidueHydrophobicity as { [k: string]: number[] })[compId];
+    return c === undefined ? 0 : c[scaleIndex]
+}
+
+function getAtomicCompId(unit: Unit.Atomic, element: ElementIndex) {
+    const { modifiedResidues } = unit.model.properties
+    const compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
+    const parentId = modifiedResidues.parentId.get(compId)
+    return parentId === undefined ? compId : parentId
+}
+
+function getCoarseCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIndex) {
+    const seqIdBegin = unit.coarseElements.seq_id_begin.value(element)
+    const seqIdEnd = unit.coarseElements.seq_id_end.value(element)
+    if (seqIdBegin === seqIdEnd) {
+        const { modifiedResidues } = unit.model.properties
+        const entityKey = unit.coarseElements.entityKey[element]
+        const seq = unit.model.sequence.byEntityKey[entityKey]
+        let compId = seq.compId.value(seqIdBegin - 1) // 1-indexed
+        const parentId = modifiedResidues.parentId.get(compId)
+        return parentId === undefined ? compId : parentId
+    }
+}
+
+export function HydrophobicityColorTheme(ctx: ThemeDataContext, props: PD.Values<HydrophobicityColorThemeParams>): ColorTheme<HydrophobicityColorThemeParams> {
+    const scaleIndex = scaleIndexMap[props.scale]
+
+    // get domain
+    let min = Infinity
+    let max = -Infinity
+    for (const name in ResidueHydrophobicity) {
+        const val = (ResidueHydrophobicity as { [k: string]: number[] })[name][scaleIndex]
+        min = Math.min(min, val)
+        max = Math.max(max, val)
+    }
+
+    const scale = ColorScale.create({
+        listOrName: props.list,
+        domain: [ max, min ],
+        minLabel: 'Hydrophobic',
+        maxLabel: 'Hydrophilic'
+    })
+
+    function color(location: Location): Color {
+        let compId: string | undefined
+        if (StructureElement.isLocation(location)) {
+            if (Unit.isAtomic(location.unit)) {
+                compId = getAtomicCompId(location.unit, location.element)
+            } else {
+                compId = getCoarseCompId(location.unit, location.element)
+            }
+        } else if (Link.isLocation(location)) {
+            if (Unit.isAtomic(location.aUnit)) {
+                compId = getAtomicCompId(location.aUnit, location.aUnit.elements[location.aIndex])
+            } else {
+                compId = getCoarseCompId(location.aUnit, location.aUnit.elements[location.aIndex])
+            }
+        }
+        return scale.color(compId ? hydrophobicity(compId, scaleIndex) : 0)
+    }
+
+    return {
+        factory: HydrophobicityColorTheme,
+        granularity: 'group',
+        color,
+        props,
+        description: Description,
+        legend: scale ? scale.legend : undefined
+    }
+}
+
+export const HydrophobicityColorThemeProvider: ColorTheme.Provider<HydrophobicityColorThemeParams> = {
+    label: 'Hydrophobicity',
+    factory: HydrophobicityColorTheme,
+    getParams: getHydrophobicityColorThemeParams,
+    defaultValues: PD.getDefaultValues(HydrophobicityColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
+}

+ 80 - 0
src/mol-theme/color/illustrative.ts

@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ElementSymbol, isNucleic, isProtein, MoleculeType } from 'mol-model/structure/model/types';
+import { Color } from 'mol-util/color';
+import { StructureElement, Unit, Link } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { ColorTheme } from '../color';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { ThemeDataContext } from '../theme';
+import { elementSymbolColor } from './element-symbol';
+
+const DefaultIllustrativeColor = Color(0xFFFFFF)
+const Description = `Assigns an illustrative color similar to David Goodsell's Molecule of the Month style.`
+
+export const IllustrativeColorThemeParams = {}
+export type IllustrativeColorThemeParams = typeof IllustrativeColorThemeParams
+export function getIllustrativeColorThemeParams(ctx: ThemeDataContext) {
+    return IllustrativeColorThemeParams // TODO return copy
+}
+
+function illustrativeColor(typeSymbol: ElementSymbol, moleculeType: MoleculeType) {
+    if (isNucleic(moleculeType)) {
+        if (typeSymbol === 'O') {
+            return Color(0xFF8C8C)
+        } else if (typeSymbol === 'P') {
+            return Color(0xFF7D7D)
+        } else {
+            return Color(0xFFA6A6)
+        }
+    } else if (isProtein(moleculeType)) {
+        if (typeSymbol === 'C') {
+            return Color(0x7FB2FF)
+        } else {
+            return Color(0x6699FF)
+        }
+    } else {
+        return elementSymbolColor(typeSymbol)
+    }
+}
+
+export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<IllustrativeColorThemeParams>): ColorTheme<IllustrativeColorThemeParams> {
+    function color(location: Location): Color {
+        if (StructureElement.isLocation(location)) {
+            if (Unit.isAtomic(location.unit)) {
+                const moleculeType = location.unit.model.atomicHierarchy.derived.residue.moleculeType[location.unit.residueIndex[location.element]]
+                const typeSymbol = location.unit.model.atomicHierarchy.atoms.type_symbol.value(location.element)
+                return illustrativeColor(typeSymbol, moleculeType)
+            }
+        } else if (Link.isLocation(location)) {
+            if (Unit.isAtomic(location.aUnit)) {
+                const elementIndex = location.aUnit.elements[location.aIndex]
+                const moleculeType = location.aUnit.model.atomicHierarchy.derived.residue.moleculeType[location.aUnit.residueIndex[elementIndex]]
+                const typeSymbol = location.aUnit.model.atomicHierarchy.atoms.type_symbol.value(elementIndex)
+                return illustrativeColor(typeSymbol, moleculeType)
+            }
+        }
+        return DefaultIllustrativeColor
+    }
+
+    return {
+        factory: IllustrativeColorTheme,
+        granularity: 'group',
+        color,
+        props,
+        description: Description,
+        // TODO add legend
+    }
+}
+
+export const IllustrativeColorThemeProvider: ColorTheme.Provider<IllustrativeColorThemeParams> = {
+    label: 'Illustrative',
+    factory: IllustrativeColorTheme,
+    getParams: getIllustrativeColorThemeParams,
+    defaultValues: PD.getDefaultValues(IllustrativeColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
+}

+ 1 - 1
src/mol-util/color/scale.ts

@@ -55,7 +55,7 @@ export interface ColorScale {
 }
 
 export const DefaultColorScaleProps = {
-    domain: [0, 1],
+    domain: [0, 1] as [number, number],
     reverse: false,
     listOrName: ColorBrewer.RedYellowBlue as Color[] | ColorListName,
     minLabel: '' as string | undefined,

+ 21 - 0
src/mol-util/string.ts

@@ -52,4 +52,25 @@ export function interpolate(str: string, params: { [k: string]: any }) {
     const names = Object.keys(params);
     const values = Object.values(params);
     return new Function(...names, `return \`${str}\`;`)(...values);
+}
+
+export function trimChar(str: string, char: string) {
+    let start = 0;
+    let end = str.length;
+    while (start < end && str[start] === char) ++start;
+    while (end > start && str[end - 1] === char) --end;
+    return (start > 0 || end < str.length) ? str.substring(start, end) : str;
+}
+
+export function trimCharStart(str: string, char: string) {
+    let start = 0;
+    const end = str.length;
+    while (start < end && str[start] === char) ++start;
+    return (start > 0) ? str.substring(start, end) : str;
+}
+
+export function trimCharEnd(str: string, char: string) {
+    let end = str.length;
+    while (end > 0 && str[end - 1] === char) --end;
+    return (end < str.length) ? str.substring(0, end) : str;
 }