Browse Source

Merge branch 'master' into cylinders

dsehnal 4 years ago
parent
commit
b0216c4ce6

+ 3 - 1
src/mol-canvas3d/canvas3d.ts

@@ -628,10 +628,12 @@ namespace Canvas3D {
             reprCount,
             resized,
             setProps: (properties, doNotRequestDraw = false) => {
-                const props: PartialCanvas3DProps = typeof properties === 'function'
+                let props: PartialCanvas3DProps = typeof properties === 'function'
                     ? produce(getProps(), properties)
                     : properties;
 
+                props = PD.normalizeParams(Canvas3DParams, props, false);
+
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null);
                 if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
                     cameraState.mode = props.camera.mode;

+ 1 - 0
src/mol-io/writer/mol/encoder.ts

@@ -90,6 +90,7 @@ export class MolEncoder extends LigandEncoder {
         const { instance, source } = getCategoryInstanceData(category, context);
         const fields = instance.fields;
         const src = source[0];
+        if (!src) return;
         const data = src.data;
 
         const it = src.keys();

+ 1 - 0
src/mol-io/writer/mol2/encoder.ts

@@ -289,6 +289,7 @@ export class Mol2Encoder extends LigandEncoder {
         const { instance, source } = getCategoryInstanceData(category, context);
         const fields = instance.fields;
         const src = source[0];
+        if (!src) return;
         const data = src.data;
 
         const it = src.keys();

+ 2 - 0
src/mol-state/object.ts

@@ -80,6 +80,8 @@ interface StateObjectCell<T extends StateObject = StateObject, F extends StateTr
         values: any
     } | undefined,
 
+    paramsNormalized: boolean,
+
     dependencies: {
         dependentBy: StateObjectCell[],
         dependsOn: StateObjectCell[]

+ 15 - 6
src/mol-state/state.ts

@@ -378,6 +378,7 @@ class State {
                 definition: {},
                 values: {}
             },
+            paramsNormalized: true,
             dependencies: { dependentBy: [], dependsOn: [] },
             cache: { }
         });
@@ -662,6 +663,7 @@ function addCellsVisitor(transform: StateTransform, _: any, { ctx, added, visite
         state: { ...transform.state },
         errorText: void 0,
         params: void 0,
+        paramsNormalized: false,
         dependencies: { dependentBy: [], dependsOn: [] },
         cache: void 0
     };
@@ -840,13 +842,20 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
     }
 }
 
-function resolveParams(ctx: UpdateContext, transform: StateTransform, src: StateObject) {
+function resolveParams(ctx: UpdateContext, transform: StateTransform, src: StateObject, cell: StateObjectCell) {
     const prms = transform.transformer.definition.params;
     const definition = prms ? prms(src, ctx.parent.globalContext) : {};
-    const defaultValues = ParamDefinition.getDefaultValues(definition);
-    (transform.params as any) = transform.params
-        ? assignIfUndefined(transform.params, defaultValues)
-        : defaultValues;
+
+    if (!cell.paramsNormalized) {
+        (transform.params as any) = ParamDefinition.normalizeParams(definition, transform.params, true);
+        cell.paramsNormalized = true;
+    } else {
+        const defaultValues = ParamDefinition.getDefaultValues(definition);
+        (transform.params as any) = transform.params
+            ? assignIfUndefined(transform.params, defaultValues)
+            : defaultValues;
+    }
+
     ParamDefinition.resolveRefs(definition, transform.params, ctx.getCellData);
     return { definition, values: transform.params };
 }
@@ -873,7 +882,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
     const parent = parentCell.obj!;
     current.sourceRef = parentCell.transform.ref;
 
-    const params = resolveParams(ctx, transform, parent);
+    const params = resolveParams(ctx, transform, parent, current);
 
     if (!oldTree.transforms.has(currentRef) || !current.params) {
         current.params = params;

+ 89 - 2
src/mol-util/param-definition.ts

@@ -77,7 +77,8 @@ export namespace ParamDefinition {
         type: 'select'
         /** array of (value, label) tuples */
         options: readonly (readonly [T, string] | readonly [T, string, string | undefined])[]
-        cycle?: boolean
+        cycle?: boolean,
+        _optionSet?: Set<T>
     }
     export function Select<T>(defaultValue: T, options: readonly (readonly [T, string] | readonly [T, string, string | undefined])[], info?: Info & { cycle?: boolean }): Select<T> {
         return setInfo<Select<T>>({ type: 'select', defaultValue: checkDefaultKey(defaultValue, options), options, cycle: info?.cycle }, info);
@@ -87,7 +88,8 @@ export namespace ParamDefinition {
         type: 'multi-select'
         /** array of (value, label) tuples */
         options: readonly (readonly [E, string])[],
-        emptyValue?: string
+        emptyValue?: string,
+        _optionSet?: Set<T>
     }
     export function MultiSelect<E extends string, T = E[]>(defaultValue: T, options: readonly (readonly [E, string])[], info?: Info & { emptyValue?: string }): MultiSelect<E, T> {
         // TODO: check if default value is a subset of options?
@@ -548,6 +550,91 @@ export namespace ParamDefinition {
         }
     }
 
+    function normalizeParam(p: Any, value: any, defaultIfUndefined: boolean): any {
+        if (value === void 0 || value === null) {
+            return defaultIfUndefined ? p.defaultValue : void 0;
+        }
+
+        // TODO: is this a good idea and will work well?
+        // if (typeof p.defaultValue !== typeof value) {
+        //     return p.defaultValue;
+        // }
+
+        if (p.type === 'value') {
+            return value;
+        } else if (p.type === 'group') {
+            const ret = { ...value };
+            for (const key of Object.keys(p.params)) {
+                const param = p.params[key];
+                if (ret[key] === void 0) {
+                    if (defaultIfUndefined) ret[key] = param.defaultValue;
+                } else {
+                    ret[key] = normalizeParam(param, ret[key], defaultIfUndefined);
+                }
+            }
+            return ret;
+        } else if (p.type === 'mapped') {
+            const v = value as NamedParams;
+            if (typeof v.name !== 'string') {
+                return p.defaultValue;
+            }
+            if (typeof v.params === 'undefined') {
+                return defaultIfUndefined ? p.defaultValue : void 0;
+            }
+
+            const options = !!p.select._optionSet?.has
+                ? p.select._optionSet
+                : (p.select._optionSet = new Set(p.select.options.map(o => o[0])));
+            if (!options.has(v.name)) {
+                return p.defaultValue;
+            }
+
+            const param = p.map(v.name);
+            return {
+                name: v.name,
+                params: normalizeParam(param, v.params, defaultIfUndefined)
+            };
+        } else if (p.type === 'select') {
+            const options = !!p._optionSet?.has
+                ? p._optionSet
+                : (p._optionSet = new Set(p.options.map(o => o[0])));
+            if (!options.has(value)) return p.defaultValue;
+            return value;
+        } else if (p.type === 'multi-select') {
+            const options = !!p._optionSet?.has
+                ? p._optionSet
+                : (p._optionSet = new Set(p.options.map(o => o[0])));
+            if (!Array.isArray(value)) return p.defaultValue;
+            const ret = value.filter(function (this: Set<any>, v: any) { return this.has(v); }, options);
+            if (value.length > 0 && ret.length === 0) return p.defaultValue;
+            return ret;
+        } else if (p.type === 'object-list') {
+            if (!Array.isArray(value)) return p.defaultValue;
+            return value.map(v => normalizeParams(p.element, v, defaultIfUndefined));
+        }
+
+        // TODO: validate/normalize all param types "properly"??
+
+        return value;
+    }
+
+    export function normalizeParams(p: Params, value: any, defaultIfUndefined: boolean) {
+        if (typeof value !== 'object') {
+            return defaultIfUndefined ? getDefaultValues(p) : value;
+        }
+
+        const ret = { ...value };
+        for (const key of Object.keys(p)) {
+            const param = p[key];
+            if (ret[key] === void 0) {
+                if (defaultIfUndefined) ret[key] = param.defaultValue;
+            } else {
+                ret[key] = normalizeParam(param, ret[key], defaultIfUndefined);
+            }
+        }
+        return ret;
+    }
+
     /**
      * Map an object to a list of [K, string][] to be used as options, stringToWords for key used by default (or identity of null).
      *

+ 2 - 1
src/servers/model/server/api-web-multiple.ts

@@ -19,7 +19,8 @@ export interface MultipleQuerySpec {
     queries: MultipleQueryEntry[],
     encoding?: Encoding,
     asTarGz?: boolean,
-    download?: boolean
+    download?: boolean,
+    filename?: string
 }
 
 export function getMultiQuerySpecFilename() {

+ 3 - 3
src/servers/model/server/api-web.ts

@@ -49,14 +49,14 @@ export function createResultWriter(response: express.Response, params: ResultWri
     const filenameBase = params.entryId && params.queryName
         ? `${params.entryId}_${splitCamelCase(params.queryName.replace(/\s/g, '_'), '-').toLowerCase()}`
         : `result`;
-    return new SimpleResponseResultWriter(`${filenameBase}.${params.encoding}`, response, params.encoding === 'bcif', params.download);
+    return new SimpleResponseResultWriter(params.filename || `${filenameBase}.${params.encoding}`, response, params.encoding === 'bcif', params.download);
 }
 
 function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) {
     function createJob(queryParams: any, req: express.Request, res: express.Response) {
         const entryId = req.params.id;
         const commonParams = normalizeRestCommonParams(req.query);
-        const resultWriterParams = { encoding: commonParams.encoding!, download: !!commonParams.download, entryId, queryName };
+        const resultWriterParams = { encoding: commonParams.encoding!, download: !!commonParams.download, filename: commonParams.filename!, entryId, queryName };
         const jobId = JobManager.add({
             entries: [JobEntry({
                 sourceId: commonParams.data_source || ModelServerConfig.defaultSource,
@@ -123,7 +123,7 @@ function serveStatic(req: express.Request, res: express.Response) {
 function createMultiJob(spec: MultipleQuerySpec, res: express.Response) {
     const writer = spec.asTarGz
         ? new TarballResponseResultWriter(getMultiQuerySpecFilename(), res)
-        : createResultWriter(res, { encoding: spec.encoding!, download: !!spec.download });
+        : createResultWriter(res, { encoding: spec.encoding!, download: !!spec.download, filename: spec.filename });
 
     if (spec.queries.length > ModelServerConfig.maxQueryManyQueries) {
         writer.doError(400, `query-many queries limit (${ModelServerConfig.maxQueryManyQueries}) exceeded.`);

+ 6 - 3
src/servers/model/server/api.ts

@@ -49,7 +49,8 @@ export const CommonQueryParamsInfo: QueryParamInfo[] = [
     { name: 'copy_all_categories', type: QueryParamType.Boolean, defaultValue: false, description: 'If true, copy all categories from the input file.' },
     { name: 'data_source', type: QueryParamType.String, defaultValue: '', description: 'Allows to control how the provided data source ID maps to input file (as specified by the server instance config).' },
     { name: 'transform', type: QueryParamType.String, description: `Transformation to apply to coordinates in '_atom_site'. Accepts a 4x4 transformation matrix, provided as array of 16 float values.` },
-    { name: 'download', type: QueryParamType.Boolean, defaultValue: false, description: 'If true, browser will download text files.' }
+    { name: 'download', type: QueryParamType.Boolean, defaultValue: false, description: 'If true, browser will download text files.' },
+    { name: 'filename', type: QueryParamType.String, defaultValue: '', description: `Controls the filename for downloaded files. Will force download if specified.` }
 ];
 
 export type Encoding = 'cif' | 'bcif' | 'sdf' | 'mol' | 'mol2';
@@ -59,7 +60,8 @@ export interface CommonQueryParamsInfo {
     copy_all_categories?: boolean
     data_source?: string,
     transform?: Mat4,
-    download?: boolean
+    download?: boolean,
+    filename?: string
 }
 
 export const AtomSiteSchemaElement = {
@@ -295,7 +297,8 @@ export function normalizeRestCommonParams(params: any): CommonQueryParamsInfo {
         copy_all_categories: isTrue(params.copy_all_categories),
         encoding: mapEncoding(('' + params.encoding).toLocaleLowerCase()),
         transform: params.transform ? ('' + params.transform).split(',').map(n => n.trim()).map(n => +n) as Mat4 : Mat4.identity(),
-        download: isTrue(params.download)
+        download: isTrue(params.download) || !!params.filename,
+        filename: params.filename
     };
 }
 

+ 1 - 0
src/servers/model/server/jobs.ts

@@ -59,6 +59,7 @@ interface JobEntryDefinition<Name extends QueryName> {
 export interface ResultWriterParams {
     encoding: Encoding,
     download: boolean,
+    filename?: string,
     entryId?: string,
     queryName?: string
 }