UNPKG

3dmol

Version:

JavaScript/TypeScript molecular visualization library

630 lines (560 loc) 21.1 kB
//a collection of miscellaneous utility functions import { GLViewer } from "GLViewer"; import { getGradient, Gradient, GradientType } from "./Gradient"; import { VolumeData } from "./VolumeData"; import { builtinColorSchemes, CC, elementColors, htmlColors, Color } from "./colors"; import { IsoSurfaceSpec } from "GLShape"; import { inflate, InflateFunctionOptions, Data } from "pako" //simplified version of jquery extend export function extend(obj1, src1) { for (var key in src1) { if (src1.hasOwnProperty(key) && src1[key] !== undefined) { obj1[key] = src1[key]; } } return obj1; }; //deep copy, cannot deal with circular refs; undefined input becomes an empty object //https://medium.com/javascript-in-plain-english/how-to-deep-copy-objects-and-arrays-in-javascript-7c911359b089 export function deepCopy(inObject) { let outObject, value, key; if (inObject == undefined) { return {}; } if (typeof inObject !== "object" || inObject === null) { return inObject; // Return the value if inObject is not an object } // Create an array or object to hold the values outObject = Array.isArray(inObject) ? [] : {}; for (key in inObject) { value = inObject[key]; // Recursively (deep) copy for nested objects, including arrays outObject[key] = deepCopy(value); } return outObject; }; export function isNumeric(obj) { var type = typeof (obj); return (type === "number" || type === "string") && !isNaN(obj - parseFloat(obj)); }; export function isEmptyObject(obj: object) { return Object.keys(obj).length === 0; }; export type Func = Function|string|undefined|null; export function makeFunction(callback:Func): Function { //for py3dmol let users provide callback as string if (callback && typeof callback === "string") { /* jshint ignore:start */ callback = eval("(" + callback + ")"); /* jshint ignore:end */ } // report to console if callback is not a valid function if (callback && typeof callback != "function") { console.warn("Invalid callback provided."); return ()=>{}; //return noop function } return callback as Function; }; //standardize voldata/volscheme in style export function adjustVolumeStyle(style: IsoSurfaceSpec) { if (style) { if (style.volformat && !(style.voldata instanceof VolumeData)) { style.voldata = new VolumeData(style.voldata, style.volformat); } if (style.volscheme) { style.volscheme = Gradient.getGradient(style.volscheme); } } }; /* * computes the bounding box around the provided atoms * @param {AtomSpec[]} atomlist * @return {Array} */ export function getExtent(atomlist, ignoreSymmetries?) { var xmin, ymin, zmin, xmax, ymax, zmax, xsum, ysum, zsum, cnt; var includeSym = !ignoreSymmetries; xmin = ymin = zmin = 9999; xmax = ymax = zmax = -9999; xsum = ysum = zsum = cnt = 0; if (atomlist.length === 0) return [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; for (var i = 0; i < atomlist.length; i++) { var atom = atomlist[i]; if (typeof atom === 'undefined' || !isFinite(atom.x) || !isFinite(atom.y) || !isFinite(atom.z)) continue; cnt++; xsum += atom.x; ysum += atom.y; zsum += atom.z; xmin = (xmin < atom.x) ? xmin : atom.x; ymin = (ymin < atom.y) ? ymin : atom.y; zmin = (zmin < atom.z) ? zmin : atom.z; xmax = (xmax > atom.x) ? xmax : atom.x; ymax = (ymax > atom.y) ? ymax : atom.y; zmax = (zmax > atom.z) ? zmax : atom.z; if (atom.symmetries && includeSym) { for (var n = 0; n < atom.symmetries.length; n++) { cnt++; xsum += atom.symmetries[n].x; ysum += atom.symmetries[n].y; zsum += atom.symmetries[n].z; xmin = (xmin < atom.symmetries[n].x) ? xmin : atom.symmetries[n].x; ymin = (ymin < atom.symmetries[n].y) ? ymin : atom.symmetries[n].y; zmin = (zmin < atom.symmetries[n].z) ? zmin : atom.symmetries[n].z; xmax = (xmax > atom.symmetries[n].x) ? xmax : atom.symmetries[n].x; ymax = (ymax > atom.symmetries[n].y) ? ymax : atom.symmetries[n].y; zmax = (zmax > atom.symmetries[n].z) ? zmax : atom.symmetries[n].z; } } } return [[xmin, ymin, zmin], [xmax, ymax, zmax], [xsum / cnt, ysum / cnt, zsum / cnt]]; }; /* get the min and max values of the specified property in the provided * @function $3Dmol.getPropertyRange * @param {AtomSpec[]} atomlist - list of atoms to evaluate * @param {string} prop - name of property * @return {Array} - [min, max] values */ export function getPropertyRange(atomlist, prop) { var min = Number.POSITIVE_INFINITY; var max = Number.NEGATIVE_INFINITY; for (var i = 0, n = atomlist.length; i < n; i++) { var atom = atomlist[i]; var val = getAtomProperty(atom, prop); if (val != null) { if (val < min) min = val; if (val > max) max = val; } } if (!isFinite(min) && !isFinite(max)) min = max = 0; else if (!isFinite(min)) min = max; else if (!isFinite(max)) max = min; return [min, max]; }; //adapted from https://stackoverflow.com/questions/3969475/javascript-pause-settimeout export class PausableTimer { ident: any; total_time_run = 0; start_time: number; countdown: number; fn: any; arg: any; constructor(fn, countdown, arg?) { this.fn = fn; this.arg = arg; this.countdown = countdown; this.start_time = new Date().getTime(); this.ident = setTimeout(fn, countdown, arg); } cancel() { clearTimeout(this.ident); } pause() { clearTimeout(this.ident); this.total_time_run = new Date().getTime() - this.start_time; } resume() { this.ident = setTimeout(this.fn, Math.max(0, this.countdown - this.total_time_run), this.arg); } }; /* * Convert a base64 encoded string to a Uint8Array * @param {string} base64 encoded string */ export function base64ToArray(base64) { var binary_string = window.atob(base64); var len = binary_string.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes; }; //return the value of an atom property prop, or null if non existent // looks first in properties, then in the atom itself export function getAtomProperty(atom, prop) { var val = null; if (atom.properties && typeof (atom.properties[prop]) != "undefined") { val = atom.properties[prop]; } else if (typeof (atom[prop]) != 'undefined') { val = atom[prop]; } return val; }; //Miscellaneous functions and classes - to be incorporated into $3Dmol proper /* * * @param {$3Dmol.Geometry} geometry * @param {$3Dmol.Mesh} mesh * @returns {undefined} */ export function mergeGeos(geometry, mesh) { var meshGeo = mesh.geometry; if (meshGeo === undefined) return; geometry.geometryGroups.push(meshGeo.geometryGroups[0]); }; /* * Parse a string that represents a style or atom selection and convert it * into an object. The goal is to make it easier to write out these specifications * without resorting to json. Objects cannot be defined recursively. * ; - delineates fields of the object * : - if the field has a value other than an empty object, it comes after a colon * , - delineates key/value pairs of a value object * If the value object consists of ONLY keys (no = present) the keys are * converted to a list. Otherwise a object of key/value pairs is created with * any missing values set to null * = OR ~ - separates key/value pairs of a value object, if not provided value is null * twiddle is supported since = has special meaning in URLs * @param (String) str * @returns {Object} */ export function specStringToObject(str) { if (typeof (str) === "object") { return str; //not string, assume was converted already } else if (typeof (str) === "undefined" || str == null) { return str; } //if this is a json string, parse it directly try { let parsed = JSON.parse(str); return parsed; } catch (error) { } str = str.replace(/%7E/g, '~'); //copy/pasting urls sometimes does this //convert things that look like numbers into numbers var massage = function (val) { if (isNumeric(val)) { //hexadecimal does not parse as float if (Math.floor(parseFloat(val)) == parseInt(val)) { return parseFloat(val); } else if (val.indexOf('.') >= 0) { return parseFloat(val); // ".7" for example, does not parseInt } else { return parseInt(val); } } //boolean conversions else if (val === 'true') { return true; } else if (val === 'false') { return false; } return val; }; var ret = {}; if (str === 'all') return ret; var fields = str.split(';'); for (var i = 0; i < fields.length; i++) { var fv = fields[i].split(':'); var f = fv[0]; var val = {}; var vstr = fv[1]; if (vstr) { vstr = vstr.replace(/~/g, "="); if (vstr.indexOf('=') !== -1) { //has key=value pairs, must be object var kvs = vstr.split(','); for (var j = 0; j < kvs.length; j++) { var kv = kvs[j].split('=', 2); val[kv[0]] = massage(kv[1]); } } else if (vstr.indexOf(',') !== -1) { //has multiple values, must list val = vstr.split(','); } else { val = massage(vstr); //value itself } } ret[f] = val; } return ret; }; function checkStatus(response) { if (!response.ok) { throw new Error(`HTTP ${response.status} - ${response.statusText}`); } return response; } /** * Fetch data from URL * * @param uri URL * @param callback Function to call with data */ export function get(uri, callback?) { var promise = fetch(uri).then(checkStatus).then((response) => response.text()); if (callback) return promise.then(callback); else return promise; } /** * Download binary data (e.g. a gzipped file) into an array buffer and provide * arraybuffer to callback. * @param {string} uri - location of data * @param {Function} [callback] - Function to call with arraybuffer as argument. * @param {string} [request] - type of request * @param {string} [postdata] - data for POST request * @return {Promise} */ export function getbin(uri, callback?, request?, postdata?) { var promise; if (request == "POST") { promise = fetch(uri, { method: 'POST', body: postdata }) .then((response) => checkStatus(response)) .then((response) => response.arrayBuffer()); } else { promise = fetch(uri).then((response) => checkStatus(response)) .then((response) => response.arrayBuffer()); } if (callback) return promise.then(callback); else return promise; }; /** * Load a PDB/PubChem structure into existing viewer. Automatically calls 'zoomTo' and 'render' on viewer after loading model * @param {string} query - String specifying pdb or pubchem id; must be prefaced with "pdb: " or "cid: ", respectively * @param {GLViewer} viewer - Add new model to existing viewer * @param {Object} options - Specify additional options * format: file format to download, if multiple are available, default format is pdb * pdbUri: URI to retrieve PDB files, default URI is http://www.rcsb.org/pdb/files/ * @param {Function} [callback] - Function to call with model as argument after data is loaded. * @return {GLModel} GLModel, Promise if callback is not provided * @example viewer.setBackgroundColor(0xffffffff); $3Dmol.download('pdb:2nbd',viewer,{onemol: true,multimodel: true},function(m) { m.setStyle({'cartoon':{colorscheme:{prop:'ss',map:$3Dmol.ssColors.Jmol}}}); viewer.zoomTo(); viewer.render(callback); }); */ export function download(query, viewer: GLViewer, options, callback?) { var type = ""; var pdbUri = ""; var uri = ""; var promise = null; var m = viewer.addModel(); if (query.indexOf(':') < 0) { //no type specifier, guess if (query.length == 4) { query = 'pdb:' + query; } else if (!isNaN(query)) { query = 'cid:' + query; } else { query = 'url:' + query; } } if (query.substring(0,5) == 'mmtf:') { console.warn('WARNING: MMTF now deprecated. Reverting to bcif.'); query = 'bcif:' + query.slice(5); } if (query.substring(0, 5) === 'bcif:') { query = query.substring(5).toUpperCase(); uri = "https://models.rcsb.org/" + query + '.bcif.gz'; if (options && typeof options.noComputeSecondaryStructure === 'undefined') { //when fetch directly from pdb, trust structure annotations options.noComputeSecondaryStructure = true; } promise = new Promise(function (resolve) { getbin(uri) .then(function (ret) { m.addMolData(ret, 'bcif.gz', options); viewer.zoomTo(); viewer.render(); resolve(m); }, function () { console.error("fetch of " + uri + " failed."); }); }); } else { if (query.substring(0, 4) === 'pdb:') { type = 'bcif'; if (options && options.format) { type = options.format; //can override and require pdb } if (options && typeof options.noComputeSecondaryStructure === 'undefined') { //when fetch directly from pdb, trust structure annotations options.noComputeSecondaryStructure = true; } query = query.substring(4).toUpperCase(); if (!query.match(/^[1-9][A-Za-z0-9]{3}$/)) { alert("Wrong PDB ID"); return; } if (type == 'bcif') { uri = 'https://models.rcsb.org/' + query.toUpperCase() + '.bcif.gz'; } else { pdbUri = options && options.pdbUri ? options.pdbUri : "https://files.rcsb.org/view/"; uri = pdbUri + query + "." + type; } } else if (query.substring(0, 4) == 'cid:') { type = "sdf"; query = query.substring(4); if (!query.match(/^[0-9]+$/)) { alert("Wrong Compound ID"); return; } uri = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/" + query + "/SDF?record_type=3d"; } else if (query.substring(0, 4) == 'url:') { uri = query.substring(4); type = uri; } var handler = function (ret) { m.addMolData(ret, type, options); viewer.zoomTo(); viewer.render(); }; promise = new Promise(function (resolve) { if (type == 'bcif') { //binary data getbin(uri) .then(function (ret) { handler(ret); resolve(m); }).catch(function () { //if mmtf server is being annoying, fallback to text pdbUri = options && options.pdbUri ? options.pdbUri : "https://files.rcsb.org/view/"; uri = pdbUri + query + ".pdb"; type = "pdb"; console.warn("falling back to pdb format"); get(uri).then(function (data) { handler(data); resolve(m); }).catch(function (e) { handler(""); resolve(m); console.error("fetch of " + uri + " failed: " + e.statusText); }); }); //an error msg has already been printed } else { get(uri).then(function (data) { handler(data); resolve(m); }).catch(function (e) { handler(""); resolve(m); console.error("fetch of " + uri + " failed: " + e.statusText); }); } }); } if (callback) { promise.then(function (m) { callback(m); }); return m; } else return promise; }; /** Return proper color for atom given style * @param {AtomSpec} atom * @param {AtomStyle} style * @return {Color} */ export function getColorFromStyle(atom, style): Color { let scheme = style.colorscheme; if (typeof builtinColorSchemes[scheme] != "undefined") { scheme = builtinColorSchemes[scheme]; } else if (typeof scheme == "string" && scheme.endsWith("Carbon")) { //any color you want of carbon let ccolor = scheme .substring(0, scheme.lastIndexOf("Carbon")) .toLowerCase(); if (typeof htmlColors[ccolor] != "undefined") { let newscheme = { ...elementColors.defaultColors }; newscheme.C = htmlColors[ccolor]; builtinColorSchemes[scheme] = { prop: "elem", map: newscheme }; scheme = builtinColorSchemes[scheme]; } } let color = atom.color; if (typeof style.color != "undefined" && style.color != "spectrum") color = style.color; if (typeof scheme != "undefined") { let prop, val; if (typeof elementColors[scheme] != "undefined") { //name of builtin colorscheme scheme = elementColors[scheme]; if (typeof scheme[atom[scheme.prop]] != "undefined") { color = scheme.map[atom[scheme.prop]]; } } else if (typeof scheme[atom[scheme.prop]] != "undefined") { //actual color scheme provided color = scheme.map[atom[scheme.prop]]; } else if ( typeof scheme.prop != "undefined" && typeof scheme.gradient != "undefined" ) { //apply a property mapping prop = scheme.prop; var grad = scheme.gradient; //redefining scheme if(!(grad instanceof GradientType)) { grad = getGradient(scheme); } let range = grad.range() || [-1, 1]; //sensible default val = getAtomProperty(atom, prop); if (val != null) { color = grad.valueToHex(val, range); } } else if ( typeof scheme.prop != "undefined" && typeof scheme.map != "undefined" ) { //apply a discrete property mapping prop = scheme.prop; val = getAtomProperty(atom, prop); if (typeof scheme.map[val] != "undefined") { color = scheme.map[val]; } } else if (typeof style.colorscheme[atom.elem] != "undefined") { //actual color scheme provided color = style.colorscheme[atom.elem]; } else { console.warn("Could not interpret colorscheme " + scheme); } } else if (typeof style.colorfunc != "undefined") { //this is a user provided function for turning an atom into a color color = style.colorfunc(atom); } let C = CC.color(color); return C; }; //given a string selector, element, or jquery object, return the HTMLElement export function getElement(element): HTMLElement | null { let ret = element; if (typeof (element) === "string") { ret = document.querySelector("#" + element); } else if (typeof element === 'object' && element.get) { //jquery ret = element.get(0); } return ret; } export function inflateString(str: string | ArrayBuffer, tostring: Boolean = true): (string | ArrayBuffer) { let data: Data; if (typeof str === 'string') { const encoder = new TextEncoder(); data = encoder.encode(str); } else { data = new Uint8Array(str); } const inflatedData = inflate(data, { to: tostring ? 'string' : null } as InflateFunctionOptions & { to: 'string' }); return inflatedData; }