//a collection of miscellaneous utility functions
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) {
var name;
for (name in obj) {
return false;
}
return true;
};
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, 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;
}