uglymol
Version:
Macromolecular Viewer for Crystallographers
1,527 lines (1,428 loc) • 55.4 kB
JavaScript
// @flow
import { OrthographicCamera, Scene, AmbientLight, Color, Vector3,
Ray, WebGLRenderer, Fog } from './fromthree.js';
import { makeLineMaterial, makeLineSegments, makeLine, makeRibbon,
makeChickenWire, makeGrid, makeSticks, makeBalls, makeWheels, makeCube,
makeRgbBox, makeLabel, addXyzCross } from './draw.js';
import { STATE, Controls } from './controls.js';
import { ElMap } from './elmap.js';
import { modelsFromPDB } from './model.js';
/*::
import type {AtomT, Model} from './model.js'
import type {Mesh} from './fromthree.js'
type ColorScheme = {
name: string,
bg: number,
fg: number,
[name:string]: number | number[],
};
type Num2 = [number, number]
type Num3 = [number, number, number];
*/
const ColorSchemes /*:ColorScheme[]*/ = [ // Viewer.prototype.ColorSchemes
{ // generally mimicks Coot
name: 'coot dark',
bg: 0x000000,
fg: 0xFFFFFF,
map_den: 0x3362B2,
map_pos: 0x298029,
map_neg: 0x8B2E2E,
center: 0xC997B0,
// atoms
H: 0x858585, // H is normally invisible
// C, N and O are taken approximately (by color-picker) from coot
C: 0xb3b300,
N: 0x7EAAFB,
O: 0xF24984,
S: 0x40ff40, // S in coot is too similar to C, here it is greener
// Coot doesn't define other colors (?)
MG: 0xc0c0c0,
P: 0xffc040,
CL: 0xa0ff60,
CA: 0xffffff,
MN: 0xff90c0,
FE: 0xa03000,
NI: 0x00ff80,
def: 0xa0a0a0, // default atom color
},
// scheme made of "solarized" colors (http://ethanschoonover.com/solarized):
// base03 base02 base01 base00 base0 base1 base2 base3
// #002b36 #073642 #586e75 #657b83 #839496 #93a1a1 #eee8d5 #fdf6e3
// yellow orange red magenta violet blue cyan green
// #b58900 #cb4b16 #dc322f #d33682 #6c71c4 #268bd2 #2aa198 #859900
{
name: 'solarized dark',
bg: 0x002b36,
fg: 0xfdf6e3,
map_den: 0x268bd2,
map_pos: 0x859900,
map_neg: 0xd33682,
center: 0xfdf6e3,
H: 0x586e75,
C: 0x93a1a1,
N: 0x6c71c4,
O: 0xcb4b16,
S: 0xb58900,
def: 0xeee8d5,
},
{
name: 'solarized light',
bg: 0xfdf6e3,
fg: 0x002b36,
map_den: 0x268bd2,
map_pos: 0x859900,
map_neg: 0xd33682,
center: 0x002b36,
H: 0x93a1a1,
C: 0x586e75,
N: 0x6c71c4,
O: 0xcb4b16,
S: 0xb58900,
def: 0x073642,
},
{ // like in Coot after Edit > Background Color > White
name: 'coot light',
bg: 0xFFFFFF,
fg: 0x000000,
map_den: 0x3362B2,
map_pos: 0x298029,
map_neg: 0x8B2E2E,
center: 0xC7C769,
H: 0x999999,
C: 0xA96464,
N: 0x1C51B3,
O: 0xC33869,
S: 0x9E7B3D,
def: 0x808080,
},
];
const INIT_HUD_TEXT = 'This is UglyMol not Coot. ' +
'<a href="#" onclick="V.toggle_help(); return false;">H shows help.</a>';
// options handled by select_next()
const COLOR_PROPS = ['element', 'B-factor', 'occupancy', 'index', 'chain'];
const RENDER_STYLES = ['lines', 'trace', 'ribbon', 'ball&stick'];
const LIGAND_STYLES = ['ball&stick', 'lines'];
const MAP_STYLES = ['marching cubes', 'squarish'/*, 'snapped MC'*/];
const LINE_STYLES = ['normal', 'simplistic'];
const LABEL_FONTS = ['bold 14px', '14px', '16px', 'bold 16px'];
function rainbow_value(v/*:number*/, vmin/*:number*/, vmax/*:number*/) {
let c = new Color(0xe0e0e0);
if (vmin < vmax) {
const ratio = (v - vmin) / (vmax - vmin);
const hue = (240 - (240 * ratio)) / 360;
c.setHSL(hue, 1.0, 0.5);
}
return c;
}
function color_by(prop, atoms /*:AtomT[]*/, elem_colors, hue_shift) {
let color_func;
const last_atom = atoms[atoms.length-1];
if (prop === 'index') {
color_func = function (atom) {
return rainbow_value(atom.i_seq, 0, last_atom.i_seq);
};
} else if (prop === 'B-factor') {
let vmin = Infinity;
let vmax = -Infinity;
for (let i = 0; i < atoms.length; i++) {
const v = atoms[i].b;
if (v > vmax) vmax = v;
if (v < vmin) vmin = v;
}
//console.log('B-factors in [' + vmin + ', ' + vmax + ']');
color_func = function (atom) {
return rainbow_value(atom.b, vmin, vmax);
};
} else if (prop === 'occupancy') {
color_func = function (atom) {
return rainbow_value(atom.occ, 0, 1);
};
} else if (prop === 'chain') {
color_func = function (atom) {
return rainbow_value(atom.chain_index, 0, last_atom.chain_index);
};
} else { // element
if (hue_shift === 0) {
color_func = function (atom) {
return elem_colors[atom.element] || elem_colors.def;
};
} else {
const c_hsl = elem_colors['C'].getHSL();
const c_col = new Color(0, 0, 0);
c_col.setHSL(c_hsl.h + hue_shift, c_hsl.s, c_hsl.l);
color_func = function (atom) {
const el = atom.element;
return el === 'C' ? c_col : (elem_colors[el] || elem_colors.def);
};
}
}
return atoms.map(color_func);
}
function scale_by_height(value, size) { // for scaling bond_line
return value * size[1] / 700;
}
class MapBag {
/*::
map: ElMap
name: string
isolevel: number
visible: boolean
types: string[]
block_ctr: Vector3
el_objects: Object[]
*/
constructor(map/*:ElMap*/, config/*:Object*/, is_diff_map/*:boolean*/) {
this.map = map;
this.name = '';
this.isolevel = is_diff_map ? 3.0 : config.default_isolevel;
this.visible = true;
this.types = is_diff_map ? ['map_pos', 'map_neg'] : ['map_den'];
this.block_ctr = new Vector3(Infinity, 0, 0);
this.el_objects = []; // three.js objects
}
}
class ModelBag {
/*::
model: Model
label: string
visible: boolean
hue_shift: number
conf: Object
win_size: Num2
objects: Object[]
atom_array: AtomT[]
static ctor_counter: number
*/
constructor(model/*:Model*/, config/*:Object*/, win_size/*:Num2*/) {
this.model = model;
this.label = '(model #' + ++ModelBag.ctor_counter + ')';
this.visible = true;
this.hue_shift = 0;
this.conf = config;
this.win_size = win_size;
this.objects = []; // list of three.js objects
this.atom_array = [];
}
get_visible_atoms() {
const atoms = this.model.atoms;
if (this.conf.hydrogens || !this.model.has_hydrogens) {
return atoms;
}
// with filter() it's twice slower (on Node 4.2)
//return atoms.filter(function(a) { return a.element !== 'H'; });
const non_h = [];
for (const atom of atoms) {
if (atom.element !== 'H') non_h.push(atom);
}
return non_h;
}
add_bonds(polymers/*:boolean*/, ligands/*:boolean*/, ball_size/*:?number*/) {
const visible_atoms = this.get_visible_atoms();
const colors = color_by(this.conf.color_prop, visible_atoms,
this.conf.colors, this.hue_shift);
let vertex_arr /*:Vector3[]*/ = [];
let color_arr = [];
const hydrogens = this.conf.hydrogens;
for (let i = 0; i < visible_atoms.length; i++) {
const atom = visible_atoms[i];
const color = colors[i];
if (!(atom.is_ligand ? ligands : polymers)) continue;
if (atom.bonds.length === 0 && ball_size == null) { // nonbonded - cross
addXyzCross(vertex_arr, atom.xyz, 0.7);
for (let n = 0; n < 6; n++) {
color_arr.push(color);
}
} else { // bonded, draw lines
for (let j = 0; j < atom.bonds.length; j++) {
const other = this.model.atoms[atom.bonds[j]];
if (!hydrogens && other.element === 'H') continue;
// Coot show X-H bonds as thinner lines in a single color.
// Here we keep it simple and render such bonds like all others.
const mid = atom.midpoint(other);
vertex_arr.push(atom.xyz, mid);
color_arr.push(color, color);
}
}
}
if (vertex_arr.length === 0) return;
let sphere_arr = visible_atoms;
let sphere_color_arr = colors;
if (!polymers || !ligands) {
sphere_arr = [];
sphere_color_arr = [];
for (let i = 0; i < visible_atoms.length; i++) {
if (visible_atoms[i].is_ligand ? ligands : polymers) {
sphere_arr.push(visible_atoms[i]);
sphere_color_arr.push(colors[i]);
}
}
}
const linewidth = scale_by_height(this.conf.bond_line, this.win_size);
if (ball_size != null) {
this.objects.push(makeSticks(vertex_arr, color_arr, ball_size / 2));
this.objects.push(makeBalls(sphere_arr, sphere_color_arr, ball_size));
} else {
const material = makeLineMaterial({
linewidth: linewidth,
win_size: this.win_size,
segments: true,
});
this.objects.push(makeLineSegments(material, vertex_arr, color_arr));
if (this.conf.line_style !== 'simplistic') {
// wheels (discs) as round caps
this.objects.push(makeWheels(sphere_arr, sphere_color_arr, linewidth));
}
}
sphere_arr.forEach(function (v) { this.atom_array.push(v); }, this);
}
add_trace() {
const segments = this.model.extract_trace();
const visible_atoms = [].concat.apply([], segments);
const colors = color_by(this.conf.color_prop, visible_atoms,
this.conf.colors, this.hue_shift);
const material = makeLineMaterial({
linewidth: scale_by_height(this.conf.bond_line, this.win_size),
win_size: this.win_size,
});
let k = 0;
for (const seg of segments) {
const color_slice = colors.slice(k, k + seg.length);
k += seg.length;
let pos = [];
for (const atom of seg) {
pos.push(atom.xyz);
}
const line = makeLine(material, pos, color_slice);
this.objects.push(line);
}
this.atom_array = visible_atoms;
}
add_ribbon(smoothness/*:number*/) {
const segments = this.model.extract_trace();
const res_map = this.model.get_residues();
const visible_atoms = [].concat.apply([], segments);
const colors = color_by(this.conf.color_prop, visible_atoms,
this.conf.colors, this.hue_shift);
let k = 0;
for (const seg of segments) {
let tangents = [];
let last = [0, 0, 0];
for (const atom of seg) {
const residue = res_map[atom.resid()];
const tang = this.model.calculate_tangent_vector(residue);
// untwisting (usually applies to beta-strands)
if (tang[0]*last[0] + tang[1]*last[1] + tang[2]*last[2] < 0) {
tang[0] = -tang[0];
tang[1] = -tang[1];
tang[2] = -tang[2];
}
tangents.push(tang);
last = tang;
}
const color_slice = colors.slice(k, k + seg.length);
k += seg.length;
const obj = makeRibbon(seg, color_slice, tangents, smoothness);
this.objects.push(obj);
}
}
}
ModelBag.ctor_counter = 0;
function vec3_to_fixed(vec, n) {
return [vec.x.toFixed(n), vec.y.toFixed(n), vec.z.toFixed(n)];
}
// for two-finger touch events
function touch_info(evt/*:TouchEvent*/) {
const touches = evt.touches;
const dx = touches[0].pageX - touches[1].pageX;
const dy = touches[0].pageY - touches[1].pageY;
return {pageX: (touches[0].pageX + touches[1].pageX) / 2,
pageY: (touches[0].pageY + touches[1].pageY) / 2,
dist: Math.sqrt(dx * dx + dy * dy)};
}
// makes sense only for full-window viewer
function parse_url_fragment() {
let ret = {};
if (typeof window === 'undefined') return ret;
const params = window.location.hash.substr(1).split('&');
for (let i = 0; i < params.length; i++) {
const kv = params[i].split('=');
let val = kv[1];
if (kv[0] === 'xyz' || kv[0] === 'eye') {
val = val.split(',').map(Number);
} else if (kv[0] === 'zoom') {
val = Number(val);
}
ret[kv[0]] = val;
}
return ret;
}
export class Viewer {
/*::
model_bags: ModelBag[]
map_bags: MapBag[]
decor: {cell_box: ?Object , selection: ?Object, zoom_grid: Object,
mark: ?Object}
labels: {[id:string]: {o: Mesh, bag: ModelBag}}
nav: ?Object
xhr_headers: {[id:string]: string}
config: Object
window_size: Num2
window_offset: Num2
last_ctr: Vector3
selected: {bag: ?ModelBag, atom: ?AtomT}
dbl_click_callback: (Object) => void
scene: Scene
light: AmbientLight
default_camera_pos: Num3
target: Vector3
camera: OrthographicCamera
controls: Controls
tied_viewer: ?Viewer
renderer: WebGLRenderer
container: ?HTMLElement
hud_el: ?HTMLElement
help_el: ?HTMLElement
initial_hud_html: string
scheduled: boolean
ColorSchemes: ColorScheme[]
MOUSE_HELP: string
KEYBOARD_HELP: string
ABOUT_HELP: string
mousemove: (MouseEvent) => void
mouseup: (MouseEvent) => void
key_bindings: Array<Function|false>
*/
constructor(options /*: {[key: string]: any}*/) {
// rendered objects
this.model_bags = [];
this.map_bags = [];
this.decor = { cell_box: null, selection: null, zoom_grid: makeGrid(),
mark: null };
this.labels = {};
this.nav = null;
this.xhr_headers = {};
this.config = {
bond_line: 4.0, // ~ to height, like in Coot (see scale_by_height())
map_line: 1.25, // for any height
map_radius: 10.0,
max_map_radius: 40,
default_isolevel: 1.5,
center_cube_size: 0.1,
map_style: MAP_STYLES[0],
render_style: RENDER_STYLES[0],
ligand_style: LIGAND_STYLES[0],
color_prop: COLOR_PROPS[0],
line_style: LINE_STYLES[0],
label_font: LABEL_FONTS[0],
colors: this.ColorSchemes[0],
hydrogens: false,
ball_size: 0.4,
};
// options of the constructor overwrite default values of the config
for (let o of options) {
if (o in this.config) {
this.config[o] = options[o];
}
}
this.set_colors();
this.window_size = [1, 1]; // it will be set in resize()
this.window_offset = [0, 0];
this.last_ctr = new Vector3(Infinity, 0, 0);
this.selected = {bag: null, atom: null};
this.dbl_click_callback = this.toggle_label;
this.scene = new Scene();
this.scene.fog = new Fog(this.config.colors.bg, 0, 1);
this.scene.add(new AmbientLight(0xffffff));
this.default_camera_pos = [0, 0, 100];
if (options.share_view) {
this.target = options.share_view.target;
this.camera = options.share_view.camera;
this.controls = options.share_view.controls;
this.tied_viewer = options.share_view;
this.tied_viewer.tied_viewer = this; // not GC friendly
} else {
this.target = new Vector3(0, 0, 0);
this.camera = new OrthographicCamera();
this.camera.position.fromArray(this.default_camera_pos);
this.controls = new Controls(this.camera, this.target);
}
this.set_common_key_bindings();
if (this.constructor === Viewer) this.set_real_space_key_bindings();
if (typeof document === 'undefined') return; // for testing on node
function get_elem(name) {
if (options[name] === null) return null;
return document.getElementById(options[name] || name);
}
this.hud_el = get_elem('hud');
try {
this.renderer = new WebGLRenderer({antialias: true});
} catch (e) {
this.hud('No WebGL in your browser?', 'ERR');
this.renderer = null;
return;
}
this.container = get_elem('viewer');
this.help_el = get_elem('help');
if (this.hud_el) {
if (this.hud_el.innerHTML === '') this.hud_el.innerHTML = INIT_HUD_TEXT;
this.initial_hud_html = this.hud_el.innerHTML;
}
if (this.container == null) return; // can be null in headless tests
this.renderer.setClearColor(this.config.colors.bg, 1);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.resize();
this.camera.zoom = this.camera.right / 35.0; // arbitrary choice
this.update_camera();
const el = this.renderer.domElement;
// $FlowFixMe: flow can't figure out that this.container != null
this.container.appendChild(el);
if (options.focusable) {
el.tabIndex = 0;
}
this.decor.zoom_grid.visible = false;
this.scene.add(this.decor.zoom_grid);
window.addEventListener('resize', this.resize.bind(this));
let keydown_el = (options.focusable ? el : window);
keydown_el.addEventListener('keydown', this.keydown.bind(this));
el.addEventListener('contextmenu', function (e) { e.preventDefault(); });
el.addEventListener('mousewheel', this.mousewheel.bind(this));
el.addEventListener('MozMousePixelScroll', this.mousewheel.bind(this));
el.addEventListener('mousedown', this.mousedown.bind(this));
el.addEventListener('touchstart', this.touchstart.bind(this));
el.addEventListener('touchmove', this.touchmove.bind(this));
el.addEventListener('touchend', this.touchend.bind(this));
el.addEventListener('touchcancel', this.touchend.bind(this));
el.addEventListener('dblclick', this.dblclick.bind(this));
let self = this;
this.mousemove = function (event/*:MouseEvent*/) {
event.preventDefault();
//event.stopPropagation();
self.controls.move(self.relX(event), self.relY(event));
};
this.mouseup = function (event/*:MouseEvent*/) {
event.preventDefault();
event.stopPropagation();
document.removeEventListener('mousemove', self.mousemove);
document.removeEventListener('mouseup', self.mouseup);
self.decor.zoom_grid.visible = false;
const not_panned = self.controls.stop();
// special case - centering on atoms after action 'pan' with no shift
if (not_panned) {
const pick = self.pick_atom(not_panned, self.camera);
if (pick != null) {
self.select_atom(pick, {steps: 60});
}
}
self.redraw_maps();
};
this.scheduled = false;
this.request_render();
}
pick_atom(coords/*:Num2*/, camera/*:OrthographicCamera*/) {
for (const bag of this.model_bags) {
if (!bag.visible) continue;
const z = (camera.near + camera.far) / (camera.near - camera.far);
let ray = new Ray();
ray.origin.set(coords[0], coords[1], z).unproject(camera);
ray.direction.set(0, 0, -1).transformDirection(camera.matrixWorld);
let near = camera.near;
// '0.15' b/c the furthest 15% is hardly visible in the fog
let far = camera.far - 0.15 * (camera.far - camera.near);
let intersects = [];
/*
// previous version - line-based search
for (const object of bag.objects) {
if (object.visible === false) continue;
if (object.userData.bond_lines) {
line_raycast(object, {ray, near, far, precision: 0.3}, intersects);
}
}
*/
// search directly atom array ignoring matrixWorld
let vec = new Vector3();
for (const atom of bag.atom_array) {
vec.set(atom.xyz[0] - ray.origin.x,
atom.xyz[1] - ray.origin.y,
atom.xyz[2] - ray.origin.z);
let distance = vec.dot(ray.direction);
if (distance < 0 || distance < near || distance > far) continue;
let dist2 = vec.addScaledVector(ray.direction, -distance).lengthSq();
if (dist2 > 0.25) continue;
intersects.push({distance, atom, dist2});
}
if (intersects.length > 0) {
intersects.sort(function (x) { return x.dist2 || Infinity; });
//const p = intersects[0].point;
//const atom = bag.model.get_nearest_atom(p.x, p.y, p.z);
const atom = intersects[0].atom;
if (atom != null) {
return {bag, atom};
}
}
}
}
set_colors(scheme/*:?number|string|ColorScheme*/) {
function to_col(x) { return new Color(x); }
if (scheme == null) {
scheme = this.config.colors;
} else if (typeof scheme === 'number') {
scheme = this.ColorSchemes[scheme % this.ColorSchemes.length];
} else if (typeof scheme === 'string') {
for (const sc of this.ColorSchemes) {
if (sc.name === scheme) {
scheme = sc;
break;
}
}
throw Error('Unknown color scheme.');
}
if (typeof scheme.bg === 'number') {
for (const key in scheme) {
if (key !== 'name') {
scheme[key] = scheme[key] instanceof Array ? scheme[key].map(to_col)
: to_col(scheme[key]);
}
}
}
this.decor.zoom_grid.color_value.set(scheme.fg);
this.config.config = scheme;
this.redraw_all();
}
// relative position on canvas in normalized device coordinates [-1, +1]
relX(evt/*:{pageX: number}*/) {
return 2 * (evt.pageX - this.window_offset[0]) / this.window_size[0] - 1;
}
relY(evt/*:{pageY: number}*/) {
return 1 - 2 * (evt.pageY - this.window_offset[1]) / this.window_size[1];
}
hud(text/*:?string*/, type/*:?string*/) {
if (typeof document === 'undefined') return; // for testing on node
let el = this.hud_el;
if (el) {
if (text != null) {
if (type === 'HTML') {
el.innerHTML = text;
} else {
el.textContent = text;
}
} else {
el.innerHTML = this.initial_hud_html;
}
const err = (type === 'ERR');
el.style.backgroundColor = (err ? '#b00' : '');
if (err && text) console.log('ERR: ' + text);
} else {
console.log('hud:', text);
}
}
redraw_center(force/*:?boolean*/) {
const size = this.config.center_cube_size;
if (force ||
this.target.distanceToSquared(this.last_ctr) > 0.01 * size * size) {
this.last_ctr.copy(this.target);
if (this.decor.mark) {
this.scene.remove(this.decor.mark);
}
this.decor.mark = makeCube(size, this.target, {
color: this.config.colors.center,
linewidth: 2,
win_size: this.window_size,
});
this.scene.add(this.decor.mark);
}
}
redraw_maps(force/*:?boolean*/) {
this.redraw_center(force);
const r = this.config.map_radius;
for (const map_bag of this.map_bags) {
if (force || this.target.distanceToSquared(map_bag.block_ctr) > r/100) {
this.redraw_map(map_bag);
}
}
}
remove_and_dispose(obj/*:Object*/) {
this.scene.remove(obj);
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (obj.material.uniforms && obj.material.uniforms.map) {
obj.material.uniforms.map.value.dispose();
}
obj.material.dispose();
}
for (let o of obj.children) {
this.remove_and_dispose(o);
}
}
clear_el_objects(map_bag/*:MapBag*/) {
for (let o of map_bag.el_objects) {
this.remove_and_dispose(o);
}
map_bag.el_objects = [];
}
clear_model_objects(model_bag/*:ModelBag*/) {
for (let o of model_bag.objects) {
this.remove_and_dispose(o);
}
model_bag.objects = [];
}
has_frag_depth() {
return this.renderer && this.renderer.extensions.get('EXT_frag_depth');
}
set_model_objects(model_bag/*:ModelBag*/) {
model_bag.objects = [];
model_bag.atom_array = [];
let ligand_balls = null;
if (model_bag.conf.ligand_style === 'ball&stick' && this.has_frag_depth()) {
ligand_balls = this.config.ball_size;
}
switch (model_bag.conf.render_style) {
case 'lines':
if (ligand_balls === null) {
model_bag.add_bonds(true, true);
} else {
model_bag.add_bonds(true, false);
model_bag.add_bonds(false, true, ligand_balls);
}
break;
case 'ball&stick':
if (!this.has_frag_depth()) {
this.hud('Ball-and-stick rendering is not working in this browser' +
'\ndue to lack of suppport for EXT_frag_depth', 'ERR');
return;
}
if (ligand_balls === null) {
model_bag.add_bonds(true, false, this.config.ball_size);
model_bag.add_bonds(false, true);
} else {
model_bag.add_bonds(true, true, this.config.ball_size);
}
break;
case 'trace':
model_bag.add_trace();
model_bag.add_bonds(false, true, ligand_balls);
break;
case 'ribbon':
model_bag.add_ribbon(8);
model_bag.add_bonds(false, true, ligand_balls);
break;
}
for (let o of model_bag.objects) {
this.scene.add(o);
}
}
// Add/remove label if `show` is specified, toggle otherwise.
toggle_label(pick/*:{bag:?ModelBag, atom:?AtomT}*/, show/*:?boolean*/) {
if (pick.atom == null) return;
const text = pick.atom.short_label();
const uid = text; // we assume that the labels inside one model are unique
const is_shown = (uid in this.labels);
if (show === undefined) show = !is_shown;
if (show) {
if (is_shown) return;
if (pick.atom == null) return; // silly flow
let atom_style = pick.atom.is_ligand ? 'ligand_style' : 'render_style';
let balls = pick.bag && pick.bag.conf[atom_style] === 'ball&stick';
const label = makeLabel(text, {
pos: pick.atom.xyz,
font: this.config.label_font,
color: '#' + this.config.colors.fg.getHexString(),
win_size: this.window_size,
z_shift: balls ? this.config.ball_size + 0.1 : 0.2,
});
if (!label) return;
if (pick.bag == null) return;
this.labels[uid] = { o: label, bag: pick.bag };
this.scene.add(label);
} else {
if (!is_shown) return;
this.remove_and_dispose(this.labels[uid].o);
delete this.labels[uid];
}
}
redraw_labels() {
for (let uid in this.labels) { // eslint-disable-line guard-for-in
const text = uid;
this.labels[uid].o.remake(text, {
font: this.config.label_font,
color: '#' + this.config.colors.fg.getHexString(),
});
}
}
toggle_map_visibility(map_bag/*:MapBag*/) {
if (typeof map_bag === 'number') {
map_bag = this.map_bags[map_bag];
}
map_bag.visible = !map_bag.visible;
this.redraw_map(map_bag);
this.request_render();
}
redraw_map(map_bag/*:MapBag*/) {
this.clear_el_objects(map_bag);
if (map_bag.visible) {
map_bag.map.block.clear();
this.add_el_objects(map_bag);
}
}
toggle_model_visibility(model_bag/*:?ModelBag*/, visible/*:?boolean*/) {
model_bag = model_bag || this.selected.bag;
if (model_bag == null) return;
model_bag.visible = visible == null ? !model_bag.visible : visible;
this.redraw_model(model_bag);
this.request_render();
}
redraw_model(model_bag/*:ModelBag*/) {
this.clear_model_objects(model_bag);
if (model_bag.visible) {
this.set_model_objects(model_bag);
}
}
redraw_models() {
for (const model_bag of this.model_bags) {
this.redraw_model(model_bag);
}
}
add_el_objects(map_bag/*:MapBag*/) {
if (!map_bag.visible || this.config.map_radius <= 0) return;
if (map_bag.map.block.empty()) {
const t = this.target;
map_bag.block_ctr.copy(t);
map_bag.map.extract_block(this.config.map_radius, [t.x, t.y, t.z]);
}
for (const mtype of map_bag.types) {
const isolevel = (mtype === 'map_neg' ? -1 : 1) * map_bag.isolevel;
const iso = map_bag.map.isomesh_in_block(isolevel, this.config.map_style);
if (iso == null) continue;
const obj = makeChickenWire(iso, {
color: this.config.colors[mtype],
linewidth: this.config.map_line,
});
map_bag.el_objects.push(obj);
this.scene.add(obj);
}
}
change_isolevel_by(map_idx/*:number*/, delta/*:number*/) {
if (map_idx >= this.map_bags.length) return;
const map_bag = this.map_bags[map_idx];
map_bag.isolevel += delta;
//TODO: move slow part into update()
this.clear_el_objects(map_bag);
this.add_el_objects(map_bag);
const abs_level = map_bag.map.abs_level(map_bag.isolevel);
let abs_text = abs_level.toFixed(4);
let tied = this.tied_viewer;
if (tied && map_idx < tied.map_bags.length) {
let tied_bag = tied.map_bags[map_idx];
// Should we tie by sigma or absolute level? Now it's sigma.
tied_bag.isolevel = map_bag.isolevel;
abs_text += ' / ' + tied_bag.map.abs_level(tied_bag.isolevel).toFixed(4);
tied.clear_el_objects(tied_bag);
tied.add_el_objects(tied_bag);
}
this.hud('map ' + (map_idx+1) + ' level = ' + abs_text + ' ' +
map_bag.map.unit + ' (' + map_bag.isolevel.toFixed(2) + ' rmsd)');
}
change_map_radius(delta/*:number*/) {
const rmax = this.config.max_map_radius;
const cf = this.config;
cf.map_radius = Math.min(Math.max(cf.map_radius + delta, 0), rmax);
cf.map_radius = Math.round(cf.map_radius * 1e9) / 1e9;
let info = 'map "radius": ' + cf.map_radius;
if (cf.map_radius === rmax) info += ' (max)';
else if (cf.map_radius === 0) info += ' (hidden maps)';
if (this.map_bags.length === 0) info += '\nNB: no map is loaded.';
this.hud(info);
this.redraw_maps(true);
}
change_slab_width_by(delta/*:number*/) {
let slab_width = this.controls.slab_width;
slab_width[0] = Math.max(slab_width[0] + delta, 0.01);
slab_width[1] = Math.max(slab_width[1] + delta, 0.01);
this.update_camera();
const final_width = this.camera.far - this.camera.near;
this.hud('clip width: ' + final_width.toPrecision(3));
}
change_zoom_by_factor(mult/*:number*/) {
this.camera.zoom *= mult;
this.update_camera();
this.hud('zoom: ' + this.camera.zoom.toPrecision(3));
}
change_bond_line(delta/*:number*/) {
this.config.bond_line = Math.max(this.config.bond_line + delta, 0.1);
this.redraw_models();
this.hud('bond width: ' + scale_by_height(this.config.bond_line,
this.window_size).toFixed(1));
}
change_map_line(delta/*:number*/) {
this.config.map_line = Math.max(this.config.map_line + delta, 0.1);
this.redraw_maps(true);
this.hud('wireframe width: ' + this.config.map_line.toFixed(1));
}
toggle_full_screen() {
let d = document;
// $FlowFixMe: Property mozFullScreenElement is missing in Document
if (d.fullscreenElement || d.mozFullScreenElement ||
// $FlowFixMe: Property webkitExitFullscreen is missing in Document
d.webkitFullscreenElement || d.msFullscreenElement) {
// $FlowFixMe: Property webkitExitFullscreen is missing in Document
let ex = d.exitFullscreen || d.webkitExitFullscreen ||
// $FlowFixMe: property `msExitFullscreen` not found in document
d.mozCancelFullScreen || d.msExitFullscreen;
// $FlowFixMe: cannot call property `exitFullscreen` of unknown type
if (ex) ex.call(d);
} else {
let el = this.container;
if (!el) return;
// $FlowFixMe: Property webkitRequestFullscreen is missing in HTMLElement
let req = el.requestFullscreen || el.webkitRequestFullscreen ||
// $FlowFixMe: property `msRequestFullscreen` not found in HTMLElement
el.mozRequestFullScreen || el.msRequestFullscreen;
if (req) req.call(el);
}
}
toggle_cell_box() {
if (this.decor.cell_box) {
this.scene.remove(this.decor.cell_box);
this.decor.cell_box = null;
} else {
const uc_func = this.get_cell_box_func();
if (uc_func) {
this.decor.cell_box = makeRgbBox(uc_func, this.config.colors.fg);
this.scene.add(this.decor.cell_box);
}
}
}
get_cell_box_func() /*:?Function*/ {
let uc = null;
if (this.selected.bag != null) {
uc = this.selected.bag.model.unit_cell;
}
// note: model may not have unit cell
if (uc == null && this.map_bags.length > 0) {
uc = this.map_bags[0].map.unit_cell;
}
return uc && uc.orthogonalize.bind(uc);
}
shift_clip(delta/*:number*/) {
let eye = this.camera.position.clone().sub(this.target);
eye.multiplyScalar(delta / eye.length());
this.target.add(eye);
this.camera.position.add(eye);
this.update_camera();
this.redraw_maps();
this.hud('clip shifted by [' + vec3_to_fixed(eye, 2).join(' ') + ']');
}
go_to_nearest_Ca() {
const t = this.target;
const bag = this.selected.bag;
if (bag == null) return;
const atom = bag.model.get_nearest_atom(t.x, t.y, t.z, 'CA');
if (atom != null) {
this.select_atom({bag, atom}, {steps: 30});
} else {
this.hud('no nearby CA');
}
}
toggle_inactive_models() {
const n = this.model_bags.length;
if (n < 2) {
this.hud((n == 0 ? 'No' : 'Only one') + ' model is loaded. ' +
'"V" is for working with multiple models.');
return;
}
let show_all = !this.model_bags.every(function (m) { return m.visible; });
for (const model_bag of this.model_bags) {
let show = show_all || model_bag === this.selected.bag;
this.toggle_model_visibility(model_bag, show);
}
this.hud(show_all ? 'All models visible' : 'Inactive models hidden');
}
permalink() {
if (typeof window === 'undefined') return;
const xyz_prec = Math.round(-Math.log10(0.001));
window.location.hash =
'#xyz=' + vec3_to_fixed(this.target, xyz_prec).join(',') +
'&eye=' + vec3_to_fixed(this.camera.position, 1).join(',') +
'&zoom=' + this.camera.zoom.toFixed(0);
this.hud('copy URL from the location bar');
}
redraw_all() {
if (!this.renderer) return;
this.scene.fog.color = this.config.colors.bg;
if (this.renderer) this.renderer.setClearColor(this.config.colors.bg, 1);
this.redraw_models();
this.redraw_maps(true);
this.redraw_labels();
}
toggle_help() {
let el = this.help_el;
if (!el) return;
el.style.display = el.style.display === 'block' ? 'none' : 'block';
if (el.innerHTML === '') {
el.innerHTML = [this.MOUSE_HELP, this.KEYBOARD_HELP,
this.ABOUT_HELP].join('\n\n');
}
}
select_next(info/*:string*/, key/*:string*/,
options/*:Array<string|ColorScheme>*/, back/*:boolean*/) {
const old_idx = options.indexOf(this.config[key]);
const len = options.length;
const new_idx = (old_idx + (back ? len - 1 : 1)) % len;
this.config[key] = options[new_idx];
let html = info + ':';
for (let i = 0; i < len; i++) {
const tag = (i === new_idx ? 'u' : 's');
const opt_name = typeof options[i] === 'string' ? options[i]
: options[i].name;
html += ' <' + tag + '>' + opt_name + '</' + tag + '>';
}
this.hud(html, 'HTML');
}
keydown(evt/*:KeyboardEvent*/) {
if (evt.ctrlKey) return;
const action = this.key_bindings[evt.keyCode];
if (action) {
(action.bind(this))(evt);
} else {
if (action === false) evt.preventDefault();
if (this.help_el) this.hud('Nothing here. Press H for help.');
}
this.request_render();
}
set_common_key_bindings() {
let kb = new Array(256);
// b
kb[66] = function (evt) {
this.select_next('color scheme', 'colors', this.ColorSchemes,
evt.shiftKey);
this.set_colors();
};
// c
kb[67] = function (evt) {
this.select_next('coloring by', 'color_prop', COLOR_PROPS, evt.shiftKey);
this.redraw_models();
};
// e
kb[69] = function toggle_fog() {
const fog = this.scene.fog;
const has_fog = (fog.far === 1);
fog.far = (has_fog ? 1e9 : 1);
this.hud((has_fog ? 'dis': 'en') + 'able fog');
this.redraw_all();
};
// h
kb[72] = this.toggle_help;
// i
kb[73] = function (evt) {
this.hud('toggled spinning');
this.controls.toggle_auto(evt.shiftKey);
};
// k
kb[75] = function () {
this.hud('toggled rocking');
this.controls.toggle_auto(0.0);
};
// m
kb[77] = function (evt) {
this.change_zoom_by_factor(evt.shiftKey ? 1.2 : 1.03);
};
// n
kb[78] = function (evt) {
this.change_zoom_by_factor(1 / (evt.shiftKey ? 1.2 : 1.03));
};
// q
kb[81] = function (evt) {
this.select_next('label font', 'label_font', LABEL_FONTS, evt.shiftKey);
this.redraw_labels();
};
// r
kb[82] = function (evt) {
if (evt.shiftKey) {
this.hud('redraw!');
this.redraw_all();
} else {
this.hud('recentered');
this.recenter();
}
};
// w
kb[87] = function (evt) {
this.select_next('map style', 'map_style', MAP_STYLES, evt.shiftKey);
this.redraw_maps(true);
};
// add, equals/firefox, equal sign
kb[107] = kb[61] = kb[187] = function (evt) {
this.change_isolevel_by(evt.shiftKey ? 1 : 0, 0.1);
};
// subtract, minus/firefox, dash
kb[109] = kb[173] = kb[189] = function (evt) {
this.change_isolevel_by(evt.shiftKey ? 1 : 0, -0.1);
};
// [
kb[219] = function () { this.change_map_radius(-2); };
// ]
kb[221] = function () { this.change_map_radius(2); };
// \ (backslash)
kb[220] = function (evt) {
this.select_next('bond lines', 'line_style', LINE_STYLES, evt.shiftKey);
this.redraw_models();
};
// shift, ctrl, alt, altgr
kb[16] = kb[17] = kb[18] = kb[225] = function () {};
// slash, single quote
kb[191] = kb[222] = false; // -> preventDefault()
this.key_bindings = kb;
}
set_real_space_key_bindings() {
let kb = this.key_bindings;
// Home
kb[36] = function (evt) {
evt.shiftKey ? this.change_map_line(0.1) : this.change_bond_line(0.2);
};
// End
kb[35] = function (evt) {
evt.shiftKey ? this.change_map_line(-0.1) : this.change_bond_line(-0.2);
};
// Space
kb[32] = function (evt) { this.center_next_residue(evt.shiftKey); };
// d
kb[68] = function () { this.change_slab_width_by(-0.1); };
// f
kb[70] = function (evt) {
evt.shiftKey ? this.toggle_full_screen() : this.change_slab_width_by(0.1);
};
// l
kb[76] = function (evt) {
this.select_next('ligands as', 'ligand_style', LIGAND_STYLES,
evt.shiftKey);
this.redraw_models();
};
// p
kb[80] = function (evt) {
evt.shiftKey ? this.permalink() : this.go_to_nearest_Ca();
};
// t
kb[84] = function (evt) {
this.select_next('rendering as', 'render_style', RENDER_STYLES,
evt.shiftKey);
this.redraw_models();
};
// u
kb[85] = function () {
this.hud('toggled unit cell box');
this.toggle_cell_box();
};
// v
kb[86] = function () { this.toggle_inactive_models(); };
// y
kb[89] = function (evt) {
this.config.hydrogens = !this.config.hydrogens;
this.hud((this.config.hydrogens ? 'show' : 'hide') +
' hydrogens (if any)');
this.redraw_models();
};
// comma
kb[188] = function (evt) { if (evt.shiftKey) this.shift_clip(1); };
// period
kb[190] = function (evt) { if (evt.shiftKey) this.shift_clip(-1); };
}
mousedown(event/*:MouseEvent*/) {
//event.preventDefault(); // default involves setting focus, which we need
event.stopPropagation();
document.addEventListener('mouseup', this.mouseup);
document.addEventListener('mousemove', this.mousemove);
let state = STATE.NONE;
if (event.button === 1 || (event.button === 0 && event.ctrlKey)) {
state = STATE.PAN;
} else if (event.button === 0) {
// in Coot shift+Left is labeling atoms like dblclick, + rotation
if (event.shiftKey) {
this.dblclick(event);
}
state = STATE.ROTATE;
} else if (event.button === 2) {
if (event.ctrlKey) {
state = event.shiftKey ? STATE.ROLL : STATE.SLAB;
} else {
this.decor.zoom_grid.visible = true;
state = STATE.ZOOM;
}
}
this.controls.start(state, this.relX(event), this.relY(event));
this.request_render();
}
dblclick(event/*:MouseEvent*/) {
if (event.button !== 0) return;
if (this.decor.selection) {
this.remove_and_dispose(this.decor.selection);
this.decor.selection = null;
}
const mouse = [this.relX(event), this.relY(event)];
const pick = this.pick_atom(mouse, this.camera);
if (pick) {
const atom = pick.atom;
this.hud(pick.bag.label + ' ' + atom.long_label());
this.dbl_click_callback(pick);
const color = this.config.colors[atom.element] || this.config.colors.def;
const size = 2.5 * scale_by_height(this.config.bond_line,
this.window_size);
this.decor.selection = makeWheels([atom], [color], size);
this.scene.add(this.decor.selection);
} else {
this.hud();
}
this.request_render();
}
touchstart(event/*:TouchEvent*/) {
const touches = event.touches;
if (touches.length === 1) {
this.controls.start(STATE.ROTATE,
this.relX(touches[0]), this.relY(touches[0]));
} else { // for now using only two touches
const info = touch_info(event);
this.controls.start(STATE.PAN_ZOOM,
this.relX(info), this.relY(info), info.dist);
}
this.request_render();
}
touchmove(event/*:TouchEvent*/) {
event.preventDefault();
event.stopPropagation();
const touches = event.touches;
if (touches.length === 1) {
this.controls.move(this.relX(touches[0]), this.relY(touches[0]));
} else { // for now using only two touches
const info = touch_info(event);
this.controls.move(this.relX(info), this.relY(info), info.dist);
}
}
touchend(/*event*/) {
this.controls.stop();
this.redraw_maps();
}
// $FlowFixMe TODO: wheel()+WheelEvent are more standard
mousewheel(evt/*:MouseWheelEvent*/) {
evt.preventDefault();
evt.stopPropagation();
// evt.wheelDelta for WebKit, evt.detail for Firefox
const delta = evt.wheelDelta || -2 * (evt.detail || 0);
this.mousewheel_action(delta, evt);
this.request_render();
}
mousewheel_action(delta/*:number*/, evt/*:WheelEvent*/) {
this.change_isolevel_by(evt.shiftKey ? 1 : 0, 0.0005 * delta);
}
resize(/*evt*/) {
const el = this.container;
if (el == null) return;
const width = el.clientWidth;
const height = el.clientHeight;
this.window_offset[0] = el.offsetLeft;
this.window_offset[1] = el.offsetTop;
this.camera.left = -width;
this.camera.right = width;
this.camera.top = height;
this.camera.bottom = -height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
if (width !== this.window_size[0] || height !== this.window_size[1]) {
this.window_size[0] = width;
this.window_size[1] = height;
this.redraw_models(); // b/c bond_line is scaled by height
}
this.request_render();
}
// If xyz set recenter on it looking toward the model center.
// Otherwise recenter on the model center looking along the z axis.
recenter(xyz/*:?Num3*/, cam/*:?Num3*/, steps/*:?number*/) {
const bag = this.selected.bag;
let new_up = new Vector3(0, 1, 0);
let eye;
if (xyz != null && cam == null && bag != null) {
// look from specified point toward the center of the molecule,
// i.e. shift camera away from the molecule center.
const mc = bag.model.get_center();
eye = new Vector3(xyz[0] - mc[0], xyz[1] - mc[1], xyz[2] - mc[2]);
eye.setLength(100);
xyz = new Vector3(xyz[0], xyz[1], xyz[2]);
cam = eye.clone().add(xyz);
} else {
if (xyz == null) {
if (bag != null) {
xyz = bag.model.get_center();
} else {
const uc_func = this.get_cell_box_func();
xyz = uc_func ? uc_func([0.5, 0.5, 0.5]) : [0, 0, 0];
}
}
xyz = new Vector3(xyz[0], xyz[1], xyz[2]);
if (cam != null) {
cam = new Vector3(cam[0], cam[1], cam[2]);
eye = cam.clone().sub(xyz);
new_up.copy(this.camera.up); // preserve the up direction
} else {
const dc = this.default_camera_pos;
cam = new Vector3(xyz.x + dc[0], xyz.y + dc[1], xyz.z + dc[2]);
}
}
if (eye != null) {
new_up.projectOnPlane(eye);
if (new_up.lengthSq() < 0.0001) new_up.x += 1;
new_up.normalize();
}
this.controls.go_to(xyz, cam, new_up, steps);
}
center_next_residue(back/*:boolean*/) {
const bag = this.selected.bag;
if (bag == null) return;
const atom = bag.model.next_residue(this.selected.atom, back);
if (atom != null) {
this.select_atom({bag, atom}, {steps: 30});
}
}
select_atom(pick/*:{bag:ModelBag, atom:AtomT}*/, options/*:Object*/={}) {
this.hud('-> ' + pick.bag.label + ' ' + pick.atom.long_label());
let xyz = pick.atom.xyz;
this.controls.go_to(new Vector3(xyz[0], xyz[1], xyz[2]),
null, null, options.steps);
this.toggle_label(this.selected, false);
this.selected = {bag: pick.bag, atom: pick.atom}; // not ...=pick b/c flow
this.toggle_label(this.selected, true);
}
update_camera() {
const dxyz = this.camera.position.distanceTo(this.target);
const w = this.controls.slab_width;
const scale = w[2] || this.camera.zoom;
this.camera.near = dxyz * (1 - w[0] / scale);
this.camera.far = dxyz * (1 + w[1] / scale);
this.camera.updateProjectionMatrix();
}
// The main loop. Running when a mouse button is pressed or when the view
// is moving (and run once more after the mouse button is released).
// It is also triggered by keydown events.
render() {
this.scheduled = true;
if (this.renderer === null) return;
if (this.controls.update()) {
this.update_camera();
}
const tied = this.tied_viewer;
if (!this.controls.is_going()) {
this.redraw_maps();
if (tied && !tied.scheduled) tied.redraw_maps();
}
this.renderer.render(this.scene, this.camera);
if (tied && !tied.scheduled) tied.renderer.render(tied.scene, tied.camera);
if (this.nav) {
this.nav.renderer.render(this.nav.scene, this.camera);
}
this.scheduled = false;
if (this.controls.is_moving()) {
this.request_render();
}
}
request_render() {
if (typeof window !== 'undefined' && !this.scheduled) {
this.scheduled = true;
window.requestAnimationFrame(this.render.bind(this));
}
}
add_model(model/*:Model*/, options/*:Object*/={}) {
const model_bag = new ModelBag(model, this.config, this.window_size);
model_bag.hue_shift = options.hue_shift || 0.06 * this.model_bags.length;
this.model_bags.push(model_bag);
this.set_model_objects(model_bag);
this.request_render();
}
add_map(map/*:ElMap*/, is_diff_map/*:boolean*/) {
//map.show_debug_info();
const map_bag = new MapBag(map, this.config, is_diff_map);
this.map_bags.push(map_bag);
this.add_el_objects(map_bag);
this.request_render();
}
load_file(url/*:string*/, options/*:{[id:string]: mixed}*/,
callback/*:Function*/) {
if (this.renderer === null) return; // no WebGL detected
let req = new XMLHttpRequest();
req.open('GET', url, true);
if (options.binary) {
req.responseType = 'arraybuffer';
} else {
// http://stackoverflow.com/questions/7374911/
req.overrideMimeType('text/plain');
}
let self = this;
Object.keys(this.xhr_headers).forEach(function (name) {
req.setRequestHeader(name, self.xhr_headers[name]);
});
req.onreadystatechange = function () {
if (req.readyState === 4) {
// chrome --allow-file-access-from-files gives status 0
if (req.status === 200 || (req.status === 0 && req.response !== null &&
req.response !== '')) {
try {
callback(req);
} catch (e) {
self.hud('Error: ' + e.message + '\nwhen processing ' + url, 'ERR');
}
} else {
self.hud('Failed to fetch ' + url, 'ERR');
}
}
};
if (options.progress) {
req.addEventListener('progress', function (evt /*:ProgressEvent*/) {
if (evt.lengthComputable && evt.loaded && evt.total) {
const fn = url.split('/').pop();
self.hud('loading ' + fn + ' ... ' + ((evt.loaded / 1024) | 0) +
' / ' + ((evt.total / 1024) | 0) + ' kB');
if (evt.loaded === evt.total) self.hud(); // clear progress message
}
});
}
try {
req.send(null);
} catch (e) {
self.hud('loading ' + url + ' failed:\n' + e, 'ERR');
}
}
set_dropzone(zone/*:Object*/, callback/*:Function*/) {
const self = this;
zone.addEventListener('dragover', function (e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
self.hud('ready for file drop...');
});
zone.addEventListener('drop', function (e) {
e.stopPropagation();
e.preventDefault();
let names = [];
for (const file of e.dataTransfer.files) {
try {
callback(file);
} catch (e) {
self.hud('Loading ' + file.name + ' failed.\n' + e.message, 'ERR');
return;
}
names.push(file.name);
}
self.hud('loading ' + names.join(', '));
});
}
set_pdb_and_map_dropzone(zone/*:Object*/) {
const self = this;
this.set_dropzone(zone, function (file) {
const reader = new FileReader();
if (/\.(pdb|ent)$/.test(file.name)) {
reader.onload = function (evt) {
self.load_pdb_from_text(evt.target.result);
self.recenter();
};
reader.readAsText(file);
} else if (/\.(map|ccp4|mrc|dsn6|omap)$/.test(file.name)) {
const map_format = /\.(dsn6|omap)$/.test(file.name) ? 'dsn6' : 'ccp4';
reader.onloadend = function (evt) {
if (evt.target.readyState == 2) {
self.load_map_from_buffer(evt.target.result, {format: map_format});
if (self.model_bags.length === 0 && self.map_bags.length ==