Browse Source

Merge branch 'master' into cartoon-repr

Alexander Rose 6 years ago
parent
commit
8f41f9dd7d

+ 2 - 0
src/mol-model/structure/query.ts

@@ -9,11 +9,13 @@ import { StructureQuery } from './query/query'
 export * from './query/context'
 import * as generators from './query/queries/generators'
 import * as modifiers from './query/queries/modifiers'
+import * as combinators from './query/queries/combinators'
 import pred from './query/predicates'
 
 export const Queries = {
     generators,
     modifiers,
+    combinators,
     pred
 }
 

+ 6 - 0
src/mol-model/structure/query/queries/combinators.ts

@@ -6,8 +6,14 @@
 
 import { StructureQuery } from '../query';
 import { StructureSelection } from '../selection';
+import { none } from './generators';
 
 export function merge(queries: ArrayLike<StructureQuery>): StructureQuery {
+    if (queries.length === 0) {
+        return none;
+    } else if (queries.length === 1) {
+        return queries[0];
+    }
     return ctx => {
         const ret = StructureSelection.UniqueBuilder(ctx.inputStructure);
         for (let i = 0; i < queries.length; i++) {

+ 1 - 0
src/mol-model/structure/query/queries/generators.ts

@@ -11,6 +11,7 @@ import { Segmentation } from 'mol-data/int'
 import { LinearGroupingBuilder } from '../utils/builders';
 import { QueryPredicate, QueryFn, QueryContextView } from '../context';
 
+export const none: StructureQuery = ctx => StructureSelection.Sequence(ctx.inputStructure, []);
 export const all: StructureQuery = ctx => StructureSelection.Singletons(ctx.inputStructure, ctx.inputStructure);
 
 export interface AtomsQueryParams {

+ 6 - 2
src/servers/model/properties.ts

@@ -7,6 +7,10 @@
 import { Model } from 'mol-model/structure';
 import { StructureQualityReport } from './properties/structure-quality-report';
 
-export async function attachModelProperties(model: Model) {
-    await StructureQualityReport.attachFromPDBeApi(model);
+export function attachModelProperties(model: Model): Promise<any>[] {
+    // return a list of promises that start attaching the props in parallel
+    // (if there are downloads etc.)
+    return [
+        StructureQualityReport.attachFromPDBeApi(model)
+    ];
 }

+ 1 - 2
src/servers/model/properties/structure-quality-report.ts

@@ -46,7 +46,6 @@ const _structure_quality_report_fields: CifField<ResidueIndex, ExportCtx>[] = [
     CifField.int<ResidueIndex, ExportCtx>('auth_seq_id', (i, d) => P.residue.auth_seq_id(d.residues[i])),
     CifField.str<ResidueIndex, ExportCtx>('auth_asym_id', (i, d) => P.chain.auth_asym_id(d.residues[i])),
 
-
     CifField.str<ResidueIndex, ExportCtx>('issues', (i, d) => d.issues.get(d.residueIndex[d.residues[i].element])!.join(','))
 ];
 
@@ -106,7 +105,7 @@ export namespace StructureQualityReport {
         if (model.customProperties.has(Descriptor)) return true;
 
         const id = model.label.toLowerCase();
-        const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`);
+        const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`, { timeout: 500 });
         const json = await rawData.json();
         const data = json[id];
         if (!data) return false;

+ 118 - 0
src/servers/model/query/atoms.ts

@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { QueryPredicate, StructureElement, StructureProperties as Props, Queries } from 'mol-model/structure';
+import { AtomsQueryParams } from 'mol-model/structure/query/queries/generators';
+
+export function getAtomsTests(params: any): Partial<AtomsQueryParams>[] {
+    if (!params) return [{ }];
+    if (Array.isArray(params)) {
+        return params.map(p => atomsTest(p));
+    } else {
+        return [atomsTest(params)];
+    }
+}
+
+function atomsTest(params: any): Partial<AtomsQueryParams> {
+    return {
+        entityTest: entityTest(params),
+        chainTest: chainTest(params),
+        residueTest: residueTest(params),
+        atomTest: atomTest(params)
+    };
+}
+
+function entityTest(params: any): QueryPredicate | undefined {
+    if (!params || typeof params.entity_id === 'undefined') return void 0;
+    const p = Props.entity.id, id = '' + params.label_entity_id;
+    return ctx => p(ctx.element) === id;
+}
+
+function chainTest(params: any): QueryPredicate | undefined {
+    if (!params) return void 0;
+
+    if (typeof params.label_asym_id !== 'undefined') {
+        const p = Props.chain.label_asym_id, id = '' + params.label_asym_id;
+        return ctx => p(ctx.element) === id;
+    }
+    if (typeof params.auth_asym_id !== 'undefined') {
+        const p = Props.chain.auth_asym_id, id = '' + params.auth_asym_id;
+        return ctx => p(ctx.element) === id;
+    }
+    return void 0;
+}
+
+function residueTest(params: any): QueryPredicate | undefined {
+    if (!params) return void 0;
+
+    const props: StructureElement.Property<any>[] = [], values: any[] = [];
+
+    if (typeof params.label_seq_id !== 'undefined') {
+        props.push(Props.residue.label_seq_id);
+        values.push(+params.label_seq_id);
+    }
+
+    if (typeof params.auth_seq_id !== 'undefined') {
+        props.push(Props.residue.auth_seq_id);
+        values.push(+params.auth_seq_id);
+    }
+
+    if (typeof params.label_comp_id !== 'undefined') {
+        props.push(Props.residue.label_comp_id);
+        values.push(params.label_comp_id);
+    }
+
+    if (typeof params.auth_comp_id !== 'undefined') {
+        props.push(Props.residue.auth_comp_id);
+        values.push(params.auth_comp_id);
+    }
+
+    if (typeof params.pdbx_PDB_ins_code !== 'undefined') {
+        props.push(Props.residue.pdbx_PDB_ins_code);
+        values.push(params.pdbx_PDB_ins_code);
+    }
+
+    return andEqual(props, values);
+}
+
+function atomTest(params: any): QueryPredicate | undefined {
+    if (!params) return void 0;
+
+    const props: StructureElement.Property<any>[] = [], values: any[] = [];
+
+    if (typeof params.label_atom_id !== 'undefined') {
+        props.push(Props.atom.label_atom_id);
+        values.push(+params.label_atom_id);
+    }
+
+    if (typeof params.auth_atom_id !== 'undefined') {
+        props.push(Props.atom.auth_atom_id);
+        values.push(+params.auth_atom_id);
+    }
+
+    if (typeof params.type_symbol !== 'undefined') {
+        props.push(Props.atom.type_symbol);
+        values.push(+params.type_symbol);
+    }
+
+    return andEqual(props, values);
+}
+
+function andEqual(props: StructureElement.Property<any>[], values: any[]): QueryPredicate | undefined {
+    switch (props.length) {
+        case 0: return void 0;
+        case 1: return ctx => props[0](ctx.element) === values[0];
+        case 2: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1];
+        case 3: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1] && props[2](ctx.element) === values[2];
+        default: {
+            const len = props.length;
+            return ctx => {
+                for (let i = 0; i < len; i++) if (!props[i](ctx.element) !== values[i]) return false;
+                return true;
+            };
+        }
+    }
+}

+ 1 - 0
src/servers/model/query/schemas.ts

@@ -0,0 +1 @@
+// TODO

+ 25 - 15
src/servers/model/server/api-web.ts

@@ -6,7 +6,6 @@
 
 import * as express from 'express';
 import Config from '../config';
-import { QueryDefinition, QueryList } from './api';
 import { ConsoleLogger } from 'mol-util/console-logger';
 import { resolveJob } from './query';
 import { JobManager } from './jobs';
@@ -81,24 +80,35 @@ async function processNextJob() {
     }
 }
 
-function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) {
-    app.get(makePath(':entryId/' + queryName), (req, res) => {
-        ConsoleLogger.log('Server', `Query '${req.params.entryId}/${queryName}'...`);
+// function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) {
+//     app.get(makePath(':entryId/' + queryName), (req, res) => {
+//         ConsoleLogger.log('Server', `Query '${req.params.entryId}/${queryName}'...`);
 
-        if (JobManager.size >= Config.maxQueueLength) {
-            res.status(503).send('Too many queries, please try again later.');
-            res.end();
-            return;
-        }
+//         if (JobManager.size >= Config.maxQueueLength) {
+//             res.status(503).send('Too many queries, please try again later.');
+//             res.end();
+//             return;
+//         }
+
+//         const jobId = JobManager.add('pdb', req.params.entryId, queryName, req.query);
+//         responseMap.set(jobId, res);
+//         if (JobManager.size === 1) processNextJob();
+//     });
+// }
 
-        const jobId = JobManager.add('pdb', req.params.entryId, queryName, req.query);
+export function initWebApi(app: express.Express) {
+    app.get(makePath('query'), (req, res) => {
+        const query = /\?(.*)$/.exec(req.url)![1];
+        const args = JSON.parse(decodeURIComponent(query));
+        const name = args.name;
+        const entryId = args.id;
+        const params = args.params || { };
+        const jobId = JobManager.add('pdb', entryId, name, params);
         responseMap.set(jobId, res);
         if (JobManager.size === 1) processNextJob();
     });
-}
 
-export function initWebApi(app: express.Express) {
-    for (const q of QueryList) {
-        mapQuery(app, q.name, q.definition);
-    }
+    // for (const q of QueryList) {
+    //     mapQuery(app, q.name, q.definition);
+    // }
 }

+ 95 - 128
src/servers/model/server/api.ts

@@ -4,9 +4,11 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StructureQuery, Queries, Structure, StructureElement, StructureSymmetry, StructureProperties as Props, QueryPredicate } from 'mol-model/structure';
+import { Queries, Structure, StructureQuery, StructureSymmetry } from 'mol-model/structure';
+import { getAtomsTests } from '../query/atoms';
 
 export enum QueryParamType {
+    JSON,
     String,
     Integer,
     Float
@@ -18,7 +20,7 @@ export interface QueryParamInfo {
     description?: string,
     required?: boolean,
     defaultValue?: any,
-    exampleValue?: string,
+    exampleValues?: string[],
     validation?: (v: any) => void
 }
 
@@ -32,124 +34,87 @@ export interface QueryDefinition {
     structureTransform?: (params: any, s: Structure) => Promise<Structure>
 }
 
-const AtomSiteParameters = {
-    entity_id: <QueryParamInfo>{ name: 'entity_id', type: QueryParamType.String, description: 'Corresponds to the \'_entity.id\' or \'*.label_entity_id\' field, depending on the context.' },
+// const AtomSiteParams = {
+//     entity_id: <QueryParamInfo>{ name: 'entity_id', type: QueryParamType.String, description: 'Corresponds to the \'_entity.id\' or \'*.label_entity_id\' field, depending on the context.' },
 
-    label_asym_id: <QueryParamInfo>{ name: 'label_asym_id', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.label_asym_id\' field.' },
-    auth_asym_id: <QueryParamInfo>{ name: 'auth_asym_id', type: QueryParamType.String, exampleValue: 'A', description: 'Corresponds to the \'_atom_site.auth_asym_id\' field.' },
+//     label_asym_id: <QueryParamInfo>{ name: 'label_asym_id', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.label_asym_id\' field.' },
+//     auth_asym_id: <QueryParamInfo>{ name: 'auth_asym_id', type: QueryParamType.String, exampleValue: 'A', description: 'Corresponds to the \'_atom_site.auth_asym_id\' field.' },
 
-    label_seq_id: <QueryParamInfo>{ name: 'label_seq_id', type: QueryParamType.Integer, description: 'Residue seq. number. Corresponds to the \'_atom_site.label_seq_id\' field.' },
-    auth_seq_id: <QueryParamInfo>{ name: 'auth_seq_id', type: QueryParamType.Integer, exampleValue: '200', description: 'Author residue seq. number. Corresponds to the \'_atom_site.auth_seq_id\' field.' },
-    label_comp_id: <QueryParamInfo>{ name: 'label_comp_id', type: QueryParamType.String, description: 'Residue name. Corresponds to the \'_atom_site.label_comp_id\' field.' },
-    auth_comp_id: <QueryParamInfo>{ name: 'auth_comp_id', type: QueryParamType.String, exampleValue: 'REA', description: 'Author residue name. Corresponds to the \'_atom_site.auth_comp_id\' field.' },
-    pdbx_PDB_ins_code: <QueryParamInfo>{ name: 'pdbx_PDB_ins_code', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.pdbx_PDB_ins_code\' field.' },
-};
-
-// function entityTest(params: any): Element.Predicate | undefined {
-//     if (typeof params.entity_id === 'undefined') return void 0;
-//     const p = Props.entity.id, id = '' + params.entityId;
-//     return Element.property(l => p(l) === id);
-// }
-
-function entityTest1_555(params: any): QueryPredicate | undefined {
-    if (typeof params.entity_id === 'undefined') return ctx => ctx.element.unit.conformation.operator.isIdentity;
-    const p = Props.entity.id, id = '' + params.entityId;
-    return ctx => ctx.element.unit.conformation.operator.isIdentity && p(ctx.element) === id;
-}
-
-function chainTest(params: any): QueryPredicate | undefined {
-    if (typeof params.label_asym_id !== 'undefined') {
-        const p = Props.chain.label_asym_id, id = '' + params.label_asym_id;
-        return ctx => p(ctx.element) === id;
-    }
-    if (typeof params.auth_asym_id !== 'undefined') {
-        const p = Props.chain.auth_asym_id, id = '' + params.auth_asym_id;
-        return ctx => p(ctx.element) === id;
-    }
-    return void 0;
-}
-
-function residueTest(params: any): QueryPredicate | undefined {
-    const props: StructureElement.Property<any>[] = [], values: any[] = [];
+//     label_seq_id: <QueryParamInfo>{ name: 'label_seq_id', type: QueryParamType.Integer, description: 'Residue seq. number. Corresponds to the \'_atom_site.label_seq_id\' field.' },
+//     auth_seq_id: <QueryParamInfo>{ name: 'auth_seq_id', type: QueryParamType.Integer, exampleValue: '200', description: 'Author residue seq. number. Corresponds to the \'_atom_site.auth_seq_id\' field.' },
+//     label_comp_id: <QueryParamInfo>{ name: 'label_comp_id', type: QueryParamType.String, description: 'Residue name. Corresponds to the \'_atom_site.label_comp_id\' field.' },
+//     auth_comp_id: <QueryParamInfo>{ name: 'auth_comp_id', type: QueryParamType.String, exampleValue: 'REA', description: 'Author residue name. Corresponds to the \'_atom_site.auth_comp_id\' field.' },
+//     pdbx_PDB_ins_code: <QueryParamInfo>{ name: 'pdbx_PDB_ins_code', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.pdbx_PDB_ins_code\' field.' },
+// };
 
-    if (typeof params.label_seq_id !== 'undefined') {
-        props.push(Props.residue.label_seq_id);
-        values.push(+params.label_seq_id);
-    }
-
-    if (typeof params.auth_seq_id !== 'undefined') {
-        props.push(Props.residue.auth_seq_id);
-        values.push(+params.auth_seq_id);
-    }
-
-    if (typeof params.label_comp_id !== 'undefined') {
-        props.push(Props.residue.label_comp_id);
-        values.push(params.label_comp_id);
-    }
-
-    if (typeof params.auth_comp_id !== 'undefined') {
-        props.push(Props.residue.auth_comp_id);
-        values.push(params.auth_comp_id);
-    }
-
-    if (typeof params.pdbx_PDB_ins_code !== 'undefined') {
-        props.push(Props.residue.pdbx_PDB_ins_code);
-        values.push(params.pdbx_PDB_ins_code);
-    }
+const AtomSiteTestParams: QueryParamInfo = {
+    name: 'atom_site',
+    type: QueryParamType.JSON,
+    description: 'Object or array of objects describing atom properties. Name are same as in wwPDB mmCIF dictionary of the atom_site category.',
+    exampleValues: [`{ label_comp_id: 'ALA' }`, `{ label_seq_id: 123, label_asym_id: 'A' }`]
+};
 
-    switch (props.length) {
-        case 0: return void 0;
-        case 1: return ctx => props[0](ctx.element) === values[0];
-        case 2: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1];
-        case 3: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1] && props[2](ctx.element) === values[2];
-        default: {
-            const len = props.length;
-            return ctx => {
-                for (let i = 0; i < len; i++) if (!props[i](ctx.element) !== values[i]) return false;
-                return true;
-            };
+const RadiusParam: QueryParamInfo = {
+    name: 'radius',
+    type: QueryParamType.Float,
+    defaultValue: 5,
+    exampleValues: ['5'],
+    description: 'Value in Angstroms.',
+    validation(v: any) {
+        if (v < 1 || v > 10) {
+            throw `Invalid radius for residue interaction query (must be a value between 1 and 10).`;
         }
     }
-}
-
-// function buildResiduesQuery(params: any): Query.Provider {
-//     return Queries.generators.atoms({ entityTest: entityTest(params), chainTest: chainTest(params), residueTest: residueTest(params) });
-// }
+};
 
 const QueryMap: { [id: string]: Partial<QueryDefinition> } = {
     'full': { niceName: 'Full Structure', query: () => Queries.generators.all, description: 'The full structure.' },
+    'atoms': {
+        niceName: 'Atoms',
+        description: 'Atoms satisfying the given criteria.',
+        query: p => Queries.combinators.merge(getAtomsTests(p.atom_site).map(test => Queries.generators.atoms(test))),
+        params: [ AtomSiteTestParams ]
+    },
+    'symmetryMates': {
+        niceName: 'Symmetry Mates',
+        description: 'Computes crystal symmetry mates within the specified radius.',
+        query: () => Queries.generators.all,
+        structureTransform(p, s) {
+            return StructureSymmetry.builderSymmetryMates(s, p.radius).run();
+        },
+    },
+    'assembly': {
+        niceName: 'Assembly',
+        description: 'Computes structural assembly.',
+        query: () => Queries.generators.all,
+        structureTransform(p, s) {
+            return StructureSymmetry.buildAssembly(s, '' + p.name).run();
+        },
+        params: [{
+            name: 'name',
+            type: QueryParamType.String,
+            defaultValue: '1',
+            exampleValues: ['1'],
+            description: 'Assembly name.'
+        }]
+    },
     'residueInteraction': {
-        niceName: 'Residues Inside a Sphere',
-        description: 'Identifies all residues within the given radius from the source residue.',
+        niceName: 'Residue Interaction',
+        description: 'Identifies all residues within the given radius from the source residue. Takes crystal symmetry into account.',
         query(p) {
-            const center = Queries.generators.atoms({ entityTest: entityTest1_555(p), chainTest: chainTest(p), residueTest: residueTest(p) });
+            const tests = getAtomsTests(p.atom_site);
+            const center = Queries.combinators.merge(tests.map(test => Queries.generators.atoms({
+                ...test,
+                entityTest: test.entityTest
+                    ? ctx => test.entityTest!(ctx) && ctx.element.unit.conformation.operator.isIdentity
+                    : ctx => ctx.element.unit.conformation.operator.isIdentity
+            })));
             return Queries.modifiers.includeSurroundings(center, { radius: p.radius, wholeResidues: true });
         },
         structureTransform(p, s) {
             return StructureSymmetry.builderSymmetryMates(s, p.radius).run();
         },
-        params: [
-            AtomSiteParameters.entity_id,
-            AtomSiteParameters.label_asym_id,
-            AtomSiteParameters.auth_asym_id,
-            AtomSiteParameters.label_comp_id,
-            AtomSiteParameters.auth_comp_id,
-            AtomSiteParameters.pdbx_PDB_ins_code,
-            AtomSiteParameters.label_seq_id,
-            AtomSiteParameters.auth_seq_id,
-            {
-                name: 'radius',
-                type: QueryParamType.Float,
-                defaultValue: 5,
-                exampleValue: '5',
-                description: 'Value in Angstroms.',
-                validation(v: any) {
-                    if (v < 1 || v > 10) {
-                        throw `Invalid radius for residue interaction query (must be a value between 1 and 10).`;
-                    }
-                }
-            },
-        ]
+        params: [ AtomSiteTestParams, RadiusParam ]
     },
 };
 
@@ -173,30 +138,32 @@ export const QueryList = (function () {
     }
 })();
 
-function _normalizeQueryParams(params: { [p: string]: string }, paramList: QueryParamInfo[]): { [p: string]: string | number | boolean } {
-    const ret: any = {};
-    for (const p of paramList) {
-        const key = p.name;
-        const value = params[key];
-        if (typeof value === 'undefined' || (typeof value !== 'undefined' && value !== null && value['length'] === 0)) {
-            if (p.required) {
-                throw `The parameter '${key}' is required.`;
-            }
-            if (typeof p.defaultValue !== 'undefined') ret[key] = p.defaultValue;
-        } else {
-            switch (p.type) {
-                case QueryParamType.String: ret[key] = value; break;
-                case QueryParamType.Integer: ret[key] = parseInt(value); break;
-                case QueryParamType.Float: ret[key] = parseFloat(value); break;
-            }
-
-            if (p.validation) p.validation(ret[key]);
-        }
-    }
-
-    return ret;
-}
+// function _normalizeQueryParams(params: { [p: string]: string }, paramList: QueryParamInfo[]): { [p: string]: string | number | boolean } {
+//     const ret: any = {};
+//     for (const p of paramList) {
+//         const key = p.name;
+//         const value = params[key];
+//         if (typeof value === 'undefined' || (typeof value !== 'undefined' && value !== null && value['length'] === 0)) {
+//             if (p.required) {
+//                 throw `The parameter '${key}' is required.`;
+//             }
+//             if (typeof p.defaultValue !== 'undefined') ret[key] = p.defaultValue;
+//         } else {
+//             switch (p.type) {
+//                 case QueryParamType.JSON: ret[key] = JSON.parse(value); break;
+//                 case QueryParamType.String: ret[key] = value; break;
+//                 case QueryParamType.Integer: ret[key] = parseInt(value); break;
+//                 case QueryParamType.Float: ret[key] = parseFloat(value); break;
+//             }
+
+//             if (p.validation) p.validation(ret[key]);
+//         }
+//     }
+
+//     return ret;
+// }
 
 export function normalizeQueryParams(query: QueryDefinition, params: any) {
-    return _normalizeQueryParams(params, query.params);
+    return params;
+    //return _normalizeQueryParams(params, query.params);
 }

+ 1 - 1
src/servers/model/server/query.ts

@@ -137,7 +137,7 @@ const _model_server_params: CifWriter.Category<Job> = {
     instance(job) {
         const params: string[][] = [];
         for (const k of Object.keys(job.normalizedParams)) {
-            params.push([k, '' + job.normalizedParams[k]]);
+            params.push([k, JSON.stringify(job.normalizedParams[k])]);
         }
         return {
             data: params,

+ 12 - 1
src/servers/model/server/structure-wrapper.ts

@@ -107,7 +107,10 @@ async function readStructure(key: string, sourceId: string, entryId: string) {
     perf.end('createModel');
 
     perf.start('attachProps');
-    await attachModelProperties(models[0]);
+    const modelProps = attachModelProperties(models[0]);
+    for (const p of modelProps) {
+        await tryAttach(key, p);
+    }
     perf.end('attachProps');
 
     const structure = Structure.ofModel(models[0]);
@@ -128,4 +131,12 @@ async function readStructure(key: string, sourceId: string, entryId: string) {
     };
 
     return ret;
+}
+
+async function tryAttach(key: string, promise: Promise<any>) {
+    try {
+        await promise;
+    } catch (e) {
+        ConsoleLogger.errorId(key, 'Custom prop:' + e);
+    }
 }