// File:js/ngl/shims.js

/**
 * @file Shims
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


//////////////
// Polyfills

( function( global ) {

    'use strict';

    // Console-polyfill. MIT license.
    // https://github.com/paulmillr/console-polyfill
    // Make it safe to do console.log() always.

    global.console = global.console || {};
    var con = global.console;
    var prop, method;
    var empty = {};
    var dummy = function(){};
    var properties = 'memory'.split( ',' );
    var methods = (
        'assert,clear,count,debug,dir,dirxml,error,exception,group,' +
        'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' +
        'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn'
    ).split(',');

    while( prop = properties.pop() ) if( !con[ prop] ) con[ prop ] = empty;
    while( method = methods.pop() ) if( !con[ method] ) con[ method ] = dummy;

    // Using `this` for web workers while maintaining compatibility with browser
    // targeted script loaders such as Browserify or Webpack where the only way to
    // get to the global object is via `window`.

} )( typeof window === 'undefined' ? this : window );


if( typeof importScripts !== 'function' && !HTMLCanvasElement.prototype.toBlob ){

    // http://code.google.com/p/chromium/issues/detail?id=67587#57

    Object.defineProperty( HTMLCanvasElement.prototype, 'toBlob', {

        value: function( callback, type, quality ){

            var bin = window.atob( this.toDataURL( type, quality ).split( ',' )[ 1 ] ),
                len = bin.length,
                len32 = len >> 2,
                a8 = new Uint8Array( len ),
                a32 = new Uint32Array( a8.buffer, 0, len32 );

            for( var i=0, j=0; i < len32; i++ ) {

                a32[i] = bin.charCodeAt( j++ ) |
                    bin.charCodeAt( j++ ) << 8 |
                    bin.charCodeAt( j++ ) << 16 |
                    bin.charCodeAt( j++ ) << 24;

            }

            var tailLength = len & 3;

            while( tailLength-- ){

                a8[ j ] = bin.charCodeAt( j++ );

            }

            callback( new Blob( [ a8 ], { 'type': type || 'image/png' } ) );

        }

    } );

}


if( !Number.isInteger ){

    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger

    Number.isInteger = function isInteger( nVal ){
        return typeof nVal === "number" && isFinite( nVal ) && nVal > -9007199254740992 && nVal < 9007199254740992 && Math.floor( nVal ) === nVal;
    };

}


if( !Number.isNaN ){

    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN

    Number.isNaN = function isNaN( value ){
        return value !== value;
    };

}


if( !Object.assign ){

    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

    Object.defineProperty( Object, "assign", {

        enumerable: false,
        configurable: true,
        writable: true,

        value: function(target, firstSource) {

            "use strict";
            if (target === undefined || target === null)
            throw new TypeError("Cannot convert first argument to object");

            var to = Object(target);

            var hasPendingException = false;
            var pendingException;

            for (var i = 1; i < arguments.length; i++) {

                var nextSource = arguments[i];
                if (nextSource === undefined || nextSource === null)
                    continue;

                var keysArray = Object.keys(Object(nextSource));
                for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {

                    var nextKey = keysArray[nextIndex];
                    try {
                        var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
                        if (desc !== undefined && desc.enumerable)
                            to[nextKey] = nextSource[nextKey];
                    } catch (e) {
                        if (!hasPendingException) {
                            hasPendingException = true;
                            pendingException = e;
                        }
                    }

                }

                if (hasPendingException)
                    throw pendingException;

            }

            return to;

        }

    } );

}


if (!String.prototype.startsWith) {

    /*! https://mths.be/startswith v0.2.0 by @mathias */

    (function() {
        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
        var defineProperty = (function() {
            // IE 8 only supports `Object.defineProperty` on DOM elements
            try {
                var object = {};
                var $defineProperty = Object.defineProperty;
                var result = $defineProperty(object, object, object) && $defineProperty;
            } catch(error) {}
            return result;
        }());
        var toString = {}.toString;
        var startsWith = function(search) {
            if (this == null) {
                throw TypeError();
            }
            var string = String(this);
            if (search && toString.call(search) == '[object RegExp]') {
                throw TypeError();
            }
            var stringLength = string.length;
            var searchString = String(search);
            var searchLength = searchString.length;
            var position = arguments.length > 1 ? arguments[1] : undefined;
            // `ToInteger`
            var pos = position ? Number(position) : 0;
            if (pos != pos) { // better `isNaN`
                pos = 0;
            }
            var start = Math.min(Math.max(pos, 0), stringLength);
            // Avoid the `indexOf` call if no match is possible
            if (searchLength + start > stringLength) {
                return false;
            }
            var index = -1;
            while (++index < searchLength) {
                if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) {
                    return false;
                }
            }
            return true;
        };
        if (defineProperty) {
            defineProperty(String.prototype, 'startsWith', {
                'value': startsWith,
                'configurable': true,
                'writable': true
            });
        } else {
            String.prototype.startsWith = startsWith;
        }
    }());

}


if (!String.prototype.endsWith) {
  String.prototype.endsWith = function(searchString, position) {
      var subjectString = this.toString();
      if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
        position = subjectString.length;
      }
      position -= searchString.length;
      var lastIndex = subjectString.indexOf(searchString, position);
      return lastIndex !== -1 && lastIndex === position;
  };
}


if( typeof importScripts !== 'function' ){

    ( function() {

        // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
        // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating

        // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel

        // MIT license

        var lastTime = 0;
        var vendors = [ 'ms', 'moz', 'webkit', 'o' ];

        for( var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x ){

            window.requestAnimationFrame = (
                window[ vendors[ x ] + 'RequestAnimationFrame' ]
            );

            window.cancelAnimationFrame = (
                window[ vendors[ x ] + 'CancelAnimationFrame' ] ||
                window[ vendors[ x ] + 'CancelRequestAnimationFrame' ]
            );

        }

        if( !window.requestAnimationFrame ){

            window.requestAnimationFrame = function( callback, element ){

                var currTime = new Date().getTime();
                var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );

                var id = window.setTimeout( function(){

                    callback( currTime + timeToCall );

                }, timeToCall );

                lastTime = currTime + timeToCall;

                return id;

            };

        }

        if( !window.cancelAnimationFrame ){

            window.cancelAnimationFrame = function( id ){
                clearTimeout( id );
            };

        }

    }() );

}


if ( Function.prototype.name === undefined && Object.defineProperty !== undefined ) {

    // Missing in IE9-11.
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name

    Object.defineProperty( Function.prototype, 'name', {

        get: function () {

            return this.toString().match( /^\s*function\s*(\S*)\s*\(/ )[ 1 ];

        }

    } );

}


if ( self.performance === undefined ) {

    self.performance = {};

}

if ( self.performance.now === undefined ) {

    ( function () {

        var start = Date.now();

        self.performance.now = function () {

            return Date.now() - start;

        }

    } )();

}


////////////////
// Workarounds

if( typeof importScripts !== 'function' ){

    HTMLElement.prototype.getBoundingClientRect = function(){

        // workaround for ie11 behavior with disconnected dom nodes

        var _getBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;

        return function(){
            try{
                return _getBoundingClientRect.apply( this, arguments );
            }catch( e ){
                return {
                    top: 0,
                    left: 0,
                    width: this.width,
                    height: this.height
                };
            }
        };

    }();

}


if( typeof importScripts !== 'function' && WebGLRenderingContext ){

    // wrap WebGL debug function used by three.js and
    // ignore calls to them when the debug flag is not set

    WebGLRenderingContext.prototype.getShaderParameter = function(){

        var _getShaderParameter = WebGLRenderingContext.prototype.getShaderParameter;

        return function(){

            if( NGL.debug ){

                return _getShaderParameter.apply( this, arguments );

            }else{

                return true;

            }

        }

    }();

    WebGLRenderingContext.prototype.getShaderInfoLog = function(){

        var _getShaderInfoLog = WebGLRenderingContext.prototype.getShaderInfoLog;

        return function(){

            if( NGL.debug ){

                return _getShaderInfoLog.apply( this, arguments );

            }else{

                return '';

            }

        }

    }();

    WebGLRenderingContext.prototype.getProgramParameter = function(){

        var _getProgramParameter = WebGLRenderingContext.prototype.getProgramParameter;

        return function( program, pname ){

            if( NGL.debug || pname !== WebGLRenderingContext.prototype.LINK_STATUS ){

                return _getProgramParameter.apply( this, arguments );

            }else{

                return true;

            }

        }

    }();

    WebGLRenderingContext.prototype.getProgramInfoLog = function(){

        var _getProgramInfoLog = WebGLRenderingContext.prototype.getProgramInfoLog;

        return function(){

            if( NGL.debug ){

                return _getProgramInfoLog.apply( this, arguments );

            }else{

                return '';

            }

        }

    }();

}

// File:js/ngl/core.js

/**
 * @file Core
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


////////
// NGL

var NGL = {

    REVISION: '0.7',
    EPS: 0.0000001,
    useWorker: true,
    indexUint16: false,
    debug: false,
    develop: (
        self.location.pathname.indexOf( "core.js" ) !== -1 ||
        self.location.pathname.indexOf( "dev.html" ) !== -1
    ),
    mainScriptFilePath: "../js/build/ngl.full.min.js",
    cssDirectory: "../css/",
    assetsDirectory: "../",
    documentationUrl: "../doc/index.html",

    webglErrorMessage: "<div style=\"display:flex; align-items:center; justify-content:center; height:100%;\"><p style=\"padding:15px; text-align:center;\">Your browser/graphics card does not seem to support <a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/WebGL\">WebGL</a>.<br /><br />Find out how to get it <a target=\"_blank\" href=\"http://get.webgl.org/\">here</a>.</p></div>"

};


// set default log handlers
NGL.log = Function.prototype.bind.call( console.log, console );
NGL.info = Function.prototype.bind.call( console.info, console );
NGL.warn = Function.prototype.bind.call( console.warn, console );
NGL.error = Function.prototype.bind.call( console.error, console );
NGL.time = Function.prototype.bind.call( console.time, console );
NGL.timeEnd = Function.prototype.bind.call( console.timeEnd, console );


NGL.LeftMouseButton = 1;
NGL.MiddleMouseButton = 2;
NGL.RightMouseButton = 3;


if( typeof importScripts === 'function' ){

    if( NGL.develop ){

        importScripts(

            "../three/three.js",
            "../three/TypedArrayUtils.js",
            "../three/controls/TrackballControls.js",
            "../three/loaders/OBJLoader.js",
            "../three/loaders/PLYLoader.js",

            "../lib/promise.min.js",
            "../lib/sprintf.min.js",
            "../lib/pako_inflate.min.js",
            "../lib/chroma.min.js",
            "../lib/svd.js",
            "../lib/signals.min.js",
            "../lib/TypedFastBitSet.js",
            "../lib/msgpack-decode.js",
            "../lib/mmtf-decode.js",

            "../ngl/shims.js",
            // "../ngl/core.js",
            "../ngl/worker.js",
            "../ngl/utils.js",
            "../ngl/proxy.js",
            "../ngl/store.js",
            "../ngl/symmetry.js",
            "../ngl/alignment.js",
            "../ngl/geometry.js",
            "../ngl/selection.js",
            "../ngl/superposition.js",
            "../ngl/structure.js",
            "../ngl/trajectory.js",
            "../ngl/surface.js",
            "../ngl/script.js",
            "../ngl/streamer.js",
            "../ngl/parser.js",
            "../ngl/writer.js",
            "../ngl/loader.js",
            "../ngl/viewer.js",
            "../ngl/buffer.js",
            "../ngl/representation.js",
            "../ngl/stage.js"

        );

    }

}


NGL.GET = function( id ){

    var a = new RegExp( id + "=([^&#=]*)" );
    var m = a.exec( window.location.search );

    if( m ){
        return decodeURIComponent( m[1] );
    }else{
        return undefined;
    }

};


NGL.boolean = function( value ){

    if( !value ){
        return false;
    }

    if( typeof value === "string" ){
        return /^1|true|t|yes|y$/i.test( value );
    }

    return true;

};


if( typeof importScripts !== 'function' ){

    ( function(){

        var debug = NGL.GET( "debug" );
        if( debug !== undefined ) NGL.debug = NGL.boolean( debug );

    } )();

}


// Registry

NGL.PluginRegistry = {

    dict: {},

    add: function( name, path ){
        this.dict[ name ] = path;
    },

    get: function( name ){
        if( name in this.dict ){
            return this.dict[ name ];
        }else{
            throw "NGL.PluginRegistry '" + name + "' not defined";
        }
    },

    get names(){
        return Object.keys( this.dict );
    },

    get count(){
        return this.names.length;
    },

    load: function( name, stage ){
        var path = this.get( name );
        stage.loadFile( path, { name: name + " plugin" } );
    }

};


NGL.ExampleRegistry = {

    dict: {},

    add: function( name, fn ){
        this.dict[ name ] = fn;
    },

    addDict: function( dict ){
        Object.keys( dict ).forEach( function( name ){
            this.add( name, dict[ name ] );
        }.bind( this ) );
    },

    get: function( name ){
        return this.dict[ name ];
    },

    get names(){
        return Object.keys( this.dict );
    },

    get count(){
        return this.names.length;
    },

    load: function( name, stage ){
        var fn = this.get( name );
        if( typeof fn === "function" ){
            fn( stage );
        }else{
            NGL.warn( "NGL.ExampleRegistry.load not available:", name );
        }
    }

};

// File:js/ngl/worker.js

/**
 * @file Worker
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


// Worker

NGL.WorkerRegistry = {

    activeWorkerCount: 0,

    funcDict: {},

    add: function( name, func ){

        NGL.WorkerRegistry.funcDict[ name ] = func;

    },

};


NGL.Worker = function( name ){

    var pending = 0;
    var postCount = 0;
    var onmessageDict = {};
    var onerrorDict = {};

    var worker = new Worker( NGL.mainScriptFilePath );

    NGL.WorkerRegistry.activeWorkerCount += 1;

    worker.onmessage = function( event ){

        pending -= 1;
        var postId = event.data.__postId;

        NGL.timeEnd( "NGL.Worker.postMessage " + name + " #" + postId );

        if( onmessageDict[ postId ] ){
            onmessageDict[ postId ].call( worker, event );
        }else{
            // NGL.debug( "No onmessage", postId, name );
        }

        delete onmessageDict[ postId ];
        delete onerrorDict[ postId ];

    };

    worker.onerror = function( event ){

        pending -= 1;
        var postId = event.data.__postId;

        if( onerrorDict[ postId ] ){
            onerrorDict[ postId ].call( worker, event );
        }else{
            NGL.error( "NGL.Worker.onerror", postId, name, event );
        }

        delete onmessageDict[ postId ];
        delete onerrorDict[ postId ];

    };

    // API

    this.name = name;

    this.post = function( aMessage, transferList, onmessage, onerror ){

        onmessageDict[ postCount ] = onmessage;
        onerrorDict[ postCount ] = onerror;

        aMessage = aMessage || {};
        aMessage.__name = name;
        aMessage.__postId = postCount;
        aMessage.__debug = NGL.debug;

        NGL.time( "NGL.Worker.postMessage " + name + " #" + postCount );

        try{
            worker.postMessage.call( worker, aMessage, transferList );
        }catch( error ){
            NGL.error( "NGL.worker.post:", error );
            worker.postMessage.call( worker, aMessage );
        }

        pending += 1;
        postCount += 1;

        return this;

    };

    this.terminate = function(){

        if( worker ){
            worker.terminate();
            NGL.WorkerRegistry.activeWorkerCount -= 1;
        }else{
            console.log( "no worker to terminate" );
        }

    };

    Object.defineProperties( this, {
        postCount: {
            get: function(){ return postCount; }
        },
        pending: {
            get: function(){ return pending; }
        }
    } );

};

NGL.Worker.prototype.constructor = NGL.Worker;


NGL.WorkerPool = function( name, maxCount ){

    maxCount = Math.min( 8, maxCount || 2 );

    var pool = [];
    var count = 0;

    // API

    this.name = name;

    this.maxCount = maxCount;

    this.post = function( aMessage, transferList, onmessage, onerror ){

        var worker = this.getNextWorker();
        worker.post( aMessage, transferList, onmessage, onerror );

        return this;

    };

    this.terminate = function(){

        pool.forEach( function( worker ){
            worker.terminate();
        } );

    };

    this.getNextWorker = function(){

        var nextWorker;
        var minPending = Infinity;

        for( var i = 0; i < maxCount; ++i ){

            if( i >= count ){

                nextWorker = new NGL.Worker( name );
                pool.push( nextWorker );
                count += 1;
                break;

            }

            var worker = pool[ i ];

            if( worker.pending === 0 ){

                minPending = worker.pending;
                nextWorker = worker;
                break;

            }else if( worker.pending < minPending ){

                minPending = worker.pending;
                nextWorker = worker;

            }

        }

        return nextWorker;

    };

    Object.defineProperties( this, {
        count: {
            get: function(){ return count; }
        }
    } );

};

NGL.WorkerPool.prototype.constructor = NGL.WorkerPool;


if( typeof importScripts === 'function' ){

    self.onmessage = function( e ){

        var name = e.data.__name;
        var postId = e.data.__postId;
        NGL.debug = e.data.__debug;

        if( name === undefined ){

            NGL.error( "message __name undefined" );

        }else if( NGL.WorkerRegistry.funcDict[ name ] === undefined ){

            NGL.error( "funcDict[ __name ] undefined", name );

        }else{

            var callback = function( aMessage, transferList ){

                aMessage = aMessage || {};
                if( postId !== undefined ) aMessage.__postId = postId;

                try{
                    self.postMessage( aMessage, transferList );
                }catch( error ){
                    NGL.error( "self.postMessage:", error );
                    self.postMessage( aMessage );
                }

            };

            NGL.WorkerRegistry.funcDict[ name ]( e, callback );

        }

    }

}

// File:js/ngl/utils.js

/**
 * @file Utils
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */

NGL.defaults = function( value, defaultValue ){

    return value !== undefined ? value : defaultValue;

};


NGL.browser = function(){

    var ua = navigator.userAgent;

    if ( /Arora/i.test( ua ) ) {

        return 'Arora';

    } else if ( /Opera|OPR/.test( ua ) ) {

        return 'Opera';

    } else if ( /Chrome/i.test( ua ) ) {

        return 'Chrome';

    } else if ( /Epiphany/i.test( ua ) ) {

        return 'Epiphany';

    } else if ( /Firefox/i.test( ua ) ) {

        return 'Firefox';

    } else if ( /Mobile(\/.*)? Safari/i.test( ua ) ) {

        return 'Mobile Safari';

    } else if ( /MSIE/i.test( ua ) ) {

        return 'Internet Explorer';

    } else if ( /Midori/i.test( ua ) ) {

        return 'Midori';

    } else if ( /Safari/i.test( ua ) ) {

        return 'Safari';

    }

    return false;

}();


NGL.getAbsolutePath = function( relativePath ){

    var loc = window.location;
    var pn = loc.pathname;
    var basePath = pn.substring( 0, pn.lastIndexOf("/") + 1 );

    return loc.origin + basePath + relativePath;

};


NGL.createObject = function( prototype, properties ){

    var object = Object.create( prototype );

    for( var key in properties ) {

        if ( properties.hasOwnProperty( key ) ) {

            object[ key ] = properties[ key ];

        }

    }

    return object;

};


NGL.deepCopy = function( src ){

    if( typeof src !== "object" ){
        return src;
    }

    var dst = Array.isArray( src ) ? [] : {};

    for( var key in src ){
        dst[ key ] = NGL.deepCopy( src[ key ] );
    }

    return dst;

}


NGL.download = function( data, downloadName ){

    if( !data ){
        NGL.warn( "NGL.download: no data given." );
        return;
    }

    downloadName = downloadName || "download";

    var a = document.createElement( 'a' );
    a.style.display = "hidden";
    document.body.appendChild( a );
    if( data instanceof Blob ){
        a.href = URL.createObjectURL( data );
    }else{
        a.href = data;
    }
    a.download = downloadName;
    a.target = "_blank";
    a.click();

    document.body.removeChild( a );
    if( data instanceof Blob ){
        URL.revokeObjectURL( data );
    }

};


NGL.submit = function( url, data, callback, onerror ){

    if( data instanceof FormData ){

        var xhr = new XMLHttpRequest();
        xhr.open( "POST", url );

        xhr.addEventListener( 'load', function ( event ) {

            if ( xhr.status === 200 || xhr.status === 304 ) {

                callback( xhr.response );

            } else {

                if( typeof onerror === "function" ){

                    onerror( xhr.status );

                }

            }

        }, false );

        xhr.send( data );

    }else{

        NGL.warn( "NGL.submit: type not supported.", data  );

    }

};


NGL.open = function( callback, extensionList ){

    extensionList = extensionList || [ "*" ];

    var fileInput = document.createElement("input");
    fileInput.type = "file";
    fileInput.multiple = true;
    fileInput.style.display = "hidden";
    document.body.appendChild( fileInput );
    fileInput.accept = "." + extensionList.join( ",." );
    fileInput.addEventListener( 'change', function( e ){

        callback( e.target.files );

    }, false );

    fileInput.click();

};


NGL.unicodeHelper = function(){

    var replace_map = {
        "{alpha}": "\u03B1",
        "{beta}": "\u03B2",
        "{gamma}": "\u03B3",
        "{dot}": "\u00B7",
        "{bullet}": "\u2022",
    }

    var keys = Object.keys( replace_map ).join('|');

    var rg = new RegExp( '(' + keys + ')', 'gi' );

    return function( str ){

        return str.replace(
            rg, function( s, p1, p2, offset, sx ){
                return replace_map[ String( s ) ];
            }
        );

    };

}();


NGL.getFileInfo = function( file ){

    var compressedExtList = [ "gz" ];

    var path, compressed, protocol;

    if( ( self.File && file instanceof File ) ||
        ( self.Blob && file instanceof self.Blob )
    ){
        path = file.name || "";
    }else{
        path = file
    }
    var queryIndex = path.lastIndexOf( '?' );
    path = path.substring( 0, queryIndex === -1 ? path.length : queryIndex );

    var name = path.replace( /^.*[\\\/]/, '' );
    var base = name.substring( 0, name.lastIndexOf( '.' ) );

    var nameSplit = name.split( '.' );
    var ext = nameSplit.length > 1 ? nameSplit.pop().toLowerCase() : "";

    var protocolMatch = path.match( /^(.+):\/\/(.+)$/ );
    if( protocolMatch ){
        protocol = protocolMatch[ 1 ].toLowerCase();
        path = protocolMatch[ 2 ];
    }

    var dir = path.substring( 0, path.lastIndexOf( '/' ) + 1 );

    if( compressedExtList.indexOf( ext ) !== -1 ){
        compressed = ext;
        var n = path.length - ext.length - 1;
        ext = path.substr( 0, n ).split( '.' ).pop().toLowerCase();
        var m = base.length - ext.length - 1;
        base = base.substr( 0, m );
    }else{
        compressed = false;
    }

    return {
        "path": path,
        "name": name,
        "ext": ext,
        "base": base,
        "dir": dir,
        "compressed": compressed,
        "protocol": protocol,
        "src": file
    };

};


NGL.fromJSON = function( input ){

    return new NGL[ input.metadata.type ]().fromJSON( input );

};


NGL.throttle = function( func, wait, options ){

    // from http://underscorejs.org/docs/underscore.html

    var context, args, result;
    var timeout = null;
    var previous = 0;

    if( !options ) options = {};

    var later = function(){
        previous = options.leading === false ? 0 : Date.now();
        timeout = null;
        result = func.apply( context, args );
        if( !timeout ) context = args = null;
    };

    return function(){

        var now = Date.now();
        if( !previous && options.leading === false ) previous = now;
        var remaining = wait - ( now - previous );
        context = this;
        args = arguments;
        if( remaining <= 0 || remaining > wait ){
            if( timeout ){
                clearTimeout( timeout );
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if( !timeout ) context = args = null;
        }else if( !timeout && options.trailing !== false ){
            timeout = setTimeout( later, remaining );
        }

        return result;

    };

};


NGL.binarySearchIndexOf = function(){
    function _compareFunction( elm1, elm2 ){
        if( elm1 < elm2 ) return -1;
        if( elm1 > elm2 ) return 1;
        return 0;
    }
    return function( array, element, compareFunction ){
        var low = 0;
        var high = array.length - 1;
        if( !compareFunction ) compareFunction = _compareFunction;
        while( low <= high ){
            var i = ( low + high ) >> 1;
            var cmp = compareFunction( element, array[ i ] );
            if( cmp > 0 ){
                low = i + 1;
            }else if( cmp < 0 ){
                high = i - 1;
            } else {
                return i;
            }
        }
        return -low - 1;
    }
}();


NGL.dataURItoImage = function( dataURI ){

    if( typeof importScripts !== 'function' ){
        var img = document.createElement( "img" );
        img.src = dataURI;
        return img;
    }

};


NGL.uniqueArray = function( array ){
    return array.sort().filter( function( value, index, sorted ){
        return ( index === 0 ) || ( value !== sorted[ index - 1 ] );
    } );
};


// String/arraybuffer conversion

NGL.Uint8ToString = function( u8a ){

    var chunkSize = 0x7000;

    if( u8a.length > chunkSize ){

      var c = [];

      for(var i = 0; i < u8a.length; i += chunkSize) {

        c.push( String.fromCharCode.apply(
          null, u8a.subarray( i, i + chunkSize )
        ) );

      }

      return c.join("");

    }else{

      return String.fromCharCode.apply( null, u8a );

    }

};


NGL.Uint8ToLines = function( u8a, chunkSize, newline ){

    NGL.time( "NGL.Uint8ToLines" );

    chunkSize = chunkSize !== undefined ? chunkSize : 1024 * 1024 * 10;
    newline = newline !== undefined ? newline : "\n";

    var partialLine = "";
    var lines = [];

    for( var i = 0; i < u8a.length; i += chunkSize ){

        var str = NGL.Uint8ToString( u8a.subarray( i, i + chunkSize ) );
        var idx = str.lastIndexOf( newline );

        if( idx === -1 ){

            partialLine += str;

        }else{

            var str2 = partialLine + str.substr( 0, idx );
            lines = lines.concat( str2.split( newline ) );

            if( idx === str.length - newline.length ){

                partialLine = "";

            }else{

                partialLine = str.substr( idx + newline.length );

            }

        }

    }

    if( partialLine !== "" ){

        lines.push( partialLine );

    }

    NGL.timeEnd( "NGL.Uint8ToLines" );

    return lines;

};


// Decompress

NGL.decompress = function( data ){

    var decompressedData;

    NGL.time( "NGL.decompress" );

    if( data instanceof ArrayBuffer ){
        data = new Uint8Array( data );
    }

    try{
        decompressedData = pako.ungzip( data );
    }catch( e ){
        if( NGL.debug ) NGL.warn( e );
        decompressedData = data;  // assume it is already uncompressed
    }

    NGL.timeEnd( "NGL.decompress" );

    return decompressedData;

};


// Counter

NGL.Counter = function(){

    var SIGNALS = signals;

    this.count = 0;

    this.signals = {

        countChanged: new SIGNALS.Signal(),

    }

};

NGL.Counter.prototype = {

    clear: function(){

        this.change( -this.count );

    },

    change: function( delta ){

        this.count += delta;
        this.signals.countChanged.dispatch( delta, this.count );

        if( this.count < 0 ){

            NGL.warn( "NGL.Counter.count below zero", this.count );

        }

    },

    increment: function(){

        this.change( 1 );

    },

    decrement: function(){

        this.change( -1 );

    },

    listen: function( counter ){

        // incorporate changes of another counter

        this.change( counter.count );

        counter.signals.countChanged.add( function( delta, count ){

            this.change( delta );

        }.bind( this ) );

    },

    onZeroOnce: function( callback, context ){

        if( this.count === 0 ){

            callback.call( context, 0, 0 );

        }else{

            var fn = function(){

                if( this.count === 0 ){

                    this.signals.countChanged.remove( fn, this );

                    callback.apply( context, arguments );

                }

            }

            this.signals.countChanged.add( fn, this );

        }

    },

    dispose: function(){

        this.clear();

    }

};


// Queue

NGL.Queue = function( fn, argList ){

    var queue = [];
    var pending = false;

    if( argList ){
        for( var i = 0, il = argList.length; i < il; ++i ){
            queue.push( argList[ i ] );
        }
        next();
    }

    function run( arg ){
        fn( arg, next );
    }

    function next(){
        var arg = queue.shift();
        if( arg !== undefined ){
            pending = true;
            setTimeout( function(){ run( arg ); } );
        }else{
            pending = false;
        }
    }

    // API

    this.push = function( arg ){
        queue.push( arg );
        if( !pending ) next();
    }

    this.kill = function( arg ){
        queue.length = 0;
    };

    this.length = function(){
        return queue.length;
    };

};

// File:js/ngl/proxy.js

/**
 * @file Proxy
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


// from Jmol http://jmol.sourceforge.net/jscolors/ (or 0xFFFFFF)
NGL.ElementColors = {
    "H": 0xFFFFFF, "HE": 0xD9FFFF, "LI": 0xCC80FF, "BE": 0xC2FF00, "B": 0xFFB5B5,
    "C": 0x909090, "N": 0x3050F8, "O": 0xFF0D0D, "F": 0x90E050, "NE": 0xB3E3F5,
    "NA": 0xAB5CF2, "MG": 0x8AFF00, "AL": 0xBFA6A6, "SI": 0xF0C8A0, "P": 0xFF8000,
    "S": 0xFFFF30, "CL": 0x1FF01F, "AR": 0x80D1E3, "K": 0x8F40D4, "CA": 0x3DFF00,
    "SC": 0xE6E6E6, "TI": 0xBFC2C7, "V": 0xA6A6AB, "CR": 0x8A99C7, "MN": 0x9C7AC7,
    "FE": 0xE06633, "CO": 0xF090A0, "NI": 0x50D050, "CU": 0xC88033, "ZN": 0x7D80B0,
    "GA": 0xC28F8F, "GE": 0x668F8F, "AS": 0xBD80E3, "SE": 0xFFA100, "BR": 0xA62929,
    "KR": 0x5CB8D1, "RB": 0x702EB0, "SR": 0x00FF00, "Y": 0x94FFFF, "ZR": 0x94E0E0,
    "NB": 0x73C2C9, "MO": 0x54B5B5, "TC": 0x3B9E9E, "RU": 0x248F8F, "RH": 0x0A7D8C,
    "PD": 0x006985, "AG": 0xC0C0C0, "CD": 0xFFD98F, "IN": 0xA67573, "SN": 0x668080,
    "SB": 0x9E63B5, "TE": 0xD47A00, "I": 0x940094, "XE": 0x940094, "CS": 0x57178F,
    "BA": 0x00C900, "LA": 0x70D4FF, "CE": 0xFFFFC7, "PR": 0xD9FFC7, "ND": 0xC7FFC7,
    "PM": 0xA3FFC7, "SM": 0x8FFFC7, "EU": 0x61FFC7, "GD": 0x45FFC7, "TB": 0x30FFC7,
    "DY": 0x1FFFC7, "HO": 0x00FF9C, "ER": 0x00E675, "TM": 0x00D452, "YB": 0x00BF38,
    "LU": 0x00AB24, "HF": 0x4DC2FF, "TA": 0x4DA6FF, "W": 0x2194D6, "RE": 0x267DAB,
    "OS": 0x266696, "IR": 0x175487, "PT": 0xD0D0E0, "AU": 0xFFD123, "HG": 0xB8B8D0,
    "TL": 0xA6544D, "PB": 0x575961, "BI": 0x9E4FB5, "PO": 0xAB5C00, "AT": 0x754F45,
    "RN": 0x428296, "FR": 0x420066, "RA": 0x007D00, "AC": 0x70ABFA, "TH": 0x00BAFF,
    "PA": 0x00A1FF, "U": 0x008FFF, "NP": 0x0080FF, "PU": 0x006BFF, "AM": 0x545CF2,
    "CM": 0x785CE3, "BK": 0x8A4FE3, "CF": 0xA136D4, "ES": 0xB31FD4, "FM": 0xB31FBA,
    "MD": 0xB30DA6, "NO": 0xBD0D87, "LR": 0xC70066, "RF": 0xCC0059, "DB": 0xD1004F,
    "SG": 0xD90045, "BH": 0xE00038, "HS": 0xE6002E, "MT": 0xEB0026, "DS": 0xFFFFFF,
    "RG": 0xFFFFFF, "CN": 0xFFFFFF, "UUT": 0xFFFFFF, "FL": 0xFFFFFF, "UUP": 0xFFFFFF,
    "LV": 0xFFFFFF, "UUH": 0xFFFFFF,

    "D": 0xFFFFC0, "T": 0xFFFFA0,

    "": 0xFFFFFF
};


// from Jmol http://jmol.sourceforge.net/jscolors/ (protein + shapely for nucleic)
/*NGL._ResidueColors = {
    "ALA": 0xC8C8C8,
    "ARG": 0x145AFF,
    "ASN": 0x00DCDC,
    "ASP": 0xE60A0A,
    "CYS": 0xE6E600,
    "GLN": 0x00DCDC,
    "GLU": 0xE60A0A,
    "GLY": 0xEBEBEB,
    "HIS": 0x8282D2,
    "ILE": 0x0F820F,
    "LEU": 0x0F820F,
    "LYS": 0x145AFF,
    "MET": 0xE6E600,
    "PHE": 0x3232AA,
    "PRO": 0xDC9682,
    "SER": 0xFA9600,
    "THR": 0xFA9600,
    "TRP": 0xB45AB4,
    "TYR": 0x3232AA,
    "VAL": 0x0F820F,

    "ASX": 0xFF69B4,
    "GLX": 0xFF69B4,
    "ASH": 0xFF69B4,
    "GLH": 0xFF69B4,

    "A": 0xA0A0FF,
    "G": 0xFF7070,
    "I": 0x80FFFF,
    "C": 0xFF8C4B,
    "T": 0xA0FFA0,
    "U": 0xFF8080,

    "DA": 0xA0A0FF,
    "DG": 0xFF7070,
    "DI": 0x80FFFF,
    "DC": 0xFF8C4B,
    "DT": 0xA0FFA0,
    "DU": 0xFF8080,

    "": 0xBEA06E
};*/
NGL.ResidueColors = {
    "ALA": 0x8CFF8C,
    "ARG": 0x00007C,
    "ASN": 0xFF7C70,
    "ASP": 0xA00042,
    "CYS": 0xFFFF70,
    "GLN": 0xFF4C4C,
    "GLU": 0x660000,
    "GLY": 0xFFFFFF,
    "HIS": 0x7070FF,
    "ILE": 0x004C00,
    "LEU": 0x455E45,
    "LYS": 0x4747B8,
    "MET": 0xB8A042,
    "PHE": 0x534C52,
    "PRO": 0x525252,
    "SER": 0xFF7042,
    "THR": 0xB84C00,
    "TRP": 0x4F4600,
    "TYR": 0x8C704C,
    "VAL": 0xFF8CFF,

    "ASX": 0xFF00FF,
    "GLX": 0xFF00FF,
    "ASH": 0xFF00FF,
    "GLH": 0xFF00FF,

    "A": 0xA0A0FF,
    "G": 0xFF7070,
    "I": 0x80FFFF,
    "C": 0xFF8C4B,
    "T": 0xA0FFA0,
    "U": 0xFF8080,

    "DA": 0xA0A0FF,
    "DG": 0xFF7070,
    "DI": 0x80FFFF,
    "DC": 0xFF8C4B,
    "DT": 0xA0FFA0,
    "DU": 0xFF8080,

    "": 0xFF00FF
};


// from Jmol http://jmol.sourceforge.net/jscolors/ (shapely)
NGL.StructureColors = {
    "alphaHelix": 0xFF0080,
    "3_10Helix": 0xA00080,
    "piHelix": 0x600080,
    "betaStrand": 0xFFC800,
    "betaTurn": 0x6080FF,
    "coil": 0xFFFFFF,

    "dna": 0xAE00FE,
    "rna": 0xFD0162,

    "carbohydrate": 0xA6A6FA,

    "": 0x808080
}


// PDB helix record encoding
NGL.HelixTypes = {
    1: "h",  // Right-handed alpha (default)
    2: "h",  // Right-handed omega
    3: "i",  // Right-handed pi
    4: "h",  // Right-handed gamma
    5: "g",  // Right-handed 310
    6: "h",  // Left-handed alpha
    7: "h",  // Left-handed omega
    8: "h",  // Left-handed gamma
    9: "h",  // 27 ribbon/helix
    10: "h",  // Polyproline
    "": "h",
}


// http://dx.doi.org/10.1021/jp8111556 (or 2.0)
NGL.VdwRadii = {
    "H": 1.1, "HE": 1.4, "LI": 1.81, "BE": 1.53, "B": 1.92, "C": 1.7,
    "N": 1.55, "O": 1.52, "F": 1.47, "NE": 1.54, "NA": 2.27, "MG": 1.73, "AL": 1.84,
    "SI": 2.1, "P": 1.8, "S": 1.8, "CL": 1.75, "AR": 1.88, "K": 2.75, "CA": 2.31,
    "SC": 2.3, "TI": 2.15, "V": 2.05, "CR": 2.05, "MN": 2.05, "FE": 2.05, "CO": 2.0,
    "NI": 2.0, "CU": 2.0, "ZN": 2.1, "GA": 1.87, "GE": 2.11, "AS": 1.85, "SE": 1.9,
    "BR": 1.83, "KR": 2.02, "RB": 3.03, "SR": 2.49, "Y": 2.4, "ZR": 2.3, "NB": 2.15,
    "MO": 2.1, "TC": 2.05, "RU": 2.05, "RH": 2.0, "PD": 2.05, "AG": 2.1, "CD": 2.2,
    "IN": 2.2, "SN": 1.93, "SB": 2.17, "TE": 2.06, "I": 1.98, "XE": 2.16, "CS": 3.43,
    "BA": 2.68, "LA": 2.5, "CE": 2.48, "PR": 2.47, "ND": 2.45, "PM": 2.43, "SM": 2.42,
    "EU": 2.4, "GD": 2.38, "TB": 2.37, "DY": 2.35, "HO": 2.33, "ER": 2.32, "TM": 2.3,
    "YB": 2.28, "LU": 2.27, "HF": 2.25, "TA": 2.2, "W": 2.1, "RE": 2.05, "OS": 2.0,
    "IR": 2.0, "PT": 2.05, "AU": 2.1, "HG": 2.05, "TL": 1.96, "PB": 2.02, "BI": 2.07,
    "PO": 1.97, "AT": 2.02, "RN": 2.2, "FR": 3.48, "RA": 2.83, "AC": 2.0, "TH": 2.4,
    "PA": 2.0, "U": 2.3, "NP": 2.0, "PU": 2.0, "AM": 2.0, "CM": 2.0, "BK": 2.0,
    "CF": 2.0, "ES": 2.0, "FM": 2.0, "MD": 2.0, "NO": 2.0, "LR": 2.0, "RF": 2.0,
    "DB": 2.0, "SG": 2.0, "BH": 2.0, "HS": 2.0, "MT": 2.0, "DS": 2.0, "RG": 2.0,
    "CN": 2.0, "UUT": 2.0, "FL": 2.0, "UUP": 2.0, "LV": 2.0, "UUH": 2.0,

    "": 2.0
};


// http://dx.doi.org/10.1039/b801115j (or 1.6)
NGL.CovalentRadii = {
    "H": 0.31, "HE": 0.28, "LI": 1.28, "BE": 0.96, "B": 0.84, "C": 0.76,
    "N": 0.71, "O": 0.66, "F": 0.57, "NE": 0.58, "NA": 1.66, "MG": 1.41, "AL": 1.21,
    "SI": 1.11, "P": 1.07, "S": 1.05, "CL": 1.02, "AR": 1.06, "K": 2.03, "CA": 1.76,
    "SC": 1.7, "TI": 1.6, "V": 1.53, "CR": 1.39, "MN": 1.39, "FE": 1.32, "CO": 1.26,
    "NI": 1.24, "CU": 1.32, "ZN": 1.22, "GA": 1.22, "GE": 1.2, "AS": 1.19, "SE": 1.2,
    "BR": 1.2, "KR": 1.16, "RB": 2.2, "SR": 1.95, "Y": 1.9, "ZR": 1.75, "NB": 1.64,
    "MO": 1.54, "TC": 1.47, "RU": 1.46, "RH": 1.42, "PD": 1.39, "AG": 1.45, "CD": 1.44,
    "IN": 1.42, "SN": 1.39, "SB": 1.39, "TE": 1.38, "I": 1.39, "XE": 1.4, "CS": 2.44,
    "BA": 2.15, "LA": 2.07, "CE": 2.04, "PR": 2.03, "ND": 2.01, "PM": 1.99, "SM": 1.98,
    "EU": 1.98, "GD": 1.96, "TB": 1.94, "DY": 1.92, "HO": 1.92, "ER": 1.89, "TM": 1.9,
    "YB": 1.87, "LU": 1.87, "HF": 1.75, "TA": 1.7, "W": 1.62, "RE": 1.51, "OS": 1.44,
    "IR": 1.41, "PT": 1.36, "AU": 1.36, "HG": 1.32, "TL": 1.45, "PB": 1.46, "BI": 1.48,
    "PO": 1.4, "AT": 1.5, "RN": 1.5, "FR": 2.6, "RA": 2.21, "AC": 2.15, "TH": 2.06,
    "PA": 2.0, "U": 1.96, "NP": 1.9, "PU": 1.87, "AM": 1.8, "CM": 1.69, "BK": 1.6,
    "CF": 1.6, "ES": 1.6, "FM": 1.6, "MD": 1.6, "NO": 1.6, "LR": 1.6, "RF": 1.6,
    "DB": 1.6, "SG": 1.6, "BH": 1.6, "HS": 1.6, "MT": 1.6, "DS": 1.6, "RG": 1.6,
    "CN": 1.6, "UUT": 1.6, "FL": 1.6, "UUP": 1.6, "LV": 1.6, "UUH": 1.6,

    "": 1.6
};


// Peter Rose (peter.rose@rcsb.org), private communication, average accross PDB
NGL.ResidueRadii = {
    "2QY": 6.58, "CY0": 11.98, "2QZ": 2.52, "CY1": 6.59, "HHK": 5.11, "CXM": 4.69, "HHI": 4.58, "CY4": 4.57,
    "S12": 18.57, "CY3": 2.79, "C5C": 5.35, "PFX": 11.84, "2R3": 6.94, "2R1": 3.78, "ILX": 4.99, "32S": 5.68,
    "BTK": 8.59, "32T": 5.72, "FAK": 9.8, "B27": 2.78, "ILM": 3.84, "C4R": 5.63, "32L": 6.75, "SYS": 3.01,
    "1MH": 5.04, "ILE": 3.65, "YNM": 6.39, "2RX": 4.91, "B3A": 2.48, "GEE": 4.76, "7MN": 7.34, "B3E": 5.4,
    "ARG": 6.33, "200": 6.89, "HIP": 5.47, "HIA": 4.64, "B3K": 5.89, "HIC": 5.76, "B3L": 4.96, "B3M": 5.07,
    "ARM": 6.86, "ARO": 7.35, "AR4": 8.42, "PG1": 10.67, "YOF": 6.44, "IML": 3.74, "SXE": 6.65, "HIQ": 7.98,
    "PFF": 6.31, "HIS": 4.52, "0TD": 3.62, "C3Y": 5.24, "1OP": 11.55, "02Y": 4.77, "02V": 4.83, "ASB": 5.59,
    "30V": 8.53, "S2P": 4.81, "ASP": 3.55, "ASN": 3.54, "2OR": 6.91, "QMM": 6.13, "2P0": 8.52, "ASL": 5.36,
    "HFA": 5.14, "5PG": 5.69, "B3X": 4.38, "AS9": 4.1, "ARV": 7.59, "B3U": 6.06, "S2C": 7.54, "B3T": 3.34,
    "175": 5.64, "GFT": 8.18, "HG7": 6.8, "B3Q": 4.48, "ASA": 3.64, "02K": 2.94, "B3Y": 7.45, "PHD": 5.35,
    "C6C": 6.42, "BUC": 5.8, "HGL": 8.07, "PHE": 5.06, "03Y": 2.6, "PHA": 5.11, "OCY": 5.0, "4PH": 6.79,
    "5OH": 4.7, "31Q": 10.46, "BTR": 7.98, "3PX": 4.7, "1PA": 8.07, "ASX": 3.54, "IOR": 7.23, "03E": 3.38,
    "PHL": 5.17, "KWS": 5.09, "PHI": 7.12, "NAL": 7.22, "S1H": 19.21, "2ML": 3.86, "2MR": 7.35, "GHG": 4.83,
    "TYY": 6.54, "2MT": 3.67, "56A": 13.01, "SVA": 5.46, "TYX": 8.31, "TYS": 8.59, "TYR": 6.38, "TYQ": 6.43,
    "HLU": 3.99, "MYK": 19.47, "TYO": 7.71, "HLX": 4.98, "TYN": 9.87, "TYJ": 6.25, "TYI": 6.49, "LYH": 5.13,
    "LYF": 12.19, "SUN": 6.73, "LYR": 18.28, "TYB": 6.46, "11W": 14.39, "LYS": 5.54, "LYN": 4.8, "11Q": 4.85,
    "LYO": 4.71, "LYZ": 1.76, "TXY": 6.44, "MYN": 4.71, "TY5": 10.6, "HMR": 5.09, "01W": 8.55, "LYX": 13.36,
    "TY8": 7.22, "TY2": 6.49, "KYN": 6.18, "KYQ": 9.75, "CZZ": 5.14, "IIL": 3.81, "HNC": 10.41, "OIC": 4.62,
    "LVN": 2.89, "QIL": 3.84, "JJL": 8.3, "VAH": 3.88, "JJJ": 7.5, "JJK": 7.43, "VAD": 2.56, "CYW": 4.65,
    "0QL": 5.72, "143": 8.22, "SVX": 7.04, "CYJ": 11.64, "SVY": 7.1, "SVZ": 6.6, "CYG": 8.03, "CYF": 13.54,
    "SVV": 5.09, "GL3": 2.72, "8SP": 14.26, "CYS": 2.78, "004": 4.33, "CYR": 10.33, "PLJ": 3.71, "EXY": 7.37,
    "HL2": 3.75, "A5N": 5.21, "CYQ": 5.67, "CZ2": 5.16, "LWY": 4.12, "PM3": 8.78, "OHS": 6.98, "OHI": 5.35,
    "3TY": 8.42, "CYD": 8.55, "DYS": 7.87, "DAH": 6.47, "4IK": 11.81, "3EG": 3.66, "AYA": 3.65, "4IN": 6.31,
    "DAB": 3.48, "4HT": 6.03, "RGL": 7.03, "DAM": 2.49, "NFA": 5.04, "WFP": 6.07, "2JC": 2.97, "HAR": 7.55,
    "2JG": 5.67, "MH6": 1.72, "2JF": 9.13, "3FG": 4.96, "MGN": 4.84, "AZH": 5.36, "AZK": 6.03, "ZBZ": 7.79,
    "TBG": 2.58, "VAL": 2.51, "MGG": 7.34, "AZS": 5.61, "FHL": 9.75, "2JH": 4.56, "IEL": 7.07, "FHO": 6.75,
    "DA2": 7.79, "FH7": 6.99, "ME0": 4.52, "3GL": 4.84, "MDO": 5.03, "AZY": 7.37, "A8E": 3.76, "ZCL": 6.71,
    "MDH": 2.58, "LA2": 14.07, "4FW": 6.1, "YCM": 5.32, "MDF": 4.95, "YCP": 3.01, "TEF": 8.63, "FGP": 4.34,
    "UF0": 19.72, "XCN": 4.57, "FGL": 2.56, "MF3": 6.37, "MEQ": 5.13, "LAA": 3.23, "IGL": 5.52, "MET": 4.49,
    "NIY": 6.81, "QCS": 5.18, "TCQ": 8.56, "MEN": 4.33, "4HL": 8.79, "MEA": 4.95, "EFC": 5.28, "LAL": 2.41,
    "2HF": 5.52, "KBE": 5.64, "OCS": 3.94, "CAF": 5.46, "NC1": 11.4, "NBQ": 9.82, "CAB": 4.19, "MBQ": 9.55,
    "193": 7.38, "192": 2.44, "0WZ": 7.61, "CAS": 5.35, "NB8": 11.98, "OBS": 11.71, "1AC": 2.42, "PCA": 3.48,
    "MCL": 9.73, "LBY": 7.75, "GAU": 4.67, "PBF": 9.75, "MCG": 6.46, "DDE": 6.86, "19W": 3.94, "MD5": 9.33,
    "MD6": 6.44, "MD3": 8.41, "MCS": 7.56, "OBF": 3.64, "UAL": 4.68, "PAT": 6.05, "IAM": 8.88, "PAQ": 8.77,
    "FDL": 9.49, "NCB": 3.45, "LCK": 9.81, "DDZ": 2.52, "2FM": 5.54, "IAR": 6.77, "OAS": 4.8, "HBN": 8.8,
    "TA4": 5.55, "1C3": 7.43, "ECX": 5.51, "PF5": 6.28, "RE3": 5.29, "FCL": 6.25, "ECC": 4.79, "LDH": 7.06,
    "NCY": 2.91, "CCS": 4.58, "PEC": 6.54, "2CO": 4.45, "LE1": 2.72, "HCM": 5.53, "07O": 8.05, "HCL": 4.96,
    "NEP": 6.94, "PE1": 8.01, "LEF": 4.37, "FC0": 5.18, "LED": 4.34, "HCS": 4.09, "DBU": 2.49, "RE0": 5.53,
    "LEN": 3.82, "1E3": 8.71, "BB9": 2.56, "BB8": 5.14, "PCS": 5.05, "BB7": 4.56, "BB6": 2.62, "LEU": 3.83,
    "DBZ": 7.08, "LET": 11.29, "DBY": 6.46, "ICY": 7.76, "MAA": 2.4, "CGA": 7.91, "5CS": 8.34, "UGY": 3.7,
    "LGY": 11.71, "N10": 8.96, "AAR": 6.39, "FT6": 7.5, "MOD": 12.62, "5CW": 7.21, "PVH": 4.58, "BBC": 6.42,
    "YYA": 7.3, "O12": 14.08, "NOT": 7.15, "KGC": 9.88, "MP4": 5.86, "0CS": 4.07, "MP8": 3.75, "VLL": 2.54,
    "VLM": 2.51, "BCS": 8.03, "MNL": 4.9, "AA4": 4.47, "SAC": 3.49, "BCX": 2.99, "3CF": 6.47, "SAH": 11.7,
    "NNH": 6.86, "CGU": 4.71, "SIB": 12.41, "TLY": 8.78, "SIC": 4.81, "VMS": 8.82, "TMD": 6.76, "MMO": 6.53,
    "PXU": 2.46, "4AW": 6.22, "OTH": 3.6, "DLS": 6.84, "MME": 4.99, "DM0": 6.99, "0FL": 2.76, "SBL": 8.96,
    "CDV": 3.72, "OTY": 6.51, "PYA": 7.75, "2AS": 3.57, "DMH": 4.92, "ELY": 7.42, "GVL": 9.6, "FVA": 2.9,
    "SAR": 2.48, "4BF": 6.92, "EME": 4.69, "CDE": 2.51, "3AR": 7.86, "3AH": 9.11, "AC5": 2.44, "FTR": 6.08,
    "MLL": 3.76, "NPH": 11.66, "NPI": 6.9, "DMT": 6.67, "PYX": 11.3, "MLE": 3.87, "PYL": 9.67, "ZZU": 6.94,
    "H5M": 3.61, "SCH": 4.46, "DMK": 3.52, "FTY": 9.07, "2AG": 3.7, "ABA": 2.55, "ZZJ": 2.44, "MLZ": 6.8,
    "MLY": 6.88, "KCX": 7.28, "ZZD": 8.16, "3A5": 5.37, "LHC": 7.75, "9AT": 2.47, "OZT": 3.4, "THO": 2.62,
    "THR": 2.5, "DFI": 3.93, "MKD": 6.42, "4CY": 4.6, "SDP": 6.07, "DFO": 3.94, "0A0": 3.45, "4DB": 9.73,
    "ML3": 6.26, "BG1": 8.02, "SD4": 4.57, "THC": 3.8, "SCS": 5.48, "TH5": 4.65, "BFD": 5.33, "AEI": 6.34,
    "TH6": 2.85, "SCY": 4.53, "TIS": 4.81, "SEE": 4.53, "BHD": 3.48, "SEB": 8.18, "SEC": 2.96, "SEP": 4.8,
    "CLH": 7.13, "TIH": 5.02, "CLG": 13.62, "SEN": 6.43, "XXA": 7.34, "SEL": 2.46, "SE7": 4.19, "4CF": 7.72,
    "G8M": 3.57, "BH2": 3.51, "UN2": 3.22, "VR0": 10.51, "MK8": 4.76, "DHA": 2.32, "LMQ": 4.69, "SFE": 5.01,
    "AHB": 3.47, "OXX": 7.05, "BIF": 9.63, "IZO": 4.47, "NMM": 8.25, "0BN": 7.0, "HZP": 3.12, "NMC": 4.23,
    "DHL": 2.69, "9DS": 9.29, "SER": 2.41, "CHG": 4.2, "MIR": 6.54, "AGQ": 7.79, "SET": 2.46, "MIS": 6.32,
    "4FB": 3.08, "0AR": 8.46, "LME": 3.99, "FZN": 24.42, "AGT": 9.04, "IYR": 6.46, "9DN": 9.31, "CHP": 5.75,
    "UNK": 1.64, "XX1": 9.92, "AGM": 6.57, "0AH": 5.78, "LLP": 10.22, "0AF": 6.72, "4DP": 9.28, "HYP": 2.25,
    "DIR": 5.8, "LLY": 8.71, "0AK": 6.11, "NLE": 4.67, "OYL": 6.42, "WVL": 4.69, "0A8": 8.1, "NLY": 6.37,
    "MHO": 4.89, "VOL": 2.55, "0A1": 7.1, "MHL": 3.92, "NLP": 4.81, "NLQ": 4.65, "MHW": 2.74, "BIL": 4.7,
    "NLO": 4.8, "MHU": 7.51, "XW1": 9.36, "LLO": 10.13, "SGB": 6.88, "MHV": 3.6, "MHS": 4.51, "0A9": 5.17,
    "0LF": 9.96, "HT7": 6.82, "X2W": 6.6, "YPZ": 9.38, "I58": 6.73, "FLA": 2.4, "M0H": 4.83, "HSL": 2.46,
    "FLE": 6.17, "KOR": 10.1, "1VR": 3.89, "HSO": 4.56, "TTS": 9.41, "RVX": 7.01, "TTQ": 7.71, "H14": 5.27,
    "HTI": 7.8, "ONH": 6.14, "LP6": 8.58, "ONL": 4.83, "AHH": 5.06, "HS8": 7.4, "HS9": 4.71, "BL2": 5.82,
    "AHP": 5.26, "6HN": 7.34, "HRP": 5.46, "POM": 3.6, "WPA": 5.11, "2ZC": 4.29, "CPC": 2.65, "AIB": 2.4,
    "XSN": 3.47, "M2S": 5.28, "GND": 6.67, "GNC": 4.6, "MVA": 2.56, "OLZ": 5.32, "M2L": 6.15, "TRF": 6.69,
    "NZH": 7.66, "SRZ": 5.27, "OLD": 10.47, "CME": 5.86, "CMH": 5.3, "ALA": 2.38, "TRQ": 7.36, "PPN": 7.24,
    "TRP": 6.07, "TRO": 5.82, "TRN": 5.95, "NYS": 8.1, "ALC": 5.26, "U3X": 11.7, "HVA": 2.58, "TS9": 3.92,
    "TRX": 7.27, "TRW": 11.8, "LPL": 7.51, "GMA": 4.4, "OMT": 5.07, "CMT": 3.54, "GME": 4.66, "NYB": 6.07,
    "PR3": 5.12, "LPD": 2.48, "GLU": 4.49, "1X6": 6.84, "LPG": 2.39, "GLX": 4.52, "PR4": 4.52, "CML": 6.16,
    "FME": 4.52, "HTR": 6.48, "PR7": 4.66, "Z3E": 7.2, "GLZ": 2.39, "BMT": 6.37, "WRP": 8.16, "GLY": 2.37,
    "OMY": 6.11, "MTY": 5.46, "OMX": 6.15, "GLN": 4.46, "2XA": 8.25, "28X": 7.84, "7JA": 9.46, "FLT": 9.65,
    "GLJ": 3.7, "OMH": 5.26, "TSY": 4.26, "PRV": 4.28, "CS4": 11.21, "DOA": 12.33, "23P": 5.42, "CS3": 8.24,
    "6CL": 6.47, "PRR": 5.58, "KST": 11.58, "CS1": 7.23, "PRS": 2.63, "ZYJ": 11.4, "IT1": 9.75, "UU5": 4.98,
    "ESB": 6.69, "UU4": 2.49, "ESC": 5.65, "LSO": 10.58, "ZYK": 11.45, "9NV": 8.99, "23F": 5.27, "ORN": 4.25,
    "HOX": 6.61, "CSD": 3.95, "FP9": 3.03, "DO2": 4.44, "SLL": 11.53, "P3Q": 9.54, "ORQ": 6.04, "MSL": 5.21,
    "DNP": 2.45, "CSB": 3.51, "WLU": 4.24, "CSA": 5.7, "MT2": 5.51, "CSO": 3.53, "TPO": 4.73, "MSP": 13.11,
    "23S": 6.09, "MSO": 4.96, "PRO": 2.41, "TPL": 5.41, "DNS": 8.79, "CSK": 3.91, "Z70": 7.4, "CSJ": 7.51,
    "DNW": 7.97, "PRK": 9.15, "GSU": 11.81, "LTA": 6.57, "HPE": 6.63, "TPQ": 6.48, "PRJ": 5.26, "PSW": 4.65,
    "L3O": 3.89, "CSU": 4.89, "ALY": 7.38, "M3L": 7.12, "CSW": 3.68, "XPR": 7.68, "D4P": 5.66, "FOE": 8.17,
    "SLZ": 5.69, "CSP": 5.26, "TQI": 7.68, "ALT": 2.72, "CSR": 5.42, "CSS": 3.61, "M3R": 7.18, "ALO": 2.57,
    "R4K": 4.67, "SMF": 9.0, "MSA": 2.73, "SMC": 3.39, "CSX": 3.47, "SME": 4.8, "ETA": 2.4, "CSZ": 3.6,
    "22G": 8.8, "MSE": 4.62, "ALN": 6.16, "PSH": 7.26, "CTE": 7.27, "DON": 6.72, "CTH": 3.45, "U2X": 11.54,
    "6CW": 7.56, "TQZ": 6.97, "3YM": 6.52, "OSE": 4.49, "2VA": 9.82, "TQQ": 7.76, "NRG": 8.35, "BPE": 7.24,
    "F2F": 6.25, "1TQ": 8.58, "I2M": 3.13, "NVA": 3.76, "R1A": 8.2, "QPA": 6.95, "C1X": 11.63, "FRD": 5.05,
    "HR7": 6.98, "SNC": 3.93, "QPH": 5.15, "26B": 8.39, "DPQ": 6.54, "DPP": 2.51, "2TY": 8.65, "TNR": 6.88,
    "PTH": 8.35, "DPL": 3.58, "APK": 8.79, "1TY": 8.84, "HRG": 7.36, "PTM": 8.74, "1U8": 3.62, "PTR": 8.64,
    "LVG": 3.01, "6FL": 4.85, "SOC": 4.05, "KPI": 9.79, "IPG": 2.91, "P2Y": 2.51, "N2C": 3.55, "T0I": 7.34,
    "MPH": 5.29, "R2T": 4.71, "TOX": 6.78, "P2Q": 9.8, "GPL": 10.77, "MPJ": 5.07, "F2Y": 6.2, "T11": 8.58,
    "9NR": 9.33, "FPR": 8.85, "9NF": 8.93, "KPY": 10.17, "9NE": 9.77, "TOQ": 7.5, "MPQ": 4.2, "FPK": 3.08,
    "HQA": 7.25, "SOY": 10.94,

    "": 5.0
};


// http://blanco.biomol.uci.edu/Whole_residue_HFscales.txt
NGL.ResidueHydrophobicity = {
    // AA  DGwif   DGwoct  Oct-IF
    "ALA": [  0.17,  0.50,  0.33 ],
    "ARG": [  0.81,  1.81,  1.00 ],
    "ASN": [  0.42,  0.85,  0.43 ],
    "ASP": [  1.23,  3.64,  2.41 ],
    "ASH": [ -0.07,  0.43,  0.50 ],
    "CYS": [ -0.24, -0.02,  0.22 ],
    "GLN": [  0.58,  0.77,  0.19 ],
    "GLU": [  2.02,  3.63,  1.61 ],
    "GLH": [ -0.01,  0.11,  0.12 ],
    "GLY": [  0.01,  1.15,  1.14 ],
    // "His+": [  0.96,  2.33,  1.37 ],
    "HIS": [  0.17,  0.11, -0.06 ],
    "ILE": [ -0.31, -1.12, -0.81 ],
    "LEU": [ -0.56, -1.25, -0.69 ],
    "LYS": [  0.99,  2.80,  1.81 ],
    "MET": [ -0.23, -0.67, -0.44 ],
    "PHE": [ -1.13, -1.71, -0.58 ],
    "PRO": [  0.45,  0.14, -0.31 ],
    "SER": [  0.13,  0.46,  0.33 ],
    "THR": [  0.14,  0.25,  0.11 ],
    "TRP": [ -1.85, -2.09, -0.24 ],
    "TYR": [ -0.94, -0.71,  0.23 ],
    "VAL": [  0.07, -0.46, -0.53 ],

    "": [ 0.00, 0.00, 0.00 ]
};


NGL.guessElement = function(){

    var elm1 = [ "H", "C", "O", "N", "S", "P" ];
    var elm2 = [ "NA", "CL" ];

    return function( atomName ){

        var at = atomName.trim().toUpperCase();
        if( parseInt( at.charAt( 0 ) ) ) at = at.substr( 1 );
        // parse again to check for a second integer
        if( parseInt( at.charAt( 0 ) ) ) at = at.substr( 1 );
        var n = at.length;

        if( n===0 ) return "";

        if( n===1 ) return at;

        if( n===2 ){

            if( elm2.indexOf( at )!==-1 ) return at;

            if( elm1.indexOf( at[0] )!==-1 ) return at[0];

        }

        if( n>=3 ){

            if( elm1.indexOf( at[0] )!==-1 ) return at[0];

        }

        return "";

    };

}();


// molecule types
NGL.UnknownType = 0;
NGL.WaterType = 1;
NGL.IonType = 2;
NGL.ProteinType = 3;
NGL.RnaType = 4;
NGL.DnaType = 5;

// backbone types
NGL.UnknownBackboneType = 0;
NGL.ProteinBackboneType = 1;
NGL.RnaBackboneType = 2;
NGL.DnaBackboneType = 3;
NGL.CgProteinBackboneType = 4;
NGL.CgRnaBackboneType = 5;
NGL.CgDnaBackboneType = 6;


NGL.AA1 = {
    'HIS': 'H',
    'ARG': 'R',
    'LYS': 'K',
    'ILE': 'I',
    'PHE': 'F',
    'LEU': 'L',
    'TRP': 'W',
    'ALA': 'A',
    'MET': 'M',
    'PRO': 'P',
    'CYS': 'C',
    'ASN': 'N',
    'VAL': 'V',
    'GLY': 'G',
    'SER': 'S',
    'GLN': 'Q',
    'TYR': 'Y',
    'ASP': 'D',
    'GLU': 'E',
    'THR': 'T',

    'ASH': 'D',
    'GLH': 'E',

    'UNK': '',
};

NGL.AA3 = Object.keys( NGL.AA1 );

NGL.RnaBases = [ "A", "C", "T", "G", "U" ];

NGL.DnaBases = [ "DA", "DC", "DT", "DG", "DU", "TCY", "MCY", "5CM" ];

NGL.PurinBases = [ "A", "G", "DA", "DG" ];

NGL.WaterNames = [ "SOL", "WAT", "HOH", "H2O", "W", "DOD", "D3O" ];

NGL.IonNames = [
    "3CO", "3NI", "4MO", "6MO", "AG", "AL", "AU", "AU3", "BA", "BR", "CA",
    "CD", "CE", "CL", "CO", "CR", "CU", "CU1", "CU3", "F", "FE", "FE2", "GA",
    "K", "LI", "MG", "MN", "MN3", "NA", "ND4", "NH4", "NI", "OH", "RB", "SR",
    "V", "Y1", "YT3", "ZN"
];

// ligands that are wrongly detected as protein or rna/dna based on atom names
NGL.LigandNames = [ "FOL" ];


NGL.ProteinBackboneAtoms = [
    "CA", "C", "N", "O", "O1", "O2", "OC1", "OC2",
    "H", "H1", "H2", "H3", "HA",
    "BB"
];

NGL.NucleicBackboneAtoms = [
    "P", "O3'", "O5'", "C5'", "C4'", "C3'", "OP1", "OP2",
    "O3*", "O5*", "C5*", "C4*", "C3*"
];


//////////
// Proxy



//////////////
// BondProxy

NGL.BondProxy = function( structure, index ){

    this.structure = structure;
    this.bondStore = structure.bondStore;
    this.index = index;

};

NGL.BondProxy.prototype = {

    constructor: NGL.BondProxy,
    type: "BondProxy",

    structure: undefined,
    bondStore: undefined,
    index: undefined,

    get atom1 () {
        return this.structure.getAtomProxy( this.atomIndex1 );
    },

    get atom2 () {
        return this.structure.getAtomProxy( this.atomIndex2 );
    },

    get atomIndex1 () {
        return this.bondStore.atomIndex1[ this.index ];
    },
    set atomIndex1 ( value ) {
        this.bondStore.atomIndex1[ this.index ] = value;
    },

    get atomIndex2 () {
        return this.bondStore.atomIndex2[ this.index ];
    },
    set atomIndex2 ( value ) {
        this.bondStore.atomIndex2[ this.index ] = value;
    },

    get bondOrder () {
        return this.bondStore.bondOrder[ this.index ];
    },
    set bondOrder ( value ) {
        this.bondStore.bondOrder[ this.index ] = value;
    },

    //

    qualifiedName: function(){

        return this.atomIndex1 + "=" + this.atomIndex2;

    },

    clone: function(){

        return new this.constructor( this.structure, this.index );

    },

    toObject: function(){

        return {
            atomIndex1: this.atomIndex1,
            atomIndex2: this.atomIndex2,
            bondOrder: this.bondOrder
        };

    }

};


//////////////
// AtomProxy

NGL.AtomProxy = function( structure, index ){

    this.structure = structure;
    this.chainStore = structure.chainStore;
    this.residueStore = structure.residueStore;
    this.atomStore = structure.atomStore;
    this.residueMap = structure.residueMap;
    this.atomMap = structure.atomMap;
    this.index = index;

};

NGL.AtomProxy.prototype = {

    constructor: NGL.AtomProxy,
    type: "AtomProxy",

    structure: undefined,
    chainStore: undefined,
    residueStore: undefined,
    atomStore: undefined,
    index: undefined,

    get modelIndex () {
        return this.chainStore.modelIndex[ this.chainIndex ];
    },
    get chainIndex () {
        return this.residueStore.chainIndex[ this.residueIndex ];
    },
    get residue () {
        console.warn("residue")
        return this.structure.getResidueProxy( this.residueIndex, false );
    },

    get residueIndex () {
        return this.atomStore.residueIndex[ this.index ];
    },
    set residueIndex ( value ) {
        this.atomStore.residueIndex[ this.index ] = value;
    },

    //

    get sstruc () {
        return this.residueStore.getSstruc( this.residueIndex );
    },
    get inscode () {
        return this.residueStore.getInscode( this.residueIndex );
    },
    get resno () {
        return this.residueStore.resno[ this.residueIndex ];
    },
    get chainname () {
        return this.chainStore.getChainname( this.chainIndex );
    },

    //

    get residueType () {
        return this.residueMap.get( this.residueStore.residueTypeId[ this.residueIndex ] );
    },
    get atomType () {
        return  this.atomMap.get( this.atomStore.atomTypeId[ this.index ] );
    },

    //

    get resname () {
        return this.residueType.resname;
    },
    get hetero () {
        return this.residueType.hetero;
    },

    //

    get atomname () {
        return this.atomType.atomname;
    },
    get element () {
        return this.atomType.element;
    },
    get vdw () {
        return this.atomType.vdw;
    },
    get covalent () {
        return this.atomType.covalent;
    },

    //

    get x () {
        return this.atomStore.x[ this.index ];
    },
    set x ( value ) {
        this.atomStore.x[ this.index ] = value;
    },

    get y () {
        return this.atomStore.y[ this.index ];
    },
    set y ( value ) {
        this.atomStore.y[ this.index ] = value;
    },

    get z () {
        return this.atomStore.z[ this.index ];
    },
    set z ( value ) {
        this.atomStore.z[ this.index ] = value;
    },

    get serial () {
        return this.atomStore.serial[ this.index ];
    },
    set serial ( value ) {
        this.atomStore.serial[ this.index ] = value;
    },

    get bfactor () {
        return this.atomStore.bfactor[ this.index ];
    },
    set bfactor ( value ) {
        this.atomStore.bfactor[ this.index ] = value;
    },

    get occupancy () {
        return this.atomStore.occupancy[ this.index ];
    },
    set occupancy ( value ) {
        this.atomStore.occupancy[ this.index ] = value;
    },

    // get bonds () {
    //     return this.atomStore.bonds[ this.index ];
    // },
    // set bonds ( value ) {
    //     this.atomStore.bonds[ this.index ] = value;
    // },

    get altloc () {
        return this.atomStore.getAltloc( this.index );
    },
    set altloc ( value ) {
        this.atomStore.setAltloc( this.index, value );
    },

    //

    isBackbone: function(){
        var backboneIndexList = this.residueType.backboneIndexList;
        // console.log(backboneIndexList)
        if( backboneIndexList.length > 0 ){
            var atomOffset = this.residueStore.atomOffset[ this.residueIndex ];
            return backboneIndexList.indexOf( this.index - atomOffset ) !== -1;
        }else{
            return false;
        }
    },

    isPolymer: function(){
        var moleculeType = this.residueType.moleculeType;
        return (
            moleculeType === NGL.ProteinType ||
            moleculeType === NGL.RnaType ||
            moleculeType === NGL.DnaType ||
            moleculeType === NGL.CgType
        );
    },

    isSidechain: function(){
        return this.isPolymer() && !this.isBackbone();
    },

    isCg: function(){
        var backboneType = this.residueType.backboneType;
        return (
            backboneType === NGL.CgProteinBackboneType ||
            backboneType === NGL.CgRnaBackboneType ||
            backboneType === NGL.CgDnaBackboneType
        );
    },

    isHetero: function(){
        return this.residueType.hetero === 1;
    },

    isProtein: function(){
        return this.residueType.moleculeType === NGL.ProteinType;
    },

    isNucleic: function(){
        var moleculeType = this.residueType.moleculeType;
        return (
            moleculeType === NGL.RnaType ||
            moleculeType === NGL.DnaType
        );
    },

    isRna: function(){
        return this.residueType.moleculeType === NGL.RnaType;
    },

    isDna: function(){
        return this.residueType.moleculeType === NGL.DnaType;
    },

    isWater: function(){
        return this.residueType.moleculeType === NGL.WaterType;
    },

    isIon: function(){
        return this.residueType.moleculeType === NGL.IonType;
    },

    distanceTo: function( atom ){
        var taa = this.atomStore;
        var aaa = atom.atomStore;
        var ti = this.index;
        var ai = atom.index;
        var x = taa.x[ ti ] - aaa.x[ ai ];
        var y = taa.y[ ti ] - aaa.y[ ai ];
        var z = taa.z[ ti ] - aaa.z[ ai ];
        var distSquared = x * x + y * y + z * z;
        return Math.sqrt( distSquared );
    },

    connectedTo: function( atom ){

        var taa = this.atomStore;
        var aaa = atom.atomStore;
        var ti = this.index;
        var ai = atom.index;

        if( taa.altloc && aaa.altloc ){
            var ta = taa.altloc[ ti ];  // use Uint8 value to compare
            var aa = aaa.altloc[ ai ];  // no need to convert to char
            // 0 is the Null character, 32 is the space character
            if( !( ta === 0 || aa === 0 || ta === 32 || aa === 32 || ( ta === aa ) ) ) return false;
        }

        var x = taa.x[ ti ] - aaa.x[ ai ];
        var y = taa.y[ ti ] - aaa.y[ ai ];
        var z = taa.z[ ti ] - aaa.z[ ai ];

        var distSquared = x * x + y * y + z * z;

        // if( this.residue.isCg() ) console.log( this.qualifiedName(), Math.sqrt( distSquared ), distSquared )
        if( distSquared < 64.0 && this.isCg() ) return true;

        if( isNaN( distSquared ) ) return false;

        var d = this.covalent + atom.covalent;
        var d1 = d + 0.3;
        var d2 = d - 0.5;

        return distSquared < ( d1 * d1 ) && distSquared > ( d2 * d2 );

    },

    positionFromArray: function( array, offset ){

        if( offset === undefined ) offset = 0;

        this.x = array[ offset + 0 ];
        this.y = array[ offset + 1 ];
        this.z = array[ offset + 2 ];

        return this;

    },

    positionToArray: function( array, offset ){

        if( array === undefined ) array = [];
        if( offset === undefined ) offset = 0;

        array[ offset + 0 ] = this.x;
        array[ offset + 1 ] = this.y;
        array[ offset + 2 ] = this.z;

        return array;

    },

    positionToVector3: function( v ){

        if( v === undefined ) v = new THREE.Vector3();

        v.x = this.x;
        v.y = this.y;
        v.z = this.z;

        return v;

    },

    positionFromVector3: function( v ){

        this.x = v.x;
        this.y = v.y;
        this.z = v.z;

        return this;

    },

    //

    qualifiedName: function( noResname ){
        var name = "";
        if( this.resname && !noResname ) name += "[" + this.resname + "]";
        if( this.resno !== undefined ) name += this.resno;
        if( this.inscode ) name += "^" + this.inscode;
        if( this.chainname ) name += ":" + this.chainname;
        if( this.atomname ) name += "." + this.atomname;
        if( this.altloc ) name += "%" + this.altloc;
        name += "/" + this.modelIndex;
        return name;
    },

    clone: function(){

        return new this.constructor( this.structure, this.index );

    },

    toObject: function(){

        return {
            index: this.index,
            residueIndex: this.residueIndex,

            atomno: this.atomno,
            resname: this.resname,
            x: this.x,
            y: this.y,
            z: this.z,
            element: this.element,
            chainname: this.chainname,
            resno: this.resno,
            serial: this.serial,
            vdw: this.vdw,
            covalent: this.covalent,
            hetero: this.hetero,
            bfactor: this.bfactor,
            altloc: this.altloc,
            atomname: this.atomname,
            modelindex: this.modelindex
        };

    }

};


/////////////////
// ResidueProxy

NGL.Residue = {

    atomnames: function(){

        var atomnames = {};

        atomnames[ NGL.ProteinBackboneType ] = {
            trace: "CA",
            direction1: "C",
            direction2: [ "O", "OC1", "O1", "OX1" ],
            backboneStart: "N",
            backboneEnd: "C",
        };

        atomnames[ NGL.RnaBackboneType ] = {
            trace: [ "C4'", "C4*" ],
            direction1: [ "C1'", "C1*" ],
            direction2: [ "C3'", "C3*" ],
            backboneStart: "P",
            backboneEnd: [ "O3'", "O3*" ]
        };

        atomnames[ NGL.DnaBackboneType ] = {
            trace: [ "C3'", "C3*" ],
            direction1: [ "C2'", "C2*" ],
            direction2: [ "O4'", "O4*" ],
            backboneStart: "P",
            backboneEnd: [ "O3'", "O3*" ]
        };

        atomnames[ NGL.CgProteinBackboneType ] = {
            trace: [ "CA", "BB" ],
            backboneStart: [ "CA", "BB" ],
            backboneEnd: [ "CA", "BB" ],
        };

        atomnames[ NGL.CgRnaBackboneType ] = {
            trace: [ "C4'", "C4*", "P" ],
            backboneStart: [ "C4'", "C4*", "P" ],
            backboneEnd: [ "C4'", "C4*", "P" ],
        };

        atomnames[ NGL.CgDnaBackboneType ] = {
            trace: [ "C3'", "C3*", "C2'", "P" ],  // C2' is used in martini ff
            backboneStart: [ "C3'", "C3*", "C2'", "P" ],
            backboneEnd: [ "C3'", "C3*", "C2'", "P" ],
        };

        atomnames[ NGL.UnknownBackboneType ] = {};

        return atomnames;

    }()

};


NGL.ResidueProxy = function( structure, index ){

    this.structure = structure;
    this.chainStore = structure.chainStore;
    this.residueStore = structure.residueStore;
    this.atomStore = structure.atomStore;
    this.residueMap = structure.residueMap;
    this.atomMap = structure.atomMap;
    this.index = index;

};

NGL.ResidueProxy.prototype = {

    constructor: NGL.ResidueProxy,
    type: "ResidueProxy",

    structure: undefined,
    chainStore: undefined,
    residueStore: undefined,
    atomStore: undefined,
    index: undefined,

    get chain () {
        return this.structure.getChainProxy( this.chainIndex );
    },

    get chainIndex () {
        return this.residueStore.chainIndex[ this.index ];
    },
    set chainIndex ( value ) {
        this.residueStore.chainIndex[ this.index ] = value;
    },

    get atomOffset () {
        return this.residueStore.atomOffset[ this.index ];
    },
    set atomOffset ( value ) {
        this.residueStore.atomOffset[ this.index ] = value;
    },

    get atomCount () {
        return this.residueStore.atomCount[ this.index ];
    },
    set atomCount ( value ) {
        this.residueStore.atomCount[ this.index ] = value;
    },

    //

    get modelIndex () {
        return this.chainStore.modelIndex[ this.chainIndex ];
    },
    get chainname () {
        return this.chainStore.getChainname( this.chainIndex );
    },

    //

    get resno () {
        return this.residueStore.resno[ this.index ];
    },
    set resno ( value ) {
        this.residueStore.resno[ this.index ] = value;
    },

    get sstruc () {
        return this.residueStore.getSstruc( this.index );
    },
    set sstruc ( value ) {
        this.residueStore.setSstruc( this.index, value );
    },

    get inscode () {
        return this.residueStore.getInscode( this.index );
    },
    set inscode ( value ) {
        this.residueStore.getInscode( this.index, value );
    },

    //

    get residueType () {
        return this.residueMap.get( this.residueStore.residueTypeId[ this.index ] );
    },

    get resname () {
        return this.residueType.resname;
    },
    get hetero () {
        return this.residueType.hetero;
    },
    get moleculeType () {
        return this.residueType.moleculeType;
    },
    get backboneType () {
        return this.residueType.backboneType;
    },
    get backboneStartType () {
        return this.residueType.backboneStartType;
    },
    get backboneEndType () {
        return this.residueType.backboneEndType;
    },
    get traceAtomIndex () {
        return this.residueType.traceAtomIndex + this.atomOffset;
    },
    get direction1AtomIndex () {
        return this.residueType.direction1AtomIndex + this.atomOffset;
    },
    get direction2AtomIndex () {
        return this.residueType.direction2AtomIndex + this.atomOffset;
    },
    get backboneStartAtomIndex () {
        return this.residueType.backboneStartAtomIndex + this.atomOffset;
    },
    get backboneEndAtomIndex () {
        return this.residueType.backboneEndAtomIndex + this.atomOffset;
    },
    get rungEndAtomIndex () {
        return this.residueType.rungEndAtomIndex + this.atomOffset;
    },

    //

    eachAtom: function( callback, selection ){

        var count = this.atomCount;
        var offset = this.atomOffset;
        var ap = this.structure._ap;
        var end = offset + count;

        if( selection && selection.atomOnlyTest ){
            var atomOnlyTest = selection.atomOnlyTest;
            for( var i = offset; i < end; ++i ){
                ap.index = i;
                if( atomOnlyTest( ap ) ) callback( ap );
            }
        }else{
            for( var i = offset; i < end; ++i ){
                ap.index = i;
                callback( ap );
            }
        }

    },

    //

    isProtein: function(){
        return this.residueType.moleculeType === NGL.ProteinType;
    },

    isNucleic: function(){
        var moleculeType = this.residueType.moleculeType;
        return (
            moleculeType === NGL.RnaType ||
            moleculeType === NGL.DnaType
        );
    },

    isRna: function(){
        return this.residueType.moleculeType === NGL.RnaType;
    },

    isDna: function(){
        return this.residueType.moleculeType === NGL.DnaType;
    },

    isCg: function(){
        var backboneType = this.residueType.backboneType;
        return (
            backboneType === NGL.CgProteinBackboneType ||
            backboneType === NGL.CgRnaBackboneType ||
            backboneType === NGL.CgDnaBackboneType
        );
    },

    isPolymer: function(){
        var moleculeType = this.residueType.moleculeType;
        return (
            moleculeType === NGL.ProteinType ||
            moleculeType === NGL.RnaType ||
            moleculeType === NGL.DnaType
        );
    },

    isHetero: function(){
        return this.residueType.hetero === 1;
    },

    isWater: function(){
        return this.residueType.moleculeType === NGL.WaterType;
    },

    isIon: function(){
        return this.residueType.moleculeType === NGL.IonType;
    },

    getAtomType: function( index ){
        return this.atomMap.get( this.atomStore.atomTypeId[ index ] );
    },

    getResname1: function(){
        // FIXME nucleic support
        return NGL.AA1[ this.resname.toUpperCase() ] || 'X';
    },

    getBackboneType: function( position ){
        switch( position ){
            case -1:
                return this.residueType.backboneStartType;
            case 1:
                return this.residueType.backboneEndType;
            default:
                return this.residueType.backboneType;
        }
    },

    getAtomIndexByName: function( atomname ){
        return this.residueType.getAtomIndexByName( atomname );
    },

    getAtomByName: function( atomname ){
        return this.residueType.getAtomByName( atomname );
    },

    hasAtomWithName: function( atomname ){
        return this.residueType.hasAtomWithName( atomname );
    },

    getAtomnameList: function(){
        console.warn("getAtomnameList")
        var n = this.atomCount;
        var offset = this.atomOffset;
        var list = new Array( n );
        for( var i = 0; i < n; ++i ){
            list[ i ] = this.getAtomType( offset + i ).atomname;
        }
        return list;
    },

    connectedTo: function( rNext ){
        var bbAtomEnd = this.structure.getAtomProxy( this.backboneEndAtomIndex );
        var bbAtomStart = this.structure.getAtomProxy( rNext.backboneStartAtomIndex );
        if( bbAtomEnd && bbAtomStart ){
            return bbAtomEnd.connectedTo( bbAtomStart );
        }else{
            return false;
        }
    },

    getNextConnectedResidue: function(){
        var rOffset = this.chainStore.residueOffset[ this.chainIndex ];
        var rCount = this.chainStore.residueCount[ this.chainIndex ];
        var nextIndex = this.index + 1;
        if( nextIndex < rOffset + rCount ){
            var rpNext = this.structure.getResidueProxy( nextIndex );
            if( this.connectedTo( rpNext ) ){
                return rpNext;
            }
        }else if( nextIndex === rOffset + rCount ){  // cyclic
            var rpFirst = this.structure.getResidueProxy( rOffset );
            if( this.connectedTo( rpFirst ) ){
                return rpFirst;
            }
        }
        return undefined;
    },

    getPreviousConnectedResidue: function( rp ){
        var rOffset = this.chainStore.residueOffset[ this.chainIndex ];
        var prevIndex = this.index - 1;
        if( prevIndex >= rOffset ){
            if( rp === undefined ) rp = this.structure.getResidueProxy();
            rp.index = prevIndex;
            if( rp.connectedTo( this ) ){
                return rp;
            }
        }else if( prevIndex === rOffset - 1 ){  // cyclic
            if( rp === undefined ) rp = this.structure.getResidueProxy();
            var rCount = this.chainStore.residueCount[ this.chainIndex ];
            rp.index = rOffset + rCount - 1;
            if( rp.connectedTo( this ) ){
                return rp;
            }
        }
        return undefined;
    },

    getBonds: function(){
        return this.residueType.getBonds( this );
    },

    //

    qualifiedName: function( noResname ){
        var name = "";
        if( this.resname && !noResname ) name += "[" + this.resname + "]";
        if( this.resno !== undefined ) name += this.resno;
        if( this.inscode ) name += "^" + this.inscode;
        if( this.chain ) name += ":" + this.chainname;
        name += "/" + this.modelIndex;
        return name;
    },

    clone: function(){
        return new this.constructor( this.structure, this.index );
    },

    toObject: function(){
        return {
            index: this.index,
            chainIndex: this.chainIndex,
            atomOffset: this.atomOffset,
            atomCount: this.atomCount,

            resno: this.resno,
            resname: this.resname,
            sstruc: this.sstruc
        };
    }

};


////////////
// Polymer

NGL.Polymer = function( structure, residueIndexStart, residueIndexEnd ){

    this.structure = structure;
    this.chainStore = structure.chainStore;
    this.residueStore = structure.residueStore;
    this.atomStore = structure.atomStore;

    this.residueIndexStart = residueIndexStart;
    this.residueIndexEnd = residueIndexEnd;
    this.residueCount = residueIndexEnd - residueIndexStart + 1;

    var rpStart = this.structure.getResidueProxy( this.residueIndexStart );
    var rpEnd = this.structure.getResidueProxy( this.residueIndexEnd );
    this.isPrevConnected = rpStart.getPreviousConnectedResidue() !== undefined;
    var rpNext = rpEnd.getNextConnectedResidue();
    this.isNextConnected = rpNext !== undefined;
    this.isNextNextConnected = this.isNextConnected && rpNext.getNextConnectedResidue() !== undefined;
    this.isCyclic = rpEnd.connectedTo( rpStart );

    this.__residueProxy = this.structure.getResidueProxy();

    // console.log( this.qualifiedName(), this );

};

NGL.Polymer.prototype = {

    constructor: NGL.Polymer,
    type: "Polymer",

    structure: undefined,
    residueStore: undefined,
    atomStore: undefined,

    residueIndexStart: undefined,
    residueIndexEnd: undefined,
    residueCount: undefined,

    //

    get chainIndex () {
        return this.residueStore.chainIndex[ this.residueIndexStart ];
    },
    get modelIndex () {
        return this.chainStore.modelIndex[ this.chainIndex ];
    },

    get chainname () {
        return this.chainStore.getChainname( this.chainIndex );
    },

    //

    isProtein: function(){
        this.__residueProxy.index = this.residueIndexStart;
        return this.__residueProxy.isProtein();
    },

    isCg: function(){
        this.__residueProxy.index = this.residueIndexStart;
        return this.__residueProxy.isCg();
    },

    isNucleic: function(){
        this.__residueProxy.index = this.residueIndexStart;
        return this.__residueProxy.isNucleic();
    },

    getMoleculeType: function(){
        this.__residueProxy.index = this.residueIndexStart;
        return this.__residueProxy.moleculeType;
    },

    getBackboneType: function( position ){
        this.__residueProxy.index = this.residueIndexStart;
        return this.__residueProxy.getBackboneType( position );
    },

    getAtomIndexByType: function( index, type ){

        // TODO pre-calculate, add to residueStore???

        if( this.isCyclic ){
            if( index === -1 ){
                index = this.residueCount - 1
            }else if( index === this.residueCount ){
                index = 0
            }
        }else{
            if( index === -1 && !this.isPrevConnected ) index += 1;
            if( index === this.residueCount && !this.isNextNextConnected ) index -= 1;
            // if( index === this.residueCount - 1 && !this.isNextConnected ) index -= 1;
        }

        var rp = this.__residueProxy;
        rp.index = this.residueIndexStart + index;
        var aIndex;

        switch( type ){
            case "trace":
                aIndex = rp.traceAtomIndex;
                break;
            case "direction1":
                aIndex = rp.direction1AtomIndex;
                break;
            case "direction2":
                aIndex = rp.direction2AtomIndex;
                break;
            default:
                var ap = rp.getAtomByName( type );
                aIndex = ap ? ap.index : undefined;
        }

        // if( !ap ){
        //     console.log( this, type, rp.residueType )
        //     // console.log( rp.qualifiedName(), rp.index, index, this.residueCount - 1 )
        //     // rp.index = this.residueIndexStart;
        //     // console.log( rp.qualifiedName(), this.residueIndexStart )
        //     // rp.index = this.residueIndexEnd;
        //     // console.log( rp.qualifiedName(), this.residueIndexEnd )
        // }

        return aIndex;

    },

    eachAtom: function( callback, selection ){

        this.eachResidue( function( rp ){
            rp.eachAtom( callback, selection );
        }, selection );

    },

    eachAtomN: function( n, callback, type ){

        var m = this.residueCount;

        var array = new Array( n );
        for( var i = 0; i < n; ++i ){
            array[ i ] = this.structure.getAtomProxy( this.getAtomIndexByType( i, type ) );
        }
        callback.apply( this, array );

        for( var j = n; j < m; ++j ){
            for( var i = 1; i < n; ++i ){
                array[ i - 1 ].index = array[ i ].index;
            }
            array[ n - 1 ].index = this.getAtomIndexByType( j, type );
            callback.apply( this, array );
        }

    },

    eachAtomN2: function( n, callback, type ){

        // console.log(this.residueOffset,this.residueCount)

        var offset = this.atomOffset;
        var count = this.atomCount;
        var end = offset + count;
        if( count < n ) return;

        var array = new Array( n );
        for( var i = 0; i < n; ++i ){
            array[ i ] = this.structure.getAtomProxy();
        }
        // console.log( array, offset, end, count )

        var as = this.structure.atomSetCache[ "__" + type ];
        if( as === undefined ){
            NGL.warn( "no precomputed atomSet for: " + type );
            as = this.structure.getAtomSet( false );
            this.eachResidue( function( rp ){
                var ap = rp.getAtomByName( type );
                as.add_unsafe( ap.index );
            } );
        }
        var j = 0;

        as.forEach( function( index ){
            if( index >= offset && index < end ){
                for( var i = 1; i < n; ++i ){
                    array[ i - 1 ].index = array[ i ].index;
                }
                array[ n - 1 ].index = index;
                j += 1;
                if( j >= n ){
                    callback.apply( this, array );
                }
            }
        } );

    },

    eachDirectionAtomsN: function( n, callback ){

        var n2 = n * 2;
        var offset = this.atomOffset;
        var count = this.atomCount;
        var end = offset + count;
        if( count < n ) return;

        var array = new Array( n2 );
        for( var i = 0; i < n2; ++i ){
            array[ i ] = this.structure.getAtomProxy();
        }

        var as1 = this.structure.atomSetCache[ "__direction1" ];
        var as2 = this.structure.atomSetCache[ "__direction2" ];
        if( as1 === undefined || as2 === undefined ){
            NGL.error( "no precomputed atomSet for direction1 or direction2" );
            return;
        }
        var j = 0;

        TypedFastBitSet.forEach( function( index1, index2 ){
            if( index1 >= offset && index1 < end && index2 >= offset && index2 < end ){
                for( var i = 1; i < n; ++i ){
                    array[ i - 1 ].index = array[ i ].index;
                    array[ i - 1 + n ].index = array[ i + n ].index;
                }
                array[ n - 1 ].index = index1;
                array[ n - 1 + n ].index = index2;
                j += 1;
                if( j >= n ){
                    callback.apply( this, array );
                }
            }
        }, as1, as2 );

    },

    eachResidue: function( callback ){

        var rp = this.structure.getResidueProxy();
        var n = this.residueCount;
        var rStartIndex = this.residueIndexStart;

        for( var i = 0; i < n; ++i ){
            rp.index = rStartIndex + i;
            callback( rp );
        }

    },

    qualifiedName: function(){
        var rpStart = this.structure.getResidueProxy( this.residueIndexStart );
        var rpEnd = this.structure.getResidueProxy( this.residueIndexEnd );
        return rpStart.qualifiedName() + " - " + rpEnd.qualifiedName();
    }

};


///////////////
// ChainProxy

NGL.ChainProxy = function( structure, index ){

    this.structure = structure;
    this.chainStore = structure.chainStore;
    this.residueStore = structure.residueStore;
    this.index = index;

};

NGL.ChainProxy.prototype = {

    constructor: NGL.ChainProxy,
    type: "ChainProxy",

    structure: undefined,
    chainStore: undefined,
    index: undefined,

    get model () {
        return this.structure.getModelProxy( this.modelIndex );
    },

    get modelIndex () {
        return this.chainStore.modelIndex[ this.index ];
    },
    set modelIndex ( value ) {
        this.chainStore.modelIndex[ this.index ] = value;
    },

    get residueOffset () {
        return this.chainStore.residueOffset[ this.index ];
    },
    set residueOffset ( value ) {
        this.chainStore.residueOffset[ this.index ] = value;
    },

    get residueCount () {
        return this.chainStore.residueCount[ this.index ];
    },
    set residueCount ( value ) {
        this.chainStore.residueCount[ this.index ] = value;
    },

    get residueEnd () {
        return this.residueOffset + this.residueCount - 1;
    },

    get atomOffset () {
        return this.residueStore.atomOffset[ this.residueOffset ];
    },
    get atomEnd () {
        return (
            this.residueStore.atomOffset[ this.residueEnd ] +
            this.residueStore.atomCount[ this.residueEnd ] - 1
        );
    },
    get atomCount () {
        return this.atomEnd - this.atomOffset + 1;
    },

    //

    get chainname () {
        return this.chainStore.getChainname( this.index );
    },
    set chainname ( value ) {
        this.chainStore.setChainname( this.index, value );
    },

    //

    eachAtom: function( callback, selection ){

        this.eachResidue( function( rp ){
            rp.eachAtom( callback, selection )
        }, selection );

    },

    eachResidue: function( callback, selection ){

        var count = this.residueCount;
        var offset = this.residueOffset;
        var rp = this.structure._rp;
        var end = offset + count;

        if( selection && selection.test ){
            var residueOnlyTest = selection.residueOnlyTest;
            if( residueOnlyTest ){
                for( var i = offset; i < end; ++i ){
                    rp.index = i;
                    if( residueOnlyTest( rp ) ){
                        callback( rp, selection );
                    }
                }
            }else{
                for( var i = offset; i < end; ++i ){
                    rp.index = i;
                    callback( rp, selection );
                }
            }
        }else{
            for( var i = offset; i < end; ++i ){
                rp.index = i;
                callback( rp );
            }
        }

    },

    eachResidueN: function( n, callback ){

        var count = this.residueCount;
        var offset = this.residueOffset;
        var end = offset + count;
        if( count < n ) return;
        var array = new Array( n );

        for( var i = 0; i < n; ++i ){
            array[ i ] = this.structure.getResidueProxy( offset + i );
        }
        callback.apply( this, array );

        for( var j = offset + n; j < end; ++j ){
            for( var i = 0; i < n; ++i ){
                array[ i ].index += 1;
            }
            callback.apply( this, array );
        }

    },

    eachPolymer: function( callback, selection ){

        var rStartIndex, rNextIndex;
        var test = selection ? selection.residueOnlyTest : undefined;
        var structure = this.model.structure;

        var count = this.residueCount;
        var offset = this.residueOffset;
        var end = offset + count;

        var rp1 = this.structure.getResidueProxy();
        var rp2 = this.structure.getResidueProxy( offset );

        var ap1 = this.structure.getAtomProxy();
        var ap2 = this.structure.getAtomProxy();

        var first = true;

        for( var i = offset + 1; i < end; ++i ){

            rp1.index = rp2.index;
            rp2.index = i;

            if( first ){
                rStartIndex = rp1.index;
                first = false;
            }
            rNextIndex = rp2.index;

            var bbType1 = first ? rp1.backboneEndType : rp1.backboneType;
            var bbType2 = rp2.backboneType;

            if( bbType1 !== NGL.UnknownBackboneType && bbType1 === bbType2 ){

                ap1.index = rp1.backboneEndAtomIndex;
                ap2.index = rp2.backboneStartAtomIndex;

            }else{

                if( bbType1 !== NGL.UnknownBackboneType ){
                    if( rp1.index - rStartIndex > 1 ){
                        // console.log("FOO1",rStartIndex, rp1.index)
                        callback( new NGL.Polymer( structure, rStartIndex, rp1.index ) );
                    }
                }

                rStartIndex = rNextIndex;

                continue;

            }

            if( !ap1 || !ap2 || !ap1.connectedTo( ap2 ) ||
                ( test && ( !test( rp1 ) || !test( rp2 ) ) ) ){
                if( rp1.index - rStartIndex > 1 ){
                    // console.log("FOO2",rStartIndex, rp1.index)
                    callback( new NGL.Polymer( structure, rStartIndex, rp1.index ) );
                }
                rStartIndex = rNextIndex;

            }

        }

        if( rNextIndex - rStartIndex > 1 ){
            if( this.structure.getResidueProxy( rStartIndex ).backboneStartType ){
                // console.log("FOO3",rStartIndex, rNextIndex)
                callback( new NGL.Polymer( structure, rStartIndex, rNextIndex ) );
            }
        }

    },

    //

    qualifiedName: function(){
        var name = ":" + this.chainname + "/" + this.modelIndex;
        return name;
    },

    clone: function(){

        return new this.constructor( this.structure, this.index );

    },

    toObject: function(){

        return {
            index: this.index,
            residueOffset: this.residueOffset,
            residueCount: this.residueCount,

            chainname: this.chainname
        };

    }

};


///////////////
// ModelProxy

NGL.ModelProxy = function( structure, index ){

    this.structure = structure;
    this.modelStore = structure.modelStore;
    this.chainStore = structure.chainStore;
    this.residueStore = structure.residueStore;
    this.index = index;

};

NGL.ModelProxy.prototype = {

    constructor: NGL.ModelProxy,
    type: "ModelProxy",

    structure: undefined,
    modelStore: undefined,
    index: undefined,

    get chainOffset () {
        return this.modelStore.chainOffset[ this.index ];
    },
    set chainOffset ( value ) {
        this.modelStore.chainOffset[ this.index ] = value;
    },

    get chainCount () {
        return this.modelStore.chainCount[ this.index ];
    },
    set chainCount ( value ) {
        this.modelStore.chainCount[ this.index ] = value;
    },

    get residueOffset () {
        return this.chainStore.residueOffset[ this.chainOffset ];
    },
    get atomOffset () {
        return this.residueStore.atomOffset[ this.residueOffset ];
    },

    get chainEnd () {
        return this.chainOffset + this.chainCount - 1;
    },
    get residueEnd () {
        return (
            this.chainStore.residueOffset[ this.chainEnd ] +
            this.chainStore.residueCount[ this.chainEnd ] - 1
        );
    },
    get atomEnd () {
        return (
            this.residueStore.atomOffset[ this.residueEnd ] +
            this.residueStore.atomCount[ this.residueEnd ] - 1
        );
    },

    get residueCount () {
        return this.residueEnd - this.residueOffset + 1;
    },
    get atomCount () {
        return this.atomEnd - this.atomOffset + 1;
    },

    //

    eachAtom: function( callback, selection ){

        this.eachChain( function( cp ){
            cp.eachAtom( callback, selection )
        }, selection );

    },

    eachResidue: function( callback, selection ){

        var i, j, o, c, r;
        var n = this.chainCount;

        if( selection && selection.chainOnlyTest ){

            var test = selection.chainOnlyTest;

            for( i = 0; i < n; ++i ){

                c = this.chains[ i ];
                if( test( c ) ) c.eachResidue( callback, selection );

                // if( !test( c ) ) continue;

                // o = c.residueCount;

                // var residueTest = selection.residueTest;

                // for( j = 0; j < o; ++j ){

                //     r = c.residues[ j ];
                //     if( residueTest( r ) ) callback( r );

                // }

            }

        }else{

            for( i = 0; i < n; ++i ){

                c = this.chains[ i ];
                c.eachResidue( callback, selection );

                // o = c.residueCount;

                // for( j = 0; j < o; ++j ){

                //     callback( c.residues[ j ] );

                // }

            }

        }

    },

    eachPolymer: function( callback, selection ){

        if( selection && selection.chainOnlyTest ){

            var chainOnlyTest = selection.chainOnlyTest;

            this.eachChain( function( cp ){
                if( chainOnlyTest( cp ) ){
                    cp.eachPolymer( callback, selection );
                }
            } );

        }else{

            this.eachChain( function( cp ){
                cp.eachPolymer( callback, selection );
            } );

        }

    },

    eachChain: function( callback, selection ){

        var count = this.chainCount;
        var offset = this.chainOffset;
        var cp = this.structure._cp;
        var end = offset + count;

        if( selection && selection.test ){
            var chainOnlyTest = selection.chainOnlyTest;
            if( chainOnlyTest ){
                for( var i = offset; i < end; ++i ){
                    cp.index = i;
                    if( chainOnlyTest( cp ) ){
                        callback( cp, selection );
                    }
                }
            }else{
                for( var i = offset; i < end; ++i ){
                    cp.index = i;
                    callback( cp, selection );
                }
            }
        }else{
            for( var i = offset; i < end; ++i ){
                cp.index = i;
                callback( cp );
            }
        }

    },

    //

    qualifiedName: function(){
        var name = "/" + this.index;
        return name;
    },

    clone: function(){

        return new this.constructor( this.structure, this.index );

    },

    toObject: function(){

        return {
            index: this.index,
            chainOffset: this.chainOffset,
            chainCount: this.chainCount,
        };

    }

};


///////////////
// Type & Map

NGL.AtomType = function( structure, atomname, element ){

    this.structure = structure;

    element = element || NGL.guessElement( atomname );

    this.atomname = atomname;
    this.element = element;
    this.vdw = NGL.VdwRadii[ element ];
    this.covalent = NGL.CovalentRadii[ element ];

};

NGL.AtomType.prototype = {

    constructor: NGL.AtomType,
    type: "AtomType",

    atomname: undefined,
    element: undefined,
    vdw: undefined,
    covalent: undefined,

    toJSON: function(){
        var output = {
            atomname: this.atomname,
            element: this.element,
        };
        return output;
    }

};


NGL.AtomMap = function( structure ){

    var idDict = {};
    var typeList = [];

    function getHash( atomname, element ){
        return atomname + "|" + element;
    }

    function add( atomname, element ){
        element = element || NGL.guessElement( atomname );
        var hash = getHash( atomname, element );
        var id = idDict[ hash ];
        if( id === undefined ){
            var atomType = new NGL.AtomType( structure, atomname, element );
            id = typeList.length;
            idDict[ hash ] = id;
            typeList.push( atomType );
        }
        return id;
    }

    function get( id ){
        return typeList[ id ];
    }

    // API

    this.add = add;
    this.get = get;

    this.list = typeList;
    this.dict = idDict;

    this.toJSON = function(){
        var output = {
            metadata: {
                version: 0.1,
                type: 'AtomMap',
                generator: 'AtomMapExporter'
            },
            idDict: idDict,
            typeList: typeList.map( function( atomType ){
                return atomType.toJSON();
            } )
        };
        return output;
    };

    this.fromJSON = function( input ){
        idDict = input.idDict;
        typeList = input.typeList.map( function( input ){
            return new NGL.AtomType( structure, input.atomname, input.element );
        } );
        this.list = typeList;
        this.dict = idDict;
    };

}


NGL.ResidueType = function( structure, resname, atomTypeIdList, hetero ){

    this.structure = structure;

    this.resname = resname;
    this.atomTypeIdList = atomTypeIdList;
    this.hetero = hetero ? 1 : 0;
    this.atomCount = atomTypeIdList.length;

    this.moleculeType = this.getMoleculeType();
    this.backboneType = this.getBackboneType( 0 );
    this.backboneEndType = this.getBackboneType( -1 );
    this.backboneStartType = this.getBackboneType( 1 );
    this.backboneIndexList = this.getBackboneIndexList();

    //

    var rAtomnames = NGL.Residue.atomnames;
    var atomnames = rAtomnames[ this.backboneType ];
    var atomnamesStart = rAtomnames[ this.backboneStartType ];
    var atomnamesEnd = rAtomnames[ this.backboneEndType ];

    var traceIndex = this.getAtomIndexByName( atomnames.trace );
    this.traceAtomIndex = traceIndex !== undefined ? traceIndex : -1;

    var dir1Index = this.getAtomIndexByName( atomnames.direction1 );
    this.direction1AtomIndex = dir1Index !== undefined ? dir1Index : -1;

    var dir2Index = this.getAtomIndexByName( atomnames.direction2 );
    this.direction2AtomIndex = dir2Index !== undefined ? dir2Index : -1;

    var bbStartIndex = this.getAtomIndexByName( atomnamesStart.backboneStart );
    this.backboneStartAtomIndex = bbStartIndex !== undefined ? bbStartIndex : -1;

    var bbEndIndex = this.getAtomIndexByName( atomnamesEnd.backboneEnd );
    this.backboneEndAtomIndex = bbEndIndex !== undefined ? bbEndIndex : -1;

    var rungEndIndex;
    if( NGL.PurinBases.indexOf( resname ) !== -1 ){
        rungEndIndex = this.getAtomIndexByName( "N1" );
    }else{
        rungEndIndex = this.getAtomIndexByName( "N3" );
    }
    this.rungEndAtomIndex = rungEndIndex !== undefined ? rungEndIndex : -1;

};

NGL.ResidueType.prototype = {

    constructor: NGL.ResidueType,
    type: "ResidueType",

    resname: undefined,
    atomTypeIdList: undefined,
    atomCount: undefined,

    getBackboneIndexList: function(){
        var backboneIndexList = [];
        var atomnameList;
        switch( this.moleculeType ){
            case NGL.ProteinType:
                atomnameList = NGL.ProteinBackboneAtoms;
                break;
            case NGL.RnaType:
            case NGL.DnaType:
                atomnameList = NGL.NucleicBackboneAtoms;
                break;
            default:
                return backboneIndexList;
        }
        var atomMap = this.structure.atomMap;
        var atomTypeIdList = this.atomTypeIdList;
        for( var i = 0, il = this.atomCount; i < il; ++i ){
            var atomType = atomMap.get( atomTypeIdList[ i ] );
            if( atomnameList.indexOf( atomType.atomname ) !== -1 ){
                backboneIndexList.push( i );
            }
        }
        return backboneIndexList;
    },

    getMoleculeType: function(){
        if( this.isProtein() ){
            return NGL.ProteinType;
        }else if( this.isRna() ){
            return NGL.RnaType;
        }else if( this.isDna() ){
            return NGL.DnaType;
        }else if( this.isWater() ){
            return NGL.WaterType;
        }else if( this.isIon() ){
            return NGL.IonType;
        }else{
            return NGL.UnknownType;
        }
    },

    getBackboneType: function( position ){
        if( this.hasProteinBackbone( position ) ){
            return NGL.ProteinBackboneType;
        }else if( this.hasRnaBackbone( position ) ){
            return NGL.RnaBackboneType;
        }else if( this.hasDnaBackbone( position ) ){
            return NGL.DnaBackboneType;
        }else if( this.hasCgProteinBackbone( position ) ){
            return NGL.CgProteinBackboneType;
        }else if( this.hasCgRnaBackbone( position ) ){
            return NGL.CgRnaBackboneType;
        }else if( this.hasCgDnaBackbone( position ) ){
            return NGL.CgDnaBackboneType;
        }else{
            return NGL.UnknownBackboneType;
        }
    },

    isProtein: function(){
        return (
            ( this.hasAtomWithName( "CA", "C", "N" ) &&
                NGL.LigandNames.indexOf( this.resname ) === -1 ) ||
            NGL.AA3.indexOf( this.resname ) !== -1
        );
    },

    isCg: function(){
        var backboneType = this.backboneType;
        return (
            backboneType === NGL.CgProteinBackboneType ||
            backboneType === NGL.CgRnaBackboneType ||
            backboneType === NGL.CgDnaBackboneType
        );
    },

    isNucleic: function(){
        return this.isRna() || this.isDna();
    },

    isRna: function(){
        return (
            ( this.hasAtomWithName( [ "P", "O3'", "O3*" ], [ "C4'", "C4*" ], [ "O2'", "O2*" ] ) &&
                NGL.LigandNames.indexOf( this.resname ) === -1 ) ||
            NGL.RnaBases.indexOf( this.resname ) !== -1
        );
    },

    isDna: function(){
        return (
            ( this.hasAtomWithName( [ "P", "O3'", "O3*" ], [ "C3'", "C3*" ] ) &&
                !this.hasAtomWithName( [ "O2'", "O2*" ] ) &&
                NGL.LigandNames.indexOf( this.resname ) === -1 ) ||
            NGL.DnaBases.indexOf( this.resname ) !== -1
        );
    },

    isPolymer: function(){
        return this.isProtein() || this.isNucleic();
    },

    isHetero: function(){
        return this.hetero === 1;
    },

    isIon: function(){
        return NGL.IonNames.indexOf( this.resname ) !== -1;
    },

    isWater: function(){
        return NGL.WaterNames.indexOf( this.resname ) !== -1;
    },

    hasBackboneAtoms: function( position, type ){
        var atomnames = NGL.Residue.atomnames[ type ];
        if( position === -1 ){
            return this.hasAtomWithName(
                atomnames.trace,
                atomnames.backboneEnd,
                atomnames.direction1,
                atomnames.direction2
            );
        }else if( position === 0 ){
            return this.hasAtomWithName(
                atomnames.trace,
                atomnames.direction1,
                atomnames.direction2
            );
        }else if( position === 1 ){
            return this.hasAtomWithName(
                atomnames.trace,
                atomnames.backboneStart,
                atomnames.direction1,
                atomnames.direction2
            );
        }else{
            return this.hasAtomWithName(
                atomnames.trace,
                atomnames.backboneStart,
                atomnames.backboneEnd,
                atomnames.direction1,
                atomnames.direction2
            );
        }
    },

    hasProteinBackbone: function( position ){
        return (
            this.isProtein() &&
            this.hasBackboneAtoms( position, NGL.ProteinBackboneType )
        );
    },

    hasRnaBackbone: function( position ){
        return (
            this.isRna() &&
            this.hasBackboneAtoms( position, NGL.RnaBackboneType )
        );
    },

    hasDnaBackbone: function( position ){
        return (
            this.isDna() &&
            this.hasBackboneAtoms( position, NGL.DnaBackboneType )
        );
    },

    hasCgProteinBackbone: function( position ){
        return (
            this.isProtein() &&
            this.hasBackboneAtoms( position, NGL.CgProteinBackboneType )
        );
    },

    hasCgRnaBackbone: function( position ){
        return (
            this.isRna() &&
            this.hasBackboneAtoms( position, NGL.CgRnaBackboneType )
        );
    },

    hasCgDnaBackbone: function( position ){
        return (
            this.isDna() &&
            this.hasBackboneAtoms( position, NGL.CgDnaBackboneType )
        );
    },

    hasBackbone: function( position ){
        return (
            this.hasProteinBackbone( position ) ||
            this.hasRnaBackbone( position ) ||
            this.hasDnaBackbone( position ) ||
            this.hasCgProteinBackbone( position ) ||
            this.hasCgRnaBackbone( position ) ||
            this.hasCgDnaBackbone( position )
        );
    },

    getAtomIndexByName: function( atomname ){
        var n = this.atomCount;
        var atomMap = this.structure.atomMap;
        var atomTypeIdList = this.atomTypeIdList;
        if( Array.isArray( atomname ) ){
            for( var i = 0; i < n; ++i ){
                var index = atomTypeIdList[ i ];
                if( atomname.indexOf( atomMap.get( index ).atomname ) !== -1 ){
                    return i;
                }
            }
        }else{
            for( var i = 0; i < n; ++i ){
                var index = atomTypeIdList[ i ];
                if( atomname === atomMap.get( index ).atomname ){
                    return i;
                }
            }
        }
        return undefined;
    },

    hasAtomWithName: function( atomname ){
        var n = arguments.length;
        for( var i = 0; i < n; ++i ){
            if( arguments[ i ] === undefined ) continue;
            if( this.getAtomIndexByName( arguments[ i ] ) === undefined ){
                return false;
            }
        }
        return true;
    },

    getBonds: function( r ){
        if( !this.bonds ){
            this.bonds = NGL.calculateResidueBonds( r );
        }
        return this.bonds;
    },

    toJSON: function(){
        var output = {
            resname: this.resname,
            atomTypeIdList: this.atomTypeIdList,
            hetero: this.hetero
        };
        return output;
    }

};


NGL.ResidueMap = function( structure ){

    var idDict = {};
    var typeList = [];

    function getHash( resname, atomTypeIdList, hetero ){
        return resname + "|" + atomTypeIdList.join( "," ) + "|" + ( hetero ? 1 : 0 );
    }

    function add( resname, atomTypeIdList, hetero ){
        var hash = getHash( resname, atomTypeIdList, hetero );
        var id = idDict[ hash ];
        if( id === undefined ){
            var residueType = new NGL.ResidueType(
                structure, resname, atomTypeIdList, hetero
            );
            id = typeList.length;
            idDict[ hash ] = id;
            typeList.push( residueType );
        }
        return id;
    }

    function get( id ){
        return typeList[ id ];
    }

    // API

    this.add = add;
    this.get = get;

    this.list = typeList;
    this.dict = idDict;

    this.toJSON = function(){
        var output = {
            metadata: {
                version: 0.1,
                type: 'ResidueMap',
                generator: 'ResidueMapExporter'
            },
            idDict: idDict,
            typeList: typeList.map( function( residueType ){
                return residueType.toJSON();
            } )
        };
        return output;
    };

    this.fromJSON = function( input ){
        idDict = input.idDict;
        typeList = input.typeList.map( function( input ){
            return new NGL.ResidueType(
                structure, input.resname, input.atomTypeIdList, input.hetero
            );
        } );
        this.list = typeList;
        this.dict = idDict;
    };

}

// File:js/ngl/store.js

/**
 * @file Store
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


//////////
// Store

NGL.Store = function( sizeOrObject ){

    if( sizeOrObject === undefined ){

        this.init( 0 );

    }else if( Number.isInteger( sizeOrObject ) ){

        this.init( sizeOrObject );

    }else{

        this.fromJSON( sizeOrObject );

    }

};

NGL.Store.prototype = {

    constructor: NGL.Store,

    type: "Store",

    init: function( size ){

        this.length = size;
        this.count = 0;

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            var itemSize = this.__fields[ i ][ 1 ];
            var arrayType = this.__fields[ i ][ 2 ];
            var arraySize = this.length * itemSize;

            switch( arrayType ){

                case "int8":
                    this[ name ] = new Int8Array( arraySize );
                    break;

                case "int16":
                    this[ name ] = new Int16Array( arraySize );
                    break;

                case "int32":
                    this[ name ] = new Int32Array( arraySize );
                    break;

                case "uint8":
                    this[ name ] = new Uint8Array( arraySize );
                    break;

                case "uint16":
                    this[ name ] = new Uint16Array( arraySize );
                    break;

                case "uint32":
                    this[ name ] = new Uint32Array( arraySize );
                    break;

                case "float32":
                    this[ name ] = new Float32Array( arraySize );
                    break;

                default:
                    NGL.warn( "arrayType unknown: " + arrayType );

            }

        }

    },

    resize: function( size ){

        // NGL.time( "NGL.Store.resize" );

        this.length = Math.round( size || 0 );
        this.count = Math.min( this.count, this.length );

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            var itemSize = this.__fields[ i ][ 1 ];
            var arraySize = this.length * itemSize;
            var tmpArray = new this[ name ].constructor( arraySize );

            if( this[ name ].length > arraySize ){
                tmpArray.set( this[ name ].subarray( 0, arraySize ) );
            }else{
                tmpArray.set( this[ name ] );
            }
            this[ name ] = tmpArray;

        }

        // NGL.timeEnd( "NGL.Store.resize" );

    },

    growIfFull: function(){

        if( this.count >= this.length ){
            var size = Math.round( this.length * 1.5 );
            this.resize( Math.max( 256, size ) );
        }

    },

    copyFrom: function( other, thisOffset, otherOffset, length ){

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            var itemSize = this.__fields[ i ][ 1 ];
            var thisField = this[ name ];
            var otherField = other[ name ];

            for( var j = 0; j < length; ++j ){
                var thisIndex = itemSize * ( thisOffset + j );
                var otherIndex = itemSize * ( otherOffset + j );
                for( var k = 0; k < itemSize; ++k ){
                    thisField[ thisIndex + k ] = otherField[ otherIndex + k ];
                }
            }

        }

    },

    copyWithin: function( offsetTarget, offsetSource, length ){

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            var itemSize = this.__fields[ i ][ 1 ];
            var thisField = this[ name ];

            for( var j = 0; j < length; ++j ){
                var targetIndex = itemSize * ( offsetTarget + j );
                var sourceIndex = itemSize * ( offsetSource + j );
                for( var k = 0; k < itemSize; ++k ){
                    thisField[ targetIndex + k ] = thisField[ sourceIndex + k ];
                }
            }

        }

    },

    sort: function( compareFunction ){

        NGL.time( "NGL.Store.sort" );

        var thisStore = this;
        var tmpStore = new this.constructor( 1 );

        function swap( index1, index2 ){
            if( index1 === index2 ) return;
            tmpStore.copyFrom( thisStore, 0, index1, 1 );
            thisStore.copyWithin( index1, index2, 1 );
            thisStore.copyFrom( tmpStore, index2, 0, 1 );
        }

        function quicksort( left, right ){
            if( left < right ){
                var pivot = Math.floor( ( left + right ) / 2 );
                var left_new = left;
                var right_new = right;
                do{
                    while( compareFunction( left_new, pivot ) < 0 ){
                        left_new += 1;
                    }
                    while( compareFunction( right_new, pivot ) > 0 ){
                        right_new -= 1;
                    }
                    if( left_new <= right_new ){
                        if( left_new === pivot ){
                            pivot = right_new;
                        }else if( right_new === pivot ){
                            pivot = left_new;
                        }
                        swap( left_new, right_new );
                        left_new += 1;
                        right_new -= 1;
                    }
                }while( left_new <= right_new );
                quicksort( left, right_new );
                quicksort( left_new, right );
            }
        }

        quicksort( 0, this.count - 1 );

        NGL.timeEnd( "NGL.Store.sort" );

    },

    toJSON: function(){

        var output = {

            metadata: {
                version: 0.1,
                type: this.type,
                generator: this.type + "Exporter"
            },

            length: this.length,
            count: this.count,

        };

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            output[ name ] = this[ name ];

        }

        return output;

    },

    fromJSON: function( input ){

        this.length = input.length;
        this.count = input.count;

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            this[ name ] = input[ name ];

        }

        return this;

    },

    getTransferable: function(){

        var transferable = [];

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            transferable.push( this[ name ].buffer );

        }

        return transferable;

    },

    clear: function(){

        this.count = 0;

    },

    dispose: function(){

        delete this.length;
        delete this.count;

        for( var i = 0, il = this.__fields.length; i < il; ++i ){

            var name = this.__fields[ i ][ 0 ];
            delete this[ name ];

        }

    }

};


//////////////
// BondStore

NGL.BondStore = function( sizeOrObject ){

    NGL.Store.call( this, sizeOrObject );

};

NGL.BondStore.prototype = NGL.createObject(

    NGL.Store.prototype, {

    constructor: NGL.BondStore,

    type: "BondStore",

    __fields: [

        [ "atomIndex1", 1, "int32" ],
        [ "atomIndex2", 1, "int32" ],
        [ "bondOrder", 1, "int8" ],

    ],

    addBond: function( atom1, atom2, bondOrder ){

        this.growIfFull();

        var i = this.count;
        this.atomIndex1[ i ] = atom1.index;
        this.atomIndex2[ i ] = atom2.index;
        this.bondOrder[ i ] = bondOrder;

        this.count += 1;

    },

    addBondIfConnected: function( atom1, atom2, bondOrder ){

        if( atom1.connectedTo( atom2 ) ){
            this.addBond( atom1, atom2, bondOrder );
            return true;
        }

        return false;

    }

} );


//////////////
// AtomStore

NGL.AtomStore = function( sizeOrObject ){

    NGL.Store.call( this, sizeOrObject );

};

NGL.AtomStore.prototype = NGL.createObject(

    NGL.Store.prototype, {

    constructor: NGL.AtomStore,

    type: "AtomStore",

    __fields: [

        [ "residueIndex", 1, "uint32" ],
        [ "atomTypeId", 1, "uint16" ],

        [ "x", 1, "float32" ],
        [ "y", 1, "float32" ],
        [ "z", 1, "float32" ],
        [ "serial", 1, "int32" ],
        [ "bfactor", 1, "float32" ],
        [ "altloc", 1, "uint8" ],
        [ "occupancy", 1, "float32" ]

    ],

    setAltloc: function( i, str ){
        this.altloc[ i ] = str.charCodeAt( 0 );
    },

    getAltloc: function( i ){
        var code = this.altloc[ i ];
        return code ? String.fromCharCode( code ) : "";
    }

} );


/////////////////
// ResidueStore

NGL.ResidueStore = function( sizeOrObject ){

    NGL.Store.call( this, sizeOrObject );

};

NGL.ResidueStore.prototype = NGL.createObject(

    NGL.Store.prototype, {

    constructor: NGL.ResidueStore,

    type: "ResidueStore",

    __fields: [

        [ "chainIndex", 1, "uint32" ],
        [ "atomOffset", 1, "uint32" ],
        [ "atomCount", 1, "uint16" ],
        [ "residueTypeId", 1, "uint16" ],

        [ "resno", 1, "int32" ],
        [ "sstruc", 1, "uint8" ],
        [ "inscode", 1, "uint8" ]

    ],

    setSstruc: function( i, str ){
        this.sstruc[ i ] = str.charCodeAt( 0 );
    },

    getSstruc: function( i ){
        var code = this.sstruc[ i ];
        return code ? String.fromCharCode( code ) : "";
    },

    setInscode: function( i, str ){
        this.inscode[ i ] = str.charCodeAt( 0 );
    },

    getInscode: function( i ){
        var code = this.inscode[ i ];
        return code ? String.fromCharCode( code ) : "";
    }

} );


///////////////
// ChainStore

NGL.ChainStore = function( sizeOrObject ){

    NGL.Store.call( this, sizeOrObject );

};

NGL.ChainStore.prototype = NGL.createObject(

    NGL.Store.prototype, {

    constructor: NGL.ChainStore,

    type: "ChainStore",

    __fields: [

        [ "modelIndex", 1, "uint16" ],
        [ "residueOffset", 1, "uint32" ],
        [ "residueCount", 1, "uint32" ],

        [ "chainname", 4, "uint8" ]

    ],

    setChainname: function( i, str ){

        var j = 4 * i;
        this.chainname[ j ] = str.charCodeAt( 0 );
        this.chainname[ j + 1 ] = str.charCodeAt( 1 );
        this.chainname[ j + 2 ] = str.charCodeAt( 2 );
        this.chainname[ j + 3 ] = str.charCodeAt( 3 );

    },

    getChainname: function( i ){

        var chainname = "";
        for( var k = 0; k < 4; ++k ){
            var code = this.chainname[ 4 * i + k ];
            if( code ){
                chainname += String.fromCharCode( code );
            }else{
                break;
            }
        }
        return chainname;

    }

} );


///////////////
// ModelStore

NGL.ModelStore = function( sizeOrObject ){

    NGL.Store.call( this, sizeOrObject );

};

NGL.ModelStore.prototype = NGL.createObject(

    NGL.Store.prototype, {

    constructor: NGL.ModelStore,

    type: "ModelStore",

    __fields: [

        [ "chainOffset", 1, "uint32" ],
        [ "chainCount", 1, "uint32" ]

    ]

} );

// File:js/ngl/symmetry.js

/**
 * @file Symmetry
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


NGL.Unitcell = function( a, b, c, alpha, beta, gamma, spacegroup, cartToFrac ){

    this.a = a || 1;
    this.b = b || 1;
    this.c = c || 1;

    this.alpha = alpha || 90;
    this.beta = beta || 90;
    this.gamma = gamma || 90;

    this.spacegroup = spacegroup || "P 1";

    //

    var alphaRad = THREE.Math.degToRad( this.alpha );
    var betaRad = THREE.Math.degToRad( this.beta );
    var gammaRad = THREE.Math.degToRad( this.gamma );
    var cosAlpha = Math.cos( alphaRad );
    var cosBeta = Math.cos( betaRad );
    var cosGamma = Math.cos( gammaRad );
    var sinAlpha = Math.sin( alphaRad );
    var sinBeta = Math.sin( betaRad );
    var sinGamma = Math.sin( gammaRad );

    this.volume = (
        this.a * this.b * this.c *
        Math.sqrt(
            1 - cosAlpha * cosAlpha - cosBeta * cosBeta - cosGamma * cosGamma +
            2.0 * cosAlpha * cosBeta * cosGamma
        )
    );

    //

    if( cartToFrac === undefined ){

        // https://github.com/biojava/biojava/blob/master/biojava-structure/src/main/java/org/biojava/nbio/structure/xtal/CrystalCell.java

        var cStar = ( this.a * this.b * sinGamma ) / this.volume;
        var cosAlphaStar = (
            ( cosBeta * cosGamma - cosAlpha ) /
            ( sinBeta * sinGamma )
        );

        this.fracToCart = new THREE.Matrix4().set(
            this.a, 0, 0, 0,
            this.b * cosGamma, this.b * sinGamma, 0, 0,
            this.c * cosBeta, -this.c * sinBeta * cosAlphaStar, 1.0 / cStar, 0,
            0, 0, 0, 1
        ).transpose();
        this.cartToFrac = new THREE.Matrix4().getInverse( this.fracToCart );

    }else{

        this.cartToFrac = cartToFrac;
        this.fracToCart = new THREE.Matrix4().getInverse( this.cartToFrac );

    }

};

NGL.Unitcell.prototype = {

    toJSON: function(){

        var output = {

            metadata: {
                version: 0.1,
                type: 'Unitcell',
                generator: 'UnitcellExporter'
            },

            a: this.a,
            b: this.b,
            c: this.c,

            alpha: this.alpha,
            beta: this.beta,
            gamma: this.gamma,

            spacegroup: this.spacegroup,
            volume: this.volume,

            cartToFrac: this.cartToFrac.toArray(),
            fracToCart: this.fracToCart.toArray(),

        }

        return output;

    },

    fromJSON: function( input ){

        this.a = input.a;
        this.b = input.b;
        this.c = input.c;

        this.alpha = input.alpha;
        this.beta = input.beta;
        this.gamma = input.gamma;

        this.spacegroup = input.spacegroup;
        this.volume = input.volume;

        this.cartToFrac.fromArray( input.cartToFrac );
        this.fracToCart.fromArray( input.fracToCart );

        return this;

    }

};


NGL.getSymmetryOperations = function( spacegroup ){

    var symopList = NGL.SymOp[ spacegroup ];

    var matrixDict = {};

    if( symopList === undefined ){

        console.warn(
            "NGL.getSymmetryOperations: spacegroup '" +
            spacegroup + "' not found in symop library"
        );
        return matrixDict;

    }

    var reInteger = /^[1-9]$/;

    symopList.forEach( function( symop ){

        var ls = symop.split( "," );

        var row = 0;
        var matrix = new THREE.Matrix4().set(
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 1
        );
        var me = matrix.elements;

        matrixDict[ symop ] = matrix;

        // console.log( "symop", ls )

        ls.forEach( function( elm ){

            // console.log( "row", row );

            var negate = false;
            var denominator = false;

            for( var i = 0, n = elm.length; i < n; ++i ){

                var c = elm[ i ];

                if( c === "-" ){

                    negate = true;

                }else if( c === "+" ){

                    negate = false;

                }else if( c === "/" ){

                    denominator = true;

                }else if( c === "X" ){

                    me[ 0 + row ] = negate ? -1 : 1;

                }else if( c === "Y" ){

                    me[ 4 + row ] = negate ? -1 : 1;

                }else if( c === "Z" ){

                    me[ 8 + row ] = negate ? -1 : 1;

                }else if( reInteger.test( c ) ){

                    var integer = parseInt( c );

                    if( denominator ){

                        me[ 12 + row ] /= integer;

                    }else{

                        me[ 12 + row ] = integer;

                    }

                }else{

                    NGL.warn(
                        "NGL.getSymmetryOperations: unknown token " +
                        "'" + c + "'"
                    );

                }

                // console.log( "token", c )

            }

            row += 1;

        } );

        // console.log( "matrix", me )

    } );

    return matrixDict;

};

// from CCP4 symop.lib
NGL.SymOp = {
    "P 1": [
        "X,Y,Z"
    ],
    "P -1": [
        "X,Y,Z",
        "-X,-Y,-Z"
    ],
    "P 1 2 1": [
        "X,Y,Z",
        "-X,Y,-Z"
    ],
    "P 1 21 1": [
        "X,Y,Z",
        "-X,Y+1/2,-Z"
    ],
    "C 1 2 1": [
        "X,Y,Z",
        "-X,Y,-Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2+Y,-Z"
    ],
    "P 1 m 1": [
        "X,Y,Z",
        "X,-Y,Z"
    ],
    "P 1 c 1": [
        "X,Y,Z",
        "X,-Y,1/2+Z"
    ],
    "C 1 m 1": [
        "X,Y,Z",
        "X,-Y,Z",
        "1/2+X,1/2+Y,Z",
        "1/2+X,1/2-Y,Z"
    ],
    "C 1 c 1": [
        "X,Y,Z",
        "X,-Y,1/2+Z",
        "1/2+X,1/2+Y,Z",
        "1/2+X,1/2-Y,1/2+Z"
    ],
    "P 1 2/m 1": [
        "X,Y,Z",
        "X,-Y,Z",
        "-X,Y,-Z",
        "-X,-Y,-Z"
    ],
    "P 1 21/m 1": [
        "X,Y,Z",
        "-X,1/2+Y,-Z",
        "-X,-Y,-Z",
        "X,1/2-Y,Z"
    ],
    "C 1 2/m 1": [
        "X,Y,Z",
        "X,-Y,Z",
        "-X,Y,-Z",
        "-X,-Y,-Z",
        "1/2+X,1/2+Y,Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2-X,1/2-Y,-Z"
    ],
    "P 1 2/c 1": [
        "X,Y,Z",
        "-X,Y,1/2-Z",
        "-X,-Y,-Z",
        "X,-Y,1/2+Z"
    ],
    "P 1 21/c 1": [
        "X,Y,Z",
        "-X,-Y,-Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2+Z"
    ],
    "C 1 2/c 1": [
        "X,Y,Z",
        "-X,Y,1/2-Z",
        "-X,-Y,-Z",
        "X,-Y,1/2+Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2-Y,1/2+Z"
    ],
    "P 2 2 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z"
    ],
    "P 2 2 21": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-X,Y,1/2-Z",
        "X,-Y,-Z"
    ],
    "P 21 21 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z"
    ],
    "P 21 21 21": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z"
    ],
    "C 2 2 21": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-X,Y,1/2-Z",
        "X,-Y,-Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z"
    ],
    "C 2 2 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z"
    ],
    "F 2 2 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z"
    ],
    "I 2 2 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,-Z",
        "-X,Y,-Z",
        "X+1/2,Y+1/2,Z+1/2",
        "-X+1/2,-Y+1/2,Z+1/2",
        "X+1/2,-Y+1/2,-Z+1/2",
        "-X+1/2,Y+1/2,-Z+1/2"
    ],
    "I 21 21 21": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,1/2-Y,Z",
        "1/2-X,Y,-Z",
        "X,-Y,1/2-Z"
    ],
    "P m m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,Z",
        "-X,Y,Z"
    ],
    "P m c 21": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "X,-Y,1/2+Z",
        "-X,Y,Z"
    ],
    "P c c 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z"
    ],
    "P m a 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,-Y,Z",
        "1/2-X,Y,Z"
    ],
    "P c a 21": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "1/2+X,-Y,Z",
        "1/2-X,Y,1/2+Z"
    ],
    "P n c 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z"
    ],
    "P m n 21": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "1/2+X,-Y,1/2+Z",
        "-X,Y,Z"
    ],
    "P b a 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "P n a 21": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "P n n 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "C m m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "C m c 21": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "X,-Y,1/2+Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,Z"
    ],
    "C c c 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "A m m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z"
    ],
    "A b m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,1/2-Y,Z",
        "-X,1/2+Y,Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z"
    ],
    "A m a 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,-Y,Z",
        "1/2-X,Y,Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "A b a 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,Y,1/2+Z"
    ],
    "F m m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,Y,1/2+Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "F d d 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/4+X,1/4-Y,1/4+Z",
        "1/4-X,1/4+Y,1/4+Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "1/4+X,3/4-Y,3/4+Z",
        "1/4-X,3/4+Y,3/4+Z",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "3/4+X,1/4-Y,3/4+Z",
        "3/4-X,1/4+Y,3/4+Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "3/4+X,3/4-Y,1/4+Z",
        "3/4-X,3/4+Y,1/4+Z"
    ],
    "I m m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "I b a 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z"
    ],
    "I m a 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,-Y,Z",
        "1/2-X,Y,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z"
    ],
    "P 2/m 2/m 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z"
    ],
    "P 2/n 2/n 2/n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "P 2/c 2/c 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z"
    ],
    "P 2/b 2/a 2/n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "P 21/m 2/m 2/a": [
        "X,Y,Z",
        "1/2-X,-Y,Z",
        "-X,Y,-Z",
        "1/2+X,-Y,-Z",
        "-X,-Y,-Z",
        "1/2+X,Y,-Z",
        "X,-Y,Z",
        "1/2-X,Y,Z"
    ],
    "P 2/n 21/n 2/a": [
        "X,Y,Z",
        "1/2-X,-Y,Z",
        "1/2-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "-X,-Y,-Z",
        "1/2+X,Y,-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z"
    ],
    "P 2/m 2/n 21/a": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "1/2+X,Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "-X,Y,Z"
    ],
    "P 21/c 2/c 2/a": [
        "X,Y,Z",
        "1/2-X,-Y,Z",
        "-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "-X,-Y,-Z",
        "1/2+X,Y,-Z",
        "X,-Y,1/2+Z",
        "1/2-X,Y,1/2+Z"
    ],
    "P 21/b 21/a 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "P 21/c 21/c 2/n": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "-X,-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "X,1/2-Y,1/2+Z",
        "1/2-X,Y,1/2+Z"
    ],
    "P 2/b 21/c 21/m": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,Z"
    ],
    "P 21/n 21/n 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "P 21/m 21/m 2/n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-X,Y+1/2,-Z",
        "X+1/2,1/2-Y,-Z",
        "1/2-X,1/2-Y,-Z",
        "X+1/2,Y+1/2,-Z",
        "X,-Y,Z",
        "-X,Y,Z"
    ],
    "P 21/b 2/c 21/n": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-X,Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "-X,-Y,-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "X,-Y,1/2+Z",
        "1/2-X,1/2+Y,Z"
    ],
    "P 21/b 21/c 21/a": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "-X,-Y,-Z",
        "1/2+X,Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,Z"
    ],
    "P 21/n 21/m 21/a": [
        "X,Y,Z",
        "-X+1/2,-Y,Z+1/2",
        "-X,Y+1/2,-Z",
        "X+1/2,-Y+1/2,-Z+1/2",
        "-X,-Y,-Z",
        "X+1/2,Y,-Z+1/2",
        "X,-Y+1/2,Z",
        "-X+1/2,Y+1/2,Z+1/2"
    ],
    "C 2/m 2/c 21/m": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-X,Y,1/2-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,1/2-Z",
        "X,-Y,1/2+Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,Z"
    ],
    "C 2/m 2/c 21/a": [
        "X,Y,Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,1/2+Y,Z"
    ],
    "C 2/m 2/m 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "C 2/c 2/c 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "C 2/m 2/m 2/a": [
        "X,Y,Z",
        "-X,1/2-Y,Z",
        "-X,1/2+Y,-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,1/2+Y,-Z",
        "X,1/2-Y,Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,-Y,Z",
        "1/2-X,Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,Y,-Z",
        "1/2+X,-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "C 2/c 2/c 2/a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "-X,Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "-X,1/2-Y,1/2-Z",
        "1/2+X,Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "1/2-X,Y,1/2+Z",
        "1/2+X,1/2+Y,Z",
        "-X,-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "X,-Y,-Z",
        "1/2-X,-Y,1/2-Z",
        "X,1/2+Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z"
    ],
    "F 2/m 2/m 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "-X,1/2-Y,1/2-Z",
        "X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2-X,-Y,1/2-Z",
        "1/2+X,Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,Y,1/2+Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z"
    ],
    "F 2/d 2/d 2/d": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "1/4-X,1/4-Y,1/4-Z",
        "1/4+X,1/4+Y,1/4-Z",
        "1/4+X,1/4-Y,1/4+Z",
        "1/4-X,1/4+Y,1/4+Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "1/4-X,3/4-Y,3/4-Z",
        "1/4+X,3/4+Y,3/4-Z",
        "1/4+X,3/4-Y,3/4+Z",
        "1/4-X,3/4+Y,3/4+Z",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "3/4-X,1/4-Y,3/4-Z",
        "3/4+X,1/4+Y,3/4-Z",
        "3/4+X,1/4-Y,3/4+Z",
        "3/4-X,1/4+Y,3/4+Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "3/4-X,3/4-Y,1/4-Z",
        "3/4+X,3/4+Y,1/4-Z",
        "3/4+X,3/4-Y,1/4+Z",
        "3/4-X,3/4+Y,1/4+Z"
    ],
    "I 2/m 2/m 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "I 2/b 2/a 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z"
    ],
    "I 21/b 21/c 21/a": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "-X,-Y,-Z",
        "1/2+X,Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,1/2-Y,Z",
        "1/2-X,Y,-Z",
        "X,-Y,1/2-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "X,1/2+Y,-Z",
        "1/2+X,-Y,Z",
        "-X,Y,1/2+Z"
    ],
    "I 21/m 21/m 21/a": [
        "X,Y,Z",
        "-X,1/2-Y,Z",
        "-X,1/2+Y,-Z",
        "X,-Y,-Z",
        "-X,-Y,-Z",
        "X,1/2+Y,-Z",
        "X,1/2-Y,Z",
        "-X,Y,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z"
    ],
    "P 4": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z"
    ],
    "P 41": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-Y,X,1/4+Z",
        "Y,-X,3/4+Z"
    ],
    "P 42": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z"
    ],
    "P 43": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-Y,X,3/4+Z",
        "Y,-X,1/4+Z"
    ],
    "I 4": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z"
    ],
    "I 41": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-Y,1/2+X,1/4+Z",
        "1/2+Y,-X,3/4+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-Y,X,3/4+Z",
        "Y,1/2-X,1/4+Z"
    ],
    "P -4": [
        "X,Y,Z",
        "-X,-Y,Z",
        "Y,-X,-Z",
        "-Y,X,-Z"
    ],
    "I -4": [
        "X,Y,Z",
        "-X,-Y,Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z"
    ],
    "P 4/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z"
    ],
    "P 42/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,1/2-Z",
        "-Y,X,1/2-Z"
    ],
    "P 4/n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,Z",
        "1/2+Y,1/2-X,Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z"
    ],
    "P 42/n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "Y,-X,-Z",
        "-Y,X,-Z"
    ],
    "I 4/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z"
    ],
    "I 41/a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-Y,1/2+X,1/4+Z",
        "1/2+Y,-X,3/4+Z",
        "-X,1/2-Y,1/4-Z",
        "1/2+X,Y,3/4-Z",
        "Y,-X,-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-Y,X,3/4+Z",
        "Y,1/2-X,1/4+Z",
        "1/2-X,-Y,3/4-Z",
        "X,1/2+Y,1/4-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "-Y,X,-Z"
    ],
    "P 4 2 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z"
    ],
    "P 4 21 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,Z",
        "1/2+Y,1/2-X,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z"
    ],
    "P 41 2 2": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-Y,X,1/4+Z",
        "Y,-X,3/4+Z",
        "-X,Y,-Z",
        "X,-Y,1/2-Z",
        "Y,X,3/4-Z",
        "-Y,-X,1/4-Z"
    ],
    "P 41 21 2": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "1/2-Y,1/2+X,1/4+Z",
        "1/2+Y,1/2-X,3/4+Z",
        "1/2-X,1/2+Y,1/4-Z",
        "1/2+X,1/2-Y,3/4-Z",
        "Y,X,-Z",
        "-Y,-X,1/2-Z"
    ],
    "P 42 2 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z"
    ],
    "P 42 21 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "Y,X,-Z",
        "-Y,-X,-Z"
    ],
    "P 43 2 2": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-Y,X,3/4+Z",
        "Y,-X,1/4+Z",
        "-X,Y,-Z",
        "X,-Y,1/2-Z",
        "Y,X,1/4-Z",
        "-Y,-X,3/4-Z"
    ],
    "P 43 21 2": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "1/2-Y,1/2+X,3/4+Z",
        "1/2+Y,1/2-X,1/4+Z",
        "1/2-X,1/2+Y,3/4-Z",
        "1/2+X,1/2-Y,1/4-Z",
        "Y,X,-Z",
        "-Y,-X,1/2-Z"
    ],
    "I 4 2 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z"
    ],
    "I 41 2 2": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-Y,1/2+X,1/4+Z",
        "1/2+Y,-X,3/4+Z",
        "1/2-X,Y,3/4-Z",
        "X,1/2-Y,1/4-Z",
        "1/2+Y,1/2+X,1/2-Z",
        "-Y,-X,-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-Y,X,3/4+Z",
        "Y,1/2-X,1/4+Z",
        "-X,1/2+Y,1/4-Z",
        "1/2+X,-Y,3/4-Z",
        "Y,X,-Z",
        "1/2-Y,1/2-X,1/2-Z"
    ],
    "P 4 m m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P 4 b m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "P 42 c m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P 42 n m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P 4 c c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z"
    ],
    "P 4 n c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "P 42 m c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z"
    ],
    "P 42 b c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "I 4 m m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Y,-X,Z",
        "Y,X,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "I 4 c m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "I 41 m d": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-Y,1/2+X,1/4+Z",
        "1/2+Y,-X,3/4+Z",
        "X,-Y,Z",
        "1/2-X,1/2+Y,1/2+Z",
        "-Y,1/2-X,1/4+Z",
        "1/2+Y,X,3/4+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-Y,X,3/4+Z",
        "Y,1/2-X,1/4+Z",
        "1/2+X,1/2-Y,1/2+Z",
        "-X,Y,Z",
        "1/2-Y,-X,3/4+Z",
        "Y,1/2+X,1/4+Z"
    ],
    "I 41 c d": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-Y,1/2+X,1/4+Z",
        "1/2+Y,-X,3/4+Z",
        "X,-Y,1/2+Z",
        "1/2-X,1/2+Y,Z",
        "-Y,1/2-X,3/4+Z",
        "1/2+Y,X,1/4+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-Y,X,3/4+Z",
        "Y,1/2-X,1/4+Z",
        "1/2+X,1/2-Y,Z",
        "-X,Y,1/2+Z",
        "1/2-Y,-X,1/4+Z",
        "Y,1/2+X,3/4+Z"
    ],
    "P -4 2 m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P -4 2 c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z"
    ],
    "P -4 21 m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "P -4 21 c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "P -4 m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "Y,X,-Z",
        "-Y,-X,-Z"
    ],
    "P -4 c 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z"
    ],
    "P -4 b 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2+Y,1/2+X,-Z",
        "1/2-Y,1/2-X,-Z"
    ],
    "P -4 n 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z"
    ],
    "I -4 m 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z"
    ],
    "I -4 c 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2+Y,1/2+X,-Z",
        "1/2-Y,1/2-X,-Z"
    ],
    "I -4 2 m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "-Y,-X,Z",
        "Y,X,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "I -4 2 d": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "1/2-X,Y,3/4-Z",
        "1/2+X,-Y,3/4-Z",
        "1/2-Y,-X,3/4+Z",
        "1/2+Y,X,3/4+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "-X,1/2+Y,1/4-Z",
        "X,1/2-Y,1/4-Z",
        "-Y,1/2-X,1/4+Z",
        "Y,1/2+X,1/4+Z"
    ],
    "P 4/m 2/m 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P 4/m 2/c 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z"
    ],
    "P 4/n 2/b 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+Y,1/2-X,-Z",
        "1/2-Y,1/2+X,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "P 4/n 2/n 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "P 4/m 21/b 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Y,1/2+X,-Z",
        "1/2-Y,1/2-X,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "P 4/m 21/n 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "P 4/n 21/m 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,Z",
        "1/2+Y,1/2-X,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "P 4/n 2/c 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,Z",
        "1/2+Y,1/2-X,Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "P 42/m 2/m 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,1/2-Z",
        "-Y,X,1/2-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z"
    ],
    "P 42/m 2/c 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,1/2-Z",
        "-Y,X,1/2-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P 42/n 2/b 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "1/2+Y,1/2+X,-Z",
        "1/2-Y,1/2-X,-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z"
    ],
    "P 42/n 2/n 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P 42/m 21/b 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,1/2+Z",
        "Y,-X,1/2+Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,1/2-Z",
        "-Y,X,1/2-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "P 42/m 21/n 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,X+1/2,Z+1/2",
        "Y+1/2,1/2-X,Z+1/2",
        "1/2-X,Y+1/2,1/2-Z",
        "X+1/2,1/2-Y,1/2-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y+1/2,1/2-X,1/2-Z",
        "1/2-Y,X+1/2,1/2-Z",
        "X+1/2,1/2-Y,Z+1/2",
        "1/2-X,Y+1/2,Z+1/2",
        "-Y,-X,Z",
        "Y,X,Z"
    ],
    "P 42/n 21/m 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "P 42/n 21/c 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "I 4/m 2/m 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Y,-X,Z",
        "Y,X,Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z"
    ],
    "I 4/m 2/c 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-Y,X,Z",
        "Y,-X,Z",
        "-X,Y,1/2-Z",
        "X,-Y,1/2-Z",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,-Y,1/2+Z",
        "-X,Y,1/2+Z",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Y,1/2+X,-Z",
        "1/2-Y,1/2-X,-Z",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z"
    ],
    "I 41/a 2/m 2/d": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-Y,1/2+X,1/4+Z",
        "1/2+Y,-X,3/4+Z",
        "1/2-X,Y,3/4-Z",
        "X,1/2-Y,1/4-Z",
        "1/2+Y,1/2+X,1/2-Z",
        "-Y,-X,-Z",
        "-X,1/2-Y,1/4-Z",
        "1/2+X,Y,3/4-Z",
        "Y,-X,-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "-X,Y,Z",
        "1/2-Y,-X,3/4+Z",
        "Y,1/2+X,1/4+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-Y,X,3/4+Z",
        "Y,1/2-X,1/4+Z",
        "-X,1/2+Y,1/4-Z",
        "1/2+X,-Y,3/4-Z",
        "Y,X,-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2-X,-Y,3/4-Z",
        "X,1/2+Y,1/4-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "-Y,X,-Z",
        "X,-Y,Z",
        "1/2-X,1/2+Y,1/2+Z",
        "-Y,1/2-X,1/4+Z",
        "1/2+Y,X,3/4+Z"
    ],
    "I 41/a 2/c 2/d": [
        "X,Y,Z",
        "1/2-X,1/2-Y,1/2+Z",
        "-Y,1/2+X,1/4+Z",
        "1/2+Y,-X,3/4+Z",
        "1/2-X,Y,1/4-Z",
        "X,1/2-Y,3/4-Z",
        "1/2+Y,1/2+X,-Z",
        "-Y,-X,1/2-Z",
        "-X,1/2-Y,1/4-Z",
        "1/2+X,Y,3/4-Z",
        "Y,-X,-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2-Y,Z",
        "-X,Y,1/2+Z",
        "1/2-Y,-X,1/4+Z",
        "Y,1/2+X,3/4+Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-Y,X,3/4+Z",
        "Y,1/2-X,1/4+Z",
        "-X,1/2+Y,3/4-Z",
        "1/2+X,-Y,1/4-Z",
        "Y,X,1/2-Z",
        "1/2-Y,1/2-X,-Z",
        "1/2-X,-Y,3/4-Z",
        "X,1/2+Y,1/4-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "-Y,X,-Z",
        "X,-Y,1/2+Z",
        "1/2-X,1/2+Y,Z",
        "-Y,1/2-X,3/4+Z",
        "1/2+Y,X,1/4+Z"
    ],
    "P 3": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z"
    ],
    "P 31": [
        "X,Y,Z",
        "-Y,X-Y,Z+1/3",
        "Y-X,-X,Z+2/3"
    ],
    "P 32": [
        "X,Y,Z",
        "-Y,X-Y,Z+2/3",
        "Y-X,-X,Z+1/3"
    ],
    "H 3": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "X+2/3,Y+1/3,Z+1/3",
        "-Y+2/3,X-Y+1/3,Z+1/3",
        "Y-X+2/3,-X+1/3,Z+1/3",
        "X+1/3,Y+2/3,Z+2/3",
        "-Y+1/3,X-Y+2/3,Z+2/3",
        "Y-X+1/3,-X+2/3,Z+2/3"
    ],
    "R 3": [
        "X,Y,Z",
        "Z,X,Y",
        "Y,Z,X"
    ],
    "P -3": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z"
    ],
    "H -3": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "2/3+X,1/3+Y,1/3+Z",
        "2/3-Y,1/3+X-Y,1/3+Z",
        "2/3+Y-X,1/3-X,1/3+Z",
        "2/3-X,1/3-Y,1/3-Z",
        "2/3+Y,1/3+Y-X,1/3-Z",
        "2/3+X-Y,1/3+X,1/3-Z",
        "1/3+X,2/3+Y,2/3+Z",
        "1/3-Y,2/3+X-Y,2/3+Z",
        "1/3+Y-X,2/3-X,2/3+Z",
        "1/3-X,2/3-Y,2/3-Z",
        "1/3+Y,2/3+Y-X,2/3-Z",
        "1/3+X-Y,2/3+X,2/3-Z"
    ],
    "R -3": [
        "X,Y,Z",
        "Z,X,Y",
        "Y,Z,X",
        "-X,-Y,-Z",
        "-Z,-X,-Y",
        "-Y,-Z,-X"
    ],
    "P 3 1 2": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-Y,-X,-Z",
        "Y-X,Y,-Z",
        "X,X-Y,-Z"
    ],
    "P 3 2 1": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z"
    ],
    "P 31 1 2": [
        "X,Y,Z",
        "-Y,X-Y,1/3+Z",
        "Y-X,-X,2/3+Z",
        "-Y,-X,2/3-Z",
        "Y-X,Y,1/3-Z",
        "X,X-Y,-Z"
    ],
    "P 31 2 1": [
        "X,Y,Z",
        "-Y,X-Y,Z+1/3",
        "Y-X,-X,Z+2/3",
        "Y,X,-Z",
        "X-Y,-Y,2/3-Z",
        "-X,Y-X,1/3-Z"
    ],
    "P 32 1 2": [
        "X,Y,Z",
        "-Y,X-Y,2/3+Z",
        "Y-X,-X,1/3+Z",
        "-Y,-X,1/3-Z",
        "Y-X,Y,2/3-Z",
        "X,X-Y,-Z"
    ],
    "P 32 2 1": [
        "X,Y,Z",
        "-Y,X-Y,Z+2/3",
        "Y-X,-X,Z+1/3",
        "Y,X,-Z",
        "X-Y,-Y,1/3-Z",
        "-X,Y-X,2/3-Z"
    ],
    "H 3 2": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "2/3+X,1/3+Y,1/3+Z",
        "2/3-Y,1/3+X-Y,1/3+Z",
        "2/3+Y-X,1/3-X,1/3+Z",
        "2/3+Y,1/3+X,1/3-Z",
        "2/3+X-Y,1/3-Y,1/3-Z",
        "2/3-X,1/3+Y-X,1/3-Z",
        "1/3+X,2/3+Y,2/3+Z",
        "1/3-Y,2/3+X-Y,2/3+Z",
        "1/3+Y-X,2/3-X,2/3+Z",
        "1/3+Y,2/3+X,2/3-Z",
        "1/3+X-Y,2/3-Y,2/3-Z",
        "1/3-X,2/3+Y-X,2/3-Z"
    ],
    "R 3 2": [
        "X,Y,Z",
        "Z,X,Y",
        "Y,Z,X",
        "-Y,-X,-Z",
        "-X,-Z,-Y",
        "-Z,-Y,-X"
    ],
    "P 3 m 1": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z"
    ],
    "P 3 1 m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,Z",
        "X-Y,-Y,Z",
        "-X,Y-X,Z"
    ],
    "P 3 c 1": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z"
    ],
    "P 3 1 c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,1/2+Z",
        "X-Y,-Y,1/2+Z",
        "-X,Y-X,1/2+Z"
    ],
    "H 3 m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z",
        "2/3+X,1/3+Y,1/3+Z",
        "2/3-Y,1/3+X-Y,1/3+Z",
        "2/3+Y-X,1/3-X,1/3+Z",
        "2/3-Y,1/3-X,1/3+Z",
        "2/3+Y-X,1/3+Y,1/3+Z",
        "2/3+X,1/3+X-Y,1/3+Z",
        "1/3+X,2/3+Y,2/3+Z",
        "1/3-Y,2/3+X-Y,2/3+Z",
        "1/3+Y-X,2/3-X,2/3+Z",
        "1/3-Y,2/3-X,2/3+Z",
        "1/3+Y-X,2/3+Y,2/3+Z",
        "1/3+X,2/3+X-Y,2/3+Z"
    ],
    "R 3 m": [
        "X,Y,Z",
        "Z,X,Y",
        "Y,Z,X",
        "Y,X,Z",
        "X,Z,Y",
        "Z,Y,X"
    ],
    "H 3 c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z",
        "2/3+X,1/3+Y,1/3+Z",
        "2/3-Y,1/3+X-Y,1/3+Z",
        "2/3+Y-X,1/3-X,1/3+Z",
        "2/3-Y,1/3-X,5/6+Z",
        "2/3+Y-X,1/3+Y,5/6+Z",
        "2/3+X,1/3+X-Y,5/6+Z",
        "1/3+X,2/3+Y,2/3+Z",
        "1/3-Y,2/3+X-Y,2/3+Z",
        "1/3+Y-X,2/3-X,2/3+Z",
        "1/3-Y,2/3-X,1/6+Z",
        "1/3+Y-X,2/3+Y,1/6+Z",
        "1/3+X,2/3+X-Y,1/6+Z"
    ],
    "R 3 c": [
        "X,Y,Z",
        "Z,X,Y",
        "Y,Z,X",
        "Y+1/2,X+1/2,Z+1/2",
        "X+1/2,Z+1/2,Y+1/2",
        "Z+1/2,Y+1/2,X+1/2"
    ],
    "P -3 1 2/m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-Y,-X,-Z",
        "Y-X,Y,-Z",
        "X,X-Y,-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "Y,X,Z",
        "X-Y,-Y,Z",
        "-X,Y-X,Z"
    ],
    "P -3 1 2/c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-Y,-X,1/2-Z",
        "Y-X,Y,1/2-Z",
        "X,X-Y,1/2-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "Y,X,1/2+Z",
        "X-Y,-Y,1/2+Z",
        "-X,Y-X,1/2+Z"
    ],
    "P -3 2/m 1": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z"
    ],
    "P -3 2/c 1": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,1/2-Z",
        "X-Y,-Y,1/2-Z",
        "-X,Y-X,1/2-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z"
    ],
    "H -3 2/m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z",
        "2/3+X,1/3+Y,1/3+Z",
        "2/3-Y,1/3+X-Y,1/3+Z",
        "2/3+Y-X,1/3-X,1/3+Z",
        "2/3+Y,1/3+X,1/3-Z",
        "2/3+X-Y,1/3-Y,1/3-Z",
        "2/3-X,1/3+Y-X,1/3-Z",
        "2/3-X,1/3-Y,1/3-Z",
        "2/3+Y,1/3+Y-X,1/3-Z",
        "2/3+X-Y,1/3+X,1/3-Z",
        "2/3-Y,1/3-X,1/3+Z",
        "2/3+Y-X,1/3+Y,1/3+Z",
        "2/3+X,1/3+X-Y,1/3+Z",
        "1/3+X,2/3+Y,2/3+Z",
        "1/3-Y,2/3+X-Y,2/3+Z",
        "1/3+Y-X,2/3-X,2/3+Z",
        "1/3+Y,2/3+X,2/3-Z",
        "1/3+X-Y,2/3-Y,2/3-Z",
        "1/3-X,2/3+Y-X,2/3-Z",
        "1/3-X,2/3-Y,2/3-Z",
        "1/3+Y,2/3+Y-X,2/3-Z",
        "1/3+X-Y,2/3+X,2/3-Z",
        "1/3-Y,2/3-X,2/3+Z",
        "1/3+Y-X,2/3+Y,2/3+Z",
        "1/3+X,2/3+X-Y,2/3+Z"
    ],
    "R -3 2/m": [
        "X,Y,Z",
        "Z,X,Y",
        "Y,Z,X",
        "-Y,-X,-Z",
        "-X,-Z,-Y",
        "-Z,-Y,-X",
        "-X,-Y,-Z",
        "-Z,-X,-Y",
        "-Y,-Z,-X",
        "Y,X,Z",
        "X,Z,Y",
        "Z,Y,X"
    ],
    "H -3 2/c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "Y,X,1/2-Z",
        "X-Y,-Y,1/2-Z",
        "-X,Y-X,1/2-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z",
        "2/3+X,1/3+Y,1/3+Z",
        "2/3-Y,1/3+X-Y,1/3+Z",
        "2/3+Y-X,1/3-X,1/3+Z",
        "2/3+Y,1/3+X,5/6-Z",
        "2/3+X-Y,1/3-Y,5/6-Z",
        "2/3-X,1/3+Y-X,5/6-Z",
        "2/3-X,1/3-Y,1/3-Z",
        "2/3+Y,1/3+Y-X,1/3-Z",
        "2/3+X-Y,1/3+X,1/3-Z",
        "2/3-Y,1/3-X,5/6+Z",
        "2/3+Y-X,1/3+Y,5/6+Z",
        "2/3+X,1/3+X-Y,5/6+Z",
        "1/3+X,2/3+Y,2/3+Z",
        "1/3-Y,2/3+X-Y,2/3+Z",
        "1/3+Y-X,2/3-X,2/3+Z",
        "1/3+Y,2/3+X,1/6-Z",
        "1/3+X-Y,2/3-Y,1/6-Z",
        "1/3-X,2/3+Y-X,1/6-Z",
        "1/3-X,2/3-Y,2/3-Z",
        "1/3+Y,2/3+Y-X,2/3-Z",
        "1/3+X-Y,2/3+X,2/3-Z",
        "1/3-Y,2/3-X,1/6+Z",
        "1/3+Y-X,2/3+Y,1/6+Z",
        "1/3+X,2/3+X-Y,1/6+Z"
    ],
    "R -3 2/c": [
        "X,Y,Z",
        "Z,X,Y",
        "Y,Z,X",
        "-Y+1/2,-X+1/2,-Z+1/2",
        "-X+1/2,-Z+1/2,-Y+1/2",
        "-Z+1/2,-Y+1/2,-X+1/2",
        "-X,-Y,-Z",
        "-Z,-X,-Y",
        "-Y,-Z,-X",
        "Y+1/2,X+1/2,Z+1/2",
        "X+1/2,Z+1/2,Y+1/2",
        "Z+1/2,Y+1/2,X+1/2"
    ],
    "P 6": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,Z",
        "Y,Y-X,Z",
        "X-Y,X,Z"
    ],
    "P 61": [
        "X,Y,Z",
        "-Y,X-Y,Z+1/3",
        "Y-X,-X,Z+2/3",
        "-X,-Y,Z+1/2",
        "Y,Y-X,Z+5/6",
        "X-Y,X,Z+1/6"
    ],
    "P 65": [
        "X,Y,Z",
        "-Y,X-Y,Z+2/3",
        "Y-X,-X,Z+1/3",
        "-X,-Y,Z+1/2",
        "Y,Y-X,Z+1/6",
        "X-Y,X,Z+5/6"
    ],
    "P 62": [
        "X,Y,Z",
        "-Y,X-Y,2/3+Z",
        "Y-X,-X,1/3+Z",
        "-X,-Y,Z",
        "Y,Y-X,2/3+Z",
        "X-Y,X,1/3+Z"
    ],
    "P 64": [
        "X,Y,Z",
        "-Y,X-Y,1/3+Z",
        "Y-X,-X,2/3+Z",
        "-X,-Y,Z",
        "Y,Y-X,1/3+Z",
        "X-Y,X,2/3+Z"
    ],
    "P 63": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/2+Z",
        "X-Y,X,1/2+Z"
    ],
    "P -6": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "X,Y,-Z",
        "-Y,X-Y,-Z",
        "Y-X,-X,-Z"
    ],
    "P 6/m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,Z",
        "Y,Y-X,Z",
        "X-Y,X,Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "X,Y,-Z",
        "-Y,X-Y,-Z",
        "Y-X,-X,-Z"
    ],
    "P 63/m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/2+Z",
        "X-Y,X,1/2+Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "X,Y,1/2-Z",
        "-Y,X-Y,1/2-Z",
        "Y-X,-X,1/2-Z"
    ],
    "P 6 2 2": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,Z",
        "Y,Y-X,Z",
        "X-Y,X,Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "-Y,-X,-Z",
        "Y-X,Y,-Z",
        "X,X-Y,-Z"
    ],
    "P 61 2 2": [
        "X,Y,Z",
        "-Y,X-Y,1/3+Z",
        "Y-X,-X,2/3+Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,5/6+Z",
        "X-Y,X,1/6+Z",
        "Y,X,1/3-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,2/3-Z",
        "-Y,-X,5/6-Z",
        "Y-X,Y,1/2-Z",
        "X,X-Y,1/6-Z"
    ],
    "P 65 2 2": [
        "X,Y,Z",
        "-Y,X-Y,2/3+Z",
        "Y-X,-X,1/3+Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/6+Z",
        "X-Y,X,5/6+Z",
        "Y,X,2/3-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,1/3-Z",
        "-Y,-X,1/6-Z",
        "Y-X,Y,1/2-Z",
        "X,X-Y,5/6-Z"
    ],
    "P 62 2 2": [
        "X,Y,Z",
        "-Y,X-Y,2/3+Z",
        "Y-X,-X,1/3+Z",
        "-X,-Y,Z",
        "Y,Y-X,2/3+Z",
        "X-Y,X,1/3+Z",
        "Y,X,2/3-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,1/3-Z",
        "-Y,-X,2/3-Z",
        "Y-X,Y,-Z",
        "X,X-Y,1/3-Z"
    ],
    "P 64 2 2": [
        "X,Y,Z",
        "-Y,X-Y,1/3+Z",
        "Y-X,-X,2/3+Z",
        "-X,-Y,Z",
        "Y,Y-X,1/3+Z",
        "X-Y,X,2/3+Z",
        "Y,X,1/3-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,2/3-Z",
        "-Y,-X,1/3-Z",
        "Y-X,Y,-Z",
        "X,X-Y,2/3-Z"
    ],
    "P 63 2 2": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/2+Z",
        "X-Y,X,1/2+Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "-Y,-X,1/2-Z",
        "Y-X,Y,1/2-Z",
        "X,X-Y,1/2-Z"
    ],
    "P 6 m m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,Z",
        "Y,Y-X,Z",
        "X-Y,X,Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z",
        "Y,X,Z",
        "X-Y,-Y,Z",
        "-X,Y-X,Z"
    ],
    "P 6 c c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,Z",
        "Y,Y-X,Z",
        "X-Y,X,Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z",
        "Y,X,1/2+Z",
        "X-Y,-Y,1/2+Z",
        "-X,Y-X,1/2+Z"
    ],
    "P 63 c m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/2+Z",
        "X-Y,X,1/2+Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z",
        "Y,X,Z",
        "X-Y,-Y,Z",
        "-X,Y-X,Z"
    ],
    "P 63 m c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/2+Z",
        "X-Y,X,1/2+Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z",
        "Y,X,1/2+Z",
        "X-Y,-Y,1/2+Z",
        "-X,Y-X,1/2+Z"
    ],
    "P -6 m 2": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "X,Y,-Z",
        "-Y,X-Y,-Z",
        "Y-X,-X,-Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z",
        "-Y,-X,-Z",
        "Y-X,Y,-Z",
        "X,X-Y,-Z"
    ],
    "P -6 c 2": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "X,Y,1/2-Z",
        "-Y,X-Y,1/2-Z",
        "Y-X,-X,1/2-Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z",
        "-Y,-X,-Z",
        "Y-X,Y,-Z",
        "X,X-Y,-Z"
    ],
    "P -6 2 m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "X,Y,-Z",
        "-Y,X-Y,-Z",
        "Y-X,-X,-Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "Y,X,Z",
        "X-Y,-Y,Z",
        "-X,Y-X,Z"
    ],
    "P -6 2 c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "X,Y,1/2-Z",
        "-Y,X-Y,1/2-Z",
        "Y-X,-X,1/2-Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "Y,X,1/2+Z",
        "X-Y,-Y,1/2+Z",
        "-X,Y-X,1/2+Z"
    ],
    "P 6/m 2/m 2/m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,Z",
        "Y,Y-X,Z",
        "X-Y,X,Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "-Y,-X,-Z",
        "Y-X,Y,-Z",
        "X,X-Y,-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "X,Y,-Z",
        "Y-X,-X,-Z",
        "-Y,X-Y,-Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z",
        "Y,X,Z",
        "X-Y,-Y,Z",
        "-X,Y-X,Z"
    ],
    "P 6/m 2/c 2/c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,Z",
        "Y,Y-X,Z",
        "X-Y,X,Z",
        "Y,X,1/2-Z",
        "X-Y,-Y,1/2-Z",
        "-X,Y-X,1/2-Z",
        "-Y,-X,1/2-Z",
        "Y-X,Y,1/2-Z",
        "X,X-Y,1/2-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "X,Y,-Z",
        "Y-X,-X,-Z",
        "-Y,X-Y,-Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z",
        "Y,X,1/2+Z",
        "X-Y,-Y,1/2+Z",
        "-X,Y-X,1/2+Z"
    ],
    "P 63/m 2/c 2/m": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/2+Z",
        "X-Y,X,1/2+Z",
        "Y,X,1/2-Z",
        "X-Y,-Y,1/2-Z",
        "-X,Y-X,1/2-Z",
        "-Y,-X,-Z",
        "Y-X,Y,-Z",
        "X,X-Y,-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "X,Y,1/2-Z",
        "Y-X,-X,1/2-Z",
        "-Y,X-Y,1/2-Z",
        "-Y,-X,1/2+Z",
        "Y-X,Y,1/2+Z",
        "X,X-Y,1/2+Z",
        "Y,X,Z",
        "X-Y,-Y,Z",
        "-X,Y-X,Z"
    ],
    "P 63/m 2/m 2/c": [
        "X,Y,Z",
        "-Y,X-Y,Z",
        "Y-X,-X,Z",
        "-X,-Y,1/2+Z",
        "Y,Y-X,1/2+Z",
        "X-Y,X,1/2+Z",
        "Y,X,-Z",
        "X-Y,-Y,-Z",
        "-X,Y-X,-Z",
        "-Y,-X,1/2-Z",
        "Y-X,Y,1/2-Z",
        "X,X-Y,1/2-Z",
        "-X,-Y,-Z",
        "Y,Y-X,-Z",
        "X-Y,X,-Z",
        "X,Y,1/2-Z",
        "Y-X,-X,1/2-Z",
        "-Y,X-Y,1/2-Z",
        "-Y,-X,Z",
        "Y-X,Y,Z",
        "X,X-Y,Z",
        "Y,X,1/2+Z",
        "X-Y,-Y,1/2+Z",
        "-X,Y-X,1/2+Z"
    ],
    "P 2 3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X"
    ],
    "F 2 3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X"
    ],
    "I 2 3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,1/2-Y",
        "1/2-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,1/2-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,1/2-X",
        "1/2-Y,1/2-Z,1/2+X"
    ],
    "P 21 3": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X"
    ],
    "I 21 3": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,1/2-Y,Z",
        "1/2-X,Y,-Z",
        "X,-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "Z,-X,1/2-Y",
        "-Z,1/2-X,Y",
        "1/2-Z,X,-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,Z,-X",
        "Y,-Z,1/2-X",
        "-Y,1/2-Z,X"
    ],
    "P 2/m -3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X"
    ],
    "P 2/n -3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Z,1/2-X,1/2-Y",
        "1/2-Z,1/2+X,1/2+Y",
        "1/2+Z,1/2+X,1/2-Y",
        "1/2+Z,1/2-X,1/2+Y",
        "1/2-Y,1/2-Z,1/2-X",
        "1/2+Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,1/2+X",
        "1/2+Y,1/2+Z,1/2-X"
    ],
    "F 2/m -3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "-X,1/2-Y,1/2-Z",
        "X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z",
        "-Z,1/2-X,1/2-Y",
        "-Z,1/2+X,1/2+Y",
        "Z,1/2+X,1/2-Y",
        "Z,1/2-X,1/2+Y",
        "-Y,1/2-Z,1/2-X",
        "Y,1/2-Z,1/2+X",
        "-Y,1/2+Z,1/2+X",
        "Y,1/2+Z,1/2-X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/2-X,-Y,1/2-Z",
        "1/2+X,Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,Y,1/2+Z",
        "1/2-Z,-X,1/2-Y",
        "1/2-Z,X,1/2+Y",
        "1/2+Z,X,1/2-Y",
        "1/2+Z,-X,1/2+Y",
        "1/2-Y,-Z,1/2-X",
        "1/2+Y,-Z,1/2+X",
        "1/2-Y,Z,1/2+X",
        "1/2+Y,Z,1/2-X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Z,1/2-X,-Y",
        "1/2-Z,1/2+X,Y",
        "1/2+Z,1/2+X,-Y",
        "1/2+Z,1/2-X,Y",
        "1/2-Y,1/2-Z,-X",
        "1/2+Y,1/2-Z,X",
        "1/2-Y,1/2+Z,X",
        "1/2+Y,1/2+Z,-X"
    ],
    "F 2/d -3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/4-X,1/4-Y,1/4-Z",
        "1/4+X,1/4+Y,1/4-Z",
        "1/4+X,1/4-Y,1/4+Z",
        "1/4-X,1/4+Y,1/4+Z",
        "1/4-Z,1/4-X,1/4-Y",
        "1/4-Z,1/4+X,1/4+Y",
        "1/4+Z,1/4+X,1/4-Y",
        "1/4+Z,1/4-X,1/4+Y",
        "1/4-Y,1/4-Z,1/4-X",
        "1/4+Y,1/4-Z,1/4+X",
        "1/4-Y,1/4+Z,1/4+X",
        "1/4+Y,1/4+Z,1/4-X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "1/4-X,3/4-Y,3/4-Z",
        "1/4+X,3/4+Y,3/4-Z",
        "1/4+X,3/4-Y,3/4+Z",
        "1/4-X,3/4+Y,3/4+Z",
        "1/4-Z,3/4-X,3/4-Y",
        "1/4-Z,3/4+X,3/4+Y",
        "1/4+Z,3/4+X,3/4-Y",
        "1/4+Z,3/4-X,3/4+Y",
        "1/4-Y,3/4-Z,3/4-X",
        "1/4+Y,3/4-Z,3/4+X",
        "1/4-Y,3/4+Z,3/4+X",
        "1/4+Y,3/4+Z,3/4-X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "3/4-X,1/4-Y,3/4-Z",
        "3/4+X,1/4+Y,3/4-Z",
        "3/4+X,1/4-Y,3/4+Z",
        "3/4-X,1/4+Y,3/4+Z",
        "3/4-Z,1/4-X,3/4-Y",
        "3/4-Z,1/4+X,3/4+Y",
        "3/4+Z,1/4+X,3/4-Y",
        "3/4+Z,1/4-X,3/4+Y",
        "3/4-Y,1/4-Z,3/4-X",
        "3/4+Y,1/4-Z,3/4+X",
        "3/4-Y,1/4+Z,3/4+X",
        "3/4+Y,1/4+Z,3/4-X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X",
        "3/4-X,3/4-Y,1/4-Z",
        "3/4+X,3/4+Y,1/4-Z",
        "3/4+X,3/4-Y,Z+1/4",
        "3/4-X,3/4+Y,Z+1/4",
        "3/4-Z,3/4-X,1/4-Y",
        "3/4-Z,3/4+X,1/4+Y",
        "3/4+Z,3/4+X,1/4-Y",
        "3/4+Z,3/4-X,1/4+Y",
        "3/4-Y,3/4-Z,1/4-X",
        "3/4+Y,3/4-Z,1/4+X",
        "3/4-Y,3/4+Z,1/4+X",
        "3/4+Y,3/4+Z,1/4-X"
    ],
    "I 2/m -3": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,1/2-Y",
        "1/2-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,1/2-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,1/2-X",
        "1/2-Y,1/2-Z,1/2+X",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Z,1/2-X,1/2-Y",
        "1/2-Z,1/2+X,1/2+Y",
        "1/2+Z,1/2+X,1/2-Y",
        "1/2+Z,1/2-X,1/2+Y",
        "1/2-Y,1/2-Z,1/2-X",
        "1/2+Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,1/2+X",
        "1/2+Y,1/2+Z,1/2-X"
    ],
    "P 21/a -3": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "-X,-Y,-Z",
        "1/2+X,Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,Z",
        "-Z,-X,-Y",
        "1/2-Z,1/2+X,Y",
        "1/2+Z,X,1/2-Y",
        "Z,1/2-X,1/2+Y",
        "-Y,-Z,-X",
        "Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,X",
        "1/2+Y,Z,1/2-X"
    ],
    "I 21/a -3": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "-X,-Y,-Z",
        "1/2+X,Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,Z",
        "-Z,-X,-Y",
        "1/2-Z,1/2+X,Y",
        "1/2+Z,X,1/2-Y",
        "Z,1/2-X,1/2+Y",
        "-Y,-Z,-X",
        "Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,X",
        "1/2+Y,Z,1/2-X",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,1/2-Y,Z",
        "1/2-X,+Y,-Z",
        "X,-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "Z,-X,1/2-Y",
        "-Z,1/2-X,Y",
        "1/2-Z,X,-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,Z,-X",
        "Y,-Z,1/2-X",
        "-Y,1/2-Z,X",
        "1/2-X,1/2-Y,1/2-Z",
        "X,1/2+Y,-Z",
        "1/2+X,-Y,Z",
        "-X,Y,1/2+Z",
        "1/2-Z,1/2-X,1/2-Y",
        "-Z,X,1/2+Y",
        "Z,1/2+X,-Y",
        "1/2+Z,-X,Y",
        "1/2-Y,1/2-Z,1/2-X",
        "1/2+Y,-Z,X",
        "-Y,Z,1/2+X",
        "Y,1/2+Z,-X"
    ],
    "P 4 3 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "Y,-X,Z",
        "-Y,X,Z",
        "X,Z,-Y",
        "-X,Z,Y",
        "-X,-Z,-Y",
        "X,-Z,Y",
        "Z,Y,-X",
        "Z,-Y,X",
        "-Z,Y,X",
        "-Z,-Y,-X"
    ],
    "P 42 3 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+X,1/2+Z,1/2-Y",
        "1/2-X,1/2+Z,1/2+Y",
        "1/2-X,1/2-Z,1/2-Y",
        "1/2+X,1/2-Z,1/2+Y",
        "1/2+Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2+X",
        "1/2-Z,1/2-Y,1/2-X"
    ],
    "F 4 3 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "Y,-X,Z",
        "-Y,X,Z",
        "X,Z,-Y",
        "-X,Z,Y",
        "-X,-Z,-Y",
        "X,-Z,Y",
        "Z,Y,-X",
        "Z,-Y,X",
        "-Z,Y,X",
        "-Z,-Y,-X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "Y,1/2+X,1/2-Z",
        "-Y,1/2-X,1/2-Z",
        "Y,1/2-X,1/2+Z",
        "-Y,1/2+X,1/2+Z",
        "X,1/2+Z,1/2-Y",
        "-X,1/2+Z,1/2+Y",
        "-X,1/2-Z,1/2-Y",
        "X,1/2-Z,1/2+Y",
        "Z,1/2+Y,1/2-X",
        "Z,1/2-Y,1/2+X",
        "-Z,1/2+Y,1/2+X",
        "-Z,1/2-Y,1/2-X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/2+Y,X,1/2-Z",
        "1/2-Y,-X,1/2-Z",
        "1/2+Y,-X,1/2+Z",
        "1/2-Y,X,1/2+Z",
        "1/2+X,Z,1/2-Y",
        "1/2-X,Z,1/2+Y",
        "1/2-X,-Z,1/2-Y",
        "1/2+X,-Z,1/2+Y",
        "1/2+Z,Y,1/2-X",
        "1/2+Z,-Y,1/2+X",
        "1/2-Z,Y,1/2+X",
        "1/2-Z,-Y,1/2-X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X",
        "1/2+Y,1/2+X,-Z",
        "1/2-Y,1/2-X,-Z",
        "1/2+Y,1/2-X,Z",
        "1/2-Y,1/2+X,Z",
        "1/2+X,1/2+Z,-Y",
        "1/2-X,1/2+Z,Y",
        "1/2-X,1/2-Z,-Y",
        "1/2+X,1/2-Z,Y",
        "1/2+Z,1/2+Y,-X",
        "1/2+Z,1/2-Y,X",
        "1/2-Z,1/2+Y,X",
        "1/2-Z,1/2-Y,-X"
    ],
    "F 41 3 2": [
        "X,Y,Z",
        "-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,-Y,1/2-Z",
        "Z,X,Y",
        "1/2+Z,-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,-Y",
        "Y,Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "3/4+Y,1/4+X,3/4-Z",
        "1/4-Y,1/4-X,1/4-Z",
        "1/4+Y,3/4-X,3/4+Z",
        "3/4-Y,3/4+X,1/4+Z",
        "3/4+X,1/4+Z,3/4-Y",
        "3/4-X,3/4+Z,1/4+Y",
        "1/4-X,1/4-Z,1/4-Y",
        "1/4+X,3/4-Z,3/4+Y",
        "3/4+Z,1/4+Y,3/4-X",
        "1/4+Z,3/4-Y,3/4+X",
        "3/4-Z,3/4+Y,1/4+X",
        "1/4-Z,1/4-Y,1/4-X",
        "X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,-Y",
        "-Z,-X,Y",
        "1/2-Z,X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "-Y,-Z,X",
        "3/4+Y,3/4+X,1/4-Z",
        "1/4-Y,3/4-X,3/4-Z",
        "1/4+Y,1/4-X,1/4+Z",
        "3/4-Y,1/4+X,3/4+Z",
        "3/4+X,3/4+Z,1/4-Y",
        "3/4-X,1/4+Z,3/4+Y",
        "1/4-X,3/4-Z,3/4-Y",
        "1/4+X,1/4-Z,1/4+Y",
        "3/4+Z,3/4+Y,1/4-X",
        "1/4+Z,1/4-Y,1/4+X",
        "3/4-Z,1/4+Y,3/4+X",
        "1/4-Z,3/4-Y,3/4-X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,1/2-Y,Z",
        "-X,1/2+Y,1/2-Z",
        "X,-Y,-Z",
        "1/2+Z,X,1/2+Y",
        "Z,-X,-Y",
        "1/2-Z,1/2-X,Y",
        "-Z,1/2+X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,-Z,-X",
        "1/2-Y,1/2-Z,X",
        "1/4+Y,1/4+X,1/4-Z",
        "3/4-Y,1/4-X,3/4-Z",
        "3/4+Y,3/4-X,1/4+Z",
        "1/4-Y,3/4+X,3/4+Z",
        "1/4+X,1/4+Z,1/4-Y",
        "1/4-X,3/4+Z,3/4+Y",
        "3/4-X,1/4-Z,3/4-Y",
        "3/4+X,3/4-Z,1/4+Y",
        "1/4+Z,1/4+Y,1/4-X",
        "3/4+Z,3/4-Y,1/4+X",
        "1/4-Z,3/4+Y,3/4+X",
        "3/4-Z,1/4-Y,3/4-X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,Y,-Z",
        "X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,Y",
        "Z,1/2-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,X,-Y",
        "1/2+Y,1/2+Z,X",
        "-Y,Z,-X",
        "Y,1/2-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/4+Y,3/4+X,3/4-Z",
        "3/4-Y,3/4-X,1/4-Z",
        "3/4+Y,1/4-X,3/4+Z",
        "1/4-Y,1/4+X,1/4+Z",
        "1/4+X,3/4+Z,3/4-Y",
        "1/4-X,1/4+Z,1/4+Y",
        "3/4-X,3/4-Z,1/4-Y",
        "3/4+X,1/4-Z,3/4+Y",
        "1/4+Z,3/4+Y,3/4-X",
        "3/4+Z,1/4-Y,3/4+X",
        "1/4-Z,1/4+Y,1/4+X",
        "3/4-Z,3/4-Y,1/4-X"
    ],
    "I 4 3 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "Y,-X,Z",
        "-Y,X,Z",
        "X,Z,-Y",
        "-X,Z,Y",
        "-X,-Z,-Y",
        "X,-Z,Y",
        "Z,Y,-X",
        "Z,-Y,X",
        "-Z,Y,X",
        "-Z,-Y,-X",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,1/2-Y",
        "1/2-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,1/2-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,1/2-X",
        "1/2-Y,1/2-Z,1/2+X",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+X,1/2+Z,1/2-Y",
        "1/2-X,1/2+Z,1/2+Y",
        "1/2-X,1/2-Z,1/2-Y",
        "1/2+X,1/2-Z,1/2+Y",
        "1/2+Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2+X",
        "1/2-Z,1/2-Y,1/2-X"
    ],
    "P 43 3 2": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "1/4+Y,3/4+X,3/4-Z",
        "1/4-Y,1/4-X,1/4-Z",
        "3/4+Y,3/4-X,1/4+Z",
        "3/4-Y,1/4+X,3/4+Z",
        "1/4+X,3/4+Z,3/4-Y",
        "3/4-X,1/4+Z,3/4+Y",
        "1/4-X,1/4-Z,1/4-Y",
        "3/4+X,3/4-Z,1/4+Y",
        "1/4+Z,3/4+Y,3/4-X",
        "3/4+Z,3/4-Y,1/4+X",
        "3/4-Z,1/4+Y,3/4+X",
        "1/4-Z,1/4-Y,1/4-X"
    ],
    "P 41 3 2": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "3/4+Y,1/4+X,1/4-Z",
        "3/4-Y,3/4-X,3/4-Z",
        "1/4+Y,1/4-X,3/4+Z",
        "1/4-Y,3/4+X,1/4+Z",
        "3/4+X,1/4+Z,1/4-Y",
        "1/4-X,3/4+Z,1/4+Y",
        "3/4-X,3/4-Z,3/4-Y",
        "1/4+X,1/4-Z,3/4+Y",
        "3/4+Z,1/4+Y,1/4-X",
        "1/4+Z,1/4-Y,3/4+X",
        "1/4-Z,3/4+Y,1/4+X",
        "3/4-Z,3/4-Y,3/4-X"
    ],
    "I 41 3 2": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "3/4+Y,1/4+X,1/4-Z",
        "3/4-Y,3/4-X,3/4-Z",
        "1/4+Y,1/4-X,3/4+Z",
        "1/4-Y,3/4+X,1/4+Z",
        "3/4+X,1/4+Z,1/4-Y",
        "1/4-X,3/4+Z,1/4+Y",
        "3/4-X,3/4-Z,3/4-Y",
        "1/4+X,1/4-Z,3/4+Y",
        "3/4+Z,1/4+Y,1/4-X",
        "1/4+Z,1/4-Y,3/4+X",
        "1/4-Z,3/4+Y,1/4+X",
        "3/4-Z,3/4-Y,3/4-X",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,1/2-Y,Z",
        "1/2-X,Y,-Z",
        "X,-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "Z,-X,1/2-Y",
        "-Z,1/2-X,Y",
        "1/2-Z,X,-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,Z,-X",
        "Y,-Z,1/2-X",
        "-Y,1/2-Z,X",
        "1/4+Y,3/4+X,3/4-Z",
        "1/4-Y,1/4-X,1/4-Z",
        "3/4+Y,3/4-X,1/4+Z",
        "3/4-Y,1/4+X,3/4+Z",
        "1/4+X,3/4+Z,3/4-Y",
        "3/4-X,1/4+Z,3/4+Y",
        "1/4-X,1/4-Z,1/4-Y",
        "3/4+X,3/4-Z,1/4+Y",
        "1/4+Z,3/4+Y,3/4-X",
        "3/4+Z,3/4-Y,1/4+X",
        "3/4-Z,1/4+Y,3/4+X",
        "1/4-Z,1/4-Y,1/4-X"
    ],
    "P -4 3 m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,Z",
        "-Y,-X,Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,Z,Y",
        "-X,Z,-Y",
        "-X,-Z,Y",
        "X,-Z,-Y",
        "Z,Y,X",
        "Z,-Y,-X",
        "-Z,Y,-X",
        "-Z,-Y,X"
    ],
    "F -4 3 m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,Z",
        "-Y,-X,Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,Z,Y",
        "-X,Z,-Y",
        "-X,-Z,Y",
        "X,-Z,-Y",
        "Z,Y,X",
        "Z,-Y,-X",
        "-Z,Y,-X",
        "-Z,-Y,X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "Y,1/2+X,1/2+Z",
        "-Y,1/2-X,1/2+Z",
        "Y,1/2-X,1/2-Z",
        "-Y,1/2+X,1/2-Z",
        "X,1/2+Z,1/2+Y",
        "-X,1/2+Z,1/2-Y",
        "-X,1/2-Z,1/2+Y",
        "X,1/2-Z,1/2-Y",
        "Z,1/2+Y,1/2+X",
        "Z,1/2-Y,1/2-X",
        "-Z,1/2+Y,1/2-X",
        "-Z,1/2-Y,1/2+X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/2+Y,X,1/2+Z",
        "1/2-Y,-X,1/2+Z",
        "1/2+Y,-X,1/2-Z",
        "1/2-Y,X,1/2-Z",
        "1/2+X,Z,1/2+Y",
        "1/2-X,Z,1/2-Y",
        "1/2-X,-Z,1/2+Y",
        "1/2+X,-Z,1/2-Y",
        "1/2+Z,Y,1/2+X",
        "1/2+Z,-Y,1/2-X",
        "1/2-Z,Y,1/2-X",
        "1/2-Z,-Y,1/2+X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X",
        "1/2+Y,1/2+X,Z",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2-X,-Z",
        "1/2-Y,1/2+X,-Z",
        "1/2+X,1/2+Z,Y",
        "1/2-X,1/2+Z,-Y",
        "1/2-X,1/2-Z,Y",
        "1/2+X,1/2-Z,-Y",
        "1/2+Z,1/2+Y,X",
        "1/2+Z,1/2-Y,-X",
        "1/2-Z,1/2+Y,-X",
        "1/2-Z,1/2-Y,X"
    ],
    "I -4 3 m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,Z",
        "-Y,-X,Z",
        "Y,-X,-Z",
        "-Y,X,-Z",
        "X,Z,Y",
        "-X,Z,-Y",
        "-X,-Z,Y",
        "X,-Z,-Y",
        "Z,Y,X",
        "Z,-Y,-X",
        "-Z,Y,-X",
        "-Z,-Y,X",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,1/2-Y",
        "1/2-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,1/2-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,1/2-X",
        "1/2-Y,1/2-Z,1/2+X",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,1/2+Z,1/2-Y",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,1/2-Z,1/2-Y",
        "1/2+Z,1/2+Y,1/2+X",
        "1/2+Z,1/2-Y,1/2-X",
        "1/2-Z,1/2+Y,1/2-X",
        "1/2-Z,1/2-Y,1/2+X"
    ],
    "P -4 3 n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,1/2+Z,1/2-Y",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,1/2-Z,1/2-Y",
        "1/2+Z,1/2+Y,1/2+X",
        "1/2+Z,1/2-Y,1/2-X",
        "1/2-Z,1/2+Y,1/2-X",
        "1/2-Z,1/2-Y,1/2+X"
    ],
    "F -4 3 c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,1/2+Z,1/2-Y",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,1/2-Z,1/2-Y",
        "1/2+Z,1/2+Y,1/2+X",
        "1/2+Z,1/2-Y,1/2-X",
        "1/2-Z,1/2+Y,1/2-X",
        "1/2-Z,1/2-Y,1/2+X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "1/2+Y,X,Z",
        "1/2-Y,-X,Z",
        "1/2+Y,-X,-Z",
        "1/2-Y,X,-Z",
        "1/2+X,Z,Y",
        "1/2-X,Z,-Y",
        "1/2-X,-Z,Y",
        "1/2+X,-Z,-Y",
        "1/2+Z,Y,X",
        "1/2+Z,-Y,-X",
        "1/2-Z,Y,-X",
        "1/2-Z,-Y,X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "Y,1/2+X,Z",
        "-Y,1/2-X,Z",
        "Y,1/2-X,-Z",
        "-Y,1/2+X,-Z",
        "X,1/2+Z,Y",
        "-X,1/2+Z,-Y",
        "-X,1/2-Z,Y",
        "X,1/2-Z,-Y",
        "Z,1/2+Y,X",
        "Z,1/2-Y,-X",
        "-Z,1/2+Y,-X",
        "-Z,1/2-Y,X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X",
        "Y,X,1/2+Z",
        "-Y,-X,1/2+Z",
        "Y,-X,1/2-Z",
        "-Y,X,1/2-Z",
        "X,Z,1/2+Y",
        "-X,Z,1/2-Y",
        "-X,-Z,1/2+Y",
        "X,-Z,1/2-Y",
        "Z,Y,1/2+X",
        "Z,-Y,1/2-X",
        "-Z,Y,1/2-X",
        "-Z,-Y,1/2+X"
    ],
    "I -4 3 d": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "1/4+Y,1/4+X,1/4+Z",
        "1/4-Y,3/4-X,3/4+Z",
        "3/4+Y,1/4-X,3/4-Z",
        "3/4-Y,3/4+X,1/4-Z",
        "1/4+X,1/4+Z,1/4+Y",
        "3/4-X,3/4+Z,1/4-Y",
        "1/4-X,3/4-Z,3/4+Y",
        "3/4+X,1/4-Z,3/4-Y",
        "1/4+Z,1/4+Y,1/4+X",
        "3/4+Z,1/4-Y,3/4-X",
        "3/4-Z,3/4+Y,1/4-X",
        "1/4-Z,3/4-Y,3/4+X",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,1/2-Y,Z",
        "1/2-X,Y,-Z",
        "X,-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "Z,-X,1/2-Y",
        "-Z,1/2-X,Y",
        "1/2-Z,X,-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,Z,-X",
        "Y,-Z,1/2-X",
        "-Y,1/2-Z,X",
        "3/4+Y,3/4+X,3/4+Z",
        "3/4-Y,1/4-X,1/4+Z",
        "1/4+Y,3/4-X,1/4-Z",
        "1/4-Y,1/4+X,3/4-Z",
        "3/4+X,3/4+Z,3/4+Y",
        "1/4-X,1/4+Z,3/4-Y",
        "3/4-X,1/4-Z,1/4+Y",
        "1/4+X,3/4-Z,1/4-Y",
        "3/4+Z,3/4+Y,3/4+X",
        "1/4+Z,3/4-Y,1/4-X",
        "1/4-Z,1/4+Y,3/4-X",
        "3/4-Z,1/4-Y,1/4+X"
    ],
    "P 4/m -3 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "Y,-X,Z",
        "-Y,X,Z",
        "X,Z,-Y",
        "-X,Z,Y",
        "-X,-Z,-Y",
        "X,-Z,Y",
        "Z,Y,-X",
        "Z,-Y,X",
        "-Z,Y,X",
        "-Z,-Y,-X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X",
        "-Y,-X,Z",
        "Y,X,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "-X,-Z,Y",
        "X,-Z,-Y",
        "X,Z,Y",
        "-X,Z,-Y",
        "-Z,-Y,X",
        "-Z,Y,-X",
        "Z,-Y,-X",
        "Z,Y,X"
    ],
    "P 4/n -3 2/n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "Y,-X,Z",
        "-Y,X,Z",
        "X,Z,-Y",
        "-X,Z,Y",
        "-X,-Z,-Y",
        "X,-Z,Y",
        "Z,Y,-X",
        "Z,-Y,X",
        "-Z,Y,X",
        "-Z,-Y,-X",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Z,1/2-X,1/2-Y",
        "1/2-Z,1/2+X,1/2+Y",
        "1/2+Z,1/2+X,1/2-Y",
        "1/2+Z,1/2-X,1/2+Y",
        "1/2-Y,1/2-Z,1/2-X",
        "1/2+Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,1/2+X",
        "1/2+Y,1/2+Z,1/2-X",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,1/2-Z,1/2-Y",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,1/2+Z,1/2-Y",
        "1/2-Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2-X",
        "1/2+Z,1/2+Y,1/2+X"
    ],
    "P 42/m -3 2/n": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+X,1/2+Z,1/2-Y",
        "1/2-X,1/2+Z,1/2+Y",
        "1/2-X,1/2-Z,1/2-Y",
        "1/2+X,1/2-Z,1/2+Y",
        "1/2+Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2+X",
        "1/2-Z,1/2-Y,1/2-X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,1/2-Z,1/2-Y",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,1/2+Z,1/2-Y",
        "1/2-Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2-X",
        "1/2+Z,1/2+Y,1/2+X"
    ],
    "P 42/n -3 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+X,1/2+Z,1/2-Y",
        "1/2-X,1/2+Z,1/2+Y",
        "1/2-X,1/2-Z,1/2-Y",
        "1/2+X,1/2-Z,1/2+Y",
        "1/2+Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2+X",
        "1/2-Z,1/2-Y,1/2-X",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Z,1/2-X,1/2-Y",
        "1/2-Z,1/2+X,1/2+Y",
        "1/2+Z,1/2+X,1/2-Y",
        "1/2+Z,1/2-X,1/2+Y",
        "1/2-Y,1/2-Z,1/2-X",
        "1/2+Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,1/2+X",
        "1/2+Y,1/2+Z,1/2-X",
        "-Y,-X,Z",
        "Y,X,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "-X,-Z,Y",
        "X,-Z,-Y",
        "X,Z,Y",
        "-X,Z,-Y",
        "-Z,-Y,X",
        "-Z,Y,-X",
        "Z,-Y,-X",
        "Z,Y,X"
    ],
    "F 4/m -3 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "Y,-X,Z",
        "-Y,X,Z",
        "X,Z,-Y",
        "-X,Z,Y",
        "-X,-Z,-Y",
        "X,-Z,Y",
        "Z,Y,-X",
        "Z,-Y,X",
        "-Z,Y,X",
        "-Z,-Y,-X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X",
        "-Y,-X,Z",
        "Y,X,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "-X,-Z,Y",
        "X,-Z,-Y",
        "X,Z,Y",
        "-X,Z,-Y",
        "-Z,-Y,X",
        "-Z,Y,-X",
        "Z,-Y,-X",
        "Z,Y,X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "Y,1/2+X,1/2-Z",
        "-Y,1/2-X,1/2-Z",
        "Y,1/2-X,1/2+Z",
        "-Y,1/2+X,1/2+Z",
        "X,1/2+Z,1/2-Y",
        "-X,1/2+Z,1/2+Y",
        "-X,1/2-Z,1/2-Y",
        "X,1/2-Z,1/2+Y",
        "Z,1/2+Y,1/2-X",
        "Z,1/2-Y,1/2+X",
        "-Z,1/2+Y,1/2+X",
        "-Z,1/2-Y,1/2-X",
        "-X,1/2-Y,1/2-Z",
        "X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z",
        "-Z,1/2-X,1/2-Y",
        "-Z,1/2+X,1/2+Y",
        "Z,1/2+X,1/2-Y",
        "Z,1/2-X,1/2+Y",
        "-Y,1/2-Z,1/2-X",
        "Y,1/2-Z,1/2+X",
        "-Y,1/2+Z,1/2+X",
        "Y,1/2+Z,1/2-X",
        "-Y,1/2-X,1/2+Z",
        "Y,1/2+X,1/2+Z",
        "-Y,1/2+X,1/2-Z",
        "Y,1/2-X,1/2-Z",
        "-X,1/2-Z,1/2+Y",
        "X,1/2-Z,1/2-Y",
        "X,1/2+Z,1/2+Y",
        "-X,1/2+Z,1/2-Y",
        "-Z,1/2-Y,1/2+X",
        "-Z,1/2+Y,1/2-X",
        "Z,1/2-Y,1/2-X",
        "Z,1/2+Y,1/2+X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/2+Y,X,1/2-Z",
        "1/2-Y,-X,1/2-Z",
        "1/2+Y,-X,1/2+Z",
        "1/2-Y,X,1/2+Z",
        "1/2+X,Z,1/2-Y",
        "1/2-X,Z,1/2+Y",
        "1/2-X,-Z,1/2-Y",
        "1/2+X,-Z,1/2+Y",
        "1/2+Z,Y,1/2-X",
        "1/2+Z,-Y,1/2+X",
        "1/2-Z,Y,1/2+X",
        "1/2-Z,-Y,1/2-X",
        "1/2-X,-Y,1/2-Z",
        "1/2+X,Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,Y,1/2+Z",
        "1/2-Z,-X,1/2-Y",
        "1/2-Z,X,1/2+Y",
        "1/2+Z,X,1/2-Y",
        "1/2+Z,-X,1/2+Y",
        "1/2-Y,-Z,1/2-X",
        "1/2+Y,-Z,1/2+X",
        "1/2-Y,Z,1/2+X",
        "1/2+Y,Z,1/2-X",
        "1/2-Y,-X,1/2+Z",
        "1/2+Y,X,1/2+Z",
        "1/2-Y,X,1/2-Z",
        "1/2+Y,-X,1/2-Z",
        "1/2-X,-Z,1/2+Y",
        "1/2+X,-Z,1/2-Y",
        "1/2+X,Z,1/2+Y",
        "1/2-X,Z,1/2-Y",
        "1/2-Z,-Y,1/2+X",
        "1/2-Z,Y,1/2-X",
        "1/2+Z,-Y,1/2-X",
        "1/2+Z,Y,1/2+X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X",
        "1/2+Y,1/2+X,-Z",
        "1/2-Y,1/2-X,-Z",
        "1/2+Y,1/2-X,Z",
        "1/2-Y,1/2+X,Z",
        "1/2+X,1/2+Z,-Y",
        "1/2-X,1/2+Z,Y",
        "1/2-X,1/2-Z,-Y",
        "1/2+X,1/2-Z,Y",
        "1/2+Z,1/2+Y,-X",
        "1/2+Z,1/2-Y,X",
        "1/2-Z,1/2+Y,X",
        "1/2-Z,1/2-Y,-X",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Z,1/2-X,-Y",
        "1/2-Z,1/2+X,Y",
        "1/2+Z,1/2+X,-Y",
        "1/2+Z,1/2-X,Y",
        "1/2-Y,1/2-Z,-X",
        "1/2+Y,1/2-Z,X",
        "1/2-Y,1/2+Z,X",
        "1/2+Y,1/2+Z,-X",
        "1/2-Y,1/2-X,Z",
        "1/2+Y,1/2+X,Z",
        "1/2-Y,1/2+X,-Z",
        "1/2+Y,1/2-X,-Z",
        "1/2-X,1/2-Z,Y",
        "1/2+X,1/2-Z,-Y",
        "1/2+X,1/2+Z,Y",
        "1/2-X,1/2+Z,-Y",
        "1/2-Z,1/2-Y,X",
        "1/2-Z,1/2+Y,-X",
        "1/2+Z,1/2-Y,-X",
        "1/2+Z,1/2+Y,X"
    ],
    "F 4/m -3 2/c": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+X,1/2+Z,1/2-Y",
        "1/2-X,1/2+Z,1/2+Y",
        "1/2-X,1/2-Z,1/2-Y",
        "1/2+X,1/2-Z,1/2+Y",
        "1/2+Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2+X",
        "1/2-Z,1/2-Y,1/2-X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,1/2-Z,1/2-Y",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,1/2+Z,1/2-Y",
        "1/2-Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2-X",
        "1/2+Z,1/2+Y,1/2+X",
        "X,1/2+Y,1/2+Z",
        "-X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2-Z",
        "Z,1/2+X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,1/2-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "1/2+Y,X,-Z",
        "1/2-Y,-X,-Z",
        "1/2+Y,-X,Z",
        "1/2-Y,X,Z",
        "1/2+X,Z,-Y",
        "1/2-X,Z,Y",
        "1/2-X,-Z,-Y",
        "1/2+X,-Z,Y",
        "1/2+Z,Y,-X",
        "1/2+Z,-Y,X",
        "1/2-Z,Y,X",
        "1/2-Z,-Y,-X",
        "-X,1/2-Y,1/2-Z",
        "X,1/2+Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "-X,1/2+Y,1/2+Z",
        "-Z,1/2-X,1/2-Y",
        "-Z,1/2+X,1/2+Y",
        "Z,1/2+X,1/2-Y",
        "Z,1/2-X,1/2+Y",
        "-Y,1/2-Z,1/2-X",
        "Y,1/2-Z,1/2+X",
        "-Y,1/2+Z,1/2+X",
        "Y,1/2+Z,1/2-X",
        "1/2-Y,-X,Z",
        "1/2+Y,X,Z",
        "1/2-Y,X,-Z",
        "1/2+Y,-X,-Z",
        "1/2-X,-Z,Y",
        "1/2+X,-Z,-Y",
        "1/2+X,Z,Y",
        "1/2-X,Z,-Y",
        "1/2-Z,-Y,X",
        "1/2-Z,Y,-X",
        "1/2+Z,-Y,-X",
        "1/2+Z,Y,X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2+Z,X,1/2+Y",
        "1/2+Z,-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "1/2-Z,X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "Y,1/2+X,-Z",
        "-Y,1/2-X,-Z",
        "Y,1/2-X,Z",
        "-Y,1/2+X,Z",
        "X,1/2+Z,-Y",
        "-X,1/2+Z,Y",
        "-X,1/2-Z,-Y",
        "X,1/2-Z,Y",
        "Z,1/2+Y,-X",
        "Z,1/2-Y,X",
        "-Z,1/2+Y,X",
        "-Z,1/2-Y,-X",
        "1/2-X,-Y,1/2-Z",
        "1/2+X,Y,1/2-Z",
        "1/2+X,-Y,1/2+Z",
        "1/2-X,Y,1/2+Z",
        "1/2-Z,-X,1/2-Y",
        "1/2-Z,X,1/2+Y",
        "1/2+Z,X,1/2-Y",
        "1/2+Z,-X,1/2+Y",
        "1/2-Y,-Z,1/2-X",
        "1/2+Y,-Z,1/2+X",
        "1/2-Y,Z,1/2+X",
        "1/2+Y,Z,1/2-X",
        "-Y,1/2-X,Z",
        "Y,1/2+X,Z",
        "-Y,1/2+X,-Z",
        "Y,1/2-X,-Z",
        "-X,1/2-Z,Y",
        "X,1/2-Z,-Y",
        "X,1/2+Z,Y",
        "-X,1/2+Z,-Y",
        "-Z,1/2-Y,X",
        "-Z,1/2+Y,-X",
        "Z,1/2-Y,-X",
        "Z,1/2+Y,X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,1/2-Y,Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,1/2-Y,-Z",
        "1/2+Z,1/2+X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,1/2-X,Y",
        "1/2-Z,1/2+X,-Y",
        "1/2+Y,1/2+Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,1/2-Z,X",
        "Y,X,1/2-Z",
        "-Y,-X,1/2-Z",
        "Y,-X,1/2+Z",
        "-Y,X,1/2+Z",
        "X,Z,1/2-Y",
        "-X,Z,1/2+Y",
        "-X,-Z,1/2-Y",
        "X,-Z,1/2+Y",
        "Z,Y,1/2-X",
        "Z,-Y,1/2+X",
        "-Z,Y,1/2+X",
        "-Z,-Y,1/2-X",
        "1/2-X,1/2-Y,-Z",
        "1/2+X,1/2+Y,-Z",
        "1/2+X,1/2-Y,Z",
        "1/2-X,1/2+Y,Z",
        "1/2-Z,1/2-X,-Y",
        "1/2-Z,1/2+X,Y",
        "1/2+Z,1/2+X,-Y",
        "1/2+Z,1/2-X,Y",
        "1/2-Y,1/2-Z,-X",
        "1/2+Y,1/2-Z,X",
        "1/2-Y,1/2+Z,X",
        "1/2+Y,1/2+Z,-X",
        "-Y,-X,1/2+Z",
        "Y,X,1/2+Z",
        "-Y,X,1/2-Z",
        "Y,-X,1/2-Z",
        "-X,-Z,1/2+Y",
        "X,-Z,1/2-Y",
        "X,Z,1/2+Y",
        "-X,Z,1/2-Y",
        "-Z,-Y,1/2+X",
        "-Z,Y,1/2-X",
        "Z,-Y,1/2-X",
        "Z,Y,1/2+X"
    ],
    "F 41/d -3 2/m": [
        "X,Y,Z",
        "-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,-Y,1/2-Z",
        "Z,X,Y",
        "1/2+Z,-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,-Y",
        "Y,Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "3/4+Y,1/4+X,3/4-Z",
        "1/4-Y,1/4-X,1/4-Z",
        "1/4+Y,3/4-X,3/4+Z",
        "3/4-Y,3/4+X,1/4+Z",
        "3/4+X,1/4+Z,3/4-Y",
        "3/4-X,3/4+Z,1/4+Y",
        "1/4-X,1/4-Z,1/4-Y",
        "1/4+X,3/4-Z,3/4+Y",
        "3/4+Z,1/4+Y,3/4-X",
        "1/4+Z,3/4-Y,3/4+X",
        "3/4-Z,3/4+Y,1/4+X",
        "1/4-Z,1/4-Y,1/4-X",
        "1/4-X,1/4-Y,1/4-Z",
        "1/4+X,3/4+Y,3/4-Z",
        "3/4+X,3/4-Y,1/4+Z",
        "3/4-X,1/4+Y,3/4+Z",
        "1/4-Z,1/4-X,1/4-Y",
        "3/4-Z,1/4+X,3/4+Y",
        "1/4+Z,3/4+X,3/4-Y",
        "3/4+Z,3/4-X,1/4+Y",
        "1/4-Y,1/4-Z,1/4-X",
        "3/4+Y,3/4-Z,1/4+X",
        "3/4-Y,1/4+Z,3/4+X",
        "1/4+Y,3/4+Z,3/4-X",
        "1/2-Y,-X,1/2+Z",
        "Y,X,Z",
        "-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,-Z",
        "1/2-X,-Z,1/2+Y",
        "1/2+X,1/2-Z,-Y",
        "X,Z,Y",
        "-X,1/2+Z,1/2-Y",
        "1/2-Z,-Y,1/2+X",
        "-Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,-X",
        "Z,Y,X",
        "X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,-Y",
        "-Z,-X,Y",
        "1/2-Z,X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "-Y,-Z,X",
        "3/4+Y,3/4+X,1/4-Z",
        "1/4-Y,3/4-X,3/4-Z",
        "1/4+Y,1/4-X,1/4+Z",
        "3/4-Y,1/4+X,3/4+Z",
        "3/4+X,3/4+Z,1/4-Y",
        "3/4-X,1/4+Z,3/4+Y",
        "1/4-X,3/4-Z,3/4-Y",
        "1/4+X,1/4-Z,1/4+Y",
        "3/4+Z,3/4+Y,1/4-X",
        "1/4+Z,1/4-Y,1/4+X",
        "3/4-Z,1/4+Y,3/4+X",
        "1/4-Z,3/4-Y,3/4-X",
        "1/4-X,3/4-Y,3/4-Z",
        "1/4+X,1/4+Y,1/4-Z",
        "3/4+X,1/4-Y,3/4+Z",
        "3/4-X,3/4+Y,1/4+Z",
        "1/4-Z,3/4-X,3/4-Y",
        "3/4-Z,3/4+X,1/4+Y",
        "1/4+Z,1/4+X,1/4-Y",
        "3/4+Z,1/4-X,3/4+Y",
        "1/4-Y,3/4-Z,3/4-X",
        "3/4+Y,1/4-Z,3/4+X",
        "3/4-Y,3/4+Z,1/4+X",
        "1/4+Y,1/4+Z,1/4-X",
        "1/2-Y,1/2-X,Z",
        "Y,1/2+X,1/2+Z",
        "-Y,X,-Z",
        "1/2+Y,-X,1/2-Z",
        "1/2-X,1/2-Z,Y",
        "1/2+X,-Z,1/2-Y",
        "X,1/2+Z,1/2+Y",
        "-X,Z,-Y",
        "1/2-Z,1/2-Y,X",
        "-Z,Y,-X",
        "1/2+Z,-Y,1/2-X",
        "Z,1/2+Y,1/2+X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,1/2-Y,Z",
        "-X,1/2+Y,1/2-Z",
        "X,-Y,-Z",
        "1/2+Z,X,1/2+Y",
        "Z,-X,-Y",
        "1/2-Z,1/2-X,Y",
        "-Z,1/2+X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,-Z,-X",
        "1/2-Y,1/2-Z,X",
        "1/4+Y,1/4+X,1/4-Z",
        "3/4-Y,1/4-X,3/4-Z",
        "3/4+Y,3/4-X,1/4+Z",
        "1/4-Y,3/4+X,3/4+Z",
        "1/4+X,1/4+Z,1/4-Y",
        "1/4-X,3/4+Z,3/4+Y",
        "3/4-X,1/4-Z,3/4-Y",
        "3/4+X,3/4-Z,1/4+Y",
        "1/4+Z,1/4+Y,1/4-X",
        "3/4+Z,3/4-Y,1/4+X",
        "1/4-Z,3/4+Y,3/4+X",
        "3/4-Z,1/4-Y,3/4-X",
        "3/4-X,1/4-Y,3/4-Z",
        "3/4+X,3/4+Y,1/4-Z",
        "1/4+X,3/4-Y,3/4+Z",
        "1/4-X,1/4+Y,1/4+Z",
        "3/4-Z,1/4-X,3/4-Y",
        "1/4-Z,1/4+X,1/4+Y",
        "3/4+Z,3/4+X,1/4-Y",
        "1/4+Z,3/4-X,3/4+Y",
        "3/4-Y,1/4-Z,3/4-X",
        "1/4+Y,3/4-Z,3/4+X",
        "1/4-Y,1/4+Z,1/4+X",
        "3/4+Y,3/4+Z,1/4-X",
        "-Y,-X,Z",
        "1/2+Y,X,1/2+Z",
        "1/2-Y,1/2+X,-Z",
        "Y,1/2-X,1/2-Z",
        "-X,-Z,Y",
        "X,1/2-Z,1/2-Y",
        "1/2+X,Z,1/2+Y",
        "1/2-X,1/2+Z,-Y",
        "-Z,-Y,X",
        "1/2-Z,1/2+Y,-X",
        "Z,1/2-Y,1/2-X",
        "1/2+Z,Y,1/2+X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,Y,-Z",
        "X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,Y",
        "Z,1/2-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,X,-Y",
        "1/2+Y,1/2+Z,X",
        "-Y,Z,-X",
        "Y,1/2-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/4+Y,3/4+X,3/4-Z",
        "3/4-Y,3/4-X,1/4-Z",
        "3/4+Y,1/4-X,3/4+Z",
        "1/4-Y,1/4+X,1/4+Z",
        "1/4+X,3/4+Z,3/4-Y",
        "1/4-X,1/4+Z,1/4+Y",
        "3/4-X,3/4-Z,1/4-Y",
        "3/4+X,1/4-Z,3/4+Y",
        "1/4+Z,3/4+Y,3/4-X",
        "3/4+Z,1/4-Y,3/4+X",
        "1/4-Z,1/4+Y,1/4+X",
        "3/4-Z,3/4-Y,1/4-X",
        "3/4-X,3/4-Y,1/4-Z",
        "3/4+X,1/4+Y,3/4-Z",
        "1/4+X,1/4-Y,1/4+Z",
        "1/4-X,3/4+Y,3/4+Z",
        "3/4-Z,3/4-X,1/4-Y",
        "1/4-Z,3/4+X,3/4+Y",
        "3/4+Z,1/4+X,3/4-Y",
        "1/4+Z,1/4-X,1/4+Y",
        "3/4-Y,3/4-Z,1/4-X",
        "1/4+Y,1/4-Z,1/4+X",
        "1/4-Y,3/4+Z,3/4+X",
        "3/4+Y,1/4+Z,3/4-X",
        "-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,Z",
        "1/2-Y,X,1/2-Z",
        "Y,-X,-Z",
        "-X,1/2-Z,1/2+Y",
        "X,-Z,-Y",
        "1/2+X,1/2+Z,Y",
        "1/2-X,Z,1/2-Y",
        "-Z,1/2-Y,1/2+X",
        "1/2-Z,Y,1/2-X",
        "Z,-Y,-X",
        "1/2+Z,1/2+Y,X"
    ],
    "F 41/d -3 2/c": [
        "X,Y,Z",
        "-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,-Z",
        "1/2+X,-Y,1/2-Z",
        "Z,X,Y",
        "1/2+Z,-X,1/2-Y",
        "-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,-Y",
        "Y,Z,X",
        "1/2-Y,1/2+Z,-X",
        "1/2+Y,-Z,1/2-X",
        "-Y,1/2-Z,1/2+X",
        "3/4+Y,1/4+X,3/4-Z",
        "1/4-Y,1/4-X,1/4-Z",
        "1/4+Y,3/4-X,3/4+Z",
        "3/4-Y,3/4+X,1/4+Z",
        "3/4+X,1/4+Z,3/4-Y",
        "3/4-X,3/4+Z,1/4+Y",
        "1/4-X,1/4-Z,1/4-Y",
        "1/4+X,3/4-Z,3/4+Y",
        "3/4+Z,1/4+Y,3/4-X",
        "1/4+Z,3/4-Y,3/4+X",
        "3/4-Z,3/4+Y,1/4+X",
        "1/4-Z,1/4-Y,1/4-X",
        "3/4-X,3/4-Y,3/4-Z",
        "3/4+X,1/4+Y,1/4-Z",
        "1/4+X,1/4-Y,3/4+Z",
        "1/4-X,3/4+Y,1/4+Z",
        "3/4-Z,3/4-X,3/4-Y",
        "1/4-Z,3/4+X,1/4+Y",
        "3/4+Z,1/4+X,1/4-Y",
        "1/4+Z,1/4-X,3/4+Y",
        "3/4-Y,3/4-Z,3/4-X",
        "1/4+Y,1/4-Z,3/4+X",
        "1/4-Y,3/4+Z,1/4+X",
        "3/4+Y,1/4+Z,1/4-X",
        "-Y,1/2-X,Z",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,X,-Z",
        "Y,-X,1/2-Z",
        "-X,1/2-Z,Y",
        "X,-Z,1/2-Y",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,Z,-Y",
        "-Z,1/2-Y,X",
        "1/2-Z,Y,-X",
        "Z,-Y,1/2-X",
        "1/2+Z,1/2+Y,1/2+X",
        "X,1/2+Y,1/2+Z",
        "-X,-Y,Z",
        "1/2-X,Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,-Y",
        "-Z,-X,Y",
        "1/2-Z,X,1/2-Y",
        "Y,1/2+Z,1/2+X",
        "1/2-Y,Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "-Y,-Z,X",
        "3/4+Y,3/4+X,1/4-Z",
        "1/4-Y,3/4-X,3/4-Z",
        "1/4+Y,1/4-X,1/4+Z",
        "3/4-Y,1/4+X,3/4+Z",
        "3/4+X,3/4+Z,1/4-Y",
        "3/4-X,1/4+Z,3/4+Y",
        "1/4-X,3/4-Z,3/4-Y",
        "1/4+X,1/4-Z,1/4+Y",
        "3/4+Z,3/4+Y,1/4-X",
        "1/4+Z,1/4-Y,1/4+X",
        "3/4-Z,1/4+Y,3/4+X",
        "1/4-Z,3/4-Y,3/4-X",
        "3/4-X,1/4-Y,1/4-Z",
        "3/4+X,3/4+Y,3/4-Z",
        "1/4+X,3/4-Y,1/4+Z",
        "1/4-X,1/4+Y,3/4+Z",
        "3/4-Z,1/4-X,1/4-Y",
        "1/4-Z,1/4+X,3/4+Y",
        "3/4+Z,3/4+X,3/4-Y",
        "1/4+Z,3/4-X,1/4+Y",
        "3/4-Y,1/4-Z,1/4-X",
        "1/4+Y,3/4-Z,1/4+X",
        "1/4-Y,1/4+Z,3/4+X",
        "3/4+Y,3/4+Z,3/4-X",
        "-Y,-X,1/2+Z",
        "1/2+Y,X,Z",
        "1/2-Y,1/2+X,1/2-Z",
        "Y,1/2-X,-Z",
        "-X,-Z,1/2+Y",
        "X,1/2-Z,-Y",
        "1/2+X,Z,Y",
        "1/2-X,1/2+Z,1/2-Y",
        "-Z,-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2-X",
        "Z,1/2-Y,-X",
        "1/2+Z,Y,X",
        "1/2+X,Y,1/2+Z",
        "1/2-X,1/2-Y,Z",
        "-X,1/2+Y,1/2-Z",
        "X,-Y,-Z",
        "1/2+Z,X,1/2+Y",
        "Z,-X,-Y",
        "1/2-Z,1/2-X,Y",
        "-Z,1/2+X,1/2-Y",
        "1/2+Y,Z,1/2+X",
        "-Y,1/2+Z,1/2-X",
        "Y,-Z,-X",
        "1/2-Y,1/2-Z,X",
        "1/4+Y,1/4+X,1/4-Z",
        "3/4-Y,1/4-X,3/4-Z",
        "3/4+Y,3/4-X,1/4+Z",
        "1/4-Y,3/4+X,3/4+Z",
        "1/4+X,1/4+Z,1/4-Y",
        "1/4-X,3/4+Z,3/4+Y",
        "3/4-X,1/4-Z,3/4-Y",
        "3/4+X,3/4-Z,1/4+Y",
        "1/4+Z,1/4+Y,1/4-X",
        "3/4+Z,3/4-Y,1/4+X",
        "1/4-Z,3/4+Y,3/4+X",
        "3/4-Z,1/4-Y,3/4-X",
        "1/4-X,3/4-Y,1/4-Z",
        "1/4+X,1/4+Y,3/4-Z",
        "3/4+X,1/4-Y,1/4+Z",
        "3/4-X,3/4+Y,3/4+Z",
        "1/4-Z,3/4-X,1/4-Y",
        "3/4-Z,3/4+X,3/4+Y",
        "1/4+Z,1/4+X,3/4-Y",
        "3/4+Z,1/4-X,1/4+Y",
        "1/4-Y,3/4-Z,1/4-X",
        "3/4+Y,1/4-Z,1/4+X",
        "3/4-Y,3/4+Z,3/4+X",
        "1/4+Y,1/4+Z,3/4-X",
        "1/2-Y,1/2-X,1/2+Z",
        "Y,1/2+X,Z",
        "-Y,X,1/2-Z",
        "1/2+Y,-X,-Z",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,-Z,-Y",
        "X,1/2+Z,Y",
        "-X,Z,1/2-Y",
        "1/2-Z,1/2-Y,1/2+X",
        "-Z,Y,1/2-X",
        "1/2+Z,-Y,-X",
        "Z,1/2+Y,X",
        "1/2+X,1/2+Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,Y,-Z",
        "X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,Y",
        "Z,1/2-X,1/2-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,X,-Y",
        "1/2+Y,1/2+Z,X",
        "-Y,Z,-X",
        "Y,1/2-Z,1/2-X",
        "1/2-Y,-Z,1/2+X",
        "1/4+Y,3/4+X,3/4-Z",
        "3/4-Y,3/4-X,1/4-Z",
        "3/4+Y,1/4-X,3/4+Z",
        "1/4-Y,1/4+X,1/4+Z",
        "1/4+X,3/4+Z,3/4-Y",
        "1/4-X,1/4+Z,1/4+Y",
        "3/4-X,3/4-Z,1/4-Y",
        "3/4+X,1/4-Z,3/4+Y",
        "1/4+Z,3/4+Y,3/4-X",
        "3/4+Z,1/4-Y,3/4+X",
        "1/4-Z,1/4+Y,1/4+X",
        "3/4-Z,3/4-Y,1/4-X",
        "1/4-X,1/4-Y,3/4-Z",
        "1/4+X,3/4+Y,1/4-Z",
        "3/4+X,3/4-Y,3/4+Z",
        "3/4-X,1/4+Y,1/4+Z",
        "1/4-Z,1/4-X,3/4-Y",
        "3/4-Z,1/4+X,1/4+Y",
        "1/4+Z,3/4+X,1/4-Y",
        "3/4+Z,3/4-X,3/4+Y",
        "1/4-Y,1/4-Z,3/4-X",
        "3/4+Y,3/4-Z,3/4+X",
        "3/4-Y,1/4+Z,1/4+X",
        "1/4+Y,3/4+Z,1/4-X",
        "1/2-Y,-X,Z",
        "Y,X,1/2+Z",
        "-Y,1/2+X,-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-X,-Z,Y",
        "1/2+X,1/2-Z,1/2-Y",
        "X,Z,1/2+Y",
        "-X,1/2+Z,-Y",
        "1/2-Z,-Y,X",
        "-Z,1/2+Y,-X",
        "1/2+Z,1/2-Y,1/2-X",
        "Z,Y,1/2+X"
    ],
    "I 4/m -3 2/m": [
        "X,Y,Z",
        "-X,-Y,Z",
        "-X,Y,-Z",
        "X,-Y,-Z",
        "Z,X,Y",
        "Z,-X,-Y",
        "-Z,-X,Y",
        "-Z,X,-Y",
        "Y,Z,X",
        "-Y,Z,-X",
        "Y,-Z,-X",
        "-Y,-Z,X",
        "Y,X,-Z",
        "-Y,-X,-Z",
        "Y,-X,Z",
        "-Y,X,Z",
        "X,Z,-Y",
        "-X,Z,Y",
        "-X,-Z,-Y",
        "X,-Z,Y",
        "Z,Y,-X",
        "Z,-Y,X",
        "-Z,Y,X",
        "-Z,-Y,-X",
        "-X,-Y,-Z",
        "X,Y,-Z",
        "X,-Y,Z",
        "-X,Y,Z",
        "-Z,-X,-Y",
        "-Z,X,Y",
        "Z,X,-Y",
        "Z,-X,Y",
        "-Y,-Z,-X",
        "Y,-Z,X",
        "-Y,Z,X",
        "Y,Z,-X",
        "-Y,-X,Z",
        "Y,X,Z",
        "-Y,X,-Z",
        "Y,-X,-Z",
        "-X,-Z,Y",
        "X,-Z,-Y",
        "X,Z,Y",
        "-X,Z,-Y",
        "-Z,-Y,X",
        "-Z,Y,-X",
        "Z,-Y,-X",
        "Z,Y,X",
        "1/2+X,1/2+Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "1/2+Z,1/2-X,1/2-Y",
        "1/2-Z,1/2-X,1/2+Y",
        "1/2-Z,1/2+X,1/2-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,1/2-X",
        "1/2-Y,1/2-Z,1/2+X",
        "1/2+Y,1/2+X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z",
        "1/2+Y,1/2-X,1/2+Z",
        "1/2-Y,1/2+X,1/2+Z",
        "1/2+X,1/2+Z,1/2-Y",
        "1/2-X,1/2+Z,1/2+Y",
        "1/2-X,1/2-Z,1/2-Y",
        "1/2+X,1/2-Z,1/2+Y",
        "1/2+Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2+X",
        "1/2-Z,1/2-Y,1/2-X",
        "1/2-X,1/2-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,1/2+Z",
        "1/2-Z,1/2-X,1/2-Y",
        "1/2-Z,1/2+X,1/2+Y",
        "1/2+Z,1/2+X,1/2-Y",
        "1/2+Z,1/2-X,1/2+Y",
        "1/2-Y,1/2-Z,1/2-X",
        "1/2+Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,1/2+X",
        "1/2+Y,1/2+Z,1/2-X",
        "1/2-Y,1/2-X,1/2+Z",
        "1/2+Y,1/2+X,1/2+Z",
        "1/2-Y,1/2+X,1/2-Z",
        "1/2+Y,1/2-X,1/2-Z",
        "1/2-X,1/2-Z,1/2+Y",
        "1/2+X,1/2-Z,1/2-Y",
        "1/2+X,1/2+Z,1/2+Y",
        "1/2-X,1/2+Z,1/2-Y",
        "1/2-Z,1/2-Y,1/2+X",
        "1/2-Z,1/2+Y,1/2-X",
        "1/2+Z,1/2-Y,1/2-X",
        "1/2+Z,1/2+Y,1/2+X"
    ],
    "I 41/a -3 2/d": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2-Y,-Z",
        "Z,X,Y",
        "1/2+Z,1/2-X,-Y",
        "1/2-Z,-X,1/2+Y",
        "-Z,1/2+X,1/2-Y",
        "Y,Z,X",
        "-Y,1/2+Z,1/2-X",
        "1/2+Y,1/2-Z,-X",
        "1/2-Y,-Z,1/2+X",
        "3/4+Y,1/4+X,1/4-Z",
        "3/4-Y,3/4-X,3/4-Z",
        "1/4+Y,1/4-X,3/4+Z",
        "1/4-Y,3/4+X,1/4+Z",
        "3/4+X,1/4+Z,1/4-Y",
        "1/4-X,3/4+Z,1/4+Y",
        "3/4-X,3/4-Z,3/4-Y",
        "1/4+X,1/4-Z,3/4+Y",
        "3/4+Z,1/4+Y,1/4-X",
        "1/4+Z,1/4-Y,3/4+X",
        "1/4-Z,3/4+Y,1/4+X",
        "3/4-Z,3/4-Y,3/4-X",
        "-X,-Y,-Z",
        "1/2+X,Y,1/2-Z",
        "X,1/2-Y,1/2+Z",
        "1/2-X,1/2+Y,Z",
        "-Z,-X,-Y",
        "1/2-Z,1/2+X,Y",
        "1/2+Z,X,1/2-Y",
        "Z,1/2-X,1/2+Y",
        "-Y,-Z,-X",
        "Y,1/2-Z,1/2+X",
        "1/2-Y,1/2+Z,X",
        "1/2+Y,Z,1/2-X",
        "1/4-Y,3/4-X,3/4+Z",
        "1/4+Y,1/4+X,1/4+Z",
        "3/4-Y,3/4+X,1/4-Z",
        "3/4+Y,1/4-X,3/4-Z",
        "1/4-X,3/4-Z,3/4+Y",
        "3/4+X,1/4-Z,3/4-Y",
        "1/4+X,1/4+Z,1/4+Y",
        "3/4-X,3/4+Z,1/4-Y",
        "1/4-Z,3/4-Y,3/4+X",
        "3/4-Z,3/4+Y,1/4-X",
        "3/4+Z,1/4-Y,3/4-X",
        "1/4+Z,1/4+Y,1/4+X",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,1/2-Y,Z",
        "1/2-X,Y,-Z",
        "X,-Y,1/2-Z",
        "1/2+Z,1/2+X,1/2+Y",
        "Z,-X,1/2-Y",
        "-Z,1/2-X,Y",
        "1/2-Z,X,-Y",
        "1/2+Y,1/2+Z,1/2+X",
        "1/2-Y,Z,-X",
        "Y,-Z,1/2-X",
        "-Y,1/2-Z,X",
        "1/4+Y,3/4+X,3/4-Z",
        "1/4-Y,1/4-X,1/4-Z",
        "3/4+Y,3/4-X,1/4+Z",
        "3/4-Y,1/4+X,3/4+Z",
        "1/4+X,3/4+Z,3/4-Y",
        "3/4-X,1/4+Z,3/4+Y",
        "1/4-X,1/4-Z,1/4-Y",
        "3/4+X,3/4-Z,1/4+Y",
        "1/4+Z,3/4+Y,3/4-X",
        "3/4+Z,3/4-Y,1/4+X",
        "3/4-Z,1/4+Y,3/4+X",
        "1/4-Z,1/4-Y,1/4-X",
        "1/2-X,1/2-Y,1/2-Z",
        "X,1/2+Y,-Z",
        "1/2+X,-Y,Z",
        "-X,Y,1/2+Z",
        "1/2-Z,1/2-X,1/2-Y",
        "-Z,X,1/2+Y",
        "Z,1/2+X,-Y",
        "1/2+Z,-X,Y",
        "1/2-Y,1/2-Z,1/2-X",
        "1/2+Y,-Z,X",
        "-Y,Z,1/2+X",
        "Y,1/2+Z,-X",
        "3/4-Y,1/4-X,1/4+Z",
        "3/4+Y,3/4+X,3/4+Z",
        "1/4-Y,1/4+X,3/4-Z",
        "1/4+Y,3/4-X,1/4-Z",
        "3/4-X,1/4-Z,1/4+Y",
        "1/4+X,3/4-Z,1/4-Y",
        "3/4+X,3/4+Z,3/4+Y",
        "1/4-X,1/4+Z,3/4-Y",
        "3/4-Z,1/4-Y,1/4+X",
        "1/4-Z,1/4+Y,3/4-X",
        "1/4+Z,3/4-Y,1/4-X",
        "3/4+Z,3/4+Y,3/4+X"
    ],
    "P 1 1 2": [
        "X,Y,Z",
        "-X,-Y,Z"
    ],
    "P 1 1 21": [
        "X,Y,Z",
        "-X,-Y,1/2+Z"
    ],
    "B 1 1 2": [
        "X,Y,Z",
        "-X,-Y,Z",
        "1/2+X,+Y,1/2+Z",
        "1/2-X,-Y,1/2+Z"
    ],
    "A 1 2 1": [
        "X,Y,Z",
        "-X,Y,-Z",
        "X,1/2+Y,1/2+Z",
        "-X,1/2+Y,1/2-Z"
    ],
    "C 1 21 1": [
        "X,Y,Z",
        "-X,1/2+Y,-Z",
        "1/2+X,1/2+Y,Z",
        "1/2-X,Y,-Z"
    ],
    "I 1 2 1": [
        "X,Y,Z",
        "-X,Y,-Z",
        "X+1/2,Y+1/2,Z+1/2",
        "-X+1/2,Y+1/2,-Z+1/2"
    ],
    "I 1 21 1": [
        "X,Y,Z",
        "-X,1/2+Y,-Z",
        "X+1/2,Y+1/2,Z+1/2",
        "-X+1/2,Y,1/2-Z"
    ],
    "P 1 1 m": [
        "X,Y,Z",
        "X,Y,-Z"
    ],
    "P 1 1 b": [
        "X,Y,Z",
        "X,1/2+Y,-Z"
    ],
    "B 1 1 m": [
        "X,Y,Z",
        "X,Y,-Z",
        "1/2+X,Y,1/2+Z",
        "1/2+X,Y,1/2-Z"
    ],
    "B 1 1 b": [
        "X,Y,Z",
        "X,1/2+Y,-Z",
        "1/2+X,Y,1/2+Z",
        "1/2+X,1/2+Y,1/2-Z"
    ],
    "P 1 1 2/m": [
        "X,Y,Z",
        "X,Y,-Z",
        "-X,-Y,Z",
        "-X,-Y,-Z"
    ],
    "P 1 1 21/m": [
        "X,Y,Z",
        "-X,-Y,1/2+Z",
        "-X,-Y,-Z",
        "X,Y,1/2-Z"
    ],
    "B 1 1 2/m": [
        "X,Y,Z",
        "X,Y,-Z",
        "-X,-Y,Z",
        "-X,-Y,-Z",
        "1/2+X,Y,1/2+Z",
        "1/2+X,Y,1/2-Z",
        "1/2-X,-Y,1/2+Z",
        "1/2-X,-Y,1/2-Z"
    ],
    "P 1 1 2/b": [
        "X,Y,Z",
        "-X,1/2-Y,Z",
        "-X,-Y,-Z",
        "X,1/2+Y,-Z"
    ],
    "P 1 1 21/b": [
        "X,Y,Z",
        "-X,-Y,-Z",
        "-X,1/2-Y,1/2+Z",
        "X,1/2+Y,1/2-Z"
    ],
    "B 1 1 2/b": [
        "X,Y,Z",
        "-X,1/2-Y,Z",
        "-X,-Y,-Z",
        "X,1/2+Y,-Z",
        "1/2+X,Y,1/2+Z",
        "1/2-X,1/2-Y,1/2+Z",
        "1/2-X,-Y,1/2-Z",
        "1/2+X,1/2+Y,1/2-Z"
    ],
    "P 21 2 2": [
        "X,Y,Z",
        "-X,Y,-Z",
        "1/2+X,-Y,-Z",
        "1/2-X,-Y,Z"
    ],
    "P 2 21 2": [
        "X,Y,Z",
        "X,1/2-Y,-Z",
        "-X,1/2+Y,-Z",
        "-X,-Y,Z"
    ],
    "P 21 21 2 (a)": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "X+1/2,-Y,-Z",
        "-X,Y+1/2,-Z"
    ],
    "P 21 2 21": [
        "X,Y,Z",
        "-X,Y,-Z",
        "1/2+X,-Y,1/2-Z",
        "1/2-X,-Y,1/2+Z"
    ],
    "P 2 21 21": [
        "X,Y,Z",
        "X,-Y,-Z",
        "-X,1/2+Y,1/2-Z",
        "-X,1/2-Y,1/2+Z"
    ],
    "C 2 2 21a)": [
        "X,Y,Z",
        "1/2-X,-Y,1/2+Z",
        "1/2+X,1/2-Y,-Z",
        "-X,1/2+Y,1/2-Z",
        "1/2+X,1/2+Y,Z",
        "-X,1/2-Y,1/2+Z",
        "X,-Y,-Z",
        "1/2-X,Y,1/2-Z"
    ],
    "C 2 2 2a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "X+1/2,-Y,-Z",
        "-X,Y+1/2,-Z",
        "1/2+ X,1/2+Y,Z",
        "-X,-Y,Z",
        "X,1/2-Y,-Z",
        "1/2-X,Y,-Z"
    ],
    "F 2 2 2a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "X+1/2,-Y,-Z",
        "-X,Y+1/2,-Z",
        "X,Y+1/2,Z+1/2",
        "1/2-X,-Y,Z+1/2",
        "X+1/2,-Y+1/2,-Z+1/2",
        "-X,Y,-Z+1/2",
        "X+1/2,Y,Z+1/2",
        "-X,1/2-Y,Z+1/2",
        "X,-Y,-Z+1/2",
        "-X+1/2,Y+1/2,-Z+1/2",
        "X+1/2,Y+1/2,Z",
        "-X,-Y,Z",
        "X,-Y+1/2,-Z",
        "-X+1/2,Y,-Z"
    ],
    "I 2 2 2a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "X+1/2,-Y,-Z",
        "-X,Y+1/2,-Z",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,1/2+Z",
        "1/2-X,Y,1/2-Z",
        "X,1/2-Y,1/2-Z"
    ],
    "P 21/m 21/m 2/n a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "-X,1/2+Y,-Z",
        "1/2+X,-Y,-Z",
        "-X,-Y,-Z",
        "X+1/2,Y+1/2,-Z",
        "X,1/2-Y,Z",
        "1/2-X,Y,Z"
    ],
    "P 42 21 2a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "-Y,X+1/2,1/2+Z",
        "Y+1/2,-X,1/2+Z",
        "-X,Y+1/2,-Z",
        "X+1/2,-Y,-Z",
        "Y,X,1/2-Z",
        "1/2-Y,1/2-X,1/2-Z"
    ],
    "I 2 3a": [
        "X,Y,Z",
        "1/2-X,1/2-Y,Z",
        "X+1/2,-Y,-Z",
        "-X,Y+1/2,-Z",
        "Y,Z,X",
        "1/2-Y,1/2-Z,X",
        "Y+1/2,-Z,-X",
        "-Y,Z+1/2,-X",
        "Z,X,Y",
        "1/2-Z,1/2-X,Y",
        "Z+1/2,-X,-Y",
        "-Z,X+1/2,-Y",
        "1/2+X,1/2+Y,1/2+Z",
        "-X,-Y,1/2+Z",
        "X,1/2-Y,1/2-Z",
        "1/2-X,Y,1/2-Z",
        "1/2+Y,1/2+Z,1/2+X",
        "-Y,-Z,1/2+X",
        "Y,1/2-Z,1/2-X",
        "1/2-Y,Z,1/2-X",
        "1/2+Z,1/2+X,1/2+Y",
        "-Z,-X,1/2+Y",
        "Z,1/2-X,1/2-Y",
        "1/2-Z,X,1/2-Y"
    ]
};

// File:js/ngl/alignment.js

/**
 * @file Alignment
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


//////////////
// Alignment

NGL.SubstitutionMatrices = function(){

    var blosum62x = [
        [4,0,-2,-1,-2,0,-2,-1,-1,-1,-1,-2,-1,-1,-1,1,0,0,-3,-2],        // A
        [0,9,-3,-4,-2,-3,-3,-1,-3,-1,-1,-3,-3,-3,-3,-1,-1,-1,-2,-2],    // C
        [-2,-3,6,2,-3,-1,-1,-3,-1,-4,-3,1,-1,0,-2,0,-1,-3,-4,-3],       // D
        [-1,-4,2,5,-3,-2,0,-3,1,-3,-2,0,-1,2,0,0,-1,-2,-3,-2],          // E
        [-2,-2,-3,-3,6,-3,-1,0,-3,0,0,-3,-4,-3,-3,-2,-2,-1,1,3],        // F
        [0,-3,-1,-2,-3,6,-2,-4,-2,-4,-3,0,-2,-2,-2,0,-2,-3,-2,-3],      // G
        [-2,-3,-1,0,-1,-2,8,-3,-1,-3,-2,1,-2,0,0,-1,-2,-3,-2,2],        // H
        [-1,-1,-3,-3,0,-4,-3,4,-3,2,1,-3,-3,-3,-3,-2,-1,3,-3,-1],       // I
        [-1,-3,-1,1,-3,-2,-1,-3,5,-2,-1,0,-1,1,2,0,-1,-2,-3,-2],        // K
        [-1,-1,-4,-3,0,-4,-3,2,-2,4,2,-3,-3,-2,-2,-2,-1,1,-2,-1],       // L
        [-1,-1,-3,-2,0,-3,-2,1,-1,2,5,-2,-2,0,-1,-1,-1,1,-1,-1],        // M
        [-2,-3,1,0,-3,0,1,-3,0,-3,-2,6,-2,0,0,1,0,-3,-4,-2],            // N
        [-1,-3,-1,-1,-4,-2,-2,-3,-1,-3,-2,-2,7,-1,-2,-1,-1,-2,-4,-3],   // P
        [-1,-3,0,2,-3,-2,0,-3,1,-2,0,0,-1,5,1,0,-1,-2,-2,-1],           // Q
        [-1,-3,-2,0,-3,-2,0,-3,2,-2,-1,0,-2,1,5,-1,-1,-3,-3,-2],        // R
        [1,-1,0,0,-2,0,-1,-2,0,-2,-1,1,-1,0,-1,4,1,-2,-3,-2],           // S
        [0,-1,-1,-1,-2,-2,-2,-1,-1,-1,-1,0,-1,-1,-1,1,5,0,-2,-2],       // T
        [0,-1,-3,-2,-1,-3,-3,3,-2,1,1,-3,-2,-2,-3,-2,0,4,-3,-1],        // V
        [-3,-2,-4,-3,1,-2,-2,-3,-3,-2,-1,-4,-4,-2,-3,-3,-2,-3,11,2],    // W
        [-2,-2,-3,-2,3,-3,2,-1,-2,-1,-1,-2,-3,-1,-2,-2,-2,-1,2,7]       // Y
    ];

    var blosum62 = [
        //A  R  N  D  C  Q  E  G  H  I  L  K  M  F  P  S  T  W  Y  V  B  Z  X
        [ 4,-1,-2,-2, 0,-1,-1, 0,-2,-1,-1,-1,-1,-2,-1, 1, 0,-3,-2, 0,-2,-1, 0], // A
        [-1, 5, 0,-2,-3, 1, 0,-2, 0,-3,-2, 2,-1,-3,-2,-1,-1,-3,-2,-3,-1, 0,-1], // R
        [-2, 0, 6, 1,-3, 0, 0, 0, 1,-3,-3, 0,-2,-3,-2, 1, 0,-4,-2,-3, 3, 0,-1], // N
        [-2,-2, 1, 6,-3, 0, 2,-1,-1,-3,-4,-1,-3,-3,-1, 0,-1,-4,-3,-3, 4, 1,-1], // D
        [ 0,-3,-3,-3, 9,-3,-4,-3,-3,-1,-1,-3,-1,-2,-3,-1,-1,-2,-2,-1,-3,-3,-2], // C
        [-1, 1, 0, 0,-3, 5, 2,-2, 0,-3,-2, 1, 0,-3,-1, 0,-1,-2,-1,-2, 0, 3,-1], // Q
        [-1, 0, 0, 2,-4, 2, 5,-2, 0,-3,-3, 1,-2,-3,-1, 0,-1,-3,-2,-2, 1, 4,-1], // E
        [ 0,-2, 0,-1,-3,-2,-2, 6,-2,-4,-4,-2,-3,-3,-2, 0,-2,-2,-3,-3,-1,-2,-1], // G
        [-2, 0, 1,-1,-3, 0, 0,-2, 8,-3,-3,-1,-2,-1,-2,-1,-2,-2, 2,-3, 0, 0,-1], // H
        [-1,-3,-3,-3,-1,-3,-3,-4,-3, 4, 2,-3, 1, 0,-3,-2,-1,-3,-1, 3,-3,-3,-1], // I
        [-1,-2,-3,-4,-1,-2,-3,-4,-3, 2, 4,-2, 2, 0,-3,-2,-1,-2,-1, 1,-4,-3,-1], // L
        [-1, 2, 0,-1,-3, 1, 1,-2,-1,-3,-2, 5,-1,-3,-1, 0,-1,-3,-2,-2, 0, 1,-1], // K
        [-1,-1,-2,-3,-1, 0,-2,-3,-2, 1, 2,-1, 5, 0,-2,-1,-1,-1,-1, 1,-3,-1,-1], // M
        [-2,-3,-3,-3,-2,-3,-3,-3,-1, 0, 0,-3, 0, 6,-4,-2,-2, 1, 3,-1,-3,-3,-1], // F
        [-1,-2,-2,-1,-3,-1,-1,-2,-2,-3,-3,-1,-2,-4, 7,-1,-1,-4,-3,-2,-2,-1,-2], // P
        [ 1,-1, 1, 0,-1, 0, 0, 0,-1,-2,-2, 0,-1,-2,-1, 4, 1,-3,-2,-2, 0, 0, 0], // S
        [ 0,-1, 0,-1,-1,-1,-1,-2,-2,-1,-1,-1,-1,-2,-1, 1, 5,-2,-2, 0,-1,-1, 0], // T
        [-3,-3,-4,-4,-2,-2,-3,-2,-2,-3,-2,-3,-1, 1,-4,-3,-2,11, 2,-3,-4,-3,-2], // W
        [-2,-2,-2,-3,-2,-1,-2,-3, 2,-1,-1,-2,-1, 3,-3,-2,-2, 2, 7,-1,-3,-2,-1], // Y
        [ 0,-3,-3,-3,-1,-2,-2,-3,-3, 3, 1,-2, 1,-1,-2,-2, 0,-3,-1, 4,-3,-2,-1], // V
        [-2,-1, 3, 4,-3, 0, 1,-1, 0,-3,-4, 0,-3,-3,-2, 0,-1,-4,-3,-3, 4, 1,-1], // B
        [-1, 0, 0, 1,-3, 3, 4,-2, 0,-3,-3, 1,-1,-3,-1, 0,-1,-3,-2,-2, 1, 4,-1], // Z
        [ 0,-1,-1,-1,-2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-2, 0, 0,-2,-1,-1,-1,-1,-1]  // X
    ];

    var nucleotides = 'ACTG';

    var aminoacidsX = 'ACDEFGHIKLMNPQRSTVWY';

    var aminoacids = 'ARNDCQEGHILKMFPSTWYVBZ?';

    function prepareMatrix( cellNames, mat ){

        var j;
        var i = 0;
        var matDict = {};

        mat.forEach( function( row ){

            j = 0;
            var rowDict = {};

            row.forEach( function( elm ){

                rowDict[ cellNames[ j++ ] ] = elm;

            } );

            matDict[ cellNames[ i++ ] ] = rowDict;

        } );

        return matDict;

    }

    return {

        blosum62: prepareMatrix( aminoacids, blosum62 ),

        blosum62x: prepareMatrix( aminoacidsX, blosum62x ),

    };

}();


NGL.Alignment = function( seq1, seq2, gapPenalty, gapExtensionPenalty, substMatrix ){

    // TODO try encoding seqs as integers and use array subst matrix, maybe faster

    this.seq1 = seq1;
    this.seq2 = seq2;

    this.gapPenalty = gapPenalty || -10;
    this.gapExtensionPenalty = gapExtensionPenalty || -1;
    this.substMatrix = substMatrix || "blosum62";

    if( this.substMatrix ){
        this.substMatrix = NGL.SubstitutionMatrices[ this.substMatrix ];
    }

};

NGL.Alignment.prototype = {

    constructor: NGL.Alignment,

    initMatrices: function(){

        this.n = this.seq1.length;
        this.m = this.seq2.length;

        // NGL.log(this.n, this.m);

        this.score = undefined;
        this.ali = '';

        this.S = [];
        this.V = [];
        this.H = [];

        for( var i = 0; i <= this.n; ++i ){

            this.S[ i ] = [];
            this.V[ i ] = [];
            this.H[ i ] = [];

            for( var j = 0; j <= this.m; ++j ){

                this.S[ i ][ j ] = 0;
                this.V[ i ][ j ] = 0;
                this.H[ i ][ j ] = 0;

            }

        }

        for( var i = 0; i <= this.n; ++i ){

            this.S[ i ][ 0 ] = this.gap( 0 );
            this.H[ i ][ 0 ] = -Infinity;

        }

        for( var j = 0; j <= this.m; ++j ){

            this.S[ 0 ][ j ] = this.gap( 0 );
            this.V[ 0 ][ j ] = -Infinity;

        }

        this.S[ 0 ][ 0 ] = 0;

        // NGL.log(this.S, this.V, this.H);

    },

    gap: function( len ){

        return this.gapPenalty + len * this.gapExtensionPenalty;

    },

    makeScoreFn: function(){

        var seq1 = this.seq1;
        var seq2 = this.seq2;

        var substMatrix = this.substMatrix;

        var c1, c2;

        if( substMatrix ){

            return function( i, j ){

                c1 = seq1[ i ];
                c2 = seq2[ j ];

                try{

                    return substMatrix[ c1 ][ c2 ];

                }catch( e ){

                    return -4;

                }

            }

        } else {

            NGL.warn('NGL.Alignment: no subst matrix');

            return function( i, j ){

                c1 = seq1[ i ];
                c2 = seq2[ j ];

                return c1 === c2 ? 5 : -3;

            }

        }

    },

    calc: function(){

        NGL.time( "NGL.Alignment.calc" );

        this.initMatrices();

        var gap0 = this.gap(0);
        var scoreFn = this.makeScoreFn();
        var gapExtensionPenalty = this.gapExtensionPenalty;

        var V = this.V;
        var H = this.H;
        var S = this.S;

        var n = this.n;
        var m = this.m;

        var Vi1, Si1, Vi, Hi, Si;

        var i, j;

        for( i = 1; i <= n; ++i ){

            Si1 = S[ i - 1 ];
            Vi1 = V[ i - 1 ];

            Vi = V[ i ];
            Hi = H[ i ];
            Si = S[ i ];

            for( j = 1; j <= m; ++j ){

                Vi[j] = Math.max(
                    Si1[ j ] + gap0,
                    Vi1[ j ] + gapExtensionPenalty
                );

                Hi[j] = Math.max(
                    Si[ j - 1 ] + gap0,
                    Hi[ j - 1 ] + gapExtensionPenalty
                );

                Si[j] = Math.max(
                    Si1[ j - 1 ] + scoreFn( i - 1, j - 1 ), // match
                    Vi[ j ], //del
                    Hi[ j ]  // ins
                );

            }

        }

        NGL.timeEnd( "NGL.Alignment.calc" );

        // NGL.log(this.S, this.V, this.H);

    },

    trace: function(){

        // NGL.time( "NGL.Alignment.trace" );

        this.ali1 = '';
        this.ali2 = '';

        var scoreFn = this.makeScoreFn();

        var i = this.n;
        var j = this.m;
        var mat = "S";

        if( this.S[i][j] >= this.V[i][j] && this.S[i][j] >= this.V[i][j] ){
            mat = "S";
            this.score = this.S[i][j];
        }else if( this.V[i][j] >= this.H[i][j] ){
            mat = "V";
            this.score = this.V[i][j];
        }else{
            mat = "H";
            this.score = this.H[i][j];
        }

        // NGL.log("NGL.Alignment: SCORE", this.score);
        // NGL.log("NGL.Alignment: S, V, H", this.S[i][j], this.V[i][j], this.H[i][j]);

        while( i > 0 && j > 0 ){

            if( mat=="S" ){

                if( this.S[i][j]==this.S[i-1][j-1] + scoreFn(i-1, j-1) ){
                    this.ali1 = this.seq1[i-1] + this.ali1;
                    this.ali2 = this.seq2[j-1] + this.ali2;
                    --i;
                    --j;
                    mat = "S";
                }else if( this.S[i][j]==this.V[i][j] ){
                    mat = "V";
                }else if( this.S[i][j]==this.H[i][j] ){
                    mat = "H";
                }else{
                    NGL.error('NGL.Alignment: S');
                    --i;
                    --j;
                }

            }else if( mat=="V" ){

                if( this.V[i][j]==this.V[i-1][j] + this.gapExtensionPenalty ){
                    this.ali1 = this.seq1[i-1] + this.ali1;
                    this.ali2 = '-' + this.ali2;
                    --i;
                    mat = "V";
                }else if( this.V[i][j]==this.S[i-1][j] + this.gap(0) ){
                    this.ali1 = this.seq1[i-1] + this.ali1;
                    this.ali2 = '-' + this.ali2;
                    --i;
                    mat = "S";
                }else{
                    NGL.error('NGL.Alignment: V');
                    --i;
                }

            }else if( mat=="H" ){

                if( this.H[i][j] == this.H[i][j-1] + this.gapExtensionPenalty ){
                    this.ali1 = '-' + this.ali1;
                    this.ali2 = this.seq2[j-1] + this.ali2;
                    --j;
                    mat = "H";
                }else if( this.H[i][j] == this.S[i][j-1] + this.gap(0) ){
                    this.ali1 = '-' + this.ali1;
                    this.ali2 = this.seq2[j-1] + this.ali2;
                    --j;
                    mat = "S";
                }else{
                    NGL.error('NGL.Alignment: H');
                    --j;
                }

            }else{

                NGL.error('NGL.Alignment: no matrix');

            }

        }

        while( i > 0 ){

            this.ali1 = this.seq1[ i - 1 ] + this.ali1;
            this.ali2 = '-' + this.ali2;
            --i;

        }

        while( j > 0 ){

            this.ali1 = '-' + this.ali1;
            this.ali2 = this.seq2[ j - 1 ] + this.ali2;
            --j;

        }

        // NGL.timeEnd( "NGL.Alignment.trace" );

        // NGL.log([this.ali1, this.ali2]);

    }

};

// File:js/ngl/geometry.js

/**
 * @file Geometry
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


/////////////////
// Interpolator

NGL.Interpolator = function( m, tension ){

    var dt = 1.0 / m;
    var delta = 0.0001;

    var vec1 = new THREE.Vector3();
    var vec2 = new THREE.Vector3();

    function interpolate( p0, p1, p2, p3, t ) {
        var v0 = ( p2 - p0 ) * tension;
        var v1 = ( p3 - p1 ) * tension;
        var t2 = t * t;
        var t3 = t * t2;
        return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 +
               ( -3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 +
               v0 * t + p1;
    }

    function interpolateToArr( v0, v1, v2, v3, t, arr, offset ){
        arr[ offset + 0 ] = interpolate( v0.x, v1.x, v2.x, v3.x, t );
        arr[ offset + 1 ] = interpolate( v0.y, v1.y, v2.y, v3.y, t );
        arr[ offset + 2 ] = interpolate( v0.z, v1.z, v2.z, v3.z, t );
    }

    function interpolateToVec( v0, v1, v2, v3, t, vec ){
        vec.x = interpolate( v0.x, v1.x, v2.x, v3.x, t );
        vec.y = interpolate( v0.y, v1.y, v2.y, v3.y, t );
        vec.z = interpolate( v0.z, v1.z, v2.z, v3.z, t );
    }

    function interpolatePosition( v0, v1, v2, v3, pos, offset ){
        for( var j = 0; j < m; ++j ){
            var l = offset + j * 3;
            var d = dt * j;
            interpolateToArr( v0, v1, v2, v3, d, pos, l );
        }
    }

    function interpolateTangent( v0, v1, v2, v3, tan, offset ){
        for( var j = 0; j < m; ++j ){
            var d = dt * j;
            var d1 = d - delta;
            var d2 = d + delta;
            var l = offset + j * 3;
            // capping as a precation
            if ( d1 < 0 ) d1 = 0;
            if ( d2 > 1 ) d2 = 1;
            //
            interpolateToVec( v0, v1, v2, v3, d1, vec1 );
            interpolateToVec( v0, v1, v2, v3, d2, vec2 );
            //
            vec2.sub( vec1 ).normalize();
            vec2.toArray( tan, l );
        }
    }

    function vectorSubdivide( interpolationFn, iterator, array, offset, isCyclic ){
        var v0;
        var v1 = iterator.next();
        var v2 = iterator.next();
        var v3 = iterator.next();
        //
        var n = iterator.size;
        var n1 = n - 1;
        var k = offset || 0;
        for( var i = 0; i < n1; ++i ){
            v0 = v1;
            v1 = v2;
            v2 = v3;
            v3 = iterator.next();
            interpolationFn( v0, v1, v2, v3, array, k );
            k += 3 * m;
        }
        if( isCyclic ){
            v0 = iterator.get( n - 2 );
            v1 = iterator.get( n - 1 );
            v2 = iterator.get( 0 );
            v3 = iterator.get( 1 );
            interpolationFn( v0, v1, v2, v3, array, k );
            k += 3 * m;
        }
    }

    //

    this.getPosition = function( iterator, array, offset, isCyclic ){
        iterator.reset();
        vectorSubdivide(
            interpolatePosition, iterator, array, offset, isCyclic
        );
        var n1 = iterator.size - 1;
        var k = n1 * m * 3;
        if( isCyclic ) k += m * 3;
        var v = iterator.get( isCyclic ? 0 : n1 );
        array[ k     ] = v.x;
        array[ k + 1 ] = v.y;
        array[ k + 2 ] = v.z;
    }

    this.getTangent = function( iterator, array, offset, isCyclic ){
        iterator.reset();
        vectorSubdivide(
            interpolateTangent, iterator, array, offset, isCyclic
        );
        var n1 = iterator.size - 1;
        var k = n1 * m * 3;
        if( isCyclic ) k += m * 3;
        NGL.Utils.copyArray( array, array, k - 3, k, 3 );
    }

    //

    var vDir = new THREE.Vector3();
    var vTan = new THREE.Vector3();
    var vNorm = new THREE.Vector3();
    var vBin = new THREE.Vector3();

    var m2 = Math.ceil( m / 2 );

    function interpolateNormalDir( u0, u1, u2, u3, v0, v1, v2, v3, tan, norm, bin, offset, shift ){
        for( var j = 0; j < m; ++j ){
            var l = offset + j * 3;
            if( shift ) l += m2 * 3;
            var d = dt * j;
            interpolateToVec( u0, u1, u2, u3, d, vec1 );
            interpolateToVec( v0, v1, v2, v3, d, vec2 );
            vDir.subVectors( vec2, vec1 ).normalize();
            vTan.fromArray( tan, l );
            vBin.crossVectors( vDir, vTan ).normalize();
            vBin.toArray( bin, l );
            vNorm.crossVectors( vTan, vBin ).normalize();
            vNorm.toArray( norm, l );
        }
    }

    function interpolateNormal( vDir, tan, norm, bin, offset ){
        for( var j = 0; j < m; ++j ){
            var l = offset + j * 3;
            vDir.copy( vNorm );
            vTan.fromArray( tan, l );
            vBin.crossVectors( vDir, vTan ).normalize();
            vBin.toArray( bin, l );
            vNorm.crossVectors( vTan, vBin ).normalize();
            vNorm.toArray( norm, l );
        }
    }

    this.getNormal = function( size, tan, norm, bin, offset, isCyclic, shift ){
        vNorm.set( 0, 0, 1 );
        var n = size;
        var n1 = n - 1;
        var k = offset || 0;
        for( var i = 0; i < n1; ++i ){
            interpolateNormal( vDir, tan, norm, bin, k );
            k += 3 * m;
        }
        if( isCyclic ){
            interpolateNormal( vDir, tan, norm, bin, k );
            k += 3 * m;
        }
        vBin.toArray( bin, k );
        vNorm.toArray( norm, k );
    };

    this.getNormalDir = function( iterDir1, iterDir2, tan, norm, bin, offset, isCyclic, shift ){
        iterDir1.reset();
        iterDir2.reset();
        //
        var vSub1 = new THREE.Vector3();
        var vSub2 = new THREE.Vector3();
        var vSub3 = new THREE.Vector3();
        var vSub4 = new THREE.Vector3();
        //
        var d1v1 = new THREE.Vector3();
        var d1v2 = new THREE.Vector3().copy( iterDir1.next() );
        var d1v3 = new THREE.Vector3().copy( iterDir1.next() );
        var d1v4 = new THREE.Vector3().copy( iterDir1.next() );
        var d2v1 = new THREE.Vector3();
        var d2v2 = new THREE.Vector3().copy( iterDir2.next() );
        var d2v3 = new THREE.Vector3().copy( iterDir2.next() );
        var d2v4 = new THREE.Vector3().copy( iterDir2.next() );
        //
        vNorm.set( 0, 0, 1 );
        var n = iterDir1.size;
        var n1 = n - 1;
        var k = offset || 0;
        for( var i = 0; i < n1; ++i ){
            d1v1.copy( d1v2 );
            d1v2.copy( d1v3 );
            d1v3.copy( d1v4 );
            d1v4.copy( iterDir1.next() );
            d2v1.copy( d2v2 );
            d2v2.copy( d2v3 );
            d2v3.copy( d2v4 );
            d2v4.copy( iterDir2.next() );
            //
            if( i === 0 ){
                vSub1.subVectors( d2v1, d1v1 );
                vSub2.subVectors( d2v2, d1v2 );
                if( vSub1.dot( vSub2 ) < 0 ){
                    vSub2.multiplyScalar( -1 );
                    d2v2.addVectors( d1v2, vSub2 );
                }
                vSub3.subVectors( d2v3, d1v3 );
                if( vSub2.dot( vSub3 ) < 0 ){
                    vSub3.multiplyScalar( -1 );
                    d2v3.addVectors( d1v3, vSub3 );
                }
            }else{
                vSub3.copy( vSub4 );
            }
            vSub4.subVectors( d2v4, d1v4 );
            if( vSub3.dot( vSub4 ) < 0 ){
                vSub4.multiplyScalar( -1 );
                d2v4.addVectors( d1v4, vSub4 );
            }
            interpolateNormalDir(
                d1v1, d1v2, d1v3, d1v4,
                d2v1, d2v2, d2v3, d2v4,
                tan, norm, bin, k, shift
            );
            k += 3 * m;
        }
        if( isCyclic ){
            d1v1.copy( iterDir1.get( n - 2 ) );
            d1v2.copy( iterDir1.get( n - 1 ) );
            d1v3.copy( iterDir1.get( 0 ) );
            d1v4.copy( iterDir1.get( 1 ) );
            d2v1.copy( iterDir2.get( n - 2 ) );
            d2v2.copy( iterDir2.get( n - 1 ) );
            d2v3.copy( iterDir2.get( 0 ) );
            d2v4.copy( iterDir2.get( 1 ) );
            //
            vSub3.copy( vSub4 );
            vSub4.subVectors( d2v4, d1v4 );
            if( vSub3.dot( vSub4 ) < 0 ){
                vSub4.multiplyScalar( -1 );
                d2v4.addVectors( d1v4, vSub4 );
            }
            interpolateNormalDir(
                d1v1, d1v2, d1v3, d1v4,
                d2v1, d2v2, d2v3, d2v4,
                tan, norm, bin, k, shift
            );
            k += 3 * m;
        }
        if( shift ){
            // FIXME shift requires data from one more preceeding residue
            vBin.fromArray( bin, m2 * 3 );
            vNorm.fromArray( norm, m2 * 3 );
            for( var j = 0; j < m2; ++j ){
                vBin.toArray( bin, j * 3 );
                vNorm.toArray( norm, j * 3 );
            }
        }else{
            vBin.toArray( bin, k );
            vNorm.toArray( norm, k );
        }
    };

    //

    function interpolateColor( item1, item2, colFn, pcolFn, col, pcol, offset ){
        for( var j = 0; j < m2; ++j ){
            var l = offset + j * 3;
            colFn( item1, col, l );  // itemColorToArray
            pcolFn( item1, pcol, l );  // itemPickingColorToArray
        }
        for( var j = m2; j < m; ++j ){
            var l = offset + j * 3;
            colFn( item2, col, l );  // itemColorToArray
            pcolFn( item2, pcol, l );  // itemPickingColorToArray
        }
    }

    this.getColor = function( iterator, colFn, pcolFn, col, pcol, offset, isCyclic ){
        iterator.reset();
        var i0 = iterator.next();  // first element not needed, replaced in the loop
        var i1 = iterator.next();
        //
        var n = iterator.size;
        var n1 = n - 1;
        var k = offset || 0;
        for( var i = 0; i < n1; ++i ){
            i0 = i1;
            i1 = iterator.next();
            interpolateColor( i0, i1, colFn, pcolFn, col, pcol, k );
            k += 3 * m;
        }
        if( isCyclic ){
            i0 = iterator.get( n - 1 );
            i1 = iterator.get( 0 );
            interpolateColor( i0, i1, colFn, pcolFn, col, pcol, k );
            k += 3 * m;
        }
        //
        col[ k     ] = col[ k - 3 ];
        col[ k + 1 ] = col[ k - 2 ];
        col[ k + 2 ] = col[ k - 1 ];
        pcol[ k     ] = pcol[ k - 3 ];
        pcol[ k + 1 ] = pcol[ k - 2 ];
        pcol[ k + 2 ] = pcol[ k - 1 ];
    }

    //

    function interpolateSize( item1, item2, sizeFn, size, offset ){
        var s1 = sizeFn( item1 );
        var s2 = sizeFn( item2 );
        for( var j = 0; j < m; ++j ){
            // linear interpolation
            var t = j / m;
            size[ offset + j ] = ( 1 - t ) * s1 + t * s2;
        }
    }

    this.getSize = function( iterator, sizeFn, size, offset, isCyclic ){
        iterator.reset();
        var i0 = iterator.next();  // first element not needed, replaced in the loop
        var i1 = iterator.next();
        //
        var n = iterator.size;
        var n1 = n - 1;
        var k = offset || 0;
        for( var i = 0; i < n1; ++i ){
            i0 = i1;
            i1 = iterator.next();
            interpolateSize( i0, i1, sizeFn, size, k );
            k += m;
        }
        if( isCyclic ){
            i0 = iterator.get( n - 1 );
            i1 = iterator.get( 0 );
            interpolateSize( i0, i1, sizeFn, size, k );
            k += m;
        }
        //
        size[ k ] = size[ k - 1 ];
    }

};


///////////
// Spline

NGL.Spline = function( polymer, params ){

    this.polymer = polymer;
    this.size = polymer.residueCount;

    var p = params || {};
    this.directional = p.directional || false;
    this.positionIterator = p.positionIterator || false
    this.subdiv = p.subdiv || 1;
    this.smoothSheet = p.smoothSheet || false;

    if( isNaN( p.tension ) ){
        this.tension = this.polymer.isNucleic() ? 0.5 : 0.9;
    }else{
        this.tension = p.tension || 0.5;
    }

    this.interpolator = new NGL.Interpolator( this.subdiv, this.tension );

};

NGL.Spline.prototype = {

    constructor: NGL.Spline,

    getAtomIterator: function( type, smooth ){

        var polymer = this.polymer;
        var structure = polymer.structure;
        var n = polymer.residueCount;

        var i = 0;
        var j = -1;

        var cache = [
            structure.getAtomProxy(),
            structure.getAtomProxy(),
            structure.getAtomProxy(),
            structure.getAtomProxy()
        ];

        var cache2 = [
            new THREE.Vector3(),
            new THREE.Vector3(),
            new THREE.Vector3(),
            new THREE.Vector3()
        ];

        function next(){
            var atomProxy = this.get( j );
            j += 1;
            return atomProxy;
        }

        var apPrev = structure.getAtomProxy();
        var apNext = structure.getAtomProxy();

        function get( idx ){
            var atomProxy = cache[ i % 4 ];
            atomProxy.index = polymer.getAtomIndexByType( idx, type );
            if( smooth && idx > 0 && idx < n && atomProxy.sstruc === "e" ){
                var vec = cache2[ i % 4 ];
                apPrev.index = polymer.getAtomIndexByType( idx + 1, type );
                apNext.index = polymer.getAtomIndexByType( idx - 1, type );
                vec.addVectors( apPrev, apNext )
                    .add( atomProxy ).add( atomProxy )
                    .multiplyScalar( 0.25 );
                i += 1;
                return vec;
            }
            i += 1;
            return atomProxy;
        }

        function reset(){
            i = 0;
            j = -1;
        }

        return {
            size: n,
            next: next,
            get: get,
            reset: reset
        };

    },

    getSubdividedColor: function( params ){

        var m = this.subdiv;
        var polymer = this.polymer;
        var n = polymer.residueCount;
        var n1 = n - 1;
        var nCol = n1 * m * 3 + 3;
        if( polymer.isCyclic ) nCol += m * 3;

        var col = new Float32Array( nCol );
        var pcol = new Float32Array( nCol );
        var iterator = this.getAtomIterator( "trace" );

        var p = params || {};
        p.structure = polymer.structure;

        var colorMaker = NGL.ColorMakerRegistry.getScheme( p );
        var pickingColorMaker = NGL.ColorMakerRegistry.getPickingScheme( p );

        function colFn( item, array, offset ){
            colorMaker.atomColorToArray( item, array, offset );
        }

        function pcolFn( item, array, offset ){
            pickingColorMaker.atomColorToArray( item, array, offset );
        }

        this.interpolator.getColor(
            iterator, colFn, pcolFn, col, pcol, 0, polymer.isCyclic
        );

        return {
            "color": col,
            "pickingColor": pcol
        };

    },

    getSubdividedPosition: function(){

        var pos = this.getPosition();

        return {
            "position": pos
        }

    },

    getSubdividedOrientation: function(){

        var tan = this.getTangent();
        var normals = this.getNormals( tan );

        return {
            "tangent": tan,
            "normal": normals.normal,
            "binormal": normals.binormal
        }

    },

    getSubdividedSize: function( type, scale ){

        var m = this.subdiv;
        var polymer = this.polymer;
        var n = polymer.residueCount;
        var n1 = n - 1;
        var nSize = n1 * m + 1;
        if( polymer.isCyclic ) nSize += m;

        var size = new Float32Array( nSize );
        var iterator = this.getAtomIterator( "trace" );

        var radiusFactory = new NGL.RadiusFactory( type, scale );

        function sizeFn( item ){
            return radiusFactory.atomRadius( item );
        }

        this.interpolator.getSize(
            iterator, sizeFn, size, 0, polymer.isCyclic
        );

        return {
            "size": size
        };

    },

    getPosition: function(){

        var m = this.subdiv;
        var polymer = this.polymer;
        var n = polymer.residueCount;
        var n1 = n - 1;
        var nPos = n1 * m * 3 + 3
        if( polymer.isCyclic ) nPos += m * 3;

        var pos = new Float32Array( nPos );
        var iterator = this.positionIterator || this.getAtomIterator( "trace", this.smoothSheet );

        this.interpolator.getPosition(
            iterator, pos, 0, polymer.isCyclic
        );

        return pos;

    },

    getTangent: function(){

        var m = this.subdiv;
        var polymer = this.polymer;
        var n = this.size;
        var n1 = n - 1;
        var nTan = n1 * m * 3 + 3
        if( polymer.isCyclic ) nTan += m * 3;

        var tan = new Float32Array( nTan );
        var iterator = this.positionIterator || this.getAtomIterator( "trace", this.smoothSheet );

        this.interpolator.getTangent(
            iterator, tan, 0, polymer.isCyclic
        );

        return tan;

    },

    getNormals: function( tan ){

        var m = this.subdiv;
        var polymer = this.polymer;
        var isProtein = polymer.isProtein();
        var n = this.size;
        var n1 = n - 1;
        var nNorm = n1 * m * 3 + 3
        if( polymer.isCyclic ) nNorm += m * 3;

        var norm = new Float32Array( nNorm );
        var bin = new Float32Array( nNorm );

        if( this.directional && !this.polymer.isCg() ){
            var iterDir1 = this.getAtomIterator( "direction1" );
            var iterDir2 = this.getAtomIterator( "direction2" );
            this.interpolator.getNormalDir(
                iterDir1, iterDir2, tan, norm, bin, 0, polymer.isCyclic, isProtein
            );
        }else{
            this.interpolator.getNormal(
                n, tan, norm, bin, 0, polymer.isCyclic, isProtein
            );
        }

        return {
            "normal": norm,
            "binormal": bin
        }

    }

};


////////////////
// Helixorient

NGL.Helixorient = function( polymer ){

    this.polymer = polymer;

    this.size = polymer.residueCount;

};

NGL.Helixorient.prototype = {

    constructor: NGL.Helixorient,

    getCenterIterator: function( smooth ){

        var center = this.getPosition().center;
        var n = center.length / 3;

        var i = 0;
        var j = -1;

        var cache = [
            new THREE.Vector3(),
            new THREE.Vector3(),
            new THREE.Vector3(),
            new THREE.Vector3()
        ];

        function next(){
            var vector = this.get( j );
            j += 1;
            return vector;
        }

        function get( idx ){
            idx = Math.min( n - 1, Math.max( 0, idx ) );
            var v = cache[ i % 4 ];
            var idx3 = 3 * idx;
            v.fromArray( center, idx3 );
            if( smooth ){
                var l, k, t;
                var w = Math.min( smooth, idx, n - idx - 1 );
                for( k = 1; k <= w; ++k ){
                    l = k * 3;
                    t = ( w + 1 - k ) / ( w + 1 );
                    v.x += t * center[ idx3 - l + 0 ] + t * center[ idx3 + l + 0 ];
                    v.y += t * center[ idx3 - l + 1 ] + t * center[ idx3 + l + 1 ];
                    v.z += t * center[ idx3 - l + 2 ] + t * center[ idx3 + l + 2 ];
                }
                v.x /= w + 1;
                v.y /= w + 1;
                v.z /= w + 1;
            }
            i += 1;
            return v;
        }

        function reset(){
            i = 0;
            j = -1;
        }

        return {
            size: n,
            next: next,
            get: get,
            reset: reset
        };

    },

    getColor: function( params ){

        var polymer = this.polymer;
        var structure = polymer.structure;
        var n = polymer.residueCount;
        var residueIndexStart = polymer.residueIndexStart;

        var col = new Float32Array( n * 3 );
        var pcol = new Float32Array( n * 3 );

        var p = params || {};
        p.structure = structure;

        var colorMaker = NGL.ColorMakerRegistry.getScheme( p );
        var pickingColorMaker = NGL.ColorMakerRegistry.getPickingScheme( p );

        var rp = structure.getResidueProxy();
        var ap = structure.getAtomProxy();

        for( var i = 0; i < n; ++i ){

            rp.index = residueIndexStart + i;
            ap.index = rp.traceAtomIndex;

            var i3 = i * 3;
            colorMaker.atomColorToArray( ap, col, i3 );
            pickingColorMaker.atomColorToArray( ap, pcol, i3 );

        }

        return {
            "color": col,
            "pickingColor": pcol
        };

    },

    getSize: function( type, scale ){

        var polymer = this.polymer;
        var structure = polymer.structure;
        var n = polymer.residueCount;
        var residueIndexStart = polymer.residueIndexStart;

        var size = new Float32Array( n );
        var radiusFactory = new NGL.RadiusFactory( type, scale );

        var rp = structure.getResidueProxy();
        var ap = structure.getAtomProxy();

        for( var i = 0; i < n; ++i ){

            rp.index = residueIndexStart + i;
            ap.index = rp.traceAtomIndex;
            size[ i ] = radiusFactory.atomRadius( ap );

        }

        return {
            "size": size
        };

    },

    getPosition: function(){

        var polymer = this.polymer;
        var structure = polymer.structure;
        var n = polymer.residueCount;
        var n3 = n - 3;

        var center = new Float32Array( 3 * n );
        var axis = new Float32Array( 3 * n );
        var diff = new Float32Array( n );
        var radius = new Float32Array( n );
        var rise = new Float32Array( n );
        var twist = new Float32Array( n );
        var resdir = new Float32Array( 3 * n );

        var tmp, j;
        var diff13Length, diff24Length;

        var r12 = new THREE.Vector3();
        var r23 = new THREE.Vector3();
        var r34 = new THREE.Vector3();

        var diff13 = new THREE.Vector3();
        var diff24 = new THREE.Vector3();

        var v1 = new THREE.Vector3();
        var v2 = new THREE.Vector3();

        var _axis = new THREE.Vector3();
        var _prevAxis = new THREE.Vector3();

        var _resdir = new THREE.Vector3();
        var _crossdir = new THREE.Vector3();
        var _center = new THREE.Vector3( 0, 0, 0 );

        var type = "trace";
        var a1 = structure.getAtomProxy();
        var a2 = structure.getAtomProxy( polymer.getAtomIndexByType( 0, type ) );
        var a3 = structure.getAtomProxy( polymer.getAtomIndexByType( 1, type ) );
        var a4 = structure.getAtomProxy( polymer.getAtomIndexByType( 2, type ) );

        for( var i = 0; i < n3; ++i ){

            a1.index = a2.index;
            a2.index = a3.index;
            a3.index = a4.index;
            a4.index = polymer.getAtomIndexByType( i + 3, type );

            j = 3 * i;

            // ported from GROMACS src/tools/gmx_helixorient.c

            r12.subVectors( a2, a1 );
            r23.subVectors( a3, a2 );
            r34.subVectors( a4, a3 );

            diff13.subVectors( r12, r23 );
            diff24.subVectors( r23, r34 );

            _axis.crossVectors( diff13, diff24 ).normalize();
            _axis.toArray( axis, j );

            if( i > 0 ){
                diff[ i ] = _axis.angleTo( _prevAxis );
            }

            tmp = Math.cos( diff13.angleTo( diff24 ) );
            twist[ i ] = 180.0 / Math.PI * Math.acos( tmp );

            diff13Length = diff13.length();
            diff24Length = diff24.length();

            radius[ i ] = (
                Math.sqrt( diff24Length * diff13Length ) /
                // clamp, to avoid instabilities for when
                // angle between diff13 and diff24 is near 0
                Math.max( 2.0, 2.0 * ( 1.0 - tmp ) )
            );

            rise[ i ] = Math.abs( r23.dot( _axis ) );

            //

            v1.copy( diff13 ).multiplyScalar( radius[ i ] / diff13Length );
            v2.copy( diff24 ).multiplyScalar( radius[ i ] / diff24Length );

            v1.subVectors( a2, v1 );
            v2.subVectors( a3, v2 );

            v1.toArray( center, j + 3 );
            v2.toArray( center, j + 6 );

            //

            _resdir.subVectors( a1, _center );
            _resdir.toArray( resdir, j );

            _prevAxis.copy( _axis );
            _center.copy( v1 );

        }

        //

        // calc axis as dir of second and third center pos
        // project first traceAtom onto axis to get first center pos
        v1.fromArray( center, 3 );
        v2.fromArray( center, 6 );
        _axis.subVectors( v1, v2 ).normalize();
        // _center.copy( res[ 0 ].getTraceAtom() );
        a1.index = polymer.getAtomIndexByType( 0, type );
        _center.copy( a1 );
        v1 = NGL.Utils.pointVectorIntersection( _center, v1, _axis );
        v1.toArray( center, 0 );

        // calc first resdir
        _resdir.subVectors( _center, v1 );
        _resdir.toArray( resdir, 0 );

        // calc axis as dir of n-1 and n-2 center pos
        // project last traceAtom onto axis to get last center pos
        v1.fromArray( center, 3 * n - 6 );
        v2.fromArray( center, 3 * n - 9 );
        _axis.subVectors( v1, v2 ).normalize();
        // _center.copy( res[ n - 1 ].getTraceAtom() );
        a1.index = polymer.getAtomIndexByType( n - 1, type );
        _center.copy( a1 );
        v1 = NGL.Utils.pointVectorIntersection( _center, v1, _axis );
        v1.toArray( center, 3 * n - 3 );

        // calc last three resdir
        for( var i = n - 3; i < n; ++i ){

            v1.fromArray( center, 3 * i );
            // _center.copy( res[ i ].getTraceAtom() );
            a1.index = polymer.getAtomIndexByType( i, type );
            _center.copy( a1 );

            _resdir.subVectors( _center, v1 );
            _resdir.toArray( resdir, 3 * i );

        }

        // average measures to define them on the residues

        var resRadius = new Float32Array( n );
        var resTwist = new Float32Array( n );
        var resRise = new Float32Array( n );
        var resBending = new Float32Array( n );

        resRadius[ 1 ] = radius[ 0 ];
        resTwist[ 1 ] = twist[ 0 ];
        resRise[ 1 ] = radius[ 0 ];

        for( var i = 2; i < n - 2; ++i ){

            resRadius[ i ] = 0.5 * ( radius[ i - 2 ] + radius[ i - 1 ] );
            resTwist[ i ] = 0.5 * ( twist[ i - 2 ] + twist[ i - 1 ] );
            resRise[ i ] = 0.5 * ( rise[ i - 2 ] + rise[ i - 1 ] );

            v1.fromArray( axis, 3 * ( i - 2 ) );
            v2.fromArray( axis, 3 * ( i - 1 ) );
            resBending[ i ] = 180.0 / Math.PI * Math.acos( Math.cos( v1.angleTo( v2 ) ) );

        }

        resRadius[ n - 2 ] = radius[ n - 4 ];
        resTwist[ n - 2 ] = twist[ n - 4 ];
        resRise[ n - 2 ] = rise[ n - 4 ];

        // average helix axes to define them on the residues

        var resAxis = new Float32Array( 3 * n );

        NGL.Utils.copyArray( axis, resAxis, 0, 0, 3 );
        NGL.Utils.copyArray( axis, resAxis, 0, 3, 3 );

        for( var i = 2; i < n - 2; ++i ){

            v1.fromArray( axis, 3 * ( i - 2 ) );
            v2.fromArray( axis, 3 * ( i - 1 ) );

            _axis.addVectors( v2, v1 ).multiplyScalar( 0.5 ).normalize();
            _axis.toArray( resAxis, 3 * i );

        }

        NGL.Utils.copyArray( axis, resAxis, 3 * n - 12, 3 * n - 6, 3 );
        NGL.Utils.copyArray( axis, resAxis, 3 * n - 12, 3 * n - 3, 3 );

        return {
            "center": center,
            "axis": resAxis,
            "bending": resBending,
            "radius": resRadius,
            "rise": resRise,
            "twist": resTwist,
            "resdir": resdir,
        };

    }

};


////////////////
// Helixbundle

NGL.Helixbundle = function( polymer ){

    this.polymer = polymer;

    this.helixorient = new NGL.Helixorient( polymer );
    this.position = this.helixorient.getPosition();

};

NGL.Helixbundle.prototype = {

    constructor: NGL.Helixbundle,

    getAxis: function( localAngle, centerDist, ssBorder, colorParams, radius, scale ){

        localAngle = localAngle || 30;
        centerDist = centerDist || 2.5;
        ssBorder = ssBorder === undefined ? false : ssBorder;

        var polymer = this.polymer;
        var structure = polymer.structure;
        var n = polymer.residueCount;
        var residueIndexStart = polymer.residueIndexStart;

        var pos = this.position;

        var cp = colorParams || {};
        cp.structure = structure;

        var colorMaker = NGL.ColorMakerRegistry.getScheme( cp );
        var pickingColorMaker = NGL.ColorMakerRegistry.getPickingScheme( cp );

        var radiusFactory = new NGL.RadiusFactory( radius, scale );

        var j = 0;
        var k = 0;

        var axis = [];
        var center = [];
        var beg = [];
        var end = [];
        var col = [];
        var pcol = [];
        var size = [];
        var residueOffset = [];
        var residueCount = [];

        var tmpAxis = [];
        var tmpCenter = [];

        var _axis, _center
        var _beg = new THREE.Vector3();
        var _end = new THREE.Vector3();

        var rp1 = structure.getResidueProxy();
        var rp2 = structure.getResidueProxy();
        var ap = structure.getAtomProxy();

        var c1 = new THREE.Vector3();
        var c2 = new THREE.Vector3();

        var split = false;

        for( var i = 0; i < n; ++i ){

            rp1.index = residueIndexStart + i;
            c1.fromArray( pos.center, i * 3 );

            if( i === n - 1 ){
                split = true;
            }else{

                rp2.index = residueIndexStart + i + 1;
                c2.fromArray( pos.center, i * 3 + 3 );

                if( ssBorder && rp1.sstruc !== rp2.sstruc ){
                    split = true;
                }else if( c1.distanceTo( c2 ) > centerDist ){
                    split = true;
                }else if( pos.bending[ i ] > localAngle ){
                    split = true;
                }

            }

            if( split ){

                if( i - j < 4 ){
                    j = i;
                    split = false;
                    continue;
                }

                ap.index = rp1.traceAtomIndex;

                // ignore first and last axis
                tmpAxis = pos.axis.subarray( j * 3 + 3, i * 3 );
                tmpCenter = pos.center.subarray( j * 3, i * 3 + 3 );

                _axis = NGL.Utils.calculateMeanVector3( tmpAxis ).normalize();
                _center = NGL.Utils.calculateMeanVector3( tmpCenter );

                _beg.fromArray( tmpCenter );
                _beg = NGL.Utils.pointVectorIntersection( _beg, _center, _axis );

                _end.fromArray( tmpCenter, tmpCenter.length - 3 );
                _end = NGL.Utils.pointVectorIntersection( _end, _center, _axis );

                _axis.subVectors( _end, _beg );

                _axis.toArray( axis, k );
                _center.toArray( center, k );
                _beg.toArray( beg, k );
                _end.toArray( end, k );

                colorMaker.atomColorToArray( ap, col, k );
                pickingColorMaker.atomColorToArray( ap, pcol, k );

                size.push( radiusFactory.atomRadius( ap ) );

                residueOffset.push( residueIndexStart + j );
                residueCount.push( residueIndexStart + i + 1 - j );

                k += 3;
                j = i;
                split = false;

            }

        }

        return {
            "axis": new Float32Array( axis ),
            "center": new Float32Array( center ),
            "begin": new Float32Array( beg ),
            "end": new Float32Array( end ),
            "color": new Float32Array( col ),
            "pickingColor": new Float32Array( pcol ),
            "size": new Float32Array( size ),
            "residueOffset": residueOffset,
            "residueCount": residueCount
        };

    }

};


///////////
// Kdtree

NGL.Kdtree = function( entity, useSquaredDist ){

    if( NGL.debug ) NGL.time( "NGL.Kdtree build" );

    if( useSquaredDist ){

        var metric = function( a, b ){
            var dx = a[0] - b[0];
            var dy = a[1] - b[1];
            var dz = a[2] - b[2];
            return dx*dx + dy*dy + dz*dz;
        };

    }else{

        var metric = function( a, b ){
            var dx = a[0] - b[0];
            var dy = a[1] - b[1];
            var dz = a[2] - b[2];
            return Math.sqrt( dx*dx + dy*dy + dz*dz );
        };

    }

    var points = new Float32Array( entity.atomCount * 4 );
    var i = 0;

    var eachFnName = entity.eachSelectedAtom ? "eachSelectedAtom" : "eachAtom";

    entity[ eachFnName ]( function( ap ){
        points[ i + 0 ] = ap.x;
        points[ i + 1 ] = ap.y;
        points[ i + 2 ] = ap.z;
        points[ i + 3 ] = ap.index;
        i += 4;
    } );

    this.points = points;
    this.kdtree = new THREE.TypedArrayUtils.Kdtree( points, metric, 4, 3 );

    if( NGL.debug ) NGL.timeEnd( "NGL.Kdtree build" );

};

NGL.Kdtree.prototype = {

    nearest: function(){

        var pointArray = new Float32Array( 3 );

        return function( point, maxNodes, maxDistance ){

            // NGL.time( "NGL.Kdtree nearest" );

            if( point instanceof THREE.Vector3 ){

                point.toArray( pointArray );

            }else if( point instanceof NGL.AtomProxy ){

                point.positionToArray( pointArray );

            }

            var nodeList = this.kdtree.nearest(
                pointArray, maxNodes, maxDistance
            );

            var points = this.points;
            var resultList = [];

            for( var i = 0, n = nodeList.length; i < n; ++i ){

                var d = nodeList[ i ];
                var node = d[ 0 ];
                var dist = d[ 1 ];

                resultList.push( {
                    index: points[ node.pos + 3 ],
                    distance: dist
                } );

            }

            // NGL.timeEnd( "NGL.Kdtree nearest" );

            return resultList;

        };

    }()

};


////////////
// Contact

NGL.Contact = function( sview1, sview2 ){

    this.sview1 = sview1;
    this.sview2 = sview2;

    // this.kdtree1 = new NGL.Kdtree( sview1 );
    this.kdtree2 = new NGL.Kdtree( sview2 );

}

NGL.Contact.prototype = {

    within: function( maxDistance, minDistance ){

        NGL.time( "NGL.Contact within" );

        var kdtree1 = this.kdtree1;
        var kdtree2 = this.kdtree2;

        var ap2 = this.sview1.getAtomProxy();
        var atomSet = this.sview1.getAtomSet( false );
        var bondStore = new NGL.BondStore();

        this.sview1.eachSelectedAtom( function( ap1 ){

            var found = false;
            var contacts = kdtree2.nearest(
                ap1, Infinity, maxDistance
            );

            for( var j = 0, m = contacts.length; j < m; ++j ){

                var d = contacts[ j ];
                ap2.index = d.index;

                if( ap1.residueIndex !== ap2.residueIndex &&
                    ( !minDistance || d.distance > minDistance ) ){
                    found = true;
                    atomSet.add_unsafe( ap2.index );
                    bondStore.addBond( ap1, ap2, 1 );
                }

            }

            if( found ){
                atomSet.add_unsafe( ap1.index );
            }

        } );

        var bondSet = new TypedFastBitSet( bondStore.count );
        bondSet.set_all( true );

        NGL.timeEnd( "NGL.Contact within" );

        return {
            atomSet: atomSet,
            bondSet: bondSet,
            bondStore: bondStore
        };

    }

}


NGL.polarContacts = function( structure, maxDistance, maxAngle ){

    maxDistance = maxDistance || 3.5;
    maxAngle = maxAngle || 40;

    var donorSelection = new NGL.Selection(
        "( ARG and ( .NE or .NH1 or .NH2 ) ) or " +
        "( ASP and .ND2 ) or " +
        "( GLN and .NE2 ) or " +
        "( HIS and ( .ND1 or .NE2 ) ) or " +
        "( LYS and .NZ ) or " +
        "( SER and .OG ) or " +
        "( THR and .OG1 ) or " +
        "( TRP and .NE1 ) or " +
        "( TYR and .OH ) or " +
        "( PROTEIN and .N )"
    );

    var acceptorSelection = new NGL.Selection(
        "( ASN and .OD1 ) or " +
        "( ASP and ( OD1 or .OD2 ) ) or " +
        "( GLN and .OE1 ) or " +
        "( GLU and ( .OE1 or .OE2 ) ) or " +
        "( HIS and ( .ND1 or .NE2 ) ) or " +
        "( SER and .OG ) or " +
        "( THR and .OG1 ) or " +
        "( TYR and .OH ) or " +
        "( PROTEIN and .O )"
    );

    var donorView = structure.getView( donorSelection );
    var acceptorView = structure.getView( acceptorSelection );

    var contact = new NGL.Contact( donorView, acceptorView );
    var data = contact.within( maxDistance );
    var bondStore = data.bondStore;

    var ap1 = structure.getAtomProxy();
    var ap2 = structure.getAtomProxy();
    var atomCA = structure.getAtomProxy();
    var atomC = structure.getAtomProxy();
    var rp = structure.getResidueProxy();
    var rpPrev = structure.getResidueProxy();
    var v1 = new THREE.Vector3();
    var v2 = new THREE.Vector3();

    var checkAngle = function( atom1, atom2, oName, cName ){

        var atomO, atomN;

        if( atom1.atomname === oName ){
            atomO = atom1;
            atomN = atom2;
        }else{
            atomO = atom2;
            atomN = atom1;
        }

        rp.index = atomO.residueIndex;
        var atomC = rp.getAtomIndexByName( cName ) + rp.atomOffset;

        v1.subVectors( atomC, atomO );
        v2.subVectors( atomC, atomN );

        return THREE.Math.radToDeg( v1.angleTo( v2 ) ) < maxAngle;

    }

    for( var i = 0, il = bondStore.count; i < il; ++i ){

        ap1.index = bondStore.atomIndex1[ i ];
        ap2.index = bondStore.atomIndex2[ i ];

        if( ( ap1.atomname === "O" && ap2.atomname === "N" ) ||
            ( ap1.atomname === "N" && ap2.atomname === "O" )
        ){

            // ignore backbone to backbone contacts
            data.bondSet.flip_unsafe( i );
            continue;

        }else if( ap1.atomname === "N" || ap2.atomname === "N" ){

            var atomN, atomX;

            if( ap1.atomname === "N" ){
                atomN = ap1;
                atomX = ap2;
            }else{
                atomN = ap2;
                atomX = ap1;
            }

            rp.index = atomN.residueIndex;
            atomCA.index = rp.getAtomIndexByName( "CA" ) + rp.atomOffset;
            if( atomCA.index === undefined ) continue;

            var prevRes = rp.getPreviousConnectedResidue( rpPrev );
            if( prevRes === undefined ) continue;

            atomC.index = prevRes.getAtomIndexByName( "C" ) + prevRes.atomOffset;
            if( atomC.index === undefined ) continue;

            v1.subVectors( atomN, atomC );
            v2.subVectors( atomN, atomCA );
            v1.add( v2 ).multiplyScalar( 0.5 );
            v2.subVectors( atomX, atomN );

            if( THREE.Math.radToDeg( v1.angleTo( v2 ) ) > maxAngle ){
                data.bondSet.flip_unsafe( i );
            }

        }else if(
            ( ap1.atomname === "OH" && ap1.resname === "TYR" ) ||
            ( ap2.atomname === "OH" && ap2.resname === "TYR" )
        ){

            if( !checkAngle( ap1, ap2, "OH", "CZ" ) ){
                data.bondSet.flip_unsafe( i );
            }

        }

    }

    return {
        atomSet: data.atomSet,
        bondSet: data.bondSet,
        bondStore: data.bondStore
    };

}


NGL.polarBackboneContacts = function( structure, maxDistance, maxAngle ){

    maxDistance = maxDistance || 3.5;
    maxAngle = maxAngle || 40;

    var donorSelection = new NGL.Selection(
        "( PROTEIN and .N )"
    );

    var acceptorSelection = new NGL.Selection(
        "( PROTEIN and .O )"
    );

    var donorView = structure.getView( donorSelection );
    var acceptorView = structure.getView( acceptorSelection );

    var contact = new NGL.Contact( donorView, acceptorView );
    var data = contact.within( maxDistance );
    var bondStore = data.bondStore;

    var ap1 = structure.getAtomProxy();
    var ap2 = structure.getAtomProxy();
    var atomCA = structure.getAtomProxy();
    var atomC = structure.getAtomProxy();
    var rp = structure.getResidueProxy();
    var rpPrev = structure.getResidueProxy();
    var v1 = new THREE.Vector3();
    var v2 = new THREE.Vector3();

    for( var i = 0, il = bondStore.count; i < il; ++i ){

        ap1.index = bondStore.atomIndex1[ i ];
        ap2.index = bondStore.atomIndex2[ i ];

        var atomN, atomO;

        if( ap1.atomname === "N" ){
            atomN = ap1;
            atomO = ap2;
        }else{
            atomN = ap2;
            atomO = ap1;
        }

        rp.index = atomN.residueIndex;

        atomCA.index = rp.getAtomIndexByName( "CA" ) + rp.atomOffset;
        if( atomCA.index === undefined ) continue;

        var prevRes = rp.getPreviousConnectedResidue( rpPrev );
        if( prevRes === undefined ) continue;

        atomC.index = prevRes.getAtomIndexByName( "C" ) + prevRes.atomOffset;
        if( atomC.index === undefined ) continue;

        v1.subVectors( atomN, atomC );
        v2.subVectors( atomN, atomCA );
        v1.add( v2 ).multiplyScalar( 0.5 );
        v2.subVectors( atomO, atomN );

        // NGL.log( THREE.Math.radToDeg( v1.angleTo( v2 ) ) );

        if( THREE.Math.radToDeg( v1.angleTo( v2 ) ) > maxAngle ){
            data.bondSet.flip_unsafe( i );
        }

    }

    return {
        atomSet: data.atomSet,
        bondSet: data.bondSet,
        bondStore: data.bondStore
    };

}

// File:js/ngl/selection.js

/**
 * @file Selection
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


//////////////
// Selection

NGL.Selection = function( string ){

    var SIGNALS = signals;

    this.signals = {
        stringChanged: new SIGNALS.Signal(),
    };

    this.setString( string );

};


NGL.Selection.Keywords = {
    "PROTEIN": 1,
    "NUCLEIC": 2,
    "RNA": 3,
    "DNA": 4,
    "POLYMER": 5,
    "WATER": 6,
    "HELIX": 7,
    "SHEET": 8,
    "BACKBONE": 9,
    "SIDECHAIN": 10,
    "ALL": 11,
    "HETERO": 12,
    "ION": 13
};


NGL.Selection.prototype = {

    constructor: NGL.Selection,

    setString: function( string, silent ){

        if( string === undefined ) string = this.string || "";
        if( string === this.string ) return;

        //

        try{
            this.parse( string );
        }catch( e ){
            // NGL.error( e.stack );
            this.selection = { "error": e.message };
        }

        this.string = string;

        this.test = this.makeAtomTest();
        this.residueTest = this.makeResidueTest();
        this.chainTest = this.makeChainTest();
        this.modelTest = this.makeModelTest();

        this.atomOnlyTest = this.makeAtomTest( true );
        this.residueOnlyTest = this.makeResidueTest( true );
        this.chainOnlyTest = this.makeChainTest( true )
        this.modelOnlyTest = this.makeModelTest( true );

        if( !silent ){
            this.signals.stringChanged.dispatch( this.string );
        }

    },

    parse: function( string ){

        this.selection = {
            operator: undefined,
            rules: []
        };

        if( !string ) return;

        var scope = this;

        var kwd = NGL.Selection.Keywords;
        var selection = this.selection;
        var selectionStack = [];
        var newSelection, oldSelection;
        var andContext = null;

        string = string.replace( /\(/g, ' ( ' ).replace( /\)/g, ' ) ' ).trim();
        if( string.charAt( 0 ) === "(" && string.substr( -1 ) === ")" ){
            string = string.slice( 1, -1 ).trim();
        }
        var chunks = string.split( /\s+/ );

        // NGL.log( string, chunks )

        var all = [ "*", "", "ALL" ];

        var c, sele, i, error, not;
        var atomname, chain, resno, resname, model, resi, altloc, inscode;
        var j = 0;

        var createNewContext = function( operator ){

            newSelection = {
                operator: operator,
                rules: []
            };
            if( selection === undefined ){
                selection = newSelection;
                scope.selection = newSelection;
            }else{
                selection.rules.push( newSelection );
                selectionStack.push( selection );
                selection = newSelection;
            }
            j = 0;

        }

        var getPrevContext = function( operator ){

            oldSelection = selection;
            selection = selectionStack.pop();
            if( selection === undefined ){
                createNewContext( operator );
                pushRule( oldSelection );
            }else{
                j = selection.rules.length;
            }

        }

        var pushRule = function( rule ){

            selection.rules.push( rule );
            j += 1;

        }

        for( i = 0; i < chunks.length; ++i ){

            c = chunks[ i ];

            // handle parens

            if( c === "(" ){

                // NGL.log( "(" );

                not = false;
                createNewContext();
                continue;

            }else if( c === ")" ){

                // NGL.log( ")" );

                getPrevContext();
                if( selection.negate ){
                    getPrevContext();
                }
                continue;

            }

            // leave 'not' context

            if( not > 0 ){

                if( c.toUpperCase() === "NOT" ){

                    not = 1;

                }else if( not === 1 ){

                    not = 2;

                }else if( not === 2 ){

                    not = false;
                    getPrevContext();

                }else{

                    throw new Error( "something went wrong with 'not'" );

                }

            }

            // handle logic operators

            if( c.toUpperCase() === "AND" ){

                // NGL.log( "AND" );

                if( selection.operator === "OR" ){
                    var lastRule = selection.rules.pop();
                    createNewContext( "AND" );
                    pushRule( lastRule );
                }else{
                    selection.operator = "AND";
                }
                continue;

            }else if( c.toUpperCase() === "OR" ){

                // NGL.log( "OR" );

                if( selection.operator === "AND" ){
                    getPrevContext( "OR" );
                }else{
                    selection.operator = "OR";
                }
                continue;

            }else if( c.toUpperCase() === "NOT" ){

                // NGL.log( "NOT", j );

                not = 1;
                createNewContext();
                selection.negate = true;
                continue;

            }else{

                // NGL.log( "chunk", c, j, selection );

            }

            // handle keyword attributes

            sele = {};

            if( c.toUpperCase() === "HETERO" ){
                sele.keyword = kwd.HETERO;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "WATER" ){
                sele.keyword = kwd.WATER;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "PROTEIN" ){
                sele.keyword = kwd.PROTEIN;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "NUCLEIC" ){
                sele.keyword = kwd.NUCLEIC;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "RNA" ){
                sele.keyword = kwd.RNA;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "DNA" ){
                sele.keyword = kwd.DNA;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "POLYMER" ){
                sele.keyword = kwd.POLYMER;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "ION" ){
                sele.keyword = kwd.ION;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "HYDROGEN" ){
                sele.element = "H";
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "SMALL" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "GLY" },
                        { resname: "ALA" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "NUCLEOPHILIC" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "SER" },
                        { resname: "THR" },
                        { resname: "CYS" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "HYDROPHOBIC" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "VAL" },
                        { resname: "LEU" },
                        { resname: "ILE" },
                        { resname: "MET" },
                        { resname: "PRO" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "AROMATIC" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "PHE" },
                        { resname: "TYR" },
                        { resname: "TRP" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "AMIDE" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "ASN" },
                        { resname: "GLN" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "ACIDIC" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "ASP" },
                        { resname: "GLU" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "BASIC" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "HIS" },
                        { resname: "LYS" },
                        { resname: "ARG" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "CHARGED" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "ASP" },
                        { resname: "GLU" },
                        { resname: "HIS" },
                        { resname: "LYS" },
                        { resname: "ARG" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "POLAR" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "ASP" },
                        { resname: "GLU" },
                        { resname: "HIS" },
                        { resname: "LYS" },
                        { resname: "ARG" },
                        { resname: "ASN" },
                        { resname: "GLN" },
                        { resname: "SER" },
                        { resname: "THR" },
                        { resname: "TYR" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "NONPOLAR" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { resname: "ALA" },
                        { resname: "CYS" },
                        { resname: "GLY" },
                        { resname: "ILE" },
                        { resname: "LEU" },
                        { resname: "MET" },
                        { resname: "PHE" },
                        { resname: "PRO" },
                        { resname: "VAL" },
                        { resname: "TRP" }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "HELIX" ){
                sele.keyword = kwd.HELIX;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "SHEET" ){
                sele.keyword = kwd.SHEET;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "TURN" ){
                sele = {
                    operator: "AND",
                    rules: [
                        {
                            operator: "OR",
                            negate: true,
                            rules: [
                                { keyword: kwd.HELIX },
                                { keyword: kwd.SHEET }
                            ]
                        },
                        {
                            operator: "OR",
                            rules: [
                                { keyword: kwd.PROTEIN },
                                { sstruc: "s" },
                                { sstruc: "t" },
                                { sstruc: "l" }
                            ]
                        }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "BACKBONE" ){
                sele.keyword = kwd.BACKBONE;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "SIDECHAIN" ){
                sele.keyword = kwd.SIDECHAIN;
                pushRule( sele );
                continue;
            }

            if( c.toUpperCase() === "SIDECHAINATTACHED" ){
                sele = {
                    operator: "OR",
                    rules: [
                        { keyword: kwd.SIDECHAIN },
                        {
                            operator: "AND",
                            negate: false,
                            rules: [
                                { keyword: kwd.PROTEIN },
                                {
                                    operator: "OR",
                                    negate: false,
                                    rules: [
                                        { atomname: "CA" },
                                        { atomname: "BB" }
                                    ]
                                }
                            ]
                        },
                        {
                            operator: "AND",
                            negate: false,
                            rules: [
                                { resname: "PRO" },
                                { atomname: "N" }
                            ]
                        },
                        {
                            operator: "AND",
                            negate: false,
                            rules: [
                                { keyword: kwd.NUCLEIC },
                                {
                                    operator: "OR",
                                    negate: true,
                                    rules: [
                                        { atomname: "P" },
                                        { atomname: "OP1" },
                                        { atomname: "OP2" },
                                        { atomname: "O3'" },
                                        { atomname: "O3*" },
                                        { atomname: "O5'" },
                                        { atomname: "O5*" },
                                        { atomname: "C5'" },
                                        { atomname: "C5*" }
                                    ]
                                }
                            ]
                        }
                    ]
                };
                pushRule( sele );
                continue;
            }

            if( all.indexOf( c.toUpperCase() )!==-1 ){
                sele.keyword = kwd.ALL;
                pushRule( sele );
                continue;
            }

            // handle atom expressions

            if( c.charAt( 0 ) === "@" ){
                var indexList = c.substr( 1 ).split( "," );
                for( var k = 0, kl = indexList.length; k < kl; ++k ){
                    indexList[ k ] = parseInt( indexList[ k ] );
                }
                indexList.sort( function( a, b ){ return a - b; } );
                sele.atomindex = indexList;
                pushRule( sele );
                continue;
            }

            if( c.charAt( 0 ) === "#" ){
                sele.element = c.substr( 1 ).toUpperCase();
                pushRule( sele );
                continue;
            }

            if( c[0] === "[" && c[c.length-1] === "]" ){
                sele.resname = c.substr( 1, c.length-2 ).toUpperCase();
                pushRule( sele );
                continue;
            }else if( ( c.length >= 1 && c.length <= 4 ) &&
                    c[0] !== "^" && c[0] !== ":" && c[0] !== "." && c[0] !== "%" && c[0] !== "/" &&
                    isNaN( parseInt( c ) ) ){

                sele.resname = c.toUpperCase();
                pushRule( sele );
                continue;
            }

            // there must be only one constraint per rule
            // otherwise a test quickly becomes not applicable
            // e.g. chainTest for chainname when resno is present too

            sele = {
                operator: "AND",
                rules: []
            };

            model = c.split("/");
            if( model.length > 1 && model[1] ){
                if( isNaN( parseInt( model[1] ) ) ){
                    throw new Error( "model must be an integer" );
                }
                sele.rules.push( {
                    model: parseInt( model[1] )
                } );
            }

            altloc = model[0].split("%");
            if( altloc.length > 1 ){
                sele.rules.push( {
                    altloc: altloc[1]
                } );
            }

            atomname = altloc[0].split(".");
            if( atomname.length > 1 && atomname[1] ){
                if( atomname[1].length > 4 ){
                    throw new Error( "atomname must be one to four characters" );
                }
                sele.rules.push( {
                    atomname: atomname[1].substring( 0, 4 ).toUpperCase()
                } );
            }

            chain = atomname[0].split(":");
            if( chain.length > 1 && chain[1] ){
                sele.rules.push( {
                    chainname: chain[1]
                } );
            }

            inscode = chain[0].split("^");
            if( inscode.length > 1 ){
                sele.rules.push( {
                    inscode: inscode[1]
                } );
            }

            if( inscode[0] ){
                resi = inscode[0].split("-");
                if( resi.length === 1 ){
                    resi = parseInt( resi[0] );
                    if( isNaN( resi ) ){
                        throw new Error( "resi must be an integer" );
                    }
                    sele.rules.push( {
                        resno: resi
                    } );
                }else if( resi.length === 2 ){
                    sele.rules.push( {
                        resno: [ parseInt( resi[0] ), parseInt( resi[1] ) ]
                    } );
                }else{
                    throw new Error( "resi range must contain one '-'" );
                }
            }

            // round up

            if( sele.rules.length === 1 ){
                pushRule( sele.rules[ 0 ] );
            }else if( sele.rules.length > 1 ){
                pushRule( sele );
            }else{
                throw new Error( "empty selection chunk" );
            }

        }

        // cleanup

        if( this.selection.operator === undefined &&
                this.selection.rules.length === 1 &&
                this.selection.rules[ 0 ].hasOwnProperty( "operator" ) ){

            this.selection = this.selection.rules[ 0 ];

        }

    },

    _makeTest: function( fn, selection ){

        if( selection === undefined ) selection = this.selection;
        if( selection === null ) return false;
        if( selection.error ) return false;

        var n = selection.rules.length;
        if( n === 0 ) return false;

        var t = selection.negate ? false : true;
        var f = selection.negate ? true : false;
        var k = NGL.Selection.Keywords;

        var s, and, ret, na;
        var subTests = [];

        for( var i = 0; i < n; ++i ){
            s = selection.rules[ i ];
            if( s.hasOwnProperty( "operator" ) ){
                subTests[ i ] = this._makeTest( fn, s );
            }
        }

        // ( x and y ) can short circuit on false
        // ( x or y ) can short circuit on true
        // not ( x and y )

        return function( entity ){

            and = selection.operator === "AND";
            na = false;

            for( var i = 0; i < n; ++i ){

                s = selection.rules[ i ];

                if( s.hasOwnProperty( "operator" ) ){

                    if( subTests[ i ] ){
                        ret = subTests[ i ]( entity );
                    }else{
                        ret = -1;
                    }

                    if( ret === -1 ){
                        // return -1;
                        na = true;
                        continue;
                    }else if( ret === true){
                        if( and ){ continue; }else{ return t; }
                    }else{
                        if( and ){ return f; }else{ continue; }
                    }

                }else{

                    if( s.keyword===k.ALL ){
                        if( and ){ continue; }else{ return t; }
                    }

                    ret = fn( entity, s );

                    // console.log( entity.qualifiedName(), ret, s, selection.negate, "t", t, "f", f )

                    if( ret === -1 ){
                        // return -1;
                        na = true;
                        continue;
                    }else if( ret === true){
                        if( and ){ continue; }else{ return t; }
                    }else{
                        if( and ){ return f; }else{ continue; }
                    }

                }

            }

            if( na ){
                return -1;
            }else{
                if( and ){ return t; }else{ return f; }
            }

        }

    },

    _filter: function( fn, selection ){

        if( selection === undefined ) selection = this.selection;
        if( selection.error ) return selection;

        var n = selection.rules.length;
        if( n === 0 ) return selection;

        var filtered = {
            operator: selection.operator,
            rules: []
        };
        if( selection.hasOwnProperty( "negate" ) ){
            filtered.negate = selection.negate;
        }

        for( var i = 0; i < n; ++i ){

            var s = selection.rules[ i ];
            if( s.hasOwnProperty( "operator" ) ){
                var fs = this._filter( fn, s );
                if( fs !== null ) filtered.rules.push( fs );
            }else if( !fn( s ) ){
                filtered.rules.push( s );
            }

        }

        if( filtered.rules.length > 0 ){

            // TODO maybe the filtered rules could be returned
            // in some case, but the way how tests are applied
            // e.g. when traversing a structure would also need
            // to change
            return selection;
            // return filtered;

        }else{

            return null;

        }

    },

    makeAtomTest: function( atomOnly ){

        var helixTypes = [ "h", "g", "i" ];
        var sheetTypes = [ "e", "b" ];
        var kwd = NGL.Selection.Keywords;

        var selection;

        if( atomOnly ){

            // console.log( this.selection )

            selection = this._filter( function( s ){
                if( s.keyword!==undefined &&
                        s.keyword!==kwd.BACKBONE && s.keyword!==kwd.SIDECHAIN
                ) return true;
                if( s.model!==undefined ) return true;
                if( s.chainname!==undefined ) return true;
                if( s.resname!==undefined ) return true;
                if( s.resno!==undefined ) return true;
                if( s.sstruc!==undefined ) return true;
                return false;
            } );

        }else{

            selection = this.selection;

        }

        var fn = function( a, s ){

            // returning -1 means the rule is not applicable
            if( s.atomname===undefined && s.element===undefined &&
                    s.altloc===undefined && s.atomindex===undefined &&
                    // s.keyword!==kwd.BACKBONE && s.keyword!==kwd.SIDECHAIN &&
                    s.keyword===undefined && s.inscode===undefined &&
                    s.resname===undefined && s.sstruc===undefined &&
                    s.resno===undefined && s.chainname===undefined &&
                    s.model===undefined
            ) return -1;

            if( s.keyword!==undefined ){
                if( s.keyword===kwd.BACKBONE && !a.isBackbone() ) return false;
                if( s.keyword===kwd.SIDECHAIN && !a.isSidechain() ) return false;

                if( s.keyword===kwd.HETERO && !a.isHetero() ) return false;
                if( s.keyword===kwd.PROTEIN && !a.isProtein() ) return false;
                if( s.keyword===kwd.NUCLEIC && !a.isNucleic() ) return false;
                if( s.keyword===kwd.RNA && !a.isRna() ) return false;
                if( s.keyword===kwd.DNA && !a.isDna() ) return false;
                if( s.keyword===kwd.POLYMER && !a.isPolymer() ) return false;
                if( s.keyword===kwd.WATER && !a.isWater() ) return false;
                if( s.keyword===kwd.HELIX && helixTypes.indexOf( a.sstruc )===-1 ) return false;
                if( s.keyword===kwd.SHEET && sheetTypes.indexOf( a.sstruc )===-1 ) return false;
                if( s.keyword===kwd.ION && !a.isIon() ) return false;
            }

            if( s.atomname!==undefined && s.atomname!==a.atomname ) return false;
            if( s.element!==undefined && s.element!==a.element ) return false;
            if( s.altloc!==undefined && s.altloc!==a.altloc ) return false;

            if( s.atomindex!==undefined &&
                    NGL.binarySearchIndexOf( s.atomindex, a.index ) < 0
            ) return false;

            if( s.resname!==undefined && s.resname!==a.resname ) return false;
            if( s.sstruc!==undefined && s.sstruc!==a.sstruc ) return false;
            if( s.resno!==undefined ){
                if( Array.isArray( s.resno ) && s.resno.length===2 ){
                    if( s.resno[0]>a.resno || s.resno[1]<a.resno ) return false;
                }else{
                    if( s.resno!==a.resno ) return false;
                }
            }
            if( s.inscode!==undefined && s.inscode!==a.inscode ) return false;

            if( s.chainname!==undefined && s.chainname!==a.chainname ) return false;
            if( s.model!==undefined && s.model!==a.modelIndex ) return false;

            return true;

        }

        return this._makeTest( fn, selection );

    },

    makeResidueTest: function( residueOnly ){

        var helixTypes = [ "h", "g", "i" ];
        var sheetTypes = [ "e", "b" ];
        var kwd = NGL.Selection.Keywords;

        var selection;

        if( residueOnly ){

            // console.log( this.selection )

            selection = this._filter( function( s ){
                if( s.keyword===kwd.BACKBONE || s.keyword===kwd.SIDECHAIN ) return true;
                if( s.model!==undefined ) return true;
                if( s.chainname!==undefined ) return true;
                if( s.atomname!==undefined ) return true;
                if( s.element!==undefined ) return true;
                if( s.altloc!==undefined ) return true;
                if( s.atomindex!==undefined ) return true;
                return false;
            } );

        }else{

            selection = this.selection;

        }

        var fn = function( r, s ){

            // returning -1 means the rule is not applicable
            if( s.resname===undefined && s.resno===undefined && s.inscode===undefined &&
                    s.sstruc===undefined && s.model===undefined && s.chainname===undefined &&
                    ( s.keyword===undefined || s.keyword===kwd.BACKBONE || s.keyword===kwd.SIDECHAIN )
            ) return -1;

            if( s.keyword!==undefined ){
                if( s.keyword===kwd.HETERO && !r.isHetero() ) return false;
                if( s.keyword===kwd.PROTEIN && !r.isProtein() ) return false;
                if( s.keyword===kwd.NUCLEIC && !r.isNucleic() ) return false;
                if( s.keyword===kwd.RNA && !r.isRna() ) return false;
                if( s.keyword===kwd.DNA && !r.isDna() ) return false;
                if( s.keyword===kwd.POLYMER && !r.isPolymer() ) return false;
                if( s.keyword===kwd.WATER && !r.isWater() ) return false;
                if( s.keyword===kwd.HELIX && helixTypes.indexOf( r.sstruc )===-1 ) return false;
                if( s.keyword===kwd.SHEET && sheetTypes.indexOf( r.sstruc )===-1 ) return false;
                if( s.keyword===kwd.ION && !r.isIon() ) return false;
            }

            if( s.resname!==undefined && s.resname!==r.resname ) return false;
            if( s.sstruc!==undefined && s.sstruc!==r.sstruc ) return false;
            if( s.resno!==undefined ){
                if( Array.isArray( s.resno ) && s.resno.length===2 ){
                    if( s.resno[0]>r.resno || s.resno[1]<r.resno ) return false;
                }else{
                    if( s.resno!==r.resno ) return false;
                }
            }
            if( s.inscode!==undefined && s.inscode!==r.inscode ) return false;

            if( s.chainname!==undefined && s.chainname!==r.chainname ) return false;
            if( s.model!==undefined && s.model!==r.modelIndex ) return false;

            return true;

        }

        return this._makeTest( fn, selection );

    },

    makeChainTest: function( chainOnly ){

        var selection;

        if( chainOnly ){

            // console.log( this.selection )

            selection = this._filter( function( s ){
                if( s.keyword!==undefined ) return true;
                // if( s.model!==undefined ) return true;
                if( s.resname!==undefined ) return true;
                if( s.resno!==undefined ) return true;
                if( s.atomname!==undefined ) return true;
                if( s.element!==undefined ) return true;
                if( s.altloc!==undefined ) return true;
                if( s.sstruc!==undefined ) return true;
                if( s.inscode!==undefined ) return true;
                if( s.atomindex!==undefined ) return true;
                return false;
            } );

        }else{

            selection = this.selection;

        }

        var fn = function( c, s ){

            // returning -1 means the rule is not applicable
            if( s.chainname===undefined && s.model===undefined ) return -1;

            if( s.chainname!==undefined && s.chainname!==c.chainname ) return false;

            if( s.model!==undefined && s.model!==c.modelIndex ) return false;

            return true;

        }

        return this._makeTest( fn, selection );

    },

    makeModelTest: function( modelOnly ){

        var selection;

        if( modelOnly ){

            // console.log( this.selection )

            selection = this._filter( function( s ){
                if( s.keyword!==undefined ) return true;
                if( s.chainname!==undefined ) return true;
                if( s.resname!==undefined ) return true;
                if( s.resno!==undefined ) return true;
                if( s.atomname!==undefined ) return true;
                if( s.element!==undefined ) return true;
                if( s.altloc!==undefined ) return true;
                if( s.sstruc!==undefined ) return true;
                if( s.inscode!==undefined ) return true;
                if( s.atomindex!==undefined ) return true;
                return false;
            } );

        }else{

            selection = this.selection;

        }

        var fn = function( m, s ){

            // returning -1 means the rule is not applicable
            if( s.model===undefined ) return -1;

            if( s.model!==undefined && s.model!==m.index ) return false;

            return true;

        }

        return this._makeTest( fn, selection );

    }

};

// File:js/ngl/superposition.js

/**
 * @file Superposition
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


/////////
// Math

NGL.Matrix = function( columns, rows ){

    var dtype = jsfeat.F32_t | jsfeat.C1_t;

    return new jsfeat.matrix_t( columns, rows, dtype );

};


//////////////////
// Superposition

NGL.Superposition = function( atoms1, atoms2 ){

    // allocate & init data structures

    var n;
    if( typeof atoms1.eachSelectedAtom === "function" ){
        n = atoms1.atomCount;
    }else if( atoms1 instanceof Float32Array ){
        n = atoms1.length / 3;
    }

    var coords1 = new NGL.Matrix( 3, n );
    var coords2 = new NGL.Matrix( 3, n );

    this.coords1t = new NGL.Matrix( n, 3 );
    this.coords2t = new NGL.Matrix( n, 3 );

    this.A = new NGL.Matrix( 3, 3 );
    this.W = new NGL.Matrix( 1, 3 );
    this.U = new NGL.Matrix( 3, 3 );
    this.V = new NGL.Matrix( 3, 3 );
    this.VH = new NGL.Matrix( 3, 3 );
    this.R = new NGL.Matrix( 3, 3 );

    this.tmp = new NGL.Matrix( 3, 3 );
    this.c = new NGL.Matrix( 3, 3 );
    this.c.data.set([ 1, 0, 0, 0, 1, 0, 0, 0, -1 ]);

    // prep coords

    this.prepCoords( atoms1, coords1 );
    this.prepCoords( atoms2, coords2 );

    // superpose

    this._superpose( coords1, coords2 );

};

NGL.Superposition.prototype = {

    constructor: NGL.Superposition,

    _superpose: function( coords1, coords2 ){

        // NGL.time( "superpose" );

        this.mean1 = jsfeat.matmath.mean_rows( coords1 );
        this.mean2 = jsfeat.matmath.mean_rows( coords2 );

        jsfeat.matmath.sub_rows( coords1, this.mean1 );
        jsfeat.matmath.sub_rows( coords2, this.mean2 );

        jsfeat.matmath.transpose( this.coords1t, coords1 );
        jsfeat.matmath.transpose( this.coords2t, coords2 );

        jsfeat.matmath.multiply_ABt( this.A, this.coords2t, this.coords1t );

        var svd = jsfeat.linalg.svd_decompose(
            this.A, this.W, this.U, this.V
        );

        jsfeat.matmath.invert_3x3( this.V, this.VH );
        jsfeat.matmath.multiply_3x3( this.R, this.U, this.VH );

        if( jsfeat.matmath.mat3x3_determinant( this.R ) < 0.0 ){

            NGL.log( "R not a right handed system" );

            jsfeat.matmath.multiply_3x3( this.tmp, this.c, this.VH );
            jsfeat.matmath.multiply_3x3( this.R, this.U, this.tmp );

        }

        // NGL.timeEnd( "superpose" );

    },

    prepCoords: function( atoms, coords ){

        var i = 0;
        var cd = coords.data;

        if( typeof atoms.eachSelectedAtom === "function" ){

            atoms.eachSelectedAtom( function( a ){

                cd[ i + 0 ] = a.x;
                cd[ i + 1 ] = a.y;
                cd[ i + 2 ] = a.z;

                i += 3;

            } );

        }else if( atoms instanceof Float32Array ){

            cd.set( atoms );

        }else{

            NGL.warn( "prepCoords: input type unknown" );

        }

    },

    transform: function( atoms ){

        // allocate data structures

        var n;
        if( typeof atoms.eachSelectedAtom === "function" ){
            n = atoms.atomCount;
        }else if( atoms instanceof Float32Array ){
            n = atoms.length / 3;
        }

        var coords = new NGL.Matrix( 3, n );
        var tmp = new NGL.Matrix( n, 3 );

        // prep coords

        this.prepCoords( atoms, coords );

        // do transform

        jsfeat.matmath.sub_rows( coords, this.mean1 );
        jsfeat.matmath.multiply_ABt( tmp, this.R, coords );
        jsfeat.matmath.transpose( coords, tmp );
        jsfeat.matmath.add_rows( coords, this.mean2 );

        var i = 0;
        var cd = coords.data;

        if( typeof atoms.eachSelectedAtom === "function" ){

            atoms.eachSelectedAtom( function( a ){

                a.x = cd[ i + 0 ];
                a.y = cd[ i + 1 ];
                a.z = cd[ i + 2 ];

                i += 3;

            } );

        }else if( atoms instanceof Float32Array ){

            atoms.set( cd.subarray( 0, n * 3 ) );

        }else{

            NGL.warn( "transform: input type unknown" );

        }

    }

};


NGL.superpose = function( s1, s2, align, sele1, sele2, xsele1, xsele2 ){

    align = align || false;
    sele1 = sele1 || "";
    sele2 = sele2 || "";
    xsele1 = xsele1 || "";
    xsele2 = xsele2 || "";

    var atoms1, atoms2;

    if( align ){

        var _s1 = s1;
        var _s2 = s2;

        if( sele1 && sele2 ){
            _s1 = new NGL.StructureSubset( s1, new NGL.Selection( sele1 ) );
            _s2 = new NGL.StructureSubset( s2, new NGL.Selection( sele2 ) );
        }

        var seq1 = _s1.getSequence();
        var seq2 = _s2.getSequence();

        // NGL.log( seq1.join("") );
        // NGL.log( seq2.join("") );

        var ali = new NGL.Alignment( seq1.join(""), seq2.join("") );

        ali.calc();
        ali.trace();

        // NGL.log( "superpose alignment score", ali.score );

        // NGL.log( ali.ali1 );
        // NGL.log( ali.ali2 );

        var l, _i, _j, x, y;
        var i = 0;
        var j = 0;
        var n = ali.ali1.length;
        var aliIdx1 = [];
        var aliIdx2 = [];

        for( l = 0; l < n; ++l ){

            x = ali.ali1[ l ];
            y = ali.ali2[ l ];

            _i = 0;
            _j = 0;

            if( x === "-" ){
                aliIdx2[ j ] = false;
            }else{
                aliIdx2[ j ] = true;
                _i = 1;
            }

            if( y === "-" ){
                aliIdx1[ i ] = false;
            }else{
                aliIdx1[ i ] = true;
                _j = 1;
            }

            i += _i;
            j += _j;

        }

        // NGL.log( i, j );

        // NGL.log( aliIdx1 );
        // NGL.log( aliIdx2 );

        atoms1 = new NGL.AtomSet();
        atoms2 = new NGL.AtomSet();

        i = 0;
        _s1.eachResidue( function( r ){

            if( !r.getResname1() || !r.getAtomByName( "CA" ) ) return;

            if( aliIdx1[ i ] ){
                atoms1.addAtom( r.getAtomByName( "CA" ) );
            }
            i += 1;

        } );

        i = 0;
        _s2.eachResidue( function( r ){

            if( !r.getResname1() || !r.getAtomByName( "CA" ) ) return;

            if( aliIdx2[ i ] ){
                atoms2.addAtom( r.getAtomByName( "CA" ) );
            }
            i += 1;

        } );

    }else{

        atoms1 = new NGL.AtomSet(
            s1, new NGL.Selection( sele1 + " and .CA" )
        );
        atoms2 = new NGL.AtomSet(
            s2, new NGL.Selection( sele2 + " and .CA" )
        );

    }

    if( xsele1 && xsele2 ){

        var _atoms1 = new NGL.AtomSet();
        var _atoms2 = new NGL.AtomSet();

        var xselection1 = new NGL.Selection( xsele1 );
        var xselection2 = new NGL.Selection( xsele2 );

        var test1 = xselection1.test;
        var test2 = xselection2.test;

        var i, a1, a2;
        var n = atoms1.atomCount;

        for( i = 0; i < n; ++i ){

            a1 = atoms1.atoms[ i ];
            a2 = atoms2.atoms[ i ];

            if( test1( a1 ) && test2( a2 ) ){

                _atoms1.addAtom( a1 );
                _atoms2.addAtom( a2 );

                // NGL.log( a1.qualifiedName(), a2.qualifiedName() )

            }

        }

        atoms1 = _atoms1;
        atoms2 = _atoms2;

    }

    var superpose = new NGL.Superposition( atoms1, atoms2 );

    var atoms = new NGL.AtomSet( s1, new NGL.Selection( "*" ) );
    superpose.transform( atoms );

    s1.center = s1.atomCenter();

}

// File:js/ngl/structure.js

/**
 * @file Structure
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


////////////
// GidPool

NGL.GidPool = {

    // REMEMBER not synced with worker

    nextGid: 1,

    objectList: [],

    rangeList: [],

    getBaseObject: function( object ){

        if( object.type === "StructureView" ){
            object = object.getStructure();
        }

        return object;

    },

    addObject: function( object ){

        object = this.getBaseObject( object );

        NGL.GidPool.objectList.push( object );
        NGL.GidPool.rangeList.push( NGL.GidPool.allocateGidRange( object ) );

        return NGL.GidPool;

    },

    removeObject: function( object ){

        object = this.getBaseObject( object );

        var idx = NGL.GidPool.objectList.indexOf( object );

        if( idx !== -1 ){

            NGL.GidPool.objectList.splice( idx, 1 );
            NGL.GidPool.rangeList.splice( idx, 1 );

            if( NGL.GidPool.objectList.length === 0 ){
                NGL.GidPool.nextGid = 1;
            }

        }

        return NGL.GidPool;

    },

    updateObject: function( object, silent ){

        object = this.getBaseObject( object );

        var idx = NGL.GidPool.objectList.indexOf( object );

        if( idx !== -1 ){

            var range = NGL.GidPool.rangeList[ idx ];

            if( range[1] === NGL.GidPool.nextGid ){
                var count = NGL.GidPool.getGidCount( object );
                NGL.GidPool.nextGid += count - ( range[1] - range[0] );
                range[ 1 ] = NGL.GidPool.nextGid;
            }else{
                NGL.GidPool.rangeList[ idx ] = NGL.GidPool.allocateGidRange( object );
            }

        }else{

            if( !silent ){
                NGL.warn( "NGL.GidPool.updateObject: object not found." );
            }

        }

        return NGL.GidPool;

    },

    getGidCount: function( object ){

        object = this.getBaseObject( object );

        var count = 0;

        if( object.type === "Structure" ){
            count = (
                object.atomStore.count +
                object.bondStore.count +
                object.backboneBondStore.count +
                object.rungBondStore.count
            );
        }else if( object.type === "Volume" ){
            count = object.__data.length;
        }else{
            NGL.warn( "NGL.GidPool.getGidCount: unknown object type" );
        }

        return count;

    },

    allocateGidRange: function( object ){

        object = this.getBaseObject( object );

        var firstGid = NGL.GidPool.nextGid;

        NGL.GidPool.nextGid += NGL.GidPool.getGidCount( object );

        if( NGL.GidPool.nextGid > Math.pow( 2, 24 ) ){
            NGL.error( "NGL.GidPool.allocateGidRange: GidPool overflown" );
        }

        return [ firstGid, NGL.GidPool.nextGid ];

    },

    freeGidRange: function( object ){

        object = this.getBaseObject( object );
        // TODO

    },

    getNextGid: function(){

        return NGL.GidPool.nextGid++;

    },

    getGid: function( object, offset ){

        object = this.getBaseObject( object );
        offset = offset || 0;

        var gid = 0;
        var idx = NGL.GidPool.objectList.indexOf( object );

        if( idx !== -1 ){

            var range = NGL.GidPool.rangeList[ idx ];
            var first = range[ 0 ];

            gid = first + offset;

        }else{

            NGL.warn( "NGL.GidPool.getGid: object not found." );

        }

        return gid;

    },

    getByGid: function( gid ){

        var entity;

        NGL.GidPool.objectList.forEach( function( o, i ){

            var range = NGL.GidPool.rangeList[ i ];
            if( gid < range[ 0 ] || gid >= range[ 1 ] ){
                return;
            }
            var offset = gid - range[ 0 ];

            if( o.type === "Structure" ){

                if( offset <= o.atomStore.count ){

                    entity = o.getAtomProxy( offset );

                }else if( offset <= o.atomStore.count + o.bondStore.count ){

                    offset -= o.atomStore.count
                    entity = o.getBondProxy( offset );

                }else if( offset <= o.atomStore.count + o.bondStore.count + o.backboneBondStore.count ){

                    offset -= ( o.atomStore.count + o.bondStore.count );
                    entity = o.getBondProxy( offset );
                    entity.bondStore = o.backboneBondStore;

                }else if( offset <= o.atomStore.count + o.bondStore.count + o.backboneBondStore.count + o.rungBondStore.count ){

                    offset -= ( o.atomStore.count + o.bondStore.count + o.backboneBondStore.count );
                    entity = o.getBondProxy( offset );
                    entity.bondStore = o.rungBondStore;

                }else{

                    NGL.warn( "NGL.GidPool.getByGid: invalid Structure gid", gid );

                }

            }else if( o.type === "Volume" ){

                entity = {
                    volume: o,
                    index: offset,
                    value: o.data[ offset ],
                    x: o.dataPosition[ offset * 3 ],
                    y: o.dataPosition[ offset * 3 + 1 ],
                    z: o.dataPosition[ offset * 3 + 2 ],
                };

            }else{

                NGL.warn( "NGL.GidPool.getByGid: unknown object type for gid", gid );

            }

        } );

        return entity;

    }

}


///////////////
// ColorMaker

NGL.ColorMakerRegistry = {

    signals: {

        // typesChanged: new signals.Signal(),

    },

    scales: {

        "": "",

        // Sequential
        "OrRd": "[S] Orange-Red",
        "PuBu": "[S] Purple-Blue",
        "BuPu": "[S] Blue-Purple",
        "Oranges": "[S] Oranges",
        "BuGn": "[S] Blue-Green",
        "YlOrBr": "[S] Yellow-Orange-Brown",
        "YlGn": "[S] Yellow-Green",
        "Reds": "[S] Reds",
        "RdPu": "[S] Red-Purple",
        "Greens": "[S] Greens",
        "YlGnBu": "[S] Yellow-Green-Blue",
        "Purples": "[S] Purples",
        "GnBu": "[S] Green-Blue",
        "Greys": "[S] Greys",
        "YlOrRd": "[S] Yellow-Orange-Red",
        "PuRd": "[S] Purple-Red",
        "Blues": "[S] Blues",
        "PuBuGn": "[S] Purple-Blue-Green",

        // Diverging
        "Spectral": "[D] Spectral",
        "RdYlGn": "[D] Red-Yellow-Green",
        "RdBu": "[D] Red-Blue",
        "PiYG": "[D] Pink-Yellowgreen",
        "PRGn": "[D] Purplered-Green",
        "RdYlBu": "[D] Red-Yellow-Blue",
        "BrBG": "[D] Brown-Bluegreen",
        "RdGy": "[D] Red-Grey",
        "PuOr": "[D] Purple-Orange",

        // Qualitative
        "Set1": "[Q] Set1",
        "Set2": "[Q] Set2",
        "Set3": "[Q] Set3",
        "Dark2": "[Q] Dark2",
        "Paired": "[Q] Paired",
        "Pastel1": "[Q] Pastel1",
        "Pastel2": "[Q] Pastel2",
        "Accent": "[Q] Accent",

        // Other
        "roygb": "[?] Rainbow",
        "rwb": "[?] Red-White-Blue",

    },

    modes: {

        "": "",

        "rgb": "Red Green Blue",
        "hsv": "Hue Saturation Value",
        "hsl": "Hue Saturation Lightness",
        "hsi": "Hue Saturation Intensity",
        "lab": "CIE L*a*b*",
        "hcl": "Hue Chroma Lightness"

    },

    types: {},

    userSchemes: {},

    getScheme: function( params ){

        var p = params || {};

        var id = p.scheme || "";

        var schemeClass;

        if( id in NGL.ColorMakerRegistry.types ){

            schemeClass = NGL.ColorMakerRegistry.types[ id ];

        }else if( id in NGL.ColorMakerRegistry.userSchemes ){

            schemeClass = NGL.ColorMakerRegistry.userSchemes[ id ];

        }else{

            schemeClass = NGL.ColorMaker;

        }

        return new schemeClass( params );

    },

    getPickingScheme: function( params ){

        var p = Object.assign( params || {} );
        p.scheme = "picking";

        return NGL.ColorMakerRegistry.getScheme( p );

    },

    getTypes: function(){

        var types = {};

        Object.keys( NGL.ColorMakerRegistry.types ).forEach( function( k ){
            // NGL.ColorMakerRegistry.types[ k ]
            types[ k ] = k;
        } );

        Object.keys( NGL.ColorMakerRegistry.userSchemes ).forEach( function( k ){
            types[ k ] = k.split( "|" )[ 1 ];
        } );

        return types;

    },

    getScales: function(){

        return NGL.ColorMakerRegistry.scales;

    },

    getModes: function(){

        return NGL.ColorMakerRegistry.modes;

    },

    addScheme: function( scheme, label ){

        if( !( scheme instanceof NGL.ColorMaker ) ){

            scheme = NGL.ColorMakerRegistry.createScheme( scheme, label );

        }

        label = label || "";
        var id = "" + THREE.Math.generateUUID() + "|" + label;

        NGL.ColorMakerRegistry.userSchemes[ id ] = scheme;
        // NGL.ColorMakerRegistry.signals.typesChanged.dispatch();

        return id;

    },

    removeScheme: function( id ){

        delete NGL.ColorMakerRegistry.userSchemes[ id ];
        // NGL.ColorMakerRegistry.signals.typesChanged.dispatch();

    },

    createScheme: function( constructor, label ){

        var ColorMaker = function( params ){

            NGL.ColorMaker.call( this, params );

            this.label = label || "";

            constructor.call( this, params );

        }

        ColorMaker.prototype = NGL.ColorMaker.prototype;

        ColorMaker.prototype.constructor = ColorMaker;

        return ColorMaker;

    },

    addSelectionScheme: function( pairList, label ){

        return NGL.ColorMakerRegistry.addScheme( function( params ){

            var colorList = [];
            var selectionList = [];

            pairList.forEach( function( pair ){

                colorList.push( new THREE.Color( pair[ 0 ] ).getHex() );
                selectionList.push( new NGL.Selection( pair[ 1 ] ) );

            } );

            var n = pairList.length;

            this.atomColor = function( a ){

                for( var i = 0; i < n; ++i ){

                    if( selectionList[ i ].test( a ) ){

                        return colorList[ i ];

                    }

                }

                return 0xFFFFFF;

            };

        }, label );

    }

}


NGL.ColorMaker = function( params ){

    var p = params || {};

    this.scale = p.scale || "uniform";
    this.mode = p.mode || "hcl";
    this.domain = p.domain || [ 0, 1 ];
    this.value = new THREE.Color( p.value || 0xFFFFFF ).getHex();

    this.structure = p.structure;
    this.volume = p.volume;
    this.surface = p.surface;

    if( this.structure ){
        this.atomProxy = this.structure.getAtomProxy();
    }

};

NGL.ColorMaker.prototype = {

    constructor: NGL.ColorMaker,

    getScale: function( params ){

        var p = params || {};

        var scale = p.scale || this.scale;
        if( scale === "rainbow" || scale === "roygb" ){
            scale = [ "red", "orange", "yellow", "green", "blue" ];
        }else if( scale === "rwb" ){
            scale = [ "red", "white", "blue" ];
        }

        return chroma
            .scale( scale )
            .mode( p.mode || this.mode )
            .domain( p.domain || this.domain )
            .out( "num" );

    },

    colorToArray: function( color, array, offset ){

        if( array === undefined ) array = [];
        if( offset === undefined ) offset = 0;

        array[ offset + 0 ] = ( color >> 16 & 255 ) / 255;
        array[ offset + 1 ] = ( color >> 8 & 255 ) / 255;
        array[ offset + 2 ] = ( color & 255 ) / 255;

        return array;

    },

    atomColor: function( a ){

        return 0xFFFFFF;

    },

    atomColorToArray: function( a, array, offset ){

        return this.colorToArray(
            this.atomColor( a ), array, offset
        );

    },

    bondColor: function( b, fromTo ){

        this.atomProxy.index = fromTo ? b.atomIndex1 : b.atomIndex2;
        return this.atomColor( this.atomProxy );

    },

    bondColorToArray: function( b, fromTo, array, offset ){

        return this.colorToArray(
            this.bondColor( b, fromTo ), array, offset
        );

    },

    volumeColor: function( i ){

        return 0xFFFFFF;

    },

    volumeColorToArray: function( i, array, offset ){

        return this.colorToArray(
            this.volumeColor( i ), array, offset
        );

    },

    positionColor: function( v ){

        return 0xFFFFFF;

    },

    positionColorToArray: function( v, array, offset ){

        return this.colorToArray(
            this.positionColor( v ), array, offset
        );

    }

};


NGL.VolumeColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    var valueScale = this.getScale();
    var volume = this.volume;
    var inverseMatrix = volume.inverseMatrix;
    var data = volume.__data;
    var nx = volume.nx;
    var ny = volume.ny;
    var nz = volume.nz;
    var vec = new THREE.Vector3();

    this.positionColor = function( v ){

        vec.copy( v );
        vec.applyMatrix4( inverseMatrix );
        vec.round();

        var index = ( ( ( ( vec.z * ny ) + vec.y ) * nx ) + vec.x );

        return valueScale( data[ index ] );

    };

};

NGL.VolumeColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.VolumeColorMaker.prototype.constructor = NGL.VolumeColorMaker;


NGL.ValueColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    var valueScale = this.getScale();

    this.volumeColor = function( i ){

        return valueScale( this.volume.data[ i ] );

    };

};

NGL.ValueColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.ValueColorMaker.prototype.constructor = NGL.ValueColorMaker;


NGL.PickingColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    var offset;
    if( this.structure ){
        offset = this.structure.atomStore.count;
        if( params.backbone ){
            offset += this.structure.bondStore.count;
        }else if( params.rung ){
            offset += this.structure.bondStore.count;
            offset += this.structure.backboneBondStore.count;
        }
    }

    this.atomColor = function( a ){

        return NGL.GidPool.getGid( this.structure, a.index );

    };

    this.bondColor = function( b, fromTo ){

        return NGL.GidPool.getGid( this.structure, offset + b.index );

    };

    this.volumeColor = function( i ){

        return NGL.GidPool.getGid( this.volume, i );

    };

};

NGL.PickingColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.PickingColorMaker.prototype.constructor = NGL.PickingColorMaker;


NGL.RandomColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    this.atomColor = function( a ){

        return Math.random() * 0xFFFFFF;

    };

};

NGL.RandomColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.RandomColorMaker.prototype.constructor = NGL.RandomColorMaker;


NGL.UniformColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    var color = this.value;

    this.atomColor = function(){

        return color;

    };

    this.bondColor = function(){

        return color;

    };

    this.valueColor = function(){

        return color;

    };

};

NGL.UniformColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.UniformColorMaker.prototype.constructor = NGL.UniformColorMaker;


NGL.AtomindexColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    if( !params.scale ){
        this.scale = "roygb";
    }
    if( !params.domain ){

        var scalePerModel = {};

        this.structure.eachModel( function( mp ){
            this.domain = [ mp.atomOffset, mp.atomEnd ];
            scalePerModel[ mp.index ] = this.getScale();
        }.bind( this ) );

        this.atomColor = function( a ){
            return scalePerModel[ a.modelIndex ]( a.index );
        };

    }else{

        var atomindexScale = this.getScale();

        this.atomColor = function( a ){
            return atomindexScale( a.index );
        };

    }

};

NGL.AtomindexColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.AtomindexColorMaker.prototype.constructor = NGL.AtomindexColorMaker;


NGL.ResidueindexColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    if( !params.scale ){
        this.scale = "roygb";
    }
    if( !params.domain ){
        this.domain = [ 0, this.structure.residueStore.count ];
    }
    var residueindexScale = this.getScale();

    this.atomColor = function( a ){
        return residueindexScale( a.residueIndex );
    };

};

NGL.ResidueindexColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.ResidueindexColorMaker.prototype.constructor = NGL.ResidueindexColorMaker;


NGL.ChainindexColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    if( !params.scale ){
        this.scale = "Spectral";
    }
    if( !params.domain ){
        this.domain = [ 0, this.structure.chainStore.count ];
    }
    var chainindexScale = this.getScale();

    this.atomColor = function( a ){
        return chainindexScale( a.chainIndex );
    };

};

NGL.ChainindexColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.ChainindexColorMaker.prototype.constructor = NGL.ChainindexColorMaker;


NGL.ModelindexColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    if( !params.scale ){
        this.scale = "roygb";
    }
    if( !params.domain ){
        this.domain = [ 0, this.structure.modelStore.count ];
    }
    var modelindexScale = this.getScale();

    this.atomColor = function( a ){
        return modelindexScale( a.modelIndex );
    };

};

NGL.ModelindexColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.ModelindexColorMaker.prototype.constructor = NGL.ModelindexColorMaker;


NGL.SstrucColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    var strucColors = NGL.StructureColors;
    var defaultStrucColor = NGL.StructureColors[""];
    var rp = this.structure.getResidueProxy();

    this.atomColor = function( ap ){

        var sstruc = ap.sstruc;

        if( sstruc === "h" ){
            return strucColors[ "alphaHelix" ];
        }else if( sstruc === "g" ){
            return strucColors[ "3_10Helix" ];
        }else if( sstruc === "i" ){
            return strucColors[ "piHelix" ];
        }else if( sstruc === "e" || sstruc === "b" ){
            return strucColors[ "betaStrand" ];
        }else{
            rp.index = ap.residueIndex;
            if( rp.isNucleic() ){
                return strucColors[ "dna" ];
            }else if( rp.isProtein() || sstruc === "s" || sstruc === "t" || sstruc === "l" ){
                return strucColors[ "coil" ];
            }else{
                return defaultStrucColor;
            }
        }

    };

};

NGL.SstrucColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.SstrucColorMaker.prototype.constructor = NGL.SstrucColorMaker;


NGL.ElementColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    var elemColors = NGL.ElementColors;
    var defaultElemColor = NGL.ElementColors[""];
    var colorValue = this.value;
    if( params.value === undefined ){
        colorValue = NGL.ElementColors[ "C" ];
    }

    this.atomColor = function( a ){

        var element = a.element;

        if( element === "C" ){
            return colorValue;
        }else{
            return elemColors[ element ] || defaultElemColor;
        }

    };

};

NGL.ElementColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.ElementColorMaker.prototype.constructor = NGL.ElementColorMaker;


NGL.ResnameColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    var resColors = NGL.ResidueColors;
    var defaultResColor = NGL.ResidueColors[""];

    this.atomColor = function( a ){

        return resColors[ a.resname ] || defaultResColor;

    };

};

NGL.ResnameColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.ResnameColorMaker.prototype.constructor = NGL.ResnameColorMaker;


NGL.BfactorColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    if( !params.scale ){
        this.scale = "OrRd";
    }

    if( !params.domain ){

        var selection;
        var min = Infinity;
        var max = -Infinity;

        if( params.sele ){
            selection = new NGL.Selection( params.sele );
        }

        this.structure.eachSelectedAtom( function( a ){
            var bfactor = a.bfactor;
            min = Math.min( min, bfactor );
            max = Math.max( max, bfactor );
        }, selection );

        this.domain = [ min, max ];

    }

    var bfactorScale = this.getScale();

    this.atomColor = function( a ){

        return bfactorScale( a.bfactor );

    };

};

NGL.BfactorColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.BfactorColorMaker.prototype.constructor = NGL.BfactorColorMaker;


NGL.OccupancyColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    if( !params.scale ){
        this.scale = "PuBu";
    }

    if( !params.domain ){
        this.domain = [ 0.0, 1.0 ];
    }

    var occupancyScale = this.getScale();

    this.atomColor = function( a ){
        return occupancyScale( a.occupancy );
    };

};

NGL.OccupancyColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.OccupancyColorMaker.prototype.constructor = NGL.OccupancyColorMaker;


NGL.HydrophobicityColorMaker = function( params ){

    NGL.ColorMaker.call( this, params );

    if( !params.scale ){
        this.scale = "RdYlGn";
    }

    var idx = 0;  // 0: DGwif, 1: DGwoct, 2: Oct-IF

    var resHF = {};
    for( var name in NGL.ResidueHydrophobicity ){
        resHF[ name ] = NGL.ResidueHydrophobicity[ name ][ idx ];
    }
    var defaultResHF = resHF[""];

    if( !params.domain ){

        var val;
        var min = Infinity;
        var max = -Infinity;

        for( var name in resHF ){

            val = resHF[ name ];
            min = Math.min( min, val );
            max = Math.max( max, val );

        }

        this.domain = [ min, 0, max ];

    }

    var hfScale = this.getScale();

    this.atomColor = function( a ){

        return hfScale( resHF[ a.resname ] || defaultResHF );

    };

};

NGL.HydrophobicityColorMaker.prototype = NGL.ColorMaker.prototype;

NGL.HydrophobicityColorMaker.prototype.constructor = NGL.HydrophobicityColorMaker;


NGL.ColorMakerRegistry.types = {

    "": NGL.ColorMaker,
    "picking": NGL.PickingColorMaker,
    "random": NGL.RandomColorMaker,
    "uniform": NGL.UniformColorMaker,
    "atomindex": NGL.AtomindexColorMaker,
    "residueindex": NGL.ResidueindexColorMaker,
    "chainindex": NGL.ChainindexColorMaker,
    "modelindex": NGL.ModelindexColorMaker,
    "sstruc": NGL.SstrucColorMaker,
    "element": NGL.ElementColorMaker,
    "resname": NGL.ResnameColorMaker,
    "bfactor": NGL.BfactorColorMaker,
    "hydrophobicity": NGL.HydrophobicityColorMaker,
    "value": NGL.ValueColorMaker,
    "volume": NGL.VolumeColorMaker,
    "occupancy": NGL.OccupancyColorMaker

};


////////////
// Factory

NGL.RadiusFactory = function( type, scale ){

    this.type = type;
    this.scale = scale || 1.0;

    this.max = 10;

};

NGL.RadiusFactory.types = {

    "": "",
    "vdw": "by vdW radius",
    "covalent": "by covalent radius",
    "sstruc": "by secondary structure",
    "bfactor": "by bfactor",
    "size": "size"

};

NGL.RadiusFactory.prototype = {

    constructor: NGL.RadiusFactory,

    atomRadius: function( a ){

        var type = this.type;
        var scale = this.scale;
        var vdwRadii = NGL.VdwRadii;
        var covalentRadii = NGL.CovalentRadii;

        var defaultVdwRadius = NGL.VdwRadii[""];
        var defaultCovalentRadius = NGL.CovalentRadii[""];
        var defaultBfactor = 1;

        var nucleic = [ "C3'", "C3*", "C4'", "C4*", "P" ];

        var r;

        switch( type ){

            case "vdw":

                r = vdwRadii[ a.element ] || defaultVdwRadius;
                break;

            case "covalent":

                r = covalentRadii[ a.element ] || defaultCovalentRadius;
                break;

            case "bfactor":

                r = a.bfactor || defaultBfactor;
                break;

            case "sstruc":

                var sstruc = a.sstruc;
                if( sstruc === "h" ){
                    r = 0.25;
                }else if( sstruc === "g" ){
                    r = 0.25;
                }else if( sstruc === "i" ){
                    r = 0.25;
                }else if( sstruc === "e" ){
                    r = 0.25;
                }else if( sstruc === "b" ){
                    r = 0.25;
                }else if( nucleic.indexOf( a.atomname ) !== -1 ){
                    r = 0.4;
                }else{
                    r = 0.1;
                }
                break;

            default:

                r = type || 1.0;
                break;

        }

        return Math.min( r * scale, this.max );

    }

};


NGL.LabelFactory = function( type, text ){

    this.type = type;
    this.text = text || {};

};

NGL.LabelFactory.types = {

    "": "",
    "atomname": "atom name",
    "atomindex": "atom index",
    "atom": "atom name + index",
    "resname": "residue name",
    "resno": "residue no",
    "res": "residue name + no",
    "text": "text"

};

NGL.LabelFactory.prototype = {

    constructor: NGL.LabelFactory,

    atomLabel: function( a ){

        var type = this.type;

        var l;

        switch( type ){

            case "atomname":
                l = a.atomname;
                break;

            case "atomindex":
                l = "" + a.index;
                break;

            case "atom":
                l = a.atomname + "|" + a.index;
                break;

            case "resname":
                l = a.resname;
                break;

            case "resno":
                l = "" + a.resno;
                break;

            case "res":
                l = ( NGL.AA1[ a.resname.toUpperCase() ] || '' ) + a.resno;
                break;

            case "text":
                l = this.text[ a.index ];
                break;

            default:
                l = a.qualifiedName();
                break;

        }

        return l === undefined ? '' : l;

    }

};


//////////////
// Structure

NGL.Structure = function( name, path ){

    var SIGNALS = signals;
    this.signals = {
        refreshed: new SIGNALS.Signal(),
    };

    this.name = name;
    this.path = path;
    this.title = "";
    this.id = "";

    this.atomSetCache = {};
    this.atomSetDict = {};
    this.biomolDict = {};
    this.helices = [];
    this.sheets = [];
    this.unitcell = undefined;
    this.selection = undefined;

    this.frames = [];
    this.boxes = [];

    this.bondStore = new NGL.BondStore( 0 );
    this.backboneBondStore = new NGL.BondStore( 0 );
    this.rungBondStore = new NGL.BondStore( 0 );
    this.atomStore = new NGL.AtomStore( 0 );
    this.residueStore = new NGL.ResidueStore( 0 );
    this.chainStore = new NGL.ChainStore( 0 );
    this.modelStore = new NGL.ModelStore( 0 );

    this.atomMap = new NGL.AtomMap( this );
    this.residueMap = new NGL.ResidueMap( this );

    this.atomSet = this.getAtomSet( this.selection );
    this.bondSet = this.getBondSet();

    this.center = new THREE.Vector3();
    this.boundingBox = new THREE.Box3();

    NGL.GidPool.addObject( this );

    this._ap = this.getAtomProxy();
    this._rp = this.getResidueProxy();
    this._cp = this.getChainProxy();

};

NGL.Structure.prototype = {

    constructor: NGL.Structure,
    type: "Structure",

    refresh: function(){

        if( NGL.debug ) NGL.time( "NGL.Structure.refresh" );

        this.atomSetCache = {};

        this.atomSet = this.getAtomSet2( this.selection );
        this.bondSet = this.getBondSet();

        for( var name in this.atomSetDict ){
            var as = this.atomSetDict[ name ];
            var as2 = this.getAtomSet2( false );
            this.atomSetCache[ "__" + name ] = as2.intersection( as );
        }

        this.boundingBox = this.getBoundingBox();
        this.center = this.boundingBox.center();

        NGL.GidPool.updateObject( this );

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.refresh" );

        this.signals.refreshed.dispatch();

    },

    getBondProxy: function( index ){

        return new NGL.BondProxy( this, index );

    },

    getAtomProxy: function( index, tmp ){

        if( tmp ){
            if( this.__tmpAtomProxy === undefined ){
                this.__tmpAtomProxy = new NGL.AtomProxy( this, index );
            }
            return this.__tmpAtomProxy;
        }else{
            return new NGL.AtomProxy( this, index );
        }

    },

    getResidueProxy: function( index, tmp ){

        if( tmp ){
            if( this.__tmpResidueProxy === undefined ){
                this.__tmpResidueProxy = new NGL.ResidueProxy( this, index );
            }
            return this.__tmpResidueProxy;
        }else{
            return new NGL.ResidueProxy( this, index );
        }

    },

    getChainProxy: function( index ){

        return new NGL.ChainProxy( this, index );

    },

    getModelProxy: function( index ){

        return new NGL.ModelProxy( this, index );

    },

    getBondSet: function( selection ){

        if( NGL.debug ) NGL.time( "NGL.Structure.getBondSet" );

        var n = this.bondStore.count;
        var bs = new TypedFastBitSet( n );
        var as = this.atomSet;

        if( as ){

            var bp = this.getBondProxy();

            for( var i = 0; i < n; ++i ){
                bp.index = i;
                if( as.has( bp.atomIndex1 ) && as.has( bp.atomIndex2 ) ){
                    bs.add_unsafe( bp.index );
                }
            }

        }else{

            bs.set_all( true );

        }

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.getBondSet" );

        return bs;

    },

    getBackboneBondSet: function( selection ){

        if( NGL.debug ) NGL.time( "NGL.Structure.getBackboneBondSet" );

        var n = this.backboneBondStore.count;
        var bs = new TypedFastBitSet( n );
        var as = this.atomSetCache[ "__backbone" ];

        if( as ){

            var bp = this.getBondProxy();
            bp.bondStore = this.backboneBondStore;

            for( var i = 0; i < n; ++i ){
                bp.index = i;
                if( as.has( bp.atomIndex1 ) && as.has( bp.atomIndex2 ) ){
                    bs.add_unsafe( bp.index );
                }
            }

        }else{

            bs.set_all( true );

        }

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.getBackboneBondSet" );

        return bs;

    },

    getRungBondSet: function( selection ){

        if( NGL.debug ) NGL.time( "NGL.Structure.getRungBondSet" );

        var n = this.rungBondStore.count;
        var bs = new TypedFastBitSet( n );
        var as = this.atomSetCache[ "__rung" ];

        if( as ){

            var bp = this.getBondProxy();
            bp.bondStore = this.rungBondStore;

            for( var i = 0; i < n; ++i ){
                bp.index = i;
                if( as.has( bp.atomIndex1 ) && as.has( bp.atomIndex2 ) ){
                    bs.add_unsafe( bp.index );
                }
            }

        }else{

            bs.set_all( true );

        }

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.getRungBondSet" );

        return bs;

    },

    getAtomSet: function( selection ){

        if( NGL.debug ) NGL.time( "NGL.Structure.getAtomSet" );

        var as;
        var n = this.atomStore.count;

        if( selection === false ){

            as = new TypedFastBitSet( n );

        }else if( selection === true ){

            as = new TypedFastBitSet( n );
            as.set_all( true );

        }else if( selection && selection.test ){

            var seleString = selection.string;
            as = this.atomSetCache[ seleString ];

            if( !seleString ) console.warn( "empty seleString" );

            if( as === undefined ){

                // TODO can be faster by setting ranges of atoms
                //      but for that must loop over hierarchy itself
                as = new TypedFastBitSet( n );
                var ap = this.getAtomProxy();
                var test = selection.test;
                for( var i = 0; i < n; ++i ){
                    ap.index = i;
                    if( test( ap ) ) as.add_unsafe( ap.index );
                }
                this.atomSetCache[ seleString ] = as;

            }else{

                // console.log( "getting atomSet from cache", seleString );

            }

        }else{

            as = new TypedFastBitSet( n );
            as.set_all( true );

        }

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.getAtomSet" );

        return as;

    },

    getAtomSet2: function( selection ){

        if( NGL.debug ) NGL.time( "NGL.Structure.getAtomSet2" );

        var as;
        var n = this.atomStore.count;

        if( selection === false ){

            as = new TypedFastBitSet( n );

        }else if( selection === true ){

            as = new TypedFastBitSet( n );
            as.set_all( true );

        }else if( selection && selection.test ){

            var seleString = selection.string;
            as = this.atomSetCache[ seleString ];

            if( !seleString ) console.warn( "empty seleString" );

            if( as === undefined ){

                as = new TypedFastBitSet( n );
                this.eachAtom( function( ap ){
                    as.add_unsafe( ap.index );
                }, selection );
                this.atomSetCache[ seleString ] = as;

            }else{

                // console.log( "getting atomSet from cache", seleString );

            }

        }else{

            as = new TypedFastBitSet( n );
            as.set_all( true );

        }

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.getAtomSet2" );

        return as;

    },

    setSelection: function( selection ){

        this.selection = selection;

        this.refresh();

    },

    getSelection: function(){

        return this.selection;

    },

    getStructure: function(){

        return this;

    },

    //

    eachBond: function( callback, selection ){

        var bp = this.getBondProxy();
        var bs = this.bondSet;

        if( selection && selection.test ){
            if( bs ){
                bs = bs.new_intersection( this.getBondSet( selection ) );
            }else{
                bs = this.getBondSet( selection );
            }
        }

        if( bs ){
            bs.forEach( function( index ){
                bp.index = index;
                callback( bp );
            } );
        }else{
            var n = this.bondStore.count;
            for( var i = 0; i < n; ++i ){
                bp.index = i;
                callback( bp );
            }
        }

    },

    getAtomSet3: function( selection ){

        if( NGL.debug ) NGL.time( "NGL.Structure.getAtomSet3" );

        var as = this.atomSet;

        if( selection && selection.test ){
            if( as ){
                as = as.new_intersection( this.getAtomSet2( selection ) );
            }else{
                as = this.getAtomSet2( selection );
            }
        }

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.getAtomSet3" );

        return as;

    },

    eachSelectedAtom: function( callback, selection ){

        var ap = this.getAtomProxy();
        var as = this.getAtomSet3( selection );
        var n = this.atomStore.count;

        if( as && as.size() < n ){
            as.forEach( function( index ){
                ap.index = index;
                callback( ap );
            } );
        }else{
            for( var i = 0; i < n; ++i ){
                ap.index = i;
                callback( ap );
            }
        }

    },

    eachAtom: function( callback, selection ){

        if( selection && selection.test ){
            this.eachModel( function( mp ){
                mp.eachAtom( callback, selection )
            }, selection );
        }else{
            var an = this.atomStore.count;
            var ap = this.getAtomProxy();
            for( var i = 0; i < an; ++i ){
                ap.index = i;
                callback( ap );
            }
        }

    },

    eachResidue: function( callback, selection ){

        if( selection && selection.test ){
            var mn = this.modelStore.count;
            var mp = this.getModelProxy();
            if( selection.modelOnlyTest ){
                var modelOnlyTest = selection.modelOnlyTest;
                for( var i = 0; i < mn; ++i ){
                    mp.index = i;
                    if( modelOnlyTest( mp ) ){
                        mp.eachResidue( callback, selection );
                    }
                }
            }else{
                for( var i = 0; i < mn; ++i ){
                    mp.index = i;
                    mp.eachResidue( callback, selection );
                }
            }
        }else{
            var rn = this.residueStore.count;
            var rp = this.getResidueProxy();
            for( var i = 0; i < rn; ++i ){
                rp.index = i;
                callback( rp );
            }
        }

    },

    eachResidueN: function( n, callback ){

        var rn = this.residueStore.count;
        if( rn < n ) return;
        var array = new Array( n );

        for( var i = 0; i < n; ++i ){
            array[ i ] = this.getResidueProxy( i );
        }
        callback.apply( this, array );

        for( var j = n; j < rn; ++j ){
            for( var i = 0; i < n; ++i ){
                array[ i ].index += 1;
            }
            callback.apply( this, array );
        }

    },

    eachPolymer: function( callback, selection ){

        if( selection && selection.modelOnlyTest ){

            var modelOnlyTest = selection.modelOnlyTest;

            this.eachModel( function( mp ){
                if( modelOnlyTest( mp ) ){
                    mp.eachPolymer( callback, selection );
                }
            } );

        }else{

            this.eachModel( function( mp ){
                mp.eachPolymer( callback, selection );
            } );

        }

    },

    eachChain: function( callback, selection ){

        if( selection && selection.test ){
            this.eachModel( function( mp ){
                mp.eachChain( callback, selection );
            } );
        }else{
            var cn = this.chainStore.count;
            var cp = this.getChainProxy();
            for( var i = 0; i < cn; ++i ){
                cp.index = i;
                callback( cp );
            }
        }

    },

    eachModel: function( callback, selection ){

        var n = this.modelStore.count;
        var mp = this.getModelProxy();

        if( selection && selection.test ){
            var modelOnlyTest = selection.modelOnlyTest;
            if( modelOnlyTest ){
                for( var i = 0; i < n; ++i ){
                    mp.index = i;
                    if( modelOnlyTest( mp ) ){
                        callback( mp, selection );
                    }
                }
            }else{
                for( var i = 0; i < n; ++i ){
                    mp.index = i;
                    callback( mp, selection );
                }
            }
        }else{
            for( var i = 0; i < n; ++i ){
                mp.index = i;
                callback( mp );
            }
        }

    },

    //

    getAtomData: function( params ){

        var p = Object.assign( {}, params );
        if( p.colorParams ) p.colorParams.structure = this.getStructure();

        var what = p.what;
        var atomSet = p.atomSet || this.atomSet;

        var radiusFactory, colorMaker, pickingColorMaker;
        var position, color, pickingColor, radius;

        var atomData = {};
        var ap = this.getAtomProxy();
        var atomCount = atomSet.size();

        if( !what || what[ "position" ] ){
            position = new Float32Array( atomCount * 3 );
            atomData[ "position" ] = position;
        }
        if( !what || what[ "color" ] ){
            color = new Float32Array( atomCount * 3 );
            atomData[ "color" ] = color;
            colorMaker = NGL.ColorMakerRegistry.getScheme( p.colorParams );
        }
        if( !what || what[ "pickingColor" ] ){
            pickingColor = new Float32Array( atomCount * 3 );
            atomData[ "pickingColor" ] = pickingColor;
            var pickingColorParams = Object.assign( p.colorParams, { scheme: "picking" } );
            pickingColorMaker = NGL.ColorMakerRegistry.getScheme( pickingColorParams );
        }
        if( !what || what[ "radius" ] ){
            radius = new Float32Array( atomCount );
            atomData[ "radius" ] = radius;
            radiusFactory = new NGL.RadiusFactory( p.radiusParams.radius, p.radiusParams.scale );
        }

        atomSet.forEach( function( index, i ){
            var i3 = i * 3;
            ap.index = index;
            if( position ){
                ap.positionToArray( position, i3 );
            }
            if( color ){
                colorMaker.atomColorToArray( ap, color, i3 );
            }
            if( pickingColor ){
                pickingColorMaker.atomColorToArray( ap, pickingColor, i3 );
            }
            if( radius ){
                radius[ i ] = radiusFactory.atomRadius( ap );
            }
        } );

        return atomData;

    },

    getBondData: function( params ){

        var p = Object.assign( {}, params );
        if( p.colorParams ) p.colorParams.structure = this.getStructure();

        var what = p.what;
        var bondSet = p.bondSet || this.bondSet;

        var radiusFactory, colorMaker, pickingColorMaker;
        var position1, position2, color1, color2, pickingColor1, pickingColor2, radius1, radius2;

        var bondData = {};
        var bp = this.getBondProxy();
        if( p.bondStore ) bp.bondStore = p.bondStore;
        var ap1 = this.getAtomProxy();
        var ap2 = this.getAtomProxy();
        var bondCount = bondSet.size();

        if( !what || what[ "position" ] ){
            position1 = new Float32Array( bondCount * 3 );
            position2 = new Float32Array( bondCount * 3 );
            bondData[ "position1" ] = position1;
            bondData[ "position2" ] = position2;
        }
        if( !what || what[ "color" ] ){
            color1 = new Float32Array( bondCount * 3 );
            color2 = new Float32Array( bondCount * 3 );
            bondData[ "color1" ] = color1;
            bondData[ "color2" ] = color2;
            colorMaker = NGL.ColorMakerRegistry.getScheme( p.colorParams );
        }
        if( !what || what[ "pickingColor" ] ){
            pickingColor1 = new Float32Array( bondCount * 3 );
            pickingColor2 = new Float32Array( bondCount * 3 );
            bondData[ "pickingColor1" ] = pickingColor1;
            bondData[ "pickingColor2" ] = pickingColor2;
            var pickingColorParams = Object.assign( p.colorParams, { scheme: "picking" } );
            pickingColorMaker = NGL.ColorMakerRegistry.getScheme( pickingColorParams );
        }
        if( !what || what[ "radius" ] ){
            radiusFactory = new NGL.RadiusFactory( p.radiusParams.radius, p.radiusParams.scale );
        }
        if( !what || what[ "radius" ] ){
            radius1 = new Float32Array( bondCount );
            if( p.radius2 ){
                radius2 = new Float32Array( bondCount );
                bondData[ "radius1" ] = radius1;
                bondData[ "radius2" ] = radius2;
            }else{
                bondData[ "radius" ] = radius1;
            }
        }

        bondSet.forEach( function( index, i ){
            var i3 = i * 3;
            bp.index = index
            ap1.index = bp.atomIndex1;
            ap2.index = bp.atomIndex2;
            if( position1 ){
                ap1.positionToArray( position1, i3 );
                ap2.positionToArray( position2, i3 );
            }
            if( color1 ){
                colorMaker.bondColorToArray( bp, 1, color1, i3 );
                colorMaker.bondColorToArray( bp, 0, color2, i3 );
            }
            if( pickingColor1 ){
                pickingColorMaker.bondColorToArray( bp, 1, pickingColor1, i3 );
                pickingColorMaker.bondColorToArray( bp, 0, pickingColor2, i3 );
            }
            if( radius1 ){
                radius1[ i ] = radiusFactory.atomRadius( ap1 );
            }
            if( radius2 ){
                radius2[ i ] = radiusFactory.atomRadius( ap2 );
            }
        } );

        return bondData;

    },

    getBackboneAtomData: function( params ){

        params = Object.assign( {
            atomSet: this.atomSetCache[ "__backbone" ],
        }, params );

        return this.getAtomData( params );

    },

    getBackboneBondData: function( params ){

        params = Object.assign( {
            bondSet: this.getBackboneBondSet(),
            bondStore: this.backboneBondStore
        }, params );

        return this.getBondData( params );

    },

    getRungAtomData: function( params ){

        params = Object.assign( {
            atomSet: this.atomSetCache[ "__rung" ],
        }, params );

        return this.getAtomData( params );

    },

    getRungBondData: function( params ){

        params = Object.assign( {
            bondSet: this.getRungBondSet(),
            bondStore: this.rungBondStore
        }, params );

        return this.getBondData( params );

    },

    //

    getView: function( selection ){

        return new NGL.StructureView( this, selection );

    },

    getBoundingBox: function( selection ){

        if( NGL.debug ) console.time( "getBoundingBox" );

        var box = new THREE.Box3();

        var minX = +Infinity;
        var minY = +Infinity;
        var minZ = +Infinity;

        var maxX = -Infinity;
        var maxY = -Infinity;
        var maxZ = -Infinity;

        this.eachSelectedAtom( function( ap ){

            var x = ap.x;
            var y = ap.y;
            var z = ap.z;

            if( x < minX ) minX = x;
            if( y < minY ) minY = y;
            if( z < minZ ) minZ = z;

            if( x > maxX ) maxX = x;
            if( y > maxY ) maxY = y;
            if( z > maxZ ) maxZ = z;

        }, selection );

        box.min.set( minX, minY, minZ );
        box.max.set( maxX, maxY, maxZ );

        if( NGL.debug ) console.timeEnd( "getBoundingBox" );

        return box;

    },

    atomCenter: function( selection ){

        if( selection ){
            return this.getBoundingBox( selection ).center();
        }else{
            return this.center.clone();
        }

    },

    getSequence: function(){

        var seq = [];
        var rp = this.getResidueProxy();

        this.eachSelectedAtom( function( ap ){
            rp.index = ap.residueIndex;
            if( ap.index === rp.traceAtomIndex ){
                seq.push( rp.getResname1() );
            }
        } );

        return seq;

    },

    getAtomIndices: function( selection ){

        // Best to use only when the selection resolves to just a few indices!!!

        var indices = [];

        this.eachAtom( function( ap ){
            indices.push( ap.index );
        }, selection );

        return indices;

    },

    atomIndex: function(){

        var i = 0;
        var index = new Float32Array( this.atomCount );

        this.eachSelectedAtom( function( ap ){
            index[ i ] = ap.index;
        } );

        return index;

    },

    //

    updatePosition: function( position ){

        var i = 0;

        this.eachSelectedAtom( function( ap ){
            ap.positionFromArray( position, i );
            i += 3;
        } );

    },

    //

    toJSON: function(){

        if( NGL.debug ) NGL.time( "NGL.Structure.toJSON" );

        var output = {

            metadata: {
                version: 0.1,
                type: 'Structure',
                generator: 'StructureExporter'
            },

            name: this.name,
            path: this.path,
            title: this.title,
            id: this.id,

            biomolDict: {},
            helices: this.helices,
            sheets: this.sheets,
            unitcell: this.unitcell ? this.unitcell.toJSON() : undefined,

            frames: this.frames,
            boxes: this.boxes,

            center: this.center.toArray(),
            boundingBox: [
                this.boundingBox.min.toArray(),
                this.boundingBox.max.toArray()
            ],

            bondStore: this.bondStore.toJSON(),
            backboneBondStore: this.backboneBondStore.toJSON(),
            rungBondStore: this.rungBondStore.toJSON(),
            atomStore: this.atomStore.toJSON(),
            residueStore: this.residueStore.toJSON(),
            chainStore: this.chainStore.toJSON(),
            modelStore: this.modelStore.toJSON(),

            bondSet: this.bondSet.toJSON(),
            atomSet: this.atomSet.toJSON(),

            atomSetDict: {},
            atomSetCache: {},

            atomMap: this.atomMap.toJSON(),
            residueMap: this.residueMap.toJSON()

        };

        for( var name in this.biomolDict ){
            output.biomolDict[ name ] = this.biomolDict[ name ].toJSON()
        }
        for( var name in this.atomSetDict ){
            output.atomSetDict[ name ] = this.atomSetDict[ name ].toJSON()
        }
        for( var name in this.atomSetCache ){
            output.atomSetCache[ name ] = this.atomSetCache[ name ].toJSON()
        }

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.toJSON" );

        return output;

    },

    fromJSON: function( input ){

        if( NGL.debug ) NGL.time( "NGL.Structure.fromJSON" );

        this.name = input.name;
        this.path = input.path;
        this.title = input.title;
        this.id = input.id;

        this.biomolDict = input.biomolDict;
        this.helices = input.helices;
        this.sheets = input.sheets;
        if( input.unitcell ) this.unitcell = new NGL.Unitcell().fromJSON( input.unitcell );

        this.frames = input.frames;
        this.boxes = input.boxes;

        this.center = new THREE.Vector3().fromArray( input.center );
        this.boundingBox = new THREE.Box3(
            new THREE.Vector3().fromArray( input.boundingBox[ 0 ] ),
            new THREE.Vector3().fromArray( input.boundingBox[ 1 ] )
        );

        this.bondStore.fromJSON( input.bondStore );
        this.backboneBondStore.fromJSON( input.backboneBondStore );
        this.rungBondStore.fromJSON( input.rungBondStore );
        this.atomStore.fromJSON( input.atomStore );
        this.residueStore.fromJSON( input.residueStore );
        this.chainStore.fromJSON( input.chainStore );
        this.modelStore.fromJSON( input.modelStore );

        this.bondSet.fromJSON( input.bondSet );
        this.atomSet.fromJSON( input.atomSet );

        this.biomolDict = {};
        for( var name in input.biomolDict ){
            var assembly = new NGL.Assembly();
            this.biomolDict[ name ] = assembly.fromJSON( input.biomolDict[ name ] );
        }
        this.atomSetDict = {};
        for( var name in input.atomSetDict ){
            var as = new TypedFastBitSet();
            this.atomSetDict[ name ] = as.fromJSON( input.atomSetDict[ name ] );
        }
        this.atomSetCache = {};
        for( var name in input.atomSetCache ){
            var as = new TypedFastBitSet();
            this.atomSetCache[ name ] = as.fromJSON( input.atomSetCache[ name ] );
        }

        this.atomMap.fromJSON( input.atomMap );
        this.residueMap.fromJSON( input.residueMap );

        NGL.GidPool.updateObject( this );

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.fromJSON" );

        return this;

    },

    getTransferable: function(){

        var transferable = [];

        transferable.concat( this.bondStore.getTransferable() );
        transferable.concat( this.backboneBondStore.getTransferable() );
        transferable.concat( this.rungBondStore.getTransferable() );
        transferable.concat( this.atomStore.getTransferable() );
        transferable.concat( this.residueStore.getTransferable() );
        transferable.concat( this.chainStore.getTransferable() );
        transferable.concat( this.modelStore.getTransferable() );

        if( this.frames ){
            var frames = this.frames;
            var n = this.frames.length;
            for( var i = 0; i < n; ++i ){
                transferable.push( frames[ i ].buffer );
            }
        }
        if( this.boxes ){
            var boxes = this.boxes;
            var n = this.boxes.length;
            for( var i = 0; i < n; ++i ){
                transferable.push( boxes[ i ].buffer );
            }
        }

        transferable.concat( this.bondSet.getTransferable() );
        transferable.concat( this.atomSet.getTransferable() );

        for( var name in this.atomSetDict ){
            transferable.concat( this.atomSetDict[ name ].getTransferable() );
        }
        for( var name in this.atomSetCache ){
            transferable.concat( this.atomSetCache[ name ].getTransferable() );
        }

        return transferable;

    },

    dispose: function(){

        NGL.GidPool.removeObject( this );

        if( this.frames ) this.frames.length = 0;
        if( this.boxes ) this.boxes.length = 0;

        this.bondStore.dispose();
        this.backboneBondStore.dispose();
        this.rungBondStore.dispose();
        this.atomStore.dispose();
        this.residueStore.dispose();
        this.chainStore.dispose();
        this.modelStore.dispose();

        delete this.bondStore;
        delete this.atomStore;
        delete this.residueStore;
        delete this.chainStore;
        delete this.modelStore;

        delete this.frames;
        delete this.boxes;
        delete this.cif;

        delete this.bondSet;
        delete this.atomSet;

    }

};


NGL.StructureView = function( structure, selection ){

    var SIGNALS = signals;
    this.signals = {
        refreshed: new SIGNALS.Signal(),
    };

    this.structure = structure;
    this.selection = selection;

    this.center = new THREE.Vector3();
    this.boundingBox = new THREE.Box3();

    // to allow creating an empty object to call .fromJSON onto
    if( !structure && !selection ) return;

    this.init();

    this.refresh();

};

NGL.StructureView.prototype = NGL.createObject(

    NGL.Structure.prototype, {

    constructor: NGL.StructureView,
    type: "StructureView",

    init: function(){

        Object.defineProperties( this, {
            atomSetDict: {
                get: function(){ return this.structure.atomSetDict }
            },
            bondStore: {
                get: function(){ return this.structure.bondStore }
            },
            backboneBondStore: {
                get: function(){ return this.structure.backboneBondStore }
            },
            rungBondStore: {
                get: function(){ return this.structure.rungBondStore }
            },
            atomStore: {
                get: function(){ return this.structure.atomStore }
            },
            residueStore: {
                get: function(){ return this.structure.residueStore }
            },
            chainStore: {
                get: function(){ return this.structure.chainStore }
            },
            modelStore: {
                get: function(){ return this.structure.modelStore }
            },
            atomMap: {
                get: function(){ return this.structure.atomMap }
            },
            residueMap: {
                get: function(){ return this.structure.residueMap }
            }
        } );

        this._ap = this.getAtomProxy();
        this._rp = this.getResidueProxy();
        this._cp = this.getChainProxy();

        // FIXME should selection be serializable?
        if( this.selection ){
            this.selection.signals.stringChanged.add( function( string ){
                this.refresh();
            }, this );
        }

        this.structure.signals.refreshed.add( this.refresh, this );

    },

    refresh: function(){

        if( NGL.debug ) NGL.time( "NGL.StructureView.refresh" );

        this.atomSetCache = {};

        this.atomSet = this.getAtomSet2( this.selection );
        if( this.structure.atomSet ){
            if( NGL.debug ) NGL.time( "NGL.StructureView.refresh#atomSet.intersection" );
            this.atomSet = this.atomSet.intersection( this.structure.atomSet );
            if( NGL.debug ) NGL.timeEnd( "NGL.StructureView.refresh#atomSet.intersection" );
        }

        this.bondSet = this.getBondSet();

        if( NGL.debug ) NGL.time( "NGL.StructureView.refresh#atomSetDict.new_intersection" );
        for( var name in this.atomSetDict ){
            var as = this.atomSetDict[ name ];
            this.atomSetCache[ "__" + name ] = as.new_intersection( this.atomSet );
        }
        if( NGL.debug ) NGL.timeEnd( "NGL.StructureView.refresh#atomSetDict.new_intersection" );

        if( NGL.debug ) NGL.time( "NGL.StructureView.refresh#size" );
        this.atomCount = this.atomSet.size();
        this.bondCount = this.bondSet.size();
        if( NGL.debug ) NGL.timeEnd( "NGL.StructureView.refresh#size" );

        this.boundingBox = this.getBoundingBox();
        this.center = this.boundingBox.center();

        if( NGL.debug ) NGL.timeEnd( "NGL.StructureView.refresh" );

        this.signals.refreshed.dispatch();

    },

    getSelection: function(){

        var parentSelection = this.structure.getSelection();
        if( parentSelection ){
            if( parentSelection.string && this.selection.string ){
                return new NGL.Selection(
                    "( " + parentSelection.string + " ) AND " +
                    "( " + this.selection.string + " )"
                );
            }else if( parentSelection.string ){
                return new NGL.Selection( parentSelection.string );
            }else if( this.selection.string ){
                return new NGL.Selection( this.selection.string );
            }else{
                return new NGL.Selection( "" );
            }
        }else{
            return this.selection;
        }

    },

    getStructure: function(){

        return this.structure.getStructure();

    },

    toJSON: function(){

        var output = {

            metadata: {
                version: 0.1,
                type: 'StructureView',
                generator: 'StructureViewExporter'
            },

            structure: this.structure.toJSON(),
            // selection: this.selection.toJSON(),

            atomSet: this.atomSet.toJSON(),
            bondSet: this.bondSet.toJSON(),

            atomCount: this.atomCount,
            bondCount: this.bondCount,

            atomSetCache: {}

        };

        for( var name in this.atomSetCache ){
            output.atomSetCache[ name ] = this.atomSetCache[ name ].toJSON()
        }

        return output;

    },

    fromJSON: function( input ){

        if( input.structure.metadata.type === "Structure" ){
            this.structure = new NGL.Structure().fromJSON( input.structure );
        }else if( input.structure.metadata.type === "StructureView" ){
            this.structure = new NGL.StructureView().fromJSON( input.structure );
        }

        this.atomSet = new TypedFastBitSet().fromJSON( input.atomSet );
        this.bondSet = new TypedFastBitSet().fromJSON( input.bondSet );

        this.atomCount = input.atomCount;
        this.bondCount = input.bondCount;

        this.atomSetCache = {};
        for( var name in input.atomSetCache ){
            var as = new TypedFastBitSet();
            this.atomSetCache[ name ] = as.fromJSON( input.atomSetCache[ name ] );
        }

        this.init();

        return this;

    },

    getTransferable: function(){

        var transferable = [];

        transferable.concat( this.structure.getTransferable() );

        transferable.concat( this.bondSet.getTransferable() );
        transferable.concat( this.atomSet.getTransferable() );

        for( var name in this.atomSetCache ){
            transferable.concat( this.atomSetCache[ name ].getTransferable() );
        }

        return transferable;

    },

    dispose: function(){

        delete this.structure;

        delete this.atomSet;
        delete this.bondSet;

        delete this.atomCount;
        delete this.bondCount;

    }

} );

// File:js/ngl/trajectory.js

/**
 * @file Trajectory
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */



NGL.makeTrajectory = function( trajSrc, structure, sele ){

    var traj;

    if( trajSrc instanceof NGL.Frames || trajSrc instanceof Promise ){

        traj = new NGL.FramesTrajectory( trajSrc, structure, sele );

    }else if( !trajSrc && structure.frames ){

        traj = new NGL.StructureTrajectory( trajSrc, structure, sele );

    }else{

        traj = new NGL.RemoteTrajectory( trajSrc, structure, sele );

    }

    return traj;

};


///////////
// Frames

NGL.Frames = function( name, path ){

    this.name = name;
    this.path = path;

    this.coordinates = [];
    this.boxes = [];

};

NGL.Frames.prototype = {

    constructor: NGL.Frames,

    toJSON: function(){

        var output = {

            metadata: {
                version: 0.1,
                type: 'Frames',
                generator: 'FramesExporter'
            },

            name: this.name,
            path: this.path,

            coordinates: this.coordinates,
            boxes: this.boxes

        };

        return output;

    },

    fromJSON: function( input ){

        this.name = input.name;
        this.path = input.path;

        this.coordinates = input.coordinates;
        this.boxes = input.boxes;

    },

    getTransferable: function(){

        var transferable = [];

        var coordinates = this.coordinates;
        var n = coordinates.length;

        for( var i = 0; i < n; ++i ){

            transferable.push( coordinates[ i ].buffer );

        }

        return transferable;

    }

};


///////////////
// Trajectory

// TODO params handling in constructor and getParameters method
NGL.Trajectory = function( trajPath, structure, selectionString ){

    var SIGNALS = signals;

    this.signals = {
        gotNumframes: new SIGNALS.Signal(),
        frameChanged: new SIGNALS.Signal(),
        selectionChanged: new SIGNALS.Signal(),
        playerChanged: new SIGNALS.Signal(),
    };

    this.params = {
        centerPbc: true,
        removePbc: true,
        superpose: true
    };

    this.name = trajPath.replace( /^.*[\\\/]/, '' );

    this.selection = new NGL.Selection(
        selectionString || "backbone and not hydrogen"
    );

    this.selection.signals.stringChanged.add( function( string ){
        this.makeIndices();
        this.resetCache();
    }, this );

    // should come after this.selection is set
    this.setStructure( structure );

    this.trajPath = trajPath;

    this.numframes = undefined;
    this.getNumframes();

};

NGL.Trajectory.prototype = {

    constructor: NGL.Trajectory,

    setStructure: function( structure ){

        this.structure = structure;
        this.atomCount = structure.atomCount;

        this.makeAtomIndices();

        this.saveInitialStructure();

        this.backboneIndices = this.structure.atomIndex(
            new NGL.Selection( "backbone and not hydrogen" )
        );
        this.makeIndices();

        this.frameCache = [];
        this.boxCache = [];
        this.pathCache = [];
        this.frameCacheSize = 0;
        this.currentFrame = -1;

    },

    saveInitialStructure: function(){

        var i = 0;
        var initialStructure = new Float32Array( 3 * this.atomCount );

        this.structure.eachSelectedAtom( function( a ){

            initialStructure[ i + 0 ] = a.x;
            initialStructure[ i + 1 ] = a.y;
            initialStructure[ i + 2 ] = a.z;

            i += 3;

        } );

        this.initialStructure = initialStructure;

    },

    setSelection: function( string ){

        this.selection.setString( string );

        return this;

    },

    makeIndices: function(){

        this.indices = this.structure.atomIndex( this.selection );

        var i, j;
        var n = this.indices.length * 3;

        this.coords1 = new Float32Array( n );
        this.coords2 = new Float32Array( n );

        var y = this.initialStructure;
        var coords2 = this.coords2;

        for( i = 0; i < n; i += 3 ){

            j = this.indices[ i / 3 ] * 3;

            coords2[ i + 0 ] = y[ j + 0 ];
            coords2[ i + 1 ] = y[ j + 1 ];
            coords2[ i + 2 ] = y[ j + 2 ];

        }

    },

    makeAtomIndices: function(){

        NGL.error( "Trajectory.makeAtomIndices not implemented" );

    },

    getNumframes: function(){

        NGL.error( "Trajectory.loadFrame not implemented" );

    },

    resetCache: function(){

        this.frameCache = [];
        this.boxCache = [];
        this.pathCache = [];
        this.frameCacheSize = 0;
        this.setFrame( this.currentFrame );

        return this;

    },

    setParameters: function( params ){

        var p = params;
        var tp = this.params;
        var resetCache = false;

        if( p.centerPbc !== undefined && p.centerPbc !== tp.centerPbc ){

            tp.centerPbc = p.centerPbc;
            resetCache = true;

        }

        if( p.removePbc !== undefined && p.removePbc !== tp.removePbc ){

            tp.removePbc = p.removePbc;
            resetCache = true;

        }

        if( p.superpose !== undefined && p.superpose !== tp.superpose ){

            tp.superpose = p.superpose;
            resetCache = true;

        }

        if( resetCache ) this.resetCache();

    },

    setFrame: function( i, callback ){

        if( i === undefined ) return this;

        this.inProgress = true;

        i = parseInt( i );

        if( i === -1 || this.frameCache[ i ] ){

            this.updateStructure( i, callback );

        }else{

            this.loadFrame( i, function(){

                this.updateStructure( i, callback );

            }.bind( this ) );

        }

        return this;

    },

    interpolate: function(){

        var spline = function( p0, p1, p2, p3, t, tension ) {

            var v0 = ( p2 - p0 ) * tension;
            var v1 = ( p3 - p1 ) * tension;
            var t2 = t * t;
            var t3 = t * t2;

            return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 +
                   ( -3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 +
                   v0 * t + p1;

        }

        var lerp = function( a, b, t ) {

            return a + ( b - a ) * t;

        }

        return function( i, ip, ipp, ippp, t, type, callback ){

            var fc = this.frameCache;

            var c = fc[ i ];
            var cp = fc[ ip ];
            var cpp = fc[ ipp ];
            var cppp = fc[ ippp ];

            var m = c.length;
            var coords = new Float32Array( m );

            if( type === "spline" ){

                for( var j = 0; j < m; j += 3 ){

                    coords[ j + 0 ] = spline(
                        cppp[ j + 0 ], cpp[ j + 0 ], cp[ j + 0 ], c[ j + 0 ], t, 1
                    );
                    coords[ j + 1 ] = spline(
                        cppp[ j + 1 ], cpp[ j + 1 ], cp[ j + 1 ], c[ j + 1 ], t, 1
                    );
                    coords[ j + 2 ] = spline(
                        cppp[ j + 2 ], cpp[ j + 2 ], cp[ j + 2 ], c[ j + 2 ], t, 1
                    );

                }

            }else{

                for( var j = 0; j < m; j += 3 ){

                    coords[ j + 0 ] = lerp( cp[ j + 0 ], c[ j + 0 ], t );
                    coords[ j + 1 ] = lerp( cp[ j + 1 ], c[ j + 1 ], t );
                    coords[ j + 2 ] = lerp( cp[ j + 2 ], c[ j + 2 ], t );

                }

            }

            this.structure.updatePosition( coords );
            this.currentFrame = i;
            this.signals.frameChanged.dispatch( i );

            if( typeof callback === "function" ){

                callback();

            }

        }

    }(),

    setFrameInterpolated: function( i, ip, ipp, ippp, t, type, callback ){

        if( i === undefined ) return this;

        var fc = this.frameCache;

        var iList = [];

        if( !fc[ ippp ] ) iList.push( ippp );
        if( !fc[ ipp ] ) iList.push( ipp );
        if( !fc[ ip ] ) iList.push( ip );
        if( !fc[ i ] ) iList.push( i );

        if( iList.length ){

            this.loadFrame( iList, function(){

                this.interpolate( i, ip, ipp, ippp, t, type, callback );

            }.bind( this ) );

        }else{

            this.interpolate( i, ip, ipp, ippp, t, type, callback );

        }

        return this;

    },

    loadFrame: function( i, callback ){

        if( Array.isArray( i ) ){

            var queue;
            var fn = function( j, wcallback ){
                this._loadFrame( j, wcallback );
                if( queue.length() === 0 && typeof callback === "function" ) callback();
            }.bind( this );
            queue = new NGL.Queue( fn, i );

        }else{

            this._loadFrame( i, callback );

        }

    },

    _loadFrame: function( i, callback ){

        NGL.error( "Trajectory._loadFrame not implemented" );

    },

    updateStructure: function( i, callback ){

        if( this._disposed ) return;

        if( i === -1 ){

            this.structure.updatePosition( this.initialStructure );

        }else{

            this.structure.updatePosition( this.frameCache[ i ] );

        }

        this.structure.trajectory = {
            name: this.trajPath,
            frame: i
        };

        if( typeof callback === "function" ){

            callback();

        }

        this.currentFrame = i;

        this.inProgress = false;

        this.signals.frameChanged.dispatch( i );

    },

    getCircularMean: function( indices, coords, box ){

        // NGL.time( "NGL.Trajectory.getCircularMean" );

        var mean = [

            NGL.Utils.circularMean( coords, box[ 0 ], 3, 0, indices ),
            NGL.Utils.circularMean( coords, box[ 1 ], 3, 1, indices ),
            NGL.Utils.circularMean( coords, box[ 2 ], 3, 2, indices )

        ];

        // NGL.timeEnd( "NGL.Trajectory.getCircularMean" );

        return mean;

    },

    centerPbc: function( coords, mean, box ){

        // NGL.time( "NGL.Trajectory.centerPbc" );

        if( box[ 0 ]===0 || box[ 8 ]===0 || box[ 4 ]===0 ){
            return;
        }

        var i;
        var n = coords.length;

        var bx = box[ 0 ], by = box[ 1 ], bz = box[ 2 ];
        var mx = mean[ 0 ], my = mean[ 1 ], mz = mean[ 2 ];

        var fx = - mx + bx + bx / 2;
        var fy = - my + by + by / 2;
        var fz = - mz + bz + bz / 2;

        for( i = 0; i < n; i += 3 ){

            coords[ i + 0 ] = ( coords[ i + 0 ] + fx ) % bx;
            coords[ i + 1 ] = ( coords[ i + 1 ] + fy ) % by;
            coords[ i + 2 ] = ( coords[ i + 2 ] + fz ) % bz;

        }

        // NGL.timeEnd( "NGL.Trajectory.centerPbc" );

    },

    removePbc: function( x, box ){

        // NGL.time( "NGL.Trajectory.removePbc" );

        if( box[ 0 ]===0 || box[ 8 ]===0 || box[ 4 ]===0 ){
            return;
        }

        // ported from GROMACS src/gmxlib/rmpbc.c:rm_gropbc()
        // in-place

        var i, j, d, dist;
        var n = x.length;

        for( i = 3; i < n; i += 3 ){

            for( j = 0; j < 3; ++j ){

                dist = x[ i + j ] - x[ i - 3 + j ];

                if( Math.abs( dist ) > 0.9 * box[ j * 3 + j ] ){

                    if( dist > 0 ){

                        for( d = 0; d < 3; ++d ){
                            x[ i + d ] -= box[ j * 3 + d ];
                        }

                    }else{

                        for( d = 0; d < 3; ++d ){
                            x[ i + d ] += box[ j * 3 + d ];
                        }

                    }
                }

            }

        }

        // NGL.timeEnd( "NGL.Trajectory.removePbc" );

        return x;

    },

    superpose: function( x ){

        // NGL.time( "NGL.Trajectory.superpose" );

        var i, j;
        var n = this.indices.length * 3;

        var coords1 = this.coords1;
        var coords2 = this.coords2;

        for( i = 0; i < n; i += 3 ){

            j = this.indices[ i / 3 ] * 3;

            coords1[ i + 0 ] = x[ j + 0 ];
            coords1[ i + 1 ] = x[ j + 1 ];
            coords1[ i + 2 ] = x[ j + 2 ];

        }

        // TODO re-use superposition object
        var sp = new NGL.Superposition( coords1, coords2 );
        sp.transform( x );

        // NGL.timeEnd( "NGL.Trajectory.superpose" );

    },

    process: function( i, box, coords, numframes ){

        this.setNumframes( numframes );

        if( box ){

            if( this.backboneIndices.length > 0 && this.params.centerPbc ){
                var box2 = [ box[ 0 ], box[ 4 ], box[ 8 ] ];
                var mean = this.getCircularMean(
                    this.backboneIndices, coords, box2
                );
                this.centerPbc( coords, mean, box2 );
            }

            if( this.params.removePbc ){
                this.removePbc( coords, box );
            }

        }

        if( this.indices.length > 0 && this.params.superpose ){
            this.superpose( coords );
        }

        this.frameCache[ i ] = coords;
        this.boxCache[ i ] = box;
        this.frameCacheSize += 1;

    },

    setNumframes: function( n ){

        if( n !== this.numframes ){

            this.numframes = n;
            this.signals.gotNumframes.dispatch( n );

        }

    },

    dispose: function(){

        this.frameCache = [];  // aid GC
        this._disposed = true;
        if( this.player ) this.player.stop();

    },

    setPlayer: function( player ){

        this.player = player;
        this.signals.playerChanged.dispatch( player );

    },

    getPath: function( index, callback ){

        NGL.error( "Trajectory.getPath not implemented" );

    },

    download: function( step ){

        // TODO format needs to include the number of atoms
        // TODO lower precision, e.g. 20 bit integers
        // TODO don't process, use raw data

        var scope = this;

        var n = this.numframes;
        var k = step;

        var m = Math.ceil( n / k );
        var u = 0;

        var bbt = new Float32Array( m * ( 9 + 3 * this.atomCount ) );

        function getData( j, v ){

            var l = v * ( 9 + 3 * scope.atomCount );

            bbt.set( scope.boxCache[ j ], l );
            bbt.set( scope.frameCache[ j ], l + 9 );

            if( v === m - 1 ){

                var blob = new Blob(
                    [ bbt ], { type: 'application/octet-binary' }
                );

                NGL.download( blob, "traj.bbt" );

            }

        }

        for( var i = 0; i < n; i += k ){

            this.loadFrame( i, function(){

                getData( i, u );

            } );

            u += 1;

        }

    }

};


NGL.RemoteTrajectory = function( trajPath, structure, selectionString ){

    NGL.Trajectory.call( this, trajPath, structure, selectionString );

};

NGL.RemoteTrajectory.prototype = NGL.createObject(

    NGL.Trajectory.prototype, {

    constructor: NGL.RemoteTrajectory,

    type: "remote",

    makeAtomIndices: function(){

        var structure = this.structure;
        var atomIndices = [];

        if( structure instanceof NGL.StructureSubset ){

            var indices = structure.structure.atomIndex( structure.selection );

            var i, r;
            var p = indices[ 0 ];
            var q = indices[ 0 ];
            var n = indices.length;

            for( i = 1; i < n; ++i ){

                r = indices[ i ];

                if( q + 1 < r ){

                    atomIndices.push( [ p, q + 1 ] );
                    p = r;

                }

                q = r;

            }

            atomIndices.push( [ p, q + 1 ] );

        }else{

            atomIndices.push( [ 0, this.atomCount ] );

        }

        this.atomIndices = atomIndices;

    },

    _loadFrame: function( i, callback ){

        // TODO implement max frameCache size, re-use arrays

        // NGL.time( "NGL.RemoteTrajectory._loadFrame" );

        var request = new XMLHttpRequest();

        var ds = NGL.DatasourceRegistry.trajectory;
        var url = ds.getFrameUrl( this.trajPath, i );
        var params = ds.getFrameParams( this.trajPath, this.atomIndices );

        request.open( "POST", url, true );
        request.responseType = "arraybuffer";
        request.setRequestHeader(
            "Content-type", "application/x-www-form-urlencoded"
        );

        request.addEventListener( 'load', function( event ){

            // NGL.timeEnd( "NGL.RemoteTrajectory._loadFrame" );

            var arrayBuffer = request.response;
            if( !arrayBuffer ){
                NGL.error( "empty arrayBuffer for '" + url + "'" );
                return;
            }

            var numframes = new Int32Array( arrayBuffer, 0, 1 )[ 0 ];
            var time = new Float32Array( arrayBuffer, 1 * 4, 1 )[ 0 ];
            var box = new Float32Array( arrayBuffer, 2 * 4, 9 );
            var coords = new Float32Array( arrayBuffer, 11 * 4 );

            this.process( i, box, coords, numframes );
            if( typeof callback === "function" ){
                callback();
            }

        }.bind( this ), false );

        request.send( params );

    },

    getNumframes: function(){

        var loader = new THREE.XHRLoader();
        var ds = NGL.DatasourceRegistry.trajectory;
        var url = ds.getNumframesUrl( this.trajPath );

        loader.load( url, function( n ){
            this.setNumframes( parseInt( n ) );
        }.bind( this ) );

    },

    getPath: function( index, callback ){

        if( this.pathCache[ index ] ){
            callback( this.pathCache[ index ] );
            return;
        }

        NGL.time( "loadPath" );

        var request = new XMLHttpRequest();

        var ds = NGL.DatasourceRegistry.trajectory;
        var url = ds.getPathUrl( this.trajPath, index );
        var params = "";

        request.open( "POST", url, true );
        request.responseType = "arraybuffer";
        request.setRequestHeader(
            "Content-type", "application/x-www-form-urlencoded"
        );

        request.addEventListener( 'load', function( event ){

            NGL.timeEnd( "loadPath" );

            var arrayBuffer = request.response;
            if( !arrayBuffer ){
                NGL.error( "empty arrayBuffer for '" + url + "'" );
                return;
            }

            var path = new Float32Array( arrayBuffer );
            // NGL.log( path )
            this.pathCache[ index ] = path;
            callback( path );

        }.bind( this ), false );

        request.send( params );

    }

} );


NGL.StructureTrajectory = function( trajPath, structure, selectionString ){

    // if( !trajPath ) trajPath = structure.path;
    trajPath = "";

    NGL.Trajectory.call( this, trajPath, structure, selectionString );

};

NGL.StructureTrajectory.prototype = NGL.createObject(

    NGL.Trajectory.prototype, {

    constructor: NGL.StructureTrajectory,

    type: "structure",

    makeAtomIndices: function(){

        var structure = this.structure;
        var atomSet = structure.atomSet;
        var count = atomSet.size();

        if( count < structure.atomStore.count ){
            var atomIndices = new Int32Array( count );
            atomSet.forEach( function( index, i ){
                atomIndices[ i ] = index;
            } );
            this.atomIndices = atomIndices;
        }else{
            this.atomIndices = null;
        }

    },

    _loadFrame: function( i, callback ){

        var coords;
        var structure = this.structure;
        var frame = this.structure.frames[ i ];

        if( this.atomIndices ){

            var indices = this.atomIndices;
            var m = indices.length;

            coords = new Float32Array( m * 3 );

            for( var j = 0; j < m; ++j ){

                var j3 = j * 3;
                var idx3 = indices[ j ] * 3;

                coords[ j3 + 0 ] = frame[ idx3 + 0 ];
                coords[ j3 + 1 ] = frame[ idx3 + 1 ];
                coords[ j3 + 2 ] = frame[ idx3 + 2 ];

            }

        }else{

            coords = new Float32Array( frame );

        }

        var box = this.structure.boxes[ i ];
        var numframes = this.structure.frames.length;

        this.process( i, box, coords, numframes );

        if( typeof callback === "function" ){

            callback();

        }

    },

    getNumframes: function(){

        this.setNumframes( this.structure.frames.length );

    },

    getPath: function( index, callback ){

        var i, j, f;
        var n = this.numframes;
        var k = index * 3;

        var path = new Float32Array( n * 3 );

        for( i = 0; i < n; ++i ){

            j = 3 * i;
            f = this.structure.frames[ i ];

            path[ j + 0 ] = f[ k + 0 ];
            path[ j + 1 ] = f[ k + 1 ];
            path[ j + 2 ] = f[ k + 2 ];

        }

        callback( path );

    }

} );


NGL.FramesTrajectory = function( frames, structure, selectionString ){

    if( frames instanceof Promise ){

        frames.then( function( _frames ){

            this.setFrames( _frames );
            this.getNumframes();

        }.bind( this ) );

    }else{

        this.setFrames( frames );

    }

    NGL.Trajectory.call( this, "", structure, selectionString );

};

NGL.FramesTrajectory.prototype = NGL.createObject(

    NGL.Trajectory.prototype, {

    constructor: NGL.FramesTrajectory,

    type: "frames",

    setFrames: function( frames ){

        this.name = frames.name;
        this.path = frames.path;

        this.frames = frames.coordinates;
        this.boxes = frames.boxes;

    },

    makeAtomIndices:  function(){

        var structure = this.structure;

        if( structure instanceof NGL.StructureView ){

            this.atomIndices = structure.structure.atomIndex(
                structure.selection
            );

        }else{

            this.atomIndices = null;

        }

    },

    _loadFrame: function( i, callback ){

        var coords;
        var structure = this.structure;
        var frame = this.frames[ i ];

        if( this.atomIndices ){

            var indices = this.atomIndices;
            var m = indices.length;

            coords = new Float32Array( m * 3 );

            for( var j = 0; j < m; ++j ){

                var j3 = j * 3;
                var idx3 = indices[ j ] * 3;

                coords[ j3 + 0 ] = frame[ idx3 + 0 ];
                coords[ j3 + 1 ] = frame[ idx3 + 1 ];
                coords[ j3 + 2 ] = frame[ idx3 + 2 ];

            }

        }else{

            coords = new Float32Array( frame );

        }

        var box = this.boxes[ i ];
        var numframes = this.frames.length;

        this.process( i, box, coords, numframes );

        if( typeof callback === "function" ){

            callback();

        }

    },

    getNumframes: function(){

        if( this.frames ){

            this.setNumframes( this.frames.length );

        }

    },

    getPath: function( index, callback ){

        var i, j, f;
        var n = this.numframes;
        var k = index * 3;

        var path = new Float32Array( n * 3 );

        for( i = 0; i < n; ++i ){

            j = 3 * i;
            f = this.frames[ i ];

            path[ j + 0 ] = f[ k + 0 ];
            path[ j + 1 ] = f[ k + 1 ];
            path[ j + 2 ] = f[ k + 2 ];

        }

        callback( path );

    }

} );


///////////
// Player

NGL.TrajectoryPlayer = function( traj, step, timeout, start, end ){

    var SIGNALS = signals;

    this.signals = {
        startedRunning: new SIGNALS.Signal(),
        haltedRunning: new SIGNALS.Signal()
    };

    traj.signals.playerChanged.add( function( player ){
        if( player !== this ){
            this.pause();
        }
    }, this );

    this.traj = traj;
    this.step = step || Math.ceil( ( traj.numframes + 1 ) / 100 );
    this.timeout = timeout || 50;
    this.start = start || 0;
    this.end = end || traj.numframes - 1;
    this.end = Math.min( this.end, traj.numframes - 1 );
    this.interpolateType = "";
    this.interpolateStep = 5;

    this.mode = "loop"; // loop, once
    this.direction = "forward"; // forward, backward

    this._stopFlag = false;
    this._running = false;

};

NGL.TrajectoryPlayer.prototype = {

    constructor: NGL.TrajectoryPlayer,

    _animate: function(){

        var i;
        this._running = true;

        if( !this.traj.inProgress && !this._stopFlag ){

            if( this.direction === "forward" ){
                i = this.traj.currentFrame + this.step;
            }else{
                i = this.traj.currentFrame - this.step;
            }

            if( i >= this.end || i < this.start ){

                if( this.mode === "once" ){

                    this.pause();

                    if( this.direction === "forward" ){
                        i = this.end;
                    }else{
                        i = this.start;
                    }

                }else{

                    if( this.direction === "forward" ){
                        i = this.start;
                    }else{
                        i = this.end;
                    }

                }

            }

            if( !this.interpolateType ){
                this.traj.setFrame( i );
            }

        }

        if( !this._stopFlag ){

            if( !this.traj.inProgress && this.interpolateType ){

                var ip, ipp, ippp;

                if( this.direction === "forward" ){

                    ip = Math.max( this.start, i - this.step );
                    ipp = Math.max( this.start, i - 2 * this.step );
                    ippp = Math.max( this.start, i - 3 * this.step );

                }else{

                    ip = Math.min( this.end, i + this.step );
                    ipp = Math.min( this.end, i + 2 * this.step );
                    ippp = Math.min( this.end, i + 3 * this.step );

                }

                this._interpolate(
                    i, ip, ipp, ippp, 1 / this.interpolateStep, 0
                );

            }else{

                setTimeout( this._animate.bind( this ), this.timeout );

            }

        }else{

            this._running = false;

        }

    },

    _interpolate: function( i, ip, ipp, ippp, d, t ){

        t += d;

        if( t <= 1 ){

            var deltaTime = Math.round( this.timeout * d );

            this.traj.setFrameInterpolated(
                i, ip, ipp, ippp, t, this.interpolateType,
                function(){
                    setTimeout( function(){
                        this._interpolate( i, ip, ipp, ippp, d, t );
                    }.bind( this ), deltaTime );
                }.bind( this )
            );

        }else{

            setTimeout( this._animate.bind( this ), 0 );

        }

    },

    toggle: function(){

        if( this._running ){
            this.pause();
        }else{
            this.play();
        }

    },

    play: function(){

        if( !this._running ){

            if( this.traj.player !== this ){
                this.traj.setPlayer( this );
            }

            var frame = this.traj.currentFrame;

            // snap to the grid implied by this.step division and multiplication
            // thus minimizing cache misses
            var i = Math.ceil( frame / this.step ) * this.step

            // wrap when restarting from the limit (i.e. end or start)
            if( this.direction === "forward" && frame >= this.end ){

                i = this.start;

            }else if( this.direction === "backward" && frame <= this.start ){

                i = this.end;

            }

            this.traj.setFrame( i );

            this._stopFlag = false;
            this._animate();
            this.signals.startedRunning.dispatch();

        }

    },

    pause: function(){

        if( this._running ){
            this._stopFlag = true;
            this.signals.haltedRunning.dispatch();
        }

    },

    stop: function(){

        this.traj.setFrame( this.start );
        this.pause();

    }

};

// File:js/ngl/surface.js

/**
 * @file Surface
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


////////////
// Surface

NGL.Surface = function( name, path, data ){

    this.name = name;
    this.path = path;
    this.info = {};

    this.center = new THREE.Vector3();
    this.boundingBox = new THREE.Box3();

    if( data instanceof THREE.Geometry ||
        data instanceof THREE.BufferGeometry ||
        data instanceof THREE.Group
    ){

        this.fromGeometry( data );

    }else if( data ){

        this.set(
            data.position,
            data.index,
            data.normal,
            data.color,
            data.atomindex
        );

    }

};

NGL.Surface.prototype = {

    constructor: NGL.Surface,
    type: "Surface",

    set: function( position, index, normal, color, atomindex ){

        this.position = position;
        this.index = index;
        this.normal = normal;
        this.color = color;
        this.atomindex = atomindex;

        this.size = position.length / 3;

    },

    fromGeometry: function( geometry ){

        if( NGL.debug ) NGL.time( "NGL.GeometrySurface.fromGeometry" );

        var geo;

        if( geometry instanceof THREE.Geometry ){
            geometry.computeVertexNormals( true );
            geo = new THREE.BufferGeometry().fromGeometry( geometry );
        }else if( geometry instanceof THREE.BufferGeometry ){
            geo = geometry;
        }else{
            geo = geometry.children[0].geometry;
        }

        if( !geo.boundingBox ) geo.computeBoundingBox();

        this.center.copy( geo.boundingBox.center() );
        this.boundingBox.copy( geo.boundingBox );

        var position, color, index, normal;

        if( geo instanceof THREE.BufferGeometry ){

            var attr = geo.attributes
            var an = attr.normal ? attr.normal.array : false;

            // assume there are no normals if the first is zero
            if( !an || ( an[ 0 ] === 0 && an[ 1 ] === 0 && an[ 2 ] === 0 ) ){
                geo.computeVertexNormals();
            }

            position = attr.position.array;
            index = attr.index ? attr.index.array : null;
            normal = attr.normal.array;

        }

        this.set( position, index, normal, color, undefined );

        if( NGL.debug ) NGL.timeEnd( "NGL.GeometrySurface.setGeometry" );

    },

    getPosition: function(){

        return this.position;

    },

    getColor: function( params ){

        var p = params || {};

        var n = this.size;
        var array;

        if( p.scheme === "volume" ){

            var v = new THREE.Vector3();
            var pos = this.position;
            var colorMaker = NGL.ColorMakerRegistry.getScheme( p );

            array = new Float32Array( n * 3 );

            for( var i = 0, a; i < n; ++i ){

                var i3 = i * 3;
                v.set( pos[ i3 ], pos[ i3 + 1 ], pos[ i3 + 2 ] );
                colorMaker.positionColorToArray( v, array, i3 );

            }

        }else if( this.atomindex ){

            p.surface = this;  // FIXME should this be p.surface???
            array = new Float32Array( n * 3 );
            var colorMaker = NGL.ColorMakerRegistry.getScheme( p );
            var atomProxy = p.structure.getAtomProxy();
            var atomindex = this.atomindex;

            for( var i = 0, a; i < n; ++i ){
                atomProxy.index = atomindex[ i ];
                colorMaker.atomColorToArray( atomProxy, array, i * 3 );
            }

        }else{

            var tc = new THREE.Color( p.value );
            array = NGL.Utils.uniformArray3( n, tc.r, tc.g, tc.b );

        }

        return array;

    },

    getPickingColor: function( params ){

        var p = Object.assign( params || {} );
        p.scheme = "picking";

        return this.getColor( p );

    },

    getNormal: function(){

        return this.normal;

    },

    getSize: function( size ){

        return NGL.Utils.uniformArray( this.size, size );

    },

    getIndex: function(){

        return this.index;

    },

    getFilteredIndex: function( sele, structure ){

        if( sele && this.atomindex ){

            var selection = new NGL.Selection( sele );
            var as = structure.getAtomSet( selection );
            var filteredIndex = [];

            var atomindex = this.atomindex;
            var index = this.index;
            var n = index.length;
            var j = 0;

            var ap1 = structure.getAtomProxy();
            var ap2 = structure.getAtomProxy();
            var ap3 = structure.getAtomProxy();

            for( var i = 0; i < n; i+=3 ){

                var idx1 = index[ i     ];
                var idx2 = index[ i + 1 ];
                var idx3 = index[ i + 2 ];

                var ai1 = atomindex[ idx1 ];
                var ai2 = atomindex[ idx2 ];
                var ai3 = atomindex[ idx3 ];

                if( as.has( ai1 ) && as.has( ai2 ) && as.has( ai3 ) ){
                    filteredIndex[ j     ] = idx1;
                    filteredIndex[ j + 1 ] = idx2;
                    filteredIndex[ j + 2 ] = idx3;
                    j += 3;
                }

            }

            var TypedArray = this.position.length / 3 > 65535 ? Uint32Array : Uint16Array;
            return new TypedArray( filteredIndex );

        }else{

            return this.index;

        }

    },

    getAtomindex: function(){

        return this.atomindex;

    },

    toJSON: function(){

        var output = {

            metadata: {
                version: 0.1,
                type: 'Surface',
                generator: 'SurfaceExporter'
            },

            name: this.name,
            path: this.path,
            info: this.info,

            position: this.position,
            index: this.index,
            normal: this.normal,
            color: this.color,
            atomindex: this.atomindex,

            size: this.size,

            center: this.center.toArray(),
            boundingBox: {
                min: this.boundingBox.min.toArray(),
                max: this.boundingBox.max.toArray()
            }

        }

        return output;

    },

    fromJSON: function( input ){

        this.name = input.name;
        this.path = input.path;
        this.info = input.info;

        this.position = input.position;
        this.index = input.index;
        this.normal = input.normal;
        this.color = input.color;
        this.atomindex = input.atomindex;

        this.size = input.size;

        this.center.fromArray( input.center );
        this.boundingBox.set(
            input.boundingBox.min,
            input.boundingBox.max
        );

        return this;

    },

    getTransferable: function(){

        var transferable = [];

        if( this.position ) transferable.push( this.position.buffer );
        if( this.index ) transferable.push( this.index.buffer );
        if( this.normal ) transferable.push( this.normal.buffer );
        if( this.color ) transferable.push( this.color.buffer );
        if( this.atomindex ) transferable.push( this.atomindex.buffer );

        return transferable;

    },

    dispose: function(){

        //

    }

};


/////////
// Grid

NGL.Grid = function( length, width, height, dataCtor, elemSize ){

    dataCtor = dataCtor || Int32Array;
    elemSize = elemSize || 1;

    var j;

    var data = new dataCtor( length * width * height * elemSize );

    function index( x, y, z ){

        return ( ( ( ( x * width ) + y ) * height ) + z ) * elemSize;

    }

    this.data = data;

    this.index = index;

    this.set = function( x, y, z ){

        var i = index( x, y, z );

        for( j = 0; j < elemSize; ++j ){
            data[ i + j ] = arguments[ 3 + j ];
        }

    };

    this.toArray = function( x, y, z, array, offset ){

        var i = index( x, y, z );

        if ( array === undefined ) array = [];
        if ( offset === undefined ) offset = 0;

        for( j = 0; j < elemSize; ++j ){
            array[ j ] = data[ i + j ];
        }

    };

    this.fromArray = function( x, y, z, array, offset ){

        var i = index( x, y, z );

        if ( offset === undefined ) offset = 0;

        for( j = 0; j < elemSize; ++j ){
            data[ i + j ] = array[ offset + j ];
        }

    };

    this.copy = function( grid ){

        this.data.set( grid.data );

    };

    this.clone = function(){

        return new NGL.Grid(

            length, width, height, dataCtor, elemSize

        ).copy( this );

    };

};


///////////
// Volume

NGL.WorkerRegistry.add( "surf", function( e, callback ){

    if( NGL.debug ) NGL.time( "WORKER surf" );

    if( self.vol === undefined ) self.vol = new NGL.Volume();

    var vol = self.vol;
    var d = e.data;
    var p = d.params;

    if( d.vol ) vol.fromJSON( d.vol );

    if( p ){
        var surface = vol.getSurface(
            p.isolevel, p.smooth, p.center, p.size
        );
    }

    if( NGL.debug ) NGL.timeEnd( "WORKER surf" );

    if( p ){
        callback( surface.toJSON(), surface.getTransferable() );
    }else{
        callback();
    }

} );


NGL.Volume = function( name, path, data, nx, ny, nz, dataAtomindex ){

    this.name = name;
    this.path = path;

    this.matrix = new THREE.Matrix4();
    this.normalMatrix = new THREE.Matrix3();
    this.inverseMatrix = new THREE.Matrix4();
    this.center = new THREE.Vector3();
    this.boundingBox = new THREE.Box3();

    this.setData( data, nx, ny, nz, dataAtomindex );

    if( this.__data.length <= Math.pow( 10, 7 ) ){
        NGL.GidPool.addObject( this );
    }

};

NGL.Volume.prototype = {

    constructor: NGL.Volume,
    type: "Volume",

    setData: function( data, nx, ny, nz, dataAtomindex ){

        this.nx = nx || 1;
        this.ny = ny || 1;
        this.nz = nz || 1;

        this.data = data || new Float32Array( 1 );
        this.__data = this.data;

        this.setDataAtomindex( dataAtomindex );

        delete this.mc;

        delete this.__isolevel;
        delete this.__smooth;
        delete this.__minValue;
        delete this.__maxValue;

        delete this.__dataPositionBuffer;
        delete this.__dataPosition;
        delete this.__dataBuffer;

        delete this.__dataMin;
        delete this.__dataMax;
        delete this.__dataMean;
        delete this.__dataRms;

        if( this.worker ) this.worker.terminate();

        if( this.__data.length <= Math.pow( 10, 7 ) ){
            NGL.GidPool.updateObject( this, true );
        }else{
            NGL.warn( "Volume too large (>10^7), not adding to GidPool" );
            NGL.GidPool.removeObject( this );
        }

    },

    setMatrix: function( matrix ){

        this.matrix.copy( matrix );

        var bb = this.boundingBox;
        var v = this.center;  // temporary re-purposing

        var x = this.nx - 1;
        var y = this.ny - 1;
        var z = this.nz - 1;

        bb.makeEmpty();

        bb.expandByPoint( v.set( x, y, z ) );
        bb.expandByPoint( v.set( x, y, 0 ) );
        bb.expandByPoint( v.set( x, 0, z ) );
        bb.expandByPoint( v.set( x, 0, 0 ) );
        bb.expandByPoint( v.set( 0, y, z ) );
        bb.expandByPoint( v.set( 0, 0, z ) );
        bb.expandByPoint( v.set( 0, y, 0 ) );
        bb.expandByPoint( v.set( 0, 0, 0 ) );

        bb.applyMatrix4( this.matrix );
        bb.center( this.center );

        // make normal matrix

        var me = this.matrix.elements;
        var r0 = new THREE.Vector3( me[0], me[1], me[2] );
        var r1 = new THREE.Vector3( me[4], me[5], me[6] );
        var r2 = new THREE.Vector3( me[8], me[9], me[10] );
        var cp = new THREE.Vector3();
        //        [ r0 ]       [ r1 x r2 ]
        // M3x3 = [ r1 ]   N = [ r2 x r0 ]
        //        [ r2 ]       [ r0 x r1 ]
        var ne = this.normalMatrix.elements;
        cp.crossVectors( r1, r2 );
        ne[ 0 ] = cp.x;
        ne[ 1 ] = cp.y;
        ne[ 2 ] = cp.z;
        cp.crossVectors( r2, r0 );
        ne[ 3 ] = cp.x;
        ne[ 4 ] = cp.y;
        ne[ 5 ] = cp.z;
        cp.crossVectors( r0, r1 );
        ne[ 6 ] = cp.x;
        ne[ 7 ] = cp.y;
        ne[ 8 ] = cp.z;

        this.inverseMatrix.getInverse( this.matrix );

    },

    setDataAtomindex: function( dataAtomindex ){

        this.dataAtomindex = dataAtomindex;
        this.__dataAtomindex = this.dataAtomindex;

        delete this.__dataAtomindexBuffer;

    },

    getBox: function( center, size, target ){

        if( !target ) target = new THREE.Box3();

        target.set( center, center );
        target.expandByScalar( size );
        target.applyMatrix4( this.inverseMatrix );

        target.min.round();
        target.max.round();

        return target;

    },

    getSurface: function( isolevel, smooth, center, size ){

        isolevel = isNaN( isolevel ) ? this.getValueForSigma( 2 ) : isolevel;
        smooth = smooth || 0;
        center = center;
        size = size;

        //

        if( this.mc === undefined ){

            this.mc = new NGL.MarchingCubes2(
                this.__data, this.nx, this.ny, this.nz, this.__dataAtomindex
            );

        }

        var box;

        if( center && size ){

            if( !this.__box ) this.__box = new THREE.Box3();
            box = this.__box;
            this.getBox( center, size, box );

        }

        var sd;

        if( smooth ){

            sd = this.mc.triangulate( isolevel, true, box );
            NGL.laplacianSmooth( sd.position, sd.index, smooth, true );

            var bg = new THREE.BufferGeometry();
            bg.addAttribute( "position", new THREE.BufferAttribute( sd.position, 3 ) );
            bg.setIndex( new THREE.BufferAttribute( sd.index, 1 ) );
            bg.computeVertexNormals();
            sd.normal = bg.attributes.normal.array;
            bg.dispose();

        }else{

            sd = this.mc.triangulate( isolevel, false, box );

        }

        this.matrix.applyToVector3Array( sd.position );

        if( sd.normal ){

            this.normalMatrix.applyToVector3Array( sd.normal );

        }

        var surface = new NGL.Surface( "", "", sd );
        surface.info[ "isolevel" ] = isolevel;
        surface.info[ "smooth" ] = smooth;

        return surface;

    },

    getSurfaceWorker: function( isolevel, smooth, center, size, callback ){

        isolevel = isNaN( isolevel ) ? this.getValueForSigma( 2 ) : isolevel;
        smooth = smooth || 0;

        //

        if( typeof Worker !== "undefined" && typeof importScripts !== 'function' ){

            if( this.workerPool === undefined ){
                this.workerPool = new NGL.WorkerPool( "surf", 2 );
            }

            var worker = this.workerPool.getNextWorker();

            worker.post(

                {
                    vol: worker.postCount === 0 ? this.toJSON() : null,
                    params: {
                        isolevel: isolevel,
                        smooth: smooth,
                        center: center,
                        size: size
                    }
                },

                undefined,

                function( e ){

                    var surface = NGL.fromJSON( e.data );
                    callback( surface );

                },

                function( e ){

                    console.warn(
                        "NGL.Volume.generateSurfaceWorker error - trying without worker", e
                    );

                    var surface = this.getSurface( isolevel, smooth, center, size );
                    callback( surface );

                }.bind( this )

            );

        }else{

            var surface = this.getSurface( isolevel, smooth, center, size );
            callback( surface );

        }

    },

    getValueForSigma: function( sigma ){

        sigma = sigma !== undefined ? sigma : 2;

        return this.getDataMean() + sigma * this.getDataRms();

    },

    getSigmaForValue: function( value ){

        value = value !== undefined ? value : 0;

        return ( value - this.getDataMean() ) / this.getDataRms();

    },

    filterData: function( minValue, maxValue, outside ){

        if( isNaN( minValue ) && this.header ){
            minValue = this.header.DMEAN + 2.0 * this.header.ARMS;
        }

        minValue = ( minValue !== undefined && !isNaN( minValue ) ) ? minValue : -Infinity;
        maxValue = maxValue !== undefined ? maxValue : Infinity;
        outside = outside || false;

        if( !this.dataPosition ){

            this.makeDataPosition();

        }

        var dataPosition = this.__dataPosition;
        var data = this.__data;

        if( minValue === this.__minValue && maxValue == this.__maxValue &&
            outside === this.__outside
        ){

            // already filtered
            return;

        }else if( minValue === -Infinity && maxValue === Infinity ){

            this.dataPosition = dataPosition;
            this.data = data;

        }else{

            var n = data.length;

            if( !this.__dataBuffer ){

                // ArrayBuffer for re-use as Float32Array backend

                this.__dataPositionBuffer = new ArrayBuffer( n * 3 * 4 );
                this.__dataBuffer = new ArrayBuffer( n * 4 );

            }

            var filteredDataPosition = new Float32Array( this.__dataPositionBuffer );
            var filteredData = new Float32Array( this.__dataBuffer );

            var j = 0;

            for( var i = 0; i < n; ++i ){

                var i3 = i * 3;
                var v = data[ i ];

                if( ( !outside && v >= minValue && v <= maxValue ) ||
                    ( outside && ( v < minValue || v > maxValue ) )
                ){

                    var j3 = j * 3;

                    filteredDataPosition[ j3 + 0 ] = dataPosition[ i3 + 0 ];
                    filteredDataPosition[ j3 + 1 ] = dataPosition[ i3 + 1 ];
                    filteredDataPosition[ j3 + 2 ] = dataPosition[ i3 + 2 ];

                    filteredData[ j ] = v;

                    j += 1;

                }

            }

            // set views

            this.dataPosition = new Float32Array( this.__dataPositionBuffer, 0, j * 3 );
            this.data = new Float32Array( this.__dataBuffer, 0, j );

        }

        this.__minValue = minValue;
        this.__maxValue = maxValue;
        this.__outside = outside;

    },

    makeDataPosition: function(){

        var nz = this.nz;
        var ny = this.ny;
        var nx = this.nx;

        var position = new Float32Array( nx * ny * nz * 3 );

        var p = 0;

        for( var z = 0; z < nz; ++z ){

            for( var y = 0; y < ny; ++y ){

                for( var x = 0; x < nx; ++x ){

                    position[ p + 0 ] = x;
                    position[ p + 1 ] = y;
                    position[ p + 2 ] = z;

                    p += 3;

                }

            }

        }

        this.matrix.applyToVector3Array( position );

        this.dataPosition = position;
        this.__dataPosition = position;

    },

    getDataAtomindex: function(){

        return this.dataAtomindex;

    },

    getDataPosition: function(){

        return this.dataPosition;

    },

    getDataColor: function( params ){

        var p = params || {};
        p.volume = this;
        p.scale = p.scale || 'Spectral';
        p.domain = p.domain || [ this.getDataMin(), this.getDataMax() ];

        var colorMaker = NGL.ColorMakerRegistry.getScheme( p );

        var n = this.dataPosition.length / 3;
        var array = new Float32Array( n * 3 );

        // var atoms = p.structure.atoms;
        // var atomindex = this.dataAtomindex;

        for( var i = 0; i < n; ++i ){

            colorMaker.volumeColorToArray( i, array, i * 3 );

            // a = atoms[ atomindex[ i ] ];
            // if( a ) colorMaker.atomColorToArray( a, array, i * 3 );

        }

        return array;

    },

    getPickingDataColor: function( params ){

        var p = Object.assign( params || {} );
        p.scheme = "picking";

        return this.getDataColor( p );

    },

    getDataSize: function( size, scale ){

        var n = this.dataPosition.length / 3;
        var array;

        switch( size ){

            case "value":

                array = new Float32Array( this.data );
                break;

            case "abs-value":

                array = new Float32Array( this.data );
                for( var i = 0; i < n; ++i ){
                    array[ i ] = Math.abs( array[ i ] );
                }
                break;

            case "value-min":

                array = new Float32Array( this.data );
                var min = this.getDataMin();
                for( var i = 0; i < n; ++i ){
                    array[ i ] -= min;
                }
                break;

            case "deviation":

                array = new Float32Array( this.data );
                break;

            default:

                array = NGL.Utils.uniformArray( n, size );
                break;

        }

        if( scale !== 1.0 ){

            for( var i = 0; i < n; ++i ){
                array[ i ] *= scale;
            }

        }

        return array;

    },

    getDataMin: function(){

        if( this.__dataMin === undefined ){

            var data = this.__data;
            var n = data.length;
            var min = Infinity;

            for( var i = 0; i < n; ++i ){
                min = Math.min( min, data[ i ] );
            }

            this.__dataMin = min;

        }

        return this.__dataMin;

    },

    getDataMax: function(){

        if( this.__dataMax === undefined ){

            var data = this.__data;
            var n = data.length;
            var max = -Infinity;

            for( var i = 0; i < n; ++i ){
                max = Math.max( max, data[ i ] );
            }

            this.__dataMax = max;

        }

        return this.__dataMax;

    },

    getDataMean: function(){

        if( this.__dataMean === undefined ){

            var data = this.__data;
            var n = data.length;
            var sum = 0;

            for( var i = 0; i < n; ++i ){
                sum += data[ i ];
            }

            this.__dataMean = sum / n;

        }

        return this.__dataMean;

    },

    getDataRms: function(){

        if( this.__dataRms === undefined ){

            var data = this.__data;
            var n = data.length;
            var sumSq = 0;
            var di, i;

            for( i = 0; i < n; ++i ){
                di = data[ i ];
                sumSq += di * di;
            }

            this.__dataRms = Math.sqrt( sumSq / n );

        }

        return this.__dataRms;

    },

    clone: function(){

        var vol = new NGL.Volume(

            this.name,
            this.path,

            this.__data,

            this.nx,
            this.ny,
            this.nz,

            this.__dataAtomindex

        );

        vol.matrix.copy( this.matrix );

        if( this.header ){

            vol.header = Object.assign( {}, this.header );

        }

        return vol;

    },

    toJSON: function(){

        var output = {

            metadata: {
                version: 0.1,
                type: 'Volume',
                generator: 'VolumeExporter'
            },

            name: this.name,
            path: this.path,

            data: this.__data,

            nx: this.nx,
            ny: this.ny,
            nz: this.nz,

            dataAtomindex: this.__dataAtomindex,

            matrix: this.matrix.toArray(),
            normalMatrix: this.normalMatrix.toArray(),
            inverseMatrix: this.inverseMatrix.toArray(),

            center: this.center.toArray(),
            boundingBox: {
                min: this.boundingBox.min.toArray(),
                max: this.boundingBox.max.toArray()
            }

        }

        if( this.header ){

            output.header = Object.assign( {}, this.header );

        }

        return output;

    },

    fromJSON: function( input ){

        this.name = input.name;
        this.path = input.path;

        this.setData(

            input.data,

            input.nx,
            input.ny,
            input.nz,

            input.dataAtomindex

        );

        this.matrix.fromArray( input.matrix );
        this.normalMatrix.fromArray( input.normalMatrix );
        this.inverseMatrix.fromArray( input.inverseMatrix );

        if( input.header ){

            this.header = Object.assign( {}, input.header );

        }

        this.center.fromArray( input.center );
        this.boundingBox.set(
            input.boundingBox.min,
            input.boundingBox.max
        );

        return this;

    },

    getTransferable: function(){

        var transferable = [

            this.__data.buffer

        ];

        if( this.__dataAtomindex ){
            transferable.push( this.__dataAtomindex.buffer );
        }

        return transferable;

    },

    dispose: function(){

        if( this.workerPool ) this.workerPool.terminate();

        NGL.GidPool.removeObject( this );

    }

};


///////////////////
// Marching cubes

NGL.MarchingCubes = function( data, nx, ny, nz, isolevel ){

    // The MIT License (MIT) Copyright (c) 2012-2013 Mikola Lysenko
    // http://0fps.net/2012/07/12/smooth-voxel-terrain-part-2/
    //
    // Based on Paul Bourke's classic implementation:
    // http://paulbourke.net/geometry/polygonise/
    // JS port by Mikola Lysenko
    //
    // Adapted for NGL by Alexander Rose

    if( NGL.debug ) NGL.time( "NGL.MarchingCubes" );

    var dims = new Int32Array( [ nx, ny, nz ] );

    var edgeTable = NGL.MarchingCubes.edgeTable;
    var triTable = NGL.MarchingCubes.triTable;
    var cubeVerts = NGL.MarchingCubes.cubeVerts;
    var edgeIndex = NGL.MarchingCubes.edgeIndex;

    var vertices = [];
    var faces = [];
    var vc3 = 0;  // vertexCount * 3
    var fc3 = 0;  // faceCount * 3

    var n = 0;
    var grid = new Float32Array( 8 );
    var edges = new Int32Array( 12 );
    var x = new Int32Array( 3 );

    // March over the volume

    for( x[2]=0; x[2] < dims[2]-1; ++x[2], n+=dims[0] ){

        for( x[1]=0; x[1] < dims[1]-1; ++x[1], ++n){

            for( x[0]=0; x[0] < dims[0]-1; ++x[0], ++n) {

                // For each cell, compute cube mask

                var cubeIndex = 0;

                for( var i=0; i<8; ++i ){

                    var v = cubeVerts[ i ]
                    var k = n + v[0] + dims[0] * ( v[1] + dims[1] * v[2] );
                    var s = data[ k ] - isolevel;

                    grid[ i ] = s;
                    cubeIndex |= ( s > 0 ) ? 1 << i : 0;

                }

                // Compute vertices

                var edgeMask = edgeTable[ cubeIndex ];

                if( edgeMask === 0 ) {
                    continue;
                }

                for( var i=0; i<12; ++i ){

                    if( ( edgeMask & ( 1 << i ) ) === 0 ){
                        continue;
                    }

                    edges[ i ] = vc3 / 3;

                    var e = edgeIndex[ i ];
                    var p0 = cubeVerts[ e[ 0 ] ];
                    var p1 = cubeVerts[ e[ 1 ] ];
                    var a = grid[ e[ 0 ] ];
                    var b = grid[ e[ 1 ] ];
                    var d = a - b;
                    var t = 0;

                    if( Math.abs( d ) > 1e-6 ){
                        t = a / d;
                    }

                    vertices[ vc3 + 0 ] = ( x[0] + p0[0] ) + t * ( p1[0] - p0[0] );
                    vertices[ vc3 + 1 ] = ( x[1] + p0[1] ) + t * ( p1[1] - p0[1] );
                    vertices[ vc3 + 2 ] = ( x[2] + p0[2] ) + t * ( p1[2] - p0[2] );

                    vc3 += 3;

                }

                // Add faces

                var f = triTable[ cubeIndex ];

                for( var i=0; i<f.length; i += 3 ){

                    faces[ fc3 + 0 ] = edges[ f[ i + 0 ] ];
                    faces[ fc3 + 1 ] = edges[ f[ i + 1 ] ];
                    faces[ fc3 + 2 ] = edges[ f[ i + 2 ] ];

                    fc3 += 3;

                }

            }

        }

    }

    if( NGL.debug ) NGL.timeEnd( "NGL.MarchingCubes" );

    var TypedArray = vertices.length / 3 > 65535 ? Uint32Array : Uint16Array;
    return {
        position: new Float32Array( vertices ),
        normal: undefined,
        index: new TypedArray( faces )
    };

};

NGL.MarchingCubes2 = function( field, nx, ny, nz, atomindex ){

    // Based on alteredq / http://alteredqualia.com/
    // port of greggman's ThreeD version of marching cubes to Three.js
    // http://webglsamples.googlecode.com/hg/blob/blob.html
    //
    // Adapted for NGL by Alexander Rose

    var edgeTable = NGL.MarchingCubes.edgeTable;
    var triTable = NGL.MarchingCubes.triTable;

    var isolevel = 0;
    var noNormals = false;
    var center = undefined;
    var size = Infinity;

    var n = nx * ny * nz;

    // deltas
    var yd = nx;
    var zd = nx * ny;

    var normalCache, vertexIndex;
    var count, icount;

    var ilist = new Int32Array( 12 );

    var positionArray = [];
    var normalArray = [];
    var indexArray = [];
    var atomindexArray = [];

    //

    this.triangulate = function( _isolevel, _noNormals, _box ){

        if( NGL.debug ) NGL.time( "NGL.MarchingCubes2.triangulate" );

        isolevel = _isolevel;
        noNormals = _noNormals;

        if( !noNormals && !normalCache ){
            normalCache = new Float32Array( n * 3 );
        }

        if( !vertexIndex ){
            vertexIndex = new Int32Array( n );
        }

        count = 0;
        icount = 0;

        if( _box !== undefined ){

            _box.min.round();
            _box.max.round();
            triangulate(
                _box.min.x, _box.min.y, _box.min.z,
                _box.max.x, _box.max.y, _box.max.z
            );

        }else{

            triangulate();

        }

        positionArray.length = count * 3;
        if( !noNormals ) normalArray.length = count * 3;
        indexArray.length = icount;
        if( atomindex ) atomindexArray.length = count;

        if( NGL.debug ) NGL.timeEnd( "NGL.MarchingCubes2.triangulate" );

        var TypedArray = positionArray.length / 3 > 65535 ? Uint32Array : Uint16Array;
        return {
            position: new Float32Array( positionArray ),
            normal: noNormals ? undefined : new Float32Array( normalArray ),
            index: new TypedArray( indexArray ),
            atomindex: atomindex ? new Int32Array( atomindexArray ) : undefined,
        };

    }

    // polygonization

    function lerp( a, b, t ) { return a + ( b - a ) * t; }

    function VIntX( q, offset, x, y, z, valp1, valp2 ) {

        if( vertexIndex[ q ] < 0 ){

            var mu = ( isolevel - valp1 ) / ( valp2 - valp1 );
            var nc = normalCache;

            var c = count * 3;

            positionArray[ c + 0 ] = x + mu;
            positionArray[ c + 1 ] = y;
            positionArray[ c + 2 ] = z;

            if( !noNormals ){

                var q3 = q * 3;

                normalArray[ c ]     = -lerp( nc[ q3 ],     nc[ q3 + 3 ], mu );
                normalArray[ c + 1 ] = -lerp( nc[ q3 + 1 ], nc[ q3 + 4 ], mu );
                normalArray[ c + 2 ] = -lerp( nc[ q3 + 2 ], nc[ q3 + 5 ], mu );

            }

            if( atomindex ) atomindexArray[ count ] = atomindex[ q + mu ];

            vertexIndex[ q ] = count;
            ilist[ offset ] = count;

            count += 1;

        }else{

            ilist[ offset ] = vertexIndex[ q ];

        }

    }

    function VIntY( q, offset, x, y, z, valp1, valp2 ) {

        if( vertexIndex[ q ] < 0 ){

            var mu = ( isolevel - valp1 ) / ( valp2 - valp1 );
            var nc = normalCache;

            var c = count * 3;

            positionArray[ c ]     = x;
            positionArray[ c + 1 ] = y + mu;
            positionArray[ c + 2 ] = z;

            if( !noNormals ){

                var q3 = q * 3;
                var q6 = q3 + yd * 3;

                normalArray[ c ]     = -lerp( nc[ q3 ],     nc[ q6 ],     mu );
                normalArray[ c + 1 ] = -lerp( nc[ q3 + 1 ], nc[ q6 + 1 ], mu );
                normalArray[ c + 2 ] = -lerp( nc[ q3 + 2 ], nc[ q6 + 2 ], mu );

            }

            if( atomindex ) atomindexArray[ count ] = atomindex[ q + mu * yd ];

            vertexIndex[ q ] = count;
            ilist[ offset ] = count;

            count += 1;

        }else{

            ilist[ offset ] = vertexIndex[ q ];

        }

    }

    function VIntZ( q, offset, x, y, z, valp1, valp2 ) {

        if( vertexIndex[ q ] < 0 ){

            var mu = ( isolevel - valp1 ) / ( valp2 - valp1 );
            var nc = normalCache;

            var c = count * 3;

            positionArray[ c ]     = x;
            positionArray[ c + 1 ] = y;
            positionArray[ c + 2 ] = z + mu;

            if( !noNormals ){

                var q3 = q * 3;
                var q6 = q3 + zd * 3;

                normalArray[ c ]     = -lerp( nc[ q3 ],     nc[ q6 ],     mu );
                normalArray[ c + 1 ] = -lerp( nc[ q3 + 1 ], nc[ q6 + 1 ], mu );
                normalArray[ c + 2 ] = -lerp( nc[ q3 + 2 ], nc[ q6 + 2 ], mu );

            }

            if( atomindex ) atomindexArray[ count ] = atomindex[ q + mu * zd ];

            vertexIndex[ q ] = count;
            ilist[ offset ] = count;

            count += 1;

        }else{

            ilist[ offset ] = vertexIndex[ q ];

        }

    }

    function compNorm( q ) {

        var q3 = q * 3;

        if ( normalCache[ q3 ] === 0.0 ) {

            normalCache[ q3     ] = field[ q - 1  ] - field[ q + 1 ];
            normalCache[ q3 + 1 ] = field[ q - yd ] - field[ q + yd ];
            normalCache[ q3 + 2 ] = field[ q - zd ] - field[ q + zd ];

        }

    }

    function polygonize( fx, fy, fz, q ) {

        // cache indices
        var q1 = q + 1,
            qy = q + yd,
            qz = q + zd,
            q1y = q1 + yd,
            q1z = q1 + zd,
            qyz = q + yd + zd,
            q1yz = q1 + yd + zd;

        var cubeindex = 0,
            field0 = field[ q ],
            field1 = field[ q1 ],
            field2 = field[ qy ],
            field3 = field[ q1y ],
            field4 = field[ qz ],
            field5 = field[ q1z ],
            field6 = field[ qyz ],
            field7 = field[ q1yz ];

        if ( field0 < isolevel ) cubeindex |= 1;
        if ( field1 < isolevel ) cubeindex |= 2;
        if ( field2 < isolevel ) cubeindex |= 8;
        if ( field3 < isolevel ) cubeindex |= 4;
        if ( field4 < isolevel ) cubeindex |= 16;
        if ( field5 < isolevel ) cubeindex |= 32;
        if ( field6 < isolevel ) cubeindex |= 128;
        if ( field7 < isolevel ) cubeindex |= 64;

        // if cube is entirely in/out of the surface - bail, nothing to draw

        var bits = edgeTable[ cubeindex ];
        if ( bits === 0 ) return 0;

        var fx2 = fx + 1,
            fy2 = fy + 1,
            fz2 = fz + 1;

        // top of the cube

        if ( bits & 1 ) {

            if( !noNormals ){
                compNorm( q );
                compNorm( q1 );
            }
            VIntX( q, 0, fx, fy, fz, field0, field1 );

        };

        if ( bits & 2 ) {

            if( !noNormals ){
                compNorm( q1 );
                compNorm( q1y );
            }
            VIntY( q1, 1, fx2, fy, fz, field1, field3 );

        };

        if ( bits & 4 ) {

            if( !noNormals ){
                compNorm( qy );
                compNorm( q1y );
            }
            VIntX( qy, 2, fx, fy2, fz, field2, field3 );

        };

        if ( bits & 8 ) {

            if( !noNormals ){
                compNorm( q );
                compNorm( qy );
            }
            VIntY( q, 3, fx, fy, fz, field0, field2 );

        };

        // bottom of the cube

        if ( bits & 16 ) {

            if( !noNormals ){
                compNorm( qz );
                compNorm( q1z );
            }
            VIntX( qz, 4, fx, fy, fz2, field4, field5 );

        };

        if ( bits & 32 ) {

            if( !noNormals ){
                compNorm( q1z );
                compNorm( q1yz );
            }
            VIntY( q1z, 5, fx2, fy, fz2, field5, field7 );

        };

        if ( bits & 64 ) {

            if( !noNormals ){
                compNorm( qyz );
                compNorm( q1yz );
            }
            VIntX( qyz, 6, fx, fy2, fz2, field6, field7 );

        };

        if ( bits & 128 ) {

            if( !noNormals ){
                compNorm( qz );
                compNorm( qyz );
            }
            VIntY( qz, 7, fx, fy, fz2, field4, field6 );

        };

        // vertical lines of the cube

        if ( bits & 256 ) {

            if( !noNormals ){
                compNorm( q );
                compNorm( qz );
            }
            VIntZ( q, 8, fx, fy, fz, field0, field4 );

        };

        if ( bits & 512 ) {

            if( !noNormals ){
                compNorm( q1 );
                compNorm( q1z );
            }
            VIntZ( q1, 9, fx2, fy, fz, field1, field5 );

        };

        if ( bits & 1024 ) {

            if( !noNormals ){
                compNorm( q1y );
                compNorm( q1yz );
            }
            VIntZ( q1y, 10, fx2, fy2, fz, field3, field7 );

        };

        if ( bits & 2048 ) {

            if( !noNormals ){
                compNorm( qy );
                compNorm( qyz );
            }
            VIntZ( qy, 11, fx, fy2, fz, field2, field6 );

        };

        cubeindex <<= 4;  // re-purpose cubeindex into an offset into triTable

        var o1, o2, o3, i = 0;

        // here is where triangles are created

        while ( triTable[ cubeindex + i ] != -1 ) {

            o1 = cubeindex + i;
            o2 = o1 + 1;
            o3 = o1 + 2;

            // FIXME normals flipping (see above) and vertex order reversal
            indexArray[ icount ]     = ilist[ triTable[ o2 ] ];
            indexArray[ icount + 1 ] = ilist[ triTable[ o1 ] ];
            indexArray[ icount + 2 ] = ilist[ triTable[ o3 ] ];

            icount += 3;
            i += 3;

        }

    }

    function triangulate( xBeg, yBeg, zBeg, xEnd, yEnd, zEnd ) {

        var q, x, y, z, fx, fy, fz, y_offset, z_offset

        xBeg = xBeg !== undefined ? xBeg : 0;
        yBeg = yBeg !== undefined ? yBeg : 0;
        zBeg = zBeg !== undefined ? zBeg : 0;

        xEnd = xEnd !== undefined ? xEnd : nx - 1;
        yEnd = yEnd !== undefined ? yEnd : ny - 1;
        zEnd = zEnd !== undefined ? zEnd : nz - 1;

        if( noNormals ){

            xBeg = Math.max( 0, xBeg );
            yBeg = Math.max( 0, yBeg );
            zBeg = Math.max( 0, zBeg );

            xEnd = Math.min( nx - 1, xEnd );
            yEnd = Math.min( ny - 1, yEnd );
            zEnd = Math.min( nz - 1, zEnd );

        }else{

            xBeg = Math.max( 1, xBeg );
            yBeg = Math.max( 1, yBeg );
            zBeg = Math.max( 1, zBeg );

            xEnd = Math.min( nx - 2, xEnd );
            yEnd = Math.min( ny - 2, yEnd );
            zEnd = Math.min( nz - 2, zEnd );

        }

        // init part of the vertexIndex
        // (takes a significant amount of time to do for all)

        var xBeg2 = Math.max( 0, xBeg - 2);
        var yBeg2 = Math.max( 0, yBeg - 2 );
        var zBeg2 = Math.max( 0, zBeg - 2 );

        var xEnd2 = Math.min( nx, xEnd + 2 );
        var yEnd2 = Math.min( ny, yEnd + 2 );
        var zEnd2 = Math.min( nz, zEnd + 2 );

        for ( z = zBeg2; z < zEnd2; ++z ) {
            z_offset = zd * z;
            for ( y = yBeg2; y < yEnd2; ++y ) {
                y_offset = z_offset + yd * y;
                for ( x = xBeg2; x < xEnd2; ++x ) {
                    q = y_offset + x;
                    vertexIndex[ q ] = -1;
                }
            }
        }

        // clip space where the isovalue is too low

        var __break;
        var __xBeg = xBeg; var __yBeg = yBeg; var __zBeg = zBeg;
        var __xEnd = xEnd; var __yEnd = yEnd; var __zEnd = zEnd;

        __break = false;
        for ( z = zBeg; z < zEnd; ++z ) {
            for ( y = yBeg; y < yEnd; ++y ) {
                for ( x = xBeg; x < xEnd; ++x ) {
                    q = ( ( nx * ny ) * z ) + ( nx * y ) + x;
                    if( field[ q ] >= isolevel ){
                        __zBeg = z;
                        __break = true;
                        break;
                    }
                }
                if( __break ) break;
            }
            if( __break ) break;
        }

        __break = false;
        for ( y = yBeg; y < yEnd; ++y ) {
            for ( z = __zBeg; z < zEnd; ++z ) {
                for ( x = xBeg; x < xEnd; ++x ) {
                    q = ( ( nx * ny ) * z ) + ( nx * y ) + x;
                    if( field[ q ] >= isolevel ){
                        __yBeg = y;
                        __break = true;
                        break;
                    }
                }
                if( __break ) break;
            }
            if( __break ) break;
        }

        __break = false;
        for ( x = xBeg; x < xEnd; ++x ) {
            for ( y = __yBeg; y < yEnd; ++y ) {
                for ( z = __zBeg; z < zEnd; ++z ) {
                    q = ( ( nx * ny ) * z ) + ( nx * y ) + x;
                    if( field[ q ] >= isolevel ){
                        __xBeg = x;
                        __break = true;
                        break;
                    }
                }
                if( __break ) break;
            }
            if( __break ) break;
        }

        __break = false;
        for ( z = zEnd; z >= zBeg; --z ) {
            for ( y = yEnd; y >= yBeg; --y ) {
                for ( x = xEnd; x >= xBeg; --x ) {
                    q = ( ( nx * ny ) * z ) + ( nx * y ) + x;
                    if( field[ q ] >= isolevel ){
                        __zEnd = z;
                        __break = true;
                        break;
                    }
                }
                if( __break ) break;
            }
            if( __break ) break;
        }

        __break = false;
        for ( y = yEnd; y >= yBeg; --y ) {
            for ( z = __zEnd; z >= zBeg; --z ) {
                for ( x = xEnd; x >= xBeg; --x ) {
                    q = ( ( nx * ny ) * z ) + ( nx * y ) + x;
                    if( field[ q ] >= isolevel ){
                        __yEnd = y;
                        __break = true;
                        break;
                    }
                }
                if( __break ) break;
            }
            if( __break ) break;
        }

        __break = false;
        for ( x = xEnd; x >= xBeg; --x ) {
            for ( y = __yEnd; y >= yBeg; --y ) {
                for ( z = __zEnd; z >= zBeg; --z ) {
                    q = ( ( nx * ny ) * z ) + ( nx * y ) + x;
                    if( field[ q ] >= isolevel ){
                        __xEnd = x;
                        __break = true;
                        break;
                    }
                }
                if( __break ) break;
            }
            if( __break ) break;
        }

        //

        if( noNormals ){

            xBeg = Math.max( 0, __xBeg - 1);
            yBeg = Math.max( 0, __yBeg - 1 );
            zBeg = Math.max( 0, __zBeg - 1 );

            xEnd = Math.min( nx - 1, __xEnd + 1 );
            yEnd = Math.min( ny - 1, __yEnd + 1 );
            zEnd = Math.min( nz - 1, __zEnd + 1 );

        }else{

            xBeg = Math.max( 1, __xBeg - 1 );
            yBeg = Math.max( 1, __yBeg - 1 );
            zBeg = Math.max( 1, __zBeg - 1 );

            xEnd = Math.min( nx - 2, __xEnd + 1 );
            yEnd = Math.min( ny - 2, __yEnd + 1 );
            zEnd = Math.min( nz - 2, __zEnd + 1 );

        }

        // polygonize part of the grid

        for ( z = zBeg; z < zEnd; ++z ) {
            z_offset = zd * z;
            for ( y = yBeg; y < yEnd; ++y ) {
                y_offset = z_offset + yd * y;
                for ( x = xBeg; x < xEnd; ++x ) {
                    q = y_offset + x;
                    polygonize( x, y, z, q );
                }
            }
        }

    }

};

NGL.MarchingCubes.edgeTable = new Uint32Array( [
    0x0  , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
    0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
    0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
    0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
    0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c,
    0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
    0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac,
    0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
    0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c,
    0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
    0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc,
    0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
    0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c,
    0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
    0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc ,
    0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
    0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
    0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
    0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
    0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
    0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
    0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
    0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
    0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460,
    0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
    0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0,
    0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
    0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230,
    0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
    0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
    0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
    0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0
] );

NGL.MarchingCubes.triTable = new Int32Array( [
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1,
    3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1,
    3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1,
    3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1,
    9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1,
    9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1,
    2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1,
    8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1,
    9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1,
    4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1,
    3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1,
    1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1,
    4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1,
    4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1,
    9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1,
    5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1,
    2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1,
    9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1,
    0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1,
    2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1,
    10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1,
    4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1,
    5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1,
    5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1,
    9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1,
    0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1,
    1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1,
    10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1,
    8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1,
    2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1,
    7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1,
    9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1,
    2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1,
    11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1,
    9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1,
    5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1,
    11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1,
    11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1,
    1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1,
    9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1,
    5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1,
    2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1,
    0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1,
    5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1,
    6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1,
    3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1,
    6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1,
    5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1,
    1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1,
    10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1,
    6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1,
    8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1,
    7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1,
    3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1,
    5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1,
    0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1,
    9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1,
    8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1,
    5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1,
    0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1,
    6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1,
    10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1,
    10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1,
    8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1,
    1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1,
    3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1,
    0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1,
    10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1,
    3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1,
    6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1,
    9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1,
    8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1,
    3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1,
    6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1,
    0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1,
    10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1,
    10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1,
    2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1,
    7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1,
    7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1,
    2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1,
    1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1,
    11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1,
    8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1,
    0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1,
    7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1,
    10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1,
    2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1,
    6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1,
    7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1,
    2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1,
    1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1,
    10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1,
    10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1,
    0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1,
    7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1,
    6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1,
    8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1,
    9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1,
    6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1,
    4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1,
    10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1,
    8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1,
    0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1,
    1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1,
    8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1,
    10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1,
    4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1,
    10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1,
    5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1,
    11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1,
    9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1,
    6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1,
    7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1,
    3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1,
    7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1,
    9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1,
    3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1,
    6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1,
    9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1,
    1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1,
    4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1,
    7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1,
    6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1,
    3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1,
    0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1,
    6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1,
    0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1,
    11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1,
    6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1,
    5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1,
    9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1,
    1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1,
    1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1,
    10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1,
    0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1,
    5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1,
    10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1,
    11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1,
    9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1,
    7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1,
    2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1,
    8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1,
    9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1,
    9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1,
    1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1,
    9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1,
    9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1,
    5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1,
    0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1,
    10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1,
    2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1,
    0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1,
    0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1,
    9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1,
    5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1,
    3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1,
    5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1,
    8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1,
    0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1,
    9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1,
    0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1,
    1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1,
    3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1,
    4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1,
    9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1,
    11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1,
    11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1,
    2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1,
    9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1,
    3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1,
    1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1,
    4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1,
    4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1,
    0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1,
    3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1,
    3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1,
    0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1,
    9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1,
    1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
] );

NGL.MarchingCubes.cubeVerts = [
    [0,0,0],
    [1,0,0],
    [1,1,0],
    [0,1,0],
    [0,0,1],
    [1,0,1],
    [1,1,1],
    [0,1,1]
];

NGL.MarchingCubes.edgeIndex = [
    [0,1], [1,2], [2,3], [3,0], [4,5], [5,6],
    [6,7], [7,4], [0,4], [1,5], [2,6], [3,7]
];


//////////////
// Smoothing

NGL.laplacianSmooth = function( verts, faces, numiter, inflate ){

    // based on D. Xu, Y. Zhang (2009) Generating Triangulated Macromolecular
    // Surfaces by Euclidean Distance Transform. PLoS ONE 4(12): e8140.
    //
    // Permission to use, copy, modify, and distribute this program for
    // any purpose, with or without fee, is hereby granted, provided that
    // the notices on the head, the reference information, and this
    // copyright notice appear in all copies or substantial portions of
    // the Software. It is provided "as is" without express or implied
    // warranty.
    //
    // ported to JavaScript and adapted to NGL by Alexander Rose

    if( NGL.debug ) NGL.time( "NGL.laplacianSmooth" );

    numiter = numiter || 1;
    inflate = inflate || true;

    var nv = verts.length / 3;
    var nf = faces.length / 3;

    if( inflate ){

        // Buffer geometry is only used to calculate normals

        var bg = new THREE.BufferGeometry();
        bg.addAttribute( "position", new THREE.BufferAttribute( verts, 3 ) );
        bg.setIndex( new THREE.BufferAttribute( faces, 1 ) );

    }

    var tps = new Float32Array( nv * 3 );

    var ndeg = 20;
    var vertdeg = new Array( ndeg );

    for( var i = 0; i < ndeg; ++i ){
        vertdeg[ i ] = new Uint32Array( nv );
    }

    for( var i = 0; i < nv; ++i ){
        vertdeg[ 0 ][ i ] = 0;
    }

    var j, jl;
    var flagvert;

    // for each face

    for( var i = 0; i < nf; ++i ){

        var ao = i * 3;
        var bo = i * 3 + 1;
        var co = i * 3 + 2;

        // vertex a

        flagvert = true;
        for( j = 0, jl = vertdeg[ 0 ][ faces[ao] ]; j < jl; ++j ){
            if( faces[ bo ] == vertdeg[ j + 1 ][ faces[ ao ]] ){
                flagvert = false;
                break;
            }
        }
        if( flagvert ){
            vertdeg[ 0 ][ faces[ ao ] ]++;
            vertdeg[ vertdeg[ 0 ][ faces[ ao ] ] ][ faces[ ao ] ] = faces[ bo ];
        }

        flagvert = true;
        for( j = 0, jl = vertdeg[ 0 ][ faces[ ao ] ]; j < jl; ++j ){
            if( faces[ co] == vertdeg[ j + 1 ][ faces[ ao ] ] ){
                flagvert = false;
                break;
            }
        }
        if( flagvert ){
            vertdeg[ 0 ][ faces[ ao ] ]++;
            vertdeg[ vertdeg[ 0 ][ faces[ ao ] ] ][ faces[ ao ] ] = faces[ co ];
        }

        // vertex b

        flagvert = true;
        for( j = 0, jl = vertdeg[ 0 ][ faces[ bo ] ]; j < jl; ++j ){
            if( faces[ ao ] == vertdeg[ j + 1 ][ faces[ bo ] ] ){
                flagvert = false;
                break;
            }
        }
        if( flagvert ){
            vertdeg[ 0 ][ faces[ bo ] ]++;
            vertdeg[ vertdeg[ 0 ][ faces[ bo ] ] ][ faces[ bo ] ] = faces[ ao ];
        }

        flagvert = true;
        for( j = 0, jl = vertdeg[ 0 ][ faces[ bo ] ]; j < jl; ++j ){
            if( faces[ co ] == vertdeg[ j + 1 ][ faces[ bo ] ] ){
                flagvert = false;
                break;
            }
        }
        if( flagvert ){
            vertdeg[ 0 ][ faces[ bo ] ]++;
            vertdeg[ vertdeg[ 0 ][ faces[ bo ] ] ][ faces[ bo ] ] = faces[ co ];
        }

        // vertex c

        flagvert = true;
        for( j = 0; j < vertdeg[ 0 ][ faces[ co ] ]; ++j ){
            if( faces[ ao ] == vertdeg[ j + 1 ][ faces[ co ] ] ){
                flagvert = false;
                break;
            }
        }
        if( flagvert ){
            vertdeg[ 0 ][ faces[ co ] ]++;
            vertdeg[ vertdeg[ 0 ][ faces[ co ] ] ][ faces[ co ] ] = faces[ ao ];
        }

        flagvert = true;
        for( j = 0, jl = vertdeg[ 0 ][ faces[ co ] ]; j < jl; ++j ){
            if( faces[ bo ] == vertdeg[ j + 1 ][ faces[ co ] ] ){
                flagvert = false;
                break;
            }
        }
        if( flagvert ){
            vertdeg[ 0 ][ faces[ co ] ]++;
            vertdeg[ vertdeg[ 0 ][ faces[ co ] ] ][ faces[ co ] ] = faces[ bo ];
        }

    }

    var wt = 1.0;
    var wt2 = 0.5;
    var i3, vi3, vi, vdi, wt_vi, wt2_vi;
    var ssign = -1;
    var scaleFactor = 1;
    var outwt = 0.75 / ( scaleFactor + 3.5 );  // area-preserving

    // smoothing iterations

    for( var k = 0; k < numiter; ++k ){

        // for each vertex

        for( var i = 0; i < nv; ++i ){

            i3 = i * 3;
            vdi = vertdeg[ 0 ][ i ];

            if( vdi < 3 ){

                tps[ i3     ] = verts[ i3     ];
                tps[ i3 + 1 ] = verts[ i3 + 1 ];
                tps[ i3 + 2 ] = verts[ i3 + 2 ];

            }else if( vdi === 3 || vdi === 4 ){

                tps[ i3     ] = 0;
                tps[ i3 + 1 ] = 0;
                tps[ i3 + 2 ] = 0;

                for( j = 0; j < vdi; ++j ){
                    vi3 = vertdeg[ j + 1 ][ i ] * 3;
                    tps[ i3     ] += verts[ vi3     ];
                    tps[ i3 + 1 ] += verts[ vi3 + 1 ];
                    tps[ i3 + 2 ] += verts[ vi3 + 2 ];
                }

                tps[ i3     ] += wt2 * verts[ i3 ];
                tps[ i3 + 1 ] += wt2 * verts[ i3 + 1 ];
                tps[ i3 + 2 ] += wt2 * verts[ i3 + 2 ];

                wt2_vi = wt2 + vdi;
                tps[ i3     ] /= wt2_vi;
                tps[ i3 + 1 ] /= wt2_vi;
                tps[ i3 + 2 ] /= wt2_vi;

            }else{

                tps[ i3     ] = 0;
                tps[ i3 + 1 ] = 0;
                tps[ i3 + 2 ] = 0;

                for( j = 0; j < vdi; ++j ){
                    vi3 = vertdeg[ j + 1 ][ i ] * 3;
                    tps[ i3     ] += verts[ vi3     ];
                    tps[ i3 + 1 ] += verts[ vi3 + 1 ];
                    tps[ i3 + 2 ] += verts[ vi3 + 2 ];
                }

                tps[ i3     ] += wt * verts[ i3 ];
                tps[ i3 + 1 ] += wt * verts[ i3 + 1 ];
                tps[ i3 + 2 ] += wt * verts[ i3 + 2 ];

                wt_vi = wt + vdi;
                tps[ i3     ] /= wt_vi;
                tps[ i3 + 1 ] /= wt_vi;
                tps[ i3 + 2 ] /= wt_vi;

            }

        }

        verts.set( tps );  // copy smoothed positions

        if( inflate ){

            bg.computeVertexNormals();
            var norms = bg.attributes.normal.array;
            var nv3 = nv * 3;

            for( i3 = 0; i3 < nv3; i3 += 3 ){

                // if(verts[i].inout) ssign=1;
                // else ssign=-1;

                verts[ i3     ] += ssign * outwt * norms[ i3     ];
                verts[ i3 + 1 ] += ssign * outwt * norms[ i3 + 1 ];
                verts[ i3 + 2 ] += ssign * outwt * norms[ i3 + 2 ];

            }

        }

    }

    if( inflate ){

        bg.dispose();

    }

    if( NGL.debug ) NGL.timeEnd( "NGL.laplacianSmooth" );

};


//////////////////////
// Molecular surface

NGL.WorkerRegistry.add( "molsurf", function( e, callback ){

    if( NGL.debug ) NGL.time( "WORKER molsurf" );

    var d = e.data;
    var p = d.params;

    if( d.structure ){

        if( d.structure.metadata.type === "Structure" ){
            self.molsurf = new NGL.MolecularSurface(
                new NGL.Structure().fromJSON( d.structure )
            );
        }else if( d.structure.metadata.type === "StructureView" ){
            self.molsurf = new NGL.MolecularSurface(
                new NGL.StructureView().fromJSON( d.structure )
            );
        }else{
            console.error( "wrong type" );
        }

    }

    var molsurf = self.molsurf;
    var surface = molsurf.getSurface( p );

    if( NGL.debug ) NGL.timeEnd( "WORKER molsurf" );

    callback( surface.toJSON(), surface.getTransferable() );

} );


NGL.MolecularSurface = function( structure ){

    this.structure = structure;

};

NGL.MolecularSurface.prototype = {

    getSurface: function( params ){

        var p = params || {};

        var edtsurf = new NGL.EDTSurface( this.structure );
        var vol = edtsurf.getVolume(
            p.type, p.probeRadius, p.scaleFactor, p.lowRes, p.cutoff
        );
        var surface = vol.getSurface( 1, p.smooth );

        surface.info[ "type" ] = p.type;
        surface.info[ "probeRadius" ] = p.probeRadius;
        surface.info[ "scaleFactor" ] = p.scaleFactor;
        surface.info[ "smooth" ] = p.smooth;
        surface.info[ "lowRes" ] = p.lowRes;
        surface.info[ "cutoff" ] = p.cutoff;

        vol.dispose();

        return surface;

    },

    getSurfaceWorker: function( params, callback ){

        var p = Object.assign( {}, params );

        if( typeof Worker !== "undefined" && typeof importScripts !== 'function' ){

            var structure = undefined;

            if( this.worker === undefined ){

                structure = this.structure.toJSON();
                this.worker = new NGL.Worker( "molsurf" );

            }

            this.worker.post(

                {
                    structure: structure,
                    params: p
                },

                undefined,

                function( e ){

                    var surface = NGL.fromJSON( e.data );
                    callback( surface );

                }.bind( this ),

                function( e ){

                    console.warn(
                        "NGL.MolecularSurface.generateSurfaceWorker error - trying without worker", e
                    );
                    this.worker.terminate();
                    this.worker = undefined;

                    var surface = this.getSurface( p );
                    callback( surface );

                }.bind( this )

            );

        }else{

            var surface = this.getSurface( p );
            callback( surface );

        }

    },

    dispose: function(){

        if( this.worker ) this.worker.terminate();

    }

};


NGL.EDTSurface = function( structure ){

    // based on D. Xu, Y. Zhang (2009) Generating Triangulated Macromolecular
    // Surfaces by Euclidean Distance Transform. PLoS ONE 4(12): e8140.
    //
    // Permission to use, copy, modify, and distribute this program for
    // any purpose, with or without fee, is hereby granted, provided that
    // the notices on the head, the reference information, and this
    // copyright notice appear in all copies or substantial portions of
    // the Software. It is provided "as is" without express or implied
    // warranty.
    //
    // ported to JavaScript by biochem_fan (http://webglmol.sourceforge.jp/)
    // refactored by dkoes (https://github.com/dkoes)
    //
    // adapted to NGL by Alexander Rose

    var bbox = structure.getBoundingBox();
    var atomProxy1 = structure.getAtomProxy();
    var atomProxy2 = structure.getAtomProxy();

    var probeRadius, scaleFactor, cutoff, lowRes;
    var pLength, pWidth, pHeight;
    var matrix, ptran;
    var depty, widxz;
    var cutRadius;
    var setAtomID;
    var vpBits, vpDistance, vpAtomID;

    var radiusProperty;
    var radiusDict;
    var selection;

    function init( btype, _probeRadius, _scaleFactor, _cutoff, _lowRes, _setAtomID ){

        probeRadius = _probeRadius || 1.4;
        scaleFactor = _scaleFactor || 2.0;
        lowRes = _lowRes || false;
        setAtomID = _setAtomID || true;

        if( lowRes ){

            radiusProperty = "resname";
            radiusDict = NGL.ResidueRadii;

            selection = new NGL.Selection( ".CA" );

        }else{

            radiusProperty = "element";
            radiusDict = NGL.VdwRadii;

            selection = undefined;

        }

        var maxRadius = 0;
        for( var name in radiusDict ){
            maxRadius = Math.max( maxRadius, radiusDict[ name ] );
        }

        var grid = NGL.getSurfaceGrid(
            bbox, maxRadius, scaleFactor, btype ? probeRadius : 0
        );

        pLength = grid.dim.x;
        pWidth = grid.dim.y;
        pHeight = grid.dim.z;

        matrix = grid.matrix;
        ptran = grid.tran;
        scaleFactor = grid.scaleFactor;

        // boundingatom caches
        depty = {};
        widxz = {};
        boundingatom( btype );

        cutRadius = probeRadius * scaleFactor;

        if( _cutoff ){
            cutoff = _cutoff;
        }else{
            cutoff = Math.max( 0.1, -1.2 + scaleFactor * probeRadius );
        }

        vpBits = new Uint8Array( pLength * pWidth * pHeight );
        if( btype ){
            vpDistance = new Float64Array( pLength * pWidth * pHeight );
        }
        if( setAtomID ){
            vpAtomID = new Int32Array( pLength * pWidth * pHeight );
        }

    }

    // constants for vpBits bitmasks
    var INOUT = 1;
    var ISDONE = 2;
    var ISBOUND = 4;

    var nb = [
        new Int32Array([  1,  0,  0 ]), new Int32Array([ -1,  0,  0 ]),
        new Int32Array([  0,  1,  0 ]), new Int32Array([  0, -1,  0 ]),
        new Int32Array([  0,  0,  1 ]), new Int32Array([  0,  0, -1 ]),
        new Int32Array([  1,  1,  0 ]), new Int32Array([  1, -1,  0 ]),
        new Int32Array([ -1,  1,  0 ]), new Int32Array([ -1, -1,  0 ]),
        new Int32Array([  1,  0,  1 ]), new Int32Array([  1,  0, -1 ]),
        new Int32Array([ -1,  0,  1 ]), new Int32Array([ -1,  0, -1 ]),
        new Int32Array([  0,  1,  1 ]), new Int32Array([  0,  1, -1 ]),
        new Int32Array([  0, -1,  1 ]), new Int32Array([  0, -1, -1 ]),
        new Int32Array([  1,  1,  1 ]), new Int32Array([  1,  1, -1 ]),
        new Int32Array([  1, -1,  1 ]), new Int32Array([ -1,  1,  1 ]),
        new Int32Array([  1, -1, -1 ]), new Int32Array([ -1, -1,  1 ]),
        new Int32Array([ -1,  1, -1 ]), new Int32Array([ -1, -1, -1 ])
    ];

    //

    this.getVolume = function( type, probeRadius, scaleFactor, lowRes, cutoff, setAtomID ){

        if( NGL.debug ) NGL.time( "NGL.EDTSurface.getVolume" );

        var btype = type !== "vws";
        setAtomID = true;

        init( btype, probeRadius, scaleFactor, cutoff, lowRes, setAtomID );

        fillvoxels( btype );
        buildboundary();

        if( type === "ms" || type === "ses" ){

            fastdistancemap();

        }

        if( type === "ses" ){

            boundingatom( false );
            fillvoxelswaals();

        }

        marchingcubeinit( type );

        var vol = new NGL.Volume(
            type, "", vpBits, pHeight, pWidth, pLength, vpAtomID
        );

        vol.setMatrix( matrix );

        if( NGL.debug ) NGL.timeEnd( "NGL.EDTSurface.getVolume" );

        return vol;

    };


    function boundingatom( btype ){

        var r, j, k;
        var txz, tdept, sradius, tradius, widxz_r;
        var depty_name, indx;

        for( var name in radiusDict ){

            r = radiusDict[ name ];

            if( depty[ name ] ) continue;

            if( !btype ){
                tradius = r * scaleFactor + 0.5;
            }else{
                tradius = ( r + probeRadius ) * scaleFactor + 0.5;
            }

            sradius = tradius * tradius;
            widxz_r = Math.floor( tradius ) + 1;
            depty_name = new Int32Array( widxz_r * widxz_r );
            indx = 0;

            for( j = 0; j < widxz_r; ++j ){

                for( k = 0; k < widxz_r; ++k ){

                    txz = j * j + k * k;

                    if( txz > sradius ){

                        depty_name[ indx ] = -1;

                    }else{

                        tdept = Math.sqrt( sradius - txz );
                        depty_name[ indx ] = Math.floor( tdept );

                    }

                    ++indx;

                }

            }

            widxz[ name ] = widxz_r;
            depty[ name ] = depty_name;

        }

    }

    function fillatom( atomIndex ){

        var cx, cy, cz, ox, oy, oz, mi, mj, mk, i, j, k, si, sj, sk;
        var ii, jj, kk;

        atomProxy1.index = atomIndex;

        if( selection && !selection.test( atomProxy1 ) ) return;

        cx = Math.floor( 0.5 + scaleFactor * ( atomProxy1.x + ptran.x ) );
        cy = Math.floor( 0.5 + scaleFactor * ( atomProxy1.y + ptran.y ) );
        cz = Math.floor( 0.5 + scaleFactor * ( atomProxy1.z + ptran.z ) );

        var at = atomProxy1[ radiusProperty ];
        var depty_at = depty[ at ];
        var nind = 0;
        var cnt = 0;
        var pWH = pWidth * pHeight;
        var n = widxz[ at ];

        var depty_at_nind;

        for( i = 0; i < n; ++i ){
        for( j = 0; j < n; ++j ) {

            depty_at_nind = depty_at[ nind ];

            if( depty_at_nind != -1 ){

                for( ii = -1; ii < 2; ++ii ){
                for( jj = -1; jj < 2; ++jj ){
                for( kk = -1; kk < 2; ++kk ){

                    if( ii !== 0 && jj !== 0 && kk !== 0 ){

                        mi = ii * i;
                        mk = kk * j;

                        for( k = 0; k <= depty_at_nind; ++k ){

                            mj = k * jj;
                            si = cx + mi;
                            sj = cy + mj;
                            sk = cz + mk;

                            if( si < 0 || sj < 0 || sk < 0 ||
                                si >= pLength || sj >= pWidth || sk >= pHeight
                            ){
                                continue;
                            }

                            var index = si * pWH + sj * pHeight + sk;

                            if( !setAtomID ){

                                vpBits[ index ] |= INOUT;

                            }else{

                                if( !( vpBits[ index ] & INOUT ) ){

                                    vpBits[ index ] |= INOUT;
                                    vpAtomID[ index ] = atomIndex;

                                }else if( vpBits[ index ] & INOUT ){
                                // }else{

                                    atomProxy2.index = vpAtomID[ index ];

                                    if( atomProxy2.index !== atomProxy1.index ){

                                        ox = cx + mi - Math.floor( 0.5 + scaleFactor * ( atomProxy2.x + ptran.x ) );
                                        oy = cy + mj - Math.floor( 0.5 + scaleFactor * ( atomProxy2.y + ptran.y ) );
                                        oz = cz + mk - Math.floor( 0.5 + scaleFactor * ( atomProxy2.z + ptran.z ) );

                                        if( mi * mi + mj * mj + mk * mk <
                                            ox * ox + oy * oy + oz * oz
                                        ){
                                            vpAtomID[ index ] = atomIndex;
                                        }

                                    }

                                }

                            }

                        }// k

                    }// if

                }// kk
                }// jj
                }// ii

            }// if

            nind++;

        }// j
        }// i

    }

    function fillvoxels( btype ){

        if( NGL.debug ) NGL.time( "NGL.EDTSurface fillvoxels" );

        var i, il;

        for( i = 0, il = vpBits.length; i < il; ++i ){
            vpBits[ i ] = 0;
            if( btype ) vpDistance[ i ] = -1.0;
            if( setAtomID ) vpAtomID[ i ] = -1;
        }

        structure.eachSelectedAtom( function( ap ){
            fillatom( ap.index );
        } );

        for( i = 0, il = vpBits.length; i < il; ++i ){
            if( vpBits[ i ] & INOUT ){
                vpBits[ i ] |= ISDONE;
            }
        }

        if( NGL.debug ) NGL.timeEnd( "NGL.EDTSurface fillvoxels" );

    }

    function fillAtomWaals( atomIndex ){

        var cx, cy, cz, ox, oy, oz, nind = 0;
        var mi, mj, mk, si, sj, sk, i, j, k, ii, jj, kk, n;

        atomProxy1.index = atomIndex;

        if( selection && !selection.test( atomProxy1 ) ) return;

        cx = Math.floor( 0.5 + scaleFactor * ( atomProxy1.x + ptran.x ) );
        cy = Math.floor( 0.5 + scaleFactor * ( atomProxy1.y + ptran.y ) );
        cz = Math.floor( 0.5 + scaleFactor * ( atomProxy1.z + ptran.z ) );

        var at = atomProxy1[ radiusProperty ];
        var pWH = pWidth * pHeight;

        for( i = 0, n = widxz[at]; i < n; ++i ){
        for( j = 0; j < n; ++j ){

            if( depty[ at ][ nind ] != -1 ){

                for( ii = -1; ii < 2; ++ii ){
                for( jj = -1; jj < 2; ++jj ){
                for( kk = -1; kk < 2; ++kk ){

                    if( ii !== 0 && jj !== 0 && kk !== 0 ){

                        mi = ii * i;
                        mk = kk * j;

                        for( k = 0; k <= depty[ at ][ nind ]; ++k ){

                            mj = k * jj;
                            si = cx + mi;
                            sj = cy + mj;
                            sk = cz + mk;

                            if( si < 0 || sj < 0 || sk < 0 ||
                                si >= pLength || sj >= pWidth || sk >= pHeight
                            ){
                                continue;
                            }

                            var index = si * pWH + sj * pHeight + sk;

                            if( !( vpBits[ index ] & ISDONE ) ){

                                vpBits[ index ] |= ISDONE;
                                if( setAtomID ) vpAtomID[ index ] = atomProxy1.index;

                            }else if( setAtomID ){

                                atomProxy2.index = vpAtomID[ index ];

                                ox = Math.floor( 0.5 + scaleFactor * ( atomProxy2.x + ptran.x ) );
                                oy = Math.floor( 0.5 + scaleFactor * ( atomProxy2.y + ptran.y ) );
                                oz = Math.floor( 0.5 + scaleFactor * ( atomProxy2.z + ptran.z ) );

                                if( mi * mi + mj * mj + mk * mk <
                                    ox * ox + oy * oy + oz * oz
                                ){
                                    vpAtomID[ index ] = atomProxy1.index;
                                }

                            }

                        }// k

                    }// if

                }// kk
                }// jj
                }// ii

            }// if

            nind++;

        }// j
        }// i

    }

    function fillvoxelswaals(){

        var i, il;

        for( i = 0, il = vpBits.length; i < il; ++i ){
            vpBits[ i ] &= ~ISDONE;  // not isdone
        }

        structure.eachSelectedAtom( function( ap ){
            fillAtomWaals( ap.index );
        } );

    }

    function buildboundary(){

        var i, j, k;
        var pWH = pWidth * pHeight;

        for( i = 0; i < pLength; ++i ){
        for( j = 0; j < pHeight; ++j ){
        for( k = 0; k < pWidth; ++k ){

            var index = i * pWH + k * pHeight + j;

            if( vpBits[ index ] & INOUT ){

                // var flagbound = false;
                var ii = 0;

                // while( !flagbound && ii < 26 ){
                while( ii < 26 ){

                    var ti = i + nb[ ii ][ 0 ];
                    var tj = j + nb[ ii ][ 2 ];
                    var tk = k + nb[ ii ][ 1 ];

                    if( ti > -1 && ti < pLength &&
                        tk > -1 && tk < pWidth &&
                        tj > -1 && tj < pHeight &&
                        !( vpBits[ ti * pWH + tk * pHeight + tj ] & INOUT )
                    ){

                        vpBits[ index ] |= ISBOUND;
                        // flagbound = true;
                        break;

                    }else{

                        ii++;

                    }

                }

            }

        } // k
        } // j
        } // i

    }

    function fastdistancemap(){

        if( NGL.debug ) NGL.time( "NGL.EDTSurface fastdistancemap" );

        var eliminate = 0;
        var certificate;
        var i, j, k, n;

        var boundPoint = new NGL.Grid(
            pLength, pWidth, pHeight, Uint16Array, 3
        );
        var pWH = pWidth * pHeight;
        var cutRSq = cutRadius * cutRadius;

        var totalsurfacevox = 0;
        var totalinnervox = 0;

        var index;

        // console.log( "lwh", pLength * pWidth * pHeight );
        if( NGL.debug ) console.log( "l, w, h", pLength, pWidth, pHeight );

        for( i = 0; i < pLength; ++i ){
            for( j = 0; j < pWidth; ++j ){
                for( k = 0; k < pHeight; ++k ){

                    index = i * pWH + j * pHeight + k;

                    vpBits[ index ] &= ~ISDONE;

                    if( vpBits[ index ] & INOUT ){

                        if( vpBits[ index ] & ISBOUND ){

                            boundPoint.set(
                                i, j, k,
                                i, j, k
                            );

                            vpDistance[ index ] = 0;
                            vpBits[ index ] |= ISDONE;

                            totalsurfacevox += 1;

                        }else{

                            totalinnervox += 1;

                        }

                    }

                }
            }
        }

        if( NGL.debug ) console.log( "totalsurfacevox", totalsurfacevox );
        if( NGL.debug ) console.log( "totalinnervox", totalinnervox );

        var inarray = new Int32Array( 3 * totalsurfacevox );
        var positin = 0;
        var outarray = new Int32Array( 3 * totalsurfacevox );
        var positout = 0;

        for( i = 0; i < pLength; ++i ){
            for( j = 0; j < pWidth; ++j ){
                for( k = 0; k < pHeight; ++k ){

                    index = i * pWH + j * pHeight + k;

                    if( vpBits[ index ] & ISBOUND ){

                        inarray[ positin     ] = i;
                        inarray[ positin + 1 ] = j;
                        inarray[ positin + 2 ] = k;
                        positin += 3;

                        vpBits[ index ] &= ~ISBOUND;

                    }

                }
            }
        }

        do{

            positout = fastoneshell( inarray, boundPoint, positin, outarray );
            positin = 0;

            if( NGL.debug ) console.log( "positout", positout / 3 );

            for( i = 0, n = positout; i < n; i+=3 ){

                index = pWH * outarray[ i ] + pHeight * outarray[ i + 1 ] + outarray[ i + 2 ];
                vpBits[ index ] &= ~ISBOUND;

                if( vpDistance[ index ] <= 1.0404 * cutRSq ){
                //if( vpDistance[ index ] <= 1.02 * cutRadius ){

                    inarray[ positin     ] = outarray[ i     ];
                    inarray[ positin + 1 ] = outarray[ i + 1 ];
                    inarray[ positin + 2 ] = outarray[ i + 2 ];
                    positin += 3;

                }

            }

        }while( positin > 0 );

        // var cutsf = Math.max( 0, scaleFactor - 0.5 );
        // cutoff = cutRadius - 0.5 / ( 0.1 + cutsf );
        var cutoffSq = cutoff * cutoff;

        var index2;
        var bp = new Uint16Array( 3 );

        for( i = 0; i < pLength; ++i ){
            for( j = 0; j < pWidth; ++j ){
                for( k = 0; k < pHeight; ++k ){

                    index = i * pWH + j * pHeight + k;
                    vpBits[ index ] &= ~ISBOUND;

                    // ses solid

                    if( vpBits[ index ] & INOUT ) {

                        if( !( vpBits[ index ] & ISDONE ) ||
                            ( ( vpBits[ index ] & ISDONE ) && vpDistance[ index ] >= cutoffSq )
                        ){

                            vpBits[ index ] |= ISBOUND;

                            if( setAtomID && ( vpBits[ index ] & ISDONE ) ){

                                boundPoint.toArray( i, j, k, bp );
                                index2 = bp[ 0 ] * pWH + bp[ 1 ] * pHeight + bp[ 2 ];

                                vpAtomID[ index ] = vpAtomID[ index2 ];

                            }

                        }
                    }

                }
            }
        }

        if( NGL.debug ) NGL.timeEnd( "NGL.EDTSurface fastdistancemap" );

    }

    function fastoneshell( inarray, boundPoint, positin, outarray ){

        if( NGL.debug ) console.log( "positin", positin / 3 );

        // *allocout,voxel2
        // ***boundPoint, int*
        // outnum, int *elimi)
        var tx, ty, tz;
        var dx, dy, dz;
        var i, j, n;
        var square;
        var index;
        var nb_j;
        var bp = new Uint16Array( 3 );
        var positout = 0;

        if( positin === 0 ){
            return positout;
        }

        var tnv_ix = -1;
        var tnv_iy = -1;
        var tnv_iz = -1;

        var pWH = pWidth * pHeight;

        for( i = 0, n = positin; i < n; i+=3 ){

            tx = inarray[ i     ];
            ty = inarray[ i + 1 ];
            tz = inarray[ i + 2 ];
            boundPoint.toArray( tx, ty, tz, bp );

            for( j = 0; j < 6; ++j ){

                nb_j = nb[ j ];
                tnv_ix = tx + nb_j[ 0 ];
                tnv_iy = ty + nb_j[ 1 ];
                tnv_iz = tz + nb_j[ 2 ];

                if( tnv_ix < pLength && tnv_ix > -1 &&
                    tnv_iy < pWidth  && tnv_iy > -1 &&
                    tnv_iz < pHeight && tnv_iz > -1
                ){

                    index = tnv_ix * pWH + pHeight * tnv_iy + tnv_iz;

                    if( ( vpBits[ index ] & INOUT ) && !( vpBits[ index ] & ISDONE ) ){

                        boundPoint.fromArray( tnv_ix, tnv_iy, tnv_iz, bp );
                        dx = tnv_ix - bp[ 0 ];
                        dy = tnv_iy - bp[ 1 ];
                        dz = tnv_iz - bp[ 2 ];
                        square = dx * dx + dy * dy + dz * dz;
                        //square = Math.sqrt( square );

                        vpDistance[ index ] = square;
                        vpBits[ index ] |= ISDONE;
                        vpBits[ index ] |= ISBOUND;

                        outarray[ positout     ] = tnv_ix;
                        outarray[ positout + 1 ] = tnv_iy;
                        outarray[ positout + 2 ] = tnv_iz;
                        positout += 3;

                    }else if( ( vpBits[ index ] & INOUT ) && ( vpBits[ index ] & ISDONE ) ){

                        dx = tnv_ix - bp[ 0 ];
                        dy = tnv_iy - bp[ 1 ];
                        dz = tnv_iz - bp[ 2 ];
                        square = dx * dx + dy * dy + dz * dz;
                        //square = Math.sqrt( square );

                        if( square < vpDistance[ index ] ){

                            boundPoint.fromArray( tnv_ix, tnv_iy, tnv_iz, bp );
                            vpDistance[ index ] = square;

                            if( !( vpBits[ index ] & ISBOUND ) ){

                                vpBits[ index ] |= ISBOUND;

                                outarray[ positout     ] = tnv_ix;
                                outarray[ positout + 1 ] = tnv_iy;
                                outarray[ positout + 2 ] = tnv_iz;
                                positout += 3;

                            }

                        }

                    }

                }
            }
        }

        // console.log("part1", positout);

        for( i = 0, n = positin; i < n; i+=3 ){

            tx = inarray[ i     ];
            ty = inarray[ i + 1 ];
            tz = inarray[ i + 2 ];
            boundPoint.toArray( tx, ty, tz, bp );

            for (j = 6; j < 18; j++) {

                nb_j = nb[ j ];
                tnv_ix = tx + nb_j[ 0 ];
                tnv_iy = ty + nb_j[ 1 ];
                tnv_iz = tz + nb_j[ 2 ];

                if( tnv_ix < pLength && tnv_ix > -1 &&
                    tnv_iy < pWidth  && tnv_iy > -1 &&
                    tnv_iz < pHeight && tnv_iz > -1
                ) {

                    index = tnv_ix * pWH + pHeight * tnv_iy + tnv_iz;

                    if ((vpBits[index] & INOUT) && !(vpBits[index] & ISDONE)) {

                        boundPoint.fromArray( tnv_ix, tnv_iy, tnv_iz, bp );
                        dx = tnv_ix - bp[ 0 ];
                        dy = tnv_iy - bp[ 1 ];
                        dz = tnv_iz - bp[ 2 ];
                        square = dx * dx + dy * dy + dz * dz;
                        //square = Math.sqrt( square );

                        vpDistance[index] = square;
                        vpBits[index] |= ISDONE;
                        vpBits[index] |= ISBOUND;

                        outarray[ positout     ] = tnv_ix;
                        outarray[ positout + 1 ] = tnv_iy;
                        outarray[ positout + 2 ] = tnv_iz;
                        positout += 3;

                    } else if ((vpBits[index] & INOUT) && (vpBits[index] & ISDONE)) {

                        dx = tnv_ix - bp[ 0 ];
                        dy = tnv_iy - bp[ 1 ];
                        dz = tnv_iz - bp[ 2 ];
                        square = dx * dx + dy * dy + dz * dz;
                        //square = Math.sqrt( square );

                        if (square < vpDistance[index]) {

                            boundPoint.fromArray( tnv_ix, tnv_iy, tnv_iz, bp );
                            vpDistance[index] = square;

                            if (!(vpBits[index] & ISBOUND)) {

                                vpBits[index] |= ISBOUND;

                                outarray[ positout     ] = tnv_ix;
                                outarray[ positout + 1 ] = tnv_iy;
                                outarray[ positout + 2 ] = tnv_iz;
                                positout += 3;

                            }

                        }

                    }

                }
            }
        }

        // console.log("part2", positout);

        for( i = 0, n = positin; i < n; i+=3 ){

            tx = inarray[ i     ];
            ty = inarray[ i + 1 ];
            tz = inarray[ i + 2 ];
            boundPoint.toArray( tx, ty, tz, bp );

            for (j = 18; j < 26; j++) {

                nb_j = nb[ j ];
                tnv_ix = tx + nb_j[ 0 ];
                tnv_iy = ty + nb_j[ 1 ];
                tnv_iz = tz + nb_j[ 2 ];

                if( tnv_ix < pLength && tnv_ix > -1 &&
                    tnv_iy < pWidth  && tnv_iy > -1 &&
                    tnv_iz < pHeight && tnv_iz > -1
                ){

                    index = tnv_ix * pWH + pHeight * tnv_iy + tnv_iz;

                    if ((vpBits[index] & INOUT) && !(vpBits[index] & ISDONE)) {

                        boundPoint.fromArray( tnv_ix, tnv_iy, tnv_iz, bp );
                        dx = tnv_ix - bp[ 0 ];
                        dy = tnv_iy - bp[ 1 ];
                        dz = tnv_iz - bp[ 2 ];
                        square = dx * dx + dy * dy + dz * dz;
                        //square = Math.sqrt( square );

                        vpDistance[index] = square;
                        vpBits[index] |= ISDONE;
                        vpBits[index] |= ISBOUND;

                        outarray[ positout     ] = tnv_ix;
                        outarray[ positout + 1 ] = tnv_iy;
                        outarray[ positout + 2 ] = tnv_iz;
                        positout += 3;

                    } else if ((vpBits[index] & INOUT)  && (vpBits[index] & ISDONE)) {

                        dx = tnv_ix - bp[ 0 ];
                        dy = tnv_iy - bp[ 1 ];
                        dz = tnv_iz - bp[ 2 ];
                        square = dx * dx + dy * dy + dz * dz;
                        //square = Math.sqrt( square );

                        if (square < vpDistance[index]) {

                            boundPoint.fromArray( tnv_ix, tnv_iy, tnv_iz, bp );
                            vpDistance[index] = square;

                            if (!(vpBits[index] & ISBOUND)) {

                                vpBits[index] |= ISBOUND;

                                outarray[ positout     ] = tnv_ix;
                                outarray[ positout + 1 ] = tnv_iy;
                                outarray[ positout + 2 ] = tnv_iz;
                                positout += 3;

                            }

                        }

                    }

                }
            }
        }

        // console.log("part3", positout);

        return positout;

    }

    function marchingcubeinit( stype ){

        var n = vpBits.length;

        if( stype === "vws" ) {

            for( var i = 0; i < n; ++i ){

                vpBits[ i ] &= ~ISBOUND;
                vpBits[ i ] = !!( vpBits[ i ] & ISDONE ) ? 1 : 0;

            }

        }else if( stype === "ms" ){  // ses without vdw => ms

            for( var i = 0; i < n; ++i ){

                vpBits[ i ] &= ~ISDONE;
                if( vpBits[ i ] & ISBOUND ){
                    vpBits[ i ] |= ISDONE;
                }
                vpBits[ i ] &= ~ISBOUND;
                vpBits[ i ] = !!( vpBits[ i ] & ISDONE ) ? 1 : 0;

            }

        }else if( stype === "ses" ){

            for( var i = 0; i < n; ++i ){

                if( ( vpBits[ i ] & ISBOUND ) && ( vpBits[ i ] & ISDONE ) ){
                    vpBits[ i ] &= ~ISBOUND;
                }else if( ( vpBits[ i ] & ISBOUND ) && !( vpBits[ i ] & ISDONE ) ){
                    vpBits[ i ] |= ISDONE;
                }
                vpBits[ i ] = !!( vpBits[ i ] & ISDONE ) ? 1 : 0;

            }

        }else if( stype === "sas" ){

            for( var i = 0; i < n; ++i ){

                vpBits[ i ] &= ~ISBOUND;
                vpBits[ i ] = !!( vpBits[ i ] & ISDONE ) ? 1 : 0;

            }

        }

    };

};


NGL.getSurfaceGrid = function( bbox, maxRadius, scaleFactor, extraMargin ){

    // need margin to avoid boundary/round off effects
    var margin = ( 1 / scaleFactor ) * 3;
    margin += maxRadius;

    var min = new THREE.Vector3().copy( bbox.min );
    var max = new THREE.Vector3().copy( bbox.max );

    min.subScalar( extraMargin + margin );
    max.addScalar( extraMargin + margin );

    min.multiplyScalar( scaleFactor ).floor().divideScalar( scaleFactor );
    max.multiplyScalar( scaleFactor ).ceil().divideScalar( scaleFactor );

    var dim = new THREE.Vector3()
        .subVectors( max, min )
        .multiplyScalar( scaleFactor )
        .ceil()
        .addScalar( 1 );

    var maxSize = Math.pow( 10, 6 ) * 256;
    var tmpSize = dim.x * dim.y * dim.z * 3;

    if( maxSize <= tmpSize ){

        scaleFactor *= Math.pow( maxSize / tmpSize, 1/3 );

        min.multiplyScalar( scaleFactor ).floor().divideScalar( scaleFactor );
        max.multiplyScalar( scaleFactor ).ceil().divideScalar( scaleFactor );

        dim.subVectors( max, min )
            .multiplyScalar( scaleFactor )
            .ceil()
            .addScalar( 1 );

    }

    var tran = new THREE.Vector3().copy( min ).negate();

    // coordinate transformation matrix
    var matrix = new THREE.Matrix4();
    matrix.multiply(
        new THREE.Matrix4().makeRotationY( THREE.Math.degToRad( 90 ) )
    );
    matrix.multiply(
        new THREE.Matrix4().makeScale(
            -1 / scaleFactor,
             1 / scaleFactor,
             1 / scaleFactor
        )
    );
    matrix.multiply(
        new THREE.Matrix4().makeTranslation(
            -scaleFactor * tran.z,
            -scaleFactor * tran.y,
            -scaleFactor * tran.x
        )
    );

    return {
        dim: dim,
        tran: tran,
        matrix: matrix,
        scaleFactor: scaleFactor
    };

};

// File:js/ngl/script.js

/**
 * @file Script
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


///////////
// Script

NGL.Script = function( functionBody, name, path ){

    var SIGNALS = signals;

    this.signals = {

        elementAdded: new SIGNALS.Signal(),
        elementRemoved: new SIGNALS.Signal(),
        nameChanged: new SIGNALS.Signal(),

    };

    this.name = name;
    this.path = path;
    this.dir = path.substring( 0, path.lastIndexOf( '/' ) + 1 );

    try {

        this.fn = new Function(

            'stage', 'panel',
            '__name__', '__path__', '__dir__',

            Object.keys( NGL.makeScriptHelper() ).join( ',' ),

            functionBody

        );

    }catch( e ){

        NGL.error( "NGL.Script compilation failed", e );
        this.fn = null;

    }

}

NGL.Script.prototype = {

    constructor: NGL.Script,

    call: function( stage, onFinish ){

        var panel = {

            add: function( element ){

                this.signals.elementAdded.dispatch( arguments );

            }.bind( this ),

            remove: function( element ){

                this.signals.elementRemoved.dispatch( arguments );

            }.bind( this ),

            setName: function( value ){

                this.signals.nameChanged.dispatch( value );

            }.bind( this )

        };

        var queue = new NGL.ScriptQueue( stage, this.dir, onFinish );
        var helper = NGL.makeScriptHelper( stage, queue, panel );

        if( this.fn ){

            var args = [
                stage, panel,
                this.name, this.path, this.dir
            ];

            try{

                var fnList = [];
                Object.keys( helper ).forEach( function( name ){
                    fnList.push( helper[ name ] );
                } );

                this.fn.apply(
                    null, args.concat( fnList )
                );

            }catch( e ){

                NGL.error( "NGL.Script.fn", e );

            }

        }else{

            NGL.log( "NGL.Script.call no function available" );

        }

        function finish(){
            if( typeof onFinish === "function" ) onFinish();
        }

        function error(){
            panel.add( new UI.Text( "ERROR" ) );
            finish();
        }

        queue.then( finish, error );

    }

}


NGL.ScriptQueue = function( stage, dir, onFinish ){

    this.stage = stage;
    this.dir = dir || "";
    this.onFinish = onFinish;

    this.promise = new Promise( function( resolve, reject ){

        resolve();

    } );

};

NGL.ScriptQueue.prototype = {

    constructor: NGL.ScriptQueue,

    load: function( file, params ){

        var status = {};

        // TODO check for pdbid or http...
        var path = this.dir + file;

        var _onLoad;
        var p = params || {};

        // allow loadFile( path, onLoad ) method signature
        if( typeof params === "function" ){

            _onLoad = params;
            p = {};

        }else{

            _onLoad = p.onLoad;

        }

        p.onLoad = function( component ){

            if( typeof _onLoad === "function" ){
                _onLoad( component );
            }

            if( status.resolve ){
                status.resolve();
            }else{
                status.success = true;
            }

        };

        p.onError = function( e ){

            if( status.reject ){
                status.reject( e );
            }else{
                status.error = e || "error";
            }

        };

        this.stage.loadFile( path, p );

        var handle = function( resolve, reject ){

            if( status.success === true ){
                resolve();
            }else if( status.error !== undefined ){
                reject( status.error );
            }else{
                status.resolve = resolve;
                status.reject = reject;
            }

        };

        this.promise = this.promise.then( function(){

            return new Promise( handle );

        } );

    },

    then: function( callback, onError ){

        this.promise = this.promise.then( callback, function( e ){

            NGL.error( "NGL.ScriptQueue.then", e );

            if( typeof onError === "function" ) onError();

        } );

    }

};


NGL.makeScriptHelper = function( stage, queue, panel ){

    var U = NGL.unicodeHelper;

    //

    function load(){

        queue.load.apply( queue, arguments );

    }

    function then(){

        queue.then.apply( queue, arguments );

    }

    //

    function components( name ){

        return stage.getComponentsByName( name );

    }

    function representations( name ){

        return stage.getRepresentationsByName( name );

    }

    function structures( name ){

        return stage.getComponentsByName( name, NGL.StructureComponent );

    }

    //

    function color( value, collection ){

        collection.setColor( value );

    }

    function visibility( value, collection ){

        collection.setVisibility( value );

    }

    function hide( collection ){

        visibility( false, collection );

    }

    function show( collection, only ){

        if( only ) hide();

        visibility( true, collection );

    }

    function superpose( comp1, comp2, align, sele1, sele2, xsele1, xsele2 ){

        comp1.superpose( comp2, align, sele1, sele2, xsele1, xsele2 );

    }

    //

    function uiText( text, newline ){

        var elm = new UI.Text( U( text ) );

        panel.add( elm );

        if( newline ) uiBreak( 1 );

        return elm;

    }

    function uiHtml( html, newline ){

        var elm = new UI.Html( U( html ) );

        panel.add( elm );

        if( newline ) uiBreak( 1 );

        return elm;

    }

    function uiBreak( n ){

        n = n === undefined ? 1 : n;

        for( var i = 0; i < n; ++i ){

            panel.add( new UI.Break() );

        }

    }

    function uiButton( label, callback ){

        var btn = new UI.Button( U( label ) ).onClick( function(){
            callback( btn );
        } );

        panel.add( btn );

        return btn;

    }

    function uiSelect( options, callback ){

        if( Array.isArray( options ) ){
            var newOptions = {};
            options.forEach( function( name ){
                newOptions[ name ] = name;
            } );
            options = newOptions;
        }

        var select = new UI.Select()
            .setOptions( options )
            .onChange( function(){
                callback( select );
            } );

        panel.add( select );

        return select;

    }

    function uiOpenButton( label, callback, extensionList ){

        var btn = new UI.Button( U( label ) ).onClick( function(){

            NGL.open( callback, extensionList );

        } );

        panel.add( btn );

        return btn;

    }

    function uiDownloadButton( label, callback, downloadName ){

        var btn = new UI.Button( U( label ) ).onClick( function(){

            NGL.download( callback(), downloadName );

        } );

        panel.add( btn );

        return btn;

    }

    function uiVisibilitySelect( collection ){

        var list = collection.list;

        function getVisible(){

            var nameList = [];

            list.forEach( function( o ){

                if( o.visible ) nameList.push( o.name );

            } );

            return nameList;

        }

        var options = { "": "[show]" };

        list.forEach( function( o ){

            options[ o.name ] = o.name;

            o.signals.visibilityChanged.add( function(){

                var nameList = getVisible();

                if( nameList.length === list.length ){
                    select.setValue( "" );
                }else if( o.visible ){
                    select.setValue( o.name );
                }else{
                    select.setValue( nameList[ 0 ] );
                }

            } );

        } );

        var select = new UI.Select()
            .setOptions( options )
            .onChange( function(){

                var name = select.getValue();

                if( name === "" ){
                    show( collection );
                }else{
                    hide( collection );
                    show( stage.getAnythingByName( name ) );
                }

            } );

        panel.add( select );

        return select;

    }

    function uiVisibilityButton( label, collection ){

        label = U( label ? label : "all" );
        collection = collection || new NGL.Collection();

        if( !( collection instanceof NGL.Collection ) &&
            !( collection instanceof NGL.ComponentCollection ) &&
            !( collection instanceof NGL.RepresentationCollection )
        ){
            collection = new NGL.Collection( [ collection ] );
        }

        var list = collection.list;

        function isVisible(){

            var visible = false;

            list.forEach( function( o ){

                if( o.visible ) visible = true;

            } );

            return visible;

        }

        function getLabel( value ){

            return ( isVisible() ? "hide " : "show " ) + label;

        }

        list.forEach( function( o ){

            o.signals.visibilityChanged.add( function(){

                btn.setLabel( getLabel() );

            } );

        } );

        var btn = new UI.Button( getLabel() ).onClick( function(){

            visibility( !isVisible(), collection );

        } );

        // panel.add( btn );

        return btn;

    }

    function uiPlayButton( label, trajComp, step, timeout, start, end ){

        var traj = trajComp.trajectory;
        label = U( label );

        var player = new NGL.TrajectoryPlayer( traj, step, timeout, start, end );
        player.mode = "once";

        var btn = new UI.Button( "play " + label )
            .onClick( function(){
                player.toggle();
            } );

        player.signals.startedRunning.add( function(){
            btn.setLabel( "pause " + label );
        } );

        player.signals.haltedRunning.add( function(){
            btn.setLabel( "play " + label );
        } );

        panel.add( btn );

        return btn;

    }

    //

    return {

        'load': load,
        'then': then,

        'components': components,
        'representations': representations,
        'structures': structures,

        'color': color,
        'visibility': visibility,
        'hide': hide,
        'show': show,
        'superpose': superpose,

        'uiText': uiText,
        'uiHtml': uiHtml,
        'uiBreak': uiBreak,
        'uiSelect': uiSelect,
        'uiButton': uiButton,
        'uiOpenButton': uiOpenButton,
        'uiDownloadButton': uiDownloadButton,
        'uiVisibilitySelect': uiVisibilitySelect,
        'uiVisibilityButton': uiVisibilityButton,
        'uiPlayButton': uiPlayButton,

    };

};

// File:js/ngl/streamer.js

/**
 * @file Streamer
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


/////////////
// Streamer

NGL.Streamer = function( src, params ){

    var p = params || {};

    this.compressed = p.compressed !== undefined ? p.compressed : false;
    this.binary = p.binary !== undefined ? p.binary : false;
    this.json = p.json !== undefined ? p.json : false;

    this.src = src;
    this.chunkSize = 1024 * 1024 * 10;
    this.newline = "\n";

    this.__pointer = 0;
    this.__partialLine = "";

    if( this.__srcName ){
        this[ this.__srcName ] = src;
    }

};

NGL.Streamer.prototype.constructor = NGL.Streamer;

NGL.Streamer.prototype = {

    constructor: NGL.Streamer,

    type: "",

    __srcName: undefined,

    onload: function(){},

    onprogress: function(){},

    onerror: function(){},

    read: function( callback ){

        this._read( function( data ){

            if( this.compressed ){

                this.data = NGL.decompress( data );

            }else{

                if( this.binary && data instanceof ArrayBuffer ){
                    data = new Uint8Array( data );
                }
                this.data = data;

            }

            if( typeof this.onload === "function" ){
                this.onload( this.data );
            }
            callback();

        }.bind( this ) );

    },

    _read: function( callback ){

        // overwrite this method when this.src does not contain the data

        callback( this.src );

    },

    _chunk: function( start, end ){

        end = Math.min( this.data.length, end );

        if( start === 0 && this.data.length === end ){

            return this.data;

        }else{

            if( this.binary || this.compressed ){
                return this.data.subarray( start, end );
            }else{
                return this.data.substring( start, end );
            }

        }

    },

    chunk: function( start ){

        var end = start + this.chunkSize;

        return this._chunk( start, end );

    },

    peekLines: function( m ){

        var data = this.data;
        var n = data.length;

        // FIXME does not work for multi-char newline
        var newline = ( this.binary || this.compressed ) ? this.newline.charCodeAt( 0 ) : this.newline;

        var i;
        var count = 0;

        for( i = 0; i < n; ++i ){

            if( data[ i ] === newline ) ++count;
            if( count === m ) break;

        }

        var chunk = this._chunk( 0, i + 1 );
        var d = this.chunkToLines( chunk, "", i > n );

        return d.lines;

    },

    lineCount: function(){
        console.warn("lineCount")
        var data = this.data;
        var n = data.length;

        // FIXME does not work for multi-char newline
        var newline = ( this.binary || this.compressed ) ? this.newline.charCodeAt( 0 ) : this.newline;

        var count = 0;
        for( var i = 0; i < n; ++i ){
            if( data[ i ] === newline ) ++count;
        }
        if( data[ n - 1 ] !== newline ) ++count;

        return count;

    },

    chunkCount: function(){

        return Math.floor( this.data.length / this.chunkSize ) + 1;

    },

    asText: function(){

        if( this.binary || this.compressed ){
            return NGL.Uint8ToString( this.data );
        }else{
            return this.data;
        }

    },

    chunkToLines: function( chunk, partialLine, isLast ){

        var newline = this.newline;

        if( !this.binary && !this.compressed && chunk.length === this.data.length ){
            return {
                lines: chunk.split( newline ),
                partialLine: ""
            };
        }

        var str = ( this.binary || this.compressed ) ? NGL.Uint8ToString( chunk ) : chunk;
        var lines = [];
        var idx = str.lastIndexOf( newline );

        if( idx === -1 ){

            partialLine += str;

        }else{

            var str2 = partialLine + str.substr( 0, idx );
            lines = lines.concat( str2.split( newline ) );

            if( idx === str.length - newline.length ){
                partialLine = "";
            }else{
                partialLine = str.substr( idx + newline.length );
            }

        }

        if( isLast && partialLine !== "" ){
            lines.push( partialLine );
        }

        return {
            lines: lines,
            partialLine: partialLine
        };

    },

    nextChunk: function(){

        var start = this.__pointer;

        if( start > this.data.length ){
            return undefined;
        }

        this.__pointer += this.chunkSize;
        return this.chunk( start );

    },

    nextChunkOfLines: function(){

        var chunk = this.nextChunk();

        if( chunk === undefined ){
            return undefined;
        }

        var isLast = this.__pointer > this.data.length;
        var d = this.chunkToLines( chunk, this.__partialLine, isLast );

        this.__partialLine = d.partialLine;

        return d.lines;

    },

    eachChunk: function( callback ){

        var chunkSize = this.chunkSize;
        var n = this.data.length;
        var chunkCount = this.chunkCount();

        for( var i = 0; i < n; i += chunkSize ){

            var chunk = this.chunk( i );
            var chunkNo = Math.round( i / chunkSize );

            callback( chunk, chunkNo, chunkCount );

        }

    },

    eachChunkOfLines: function( callback ){

        var newline = this.newline;

        this.eachChunk( function( chunk, chunkNo, chunkCount ){

            var isLast = chunkNo === chunkCount + 1;
            var d = this.chunkToLines( chunk, this.__partialLine, isLast );

            this.__partialLine = d.partialLine;

            callback( d.lines, chunkNo, chunkCount );

        }.bind( this ) );

    },

    toJSON: function(){

        var type = this.type.substr( 0, 1 ).toUpperCase() +
                    this.type.substr( 1 );

        var output = {

            metadata: {
                version: 0.1,
                type: type + 'Streamer',
                generator: type + 'StreamerExporter'
            },

            src: this.src,
            compressed: this.compressed,
            binary: this.binary,
            chunkSize: this.chunkSize,
            newline: this.newline,



        }

        if( this.__srcName ){
            output[ this.__srcName ] = this[ this.__srcName ];
        }

        return output;

    },

    fromJSON: function( input ){

        this.src = input.src;
        this.compressed = input.compressed;
        this.binary = input.binary;
        this.chunkSize = input.chunkSize;
        this.newline = input.newline;

        if( this.__srcName ){
            this[ this.__srcName ] = input[ this.__srcName ];
        }

        return this;

    },

    getTransferable: function(){

        var transferable = [];

        return transferable;

    },

    dispose: function(){

        delete this.src;

        if( this.__srcName ){
            delete this[ this.__srcName ];
        }

    }

};


NGL.NetworkStreamer = function( url, params ){

    NGL.Streamer.call( this, url, params );

};

NGL.NetworkStreamer.prototype = NGL.createObject(

    NGL.Streamer.prototype, {

    constructor: NGL.NetworkStreamer,

    type: "network",

    __srcName: "url",

    _read: function( callback ){

        var url = this.src;

        if( typeof importScripts === 'function' ){

            // FIXME
            // adjust relative path when inside a web worker
            if( url.substr( 0, 3 ) === "../" ) url = "../" + url;

        }

        var xhr = new XMLHttpRequest();
        xhr.open( "GET", url, true );

        //

        xhr.addEventListener( 'load', function ( event ) {

            if( xhr.status === 200 || xhr.status === 304 ||
                // when requesting from local file system
                // the status in Google Chrome/Chromium is 0
                xhr.status === 0
             ){

                callback( xhr.response );

            } else {

                if( typeof this.onerror === "function" ){

                    this.onerror( xhr.status );

                }

                throw "NGL.NetworkStreamer._read: status code " + xhr.status;

            }

        }.bind( this ), false );

        //

        // if( typeof this.onprogress === "function" ){

        //     xhr.addEventListener( 'progress', function ( event ) {

        //         this.onprogress( event );

        //     }.bind( this ), false );

        // }

        //

        if( typeof this.onerror === "function" ){

            xhr.addEventListener( 'error', function ( event ) {

                this.onerror( event );

            }.bind( this ), false );

        }

        //

        if( this.compressed || this.binary ){
            xhr.responseType = "arraybuffer";
        }else if( this.json ){
            xhr.responseType = "json";
        }
        // xhr.crossOrigin = true;

        xhr.send( null );

        // try {
        //     xhr.send( null );
        // }catch( e ){
        //     if( typeof this.onerror === "function" ){
        //         this.onerror( e.message );
        //     }
        // }

    }

} );


NGL.FileStreamer = function( file, params ){

    NGL.Streamer.call( this, file, params );

};

NGL.FileStreamer.prototype = NGL.createObject(

    NGL.Streamer.prototype, {

    constructor: NGL.FileStreamer,

    type: "file",

    __srcName: "file",

    _read: function( callback ){

        if( typeof importScripts === 'function' ){

            // Use FileReaderSync within Worker

            var reader = new FileReaderSync();
            var data;
            if( this.binary || this.compressed ){
                data = reader.readAsArrayBuffer( this.file );
            }else{
                data = reader.readAsText( this.file );
            }

            //

            callback( data );

        }else{

            var reader = new FileReader();

            //

            reader.onload = function( event ){

                callback( event.target.result );

            }.bind( this );

            //

            if( typeof this.onprogress === "function" ){

                reader.onprogress = function ( event ) {

                    this.onprogress( event );

                }.bind( this );

            }

            //

            if( typeof this.onerror === "function" ){

                reader.onerror = function ( event ) {

                    this.onerror( event );

                }.bind( this );

            }

            //

            if( this.binary || this.compressed ){
                reader.readAsArrayBuffer( this.file );
            }else{
                reader.readAsText( this.file );
            }

        }

    }

} );


NGL.StringStreamer = function( str, params ){

    NGL.Streamer.call( this, str, params );

};

NGL.StringStreamer.prototype = NGL.createObject(

    NGL.Streamer.prototype, {

    constructor: NGL.StringStreamer,

    type: "string",

    __srcName: "str",

    _chunk: function( start, end ){

        return this.data.substr( start, end );

    },

} );


NGL.BinaryStreamer = function( bin, params ){

    if( bin instanceof ArrayBuffer ) bin = new Uint8Array( bin );

    NGL.Streamer.call( this, bin, params );

};

NGL.BinaryStreamer.prototype = NGL.createObject(

    NGL.Streamer.prototype, {

    constructor: NGL.BinaryStreamer,

    type: "binary",

    __srcName: "bin",

    getTransferable: function(){

        var transferable = NGL.Streamer.prototype.getTransferable.call( this );

        if( this.bin instanceof Uint8Array ){
            transferable.push( this.bin.buffer );
        }

        return transferable;

    }

} );

// File:js/ngl/parser.js

/**
 * @file Parser
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


NGL.reorderAtoms = function( structure ){

    if( NGL.debug ) NGL.time( "NGL.reorderAtoms" );

    var ap1 = structure.getAtomProxy();
    var ap2 = structure.getAtomProxy();

    function compareModelChainResno( index1, index2 ){
        ap1.index = index1;
        ap2.index = index2;
        if( ap1.modelindex < ap2.modelindex ){
            return -1;
        }else if( ap1.modelindex > ap2.modelindex ){
            return 1;
        }else{
            if( ap1.chainname < ap2.chainname ){
                return -1;
            }else if( ap1.chainname > ap2.chainname ){
                return 1;
            }else{
                if( ap1.resno < ap2.resno ){
                    return -1;
                }else if( ap1.resno > ap2.resno ){
                    return 1;
                }else{
                    return 0;
                }
            }
        }
    }

    structure.atomStore.sort( compareModelChainResno );

    if( NGL.debug ) NGL.timeEnd( "NGL.reorderAtoms" );

};


NGL.StructureBuilder = function( structure ){

    var currentModelindex = null;
    var currentChainname = null;
    var currentResname = null;
    var currentResno = null;
    var currentInscode = null;
    var currentHetero = null;

    var previousResname;
    var previousHetero

    var atomStore = structure.atomStore;
    var residueStore = structure.residueStore;
    var chainStore = structure.chainStore;
    var modelStore = structure.modelStore;

    var residueMap = structure.residueMap;

    var ai = -1;
    var ri = -1;
    var ci = -1;
    var mi = -1;

    function addResidueType( ri ){
        var count = residueStore.atomCount[ ri ];
        var offset = residueStore.atomOffset[ ri ];
        var atomTypeIdList = new Array( count );
        for( var i = 0; i < count; ++i ){
            atomTypeIdList[ i ] = atomStore.atomTypeId[ offset + i ];
        }
        residueStore.residueTypeId[ ri ] = residueMap.add(
            previousResname, atomTypeIdList, previousHetero
        );
    }

    this.addAtom = function( modelindex, chainname, resname, resno, hetero, sstruc, inscode ){

        var addModel = false;
        var addChain = false;
        var addResidue = false;

        if( currentModelindex !== modelindex ){
            addModel = true;
            addChain = true;
            addResidue = true;
            mi += 1;
            ci += 1;
            ri += 1;
        }else if( currentChainname !== chainname ){
            addChain = true;
            addResidue = true;
            ci += 1;
            ri += 1;
        }else if( currentResno !== resno || currentResname !== resname || currentInscode !== inscode ){
            addResidue = true;
            ri += 1;
        }
        ai += 1;

        if( addModel ){
            modelStore.growIfFull();
            modelStore.chainOffset[ mi ] = ci;
            modelStore.chainCount[ mi ] = 0;
            modelStore.count += 1;
            chainStore.modelIndex[ ci ] = mi;
        }

        if( addChain ){
            chainStore.growIfFull();
            chainStore.setChainname( ci, chainname );
            chainStore.residueOffset[ ci ] = ri;
            chainStore.residueCount[ ci ] = 0;
            chainStore.count += 1;
            chainStore.modelIndex[ ci ] = mi;
            modelStore.chainCount[ mi ] += 1;
            residueStore.chainIndex[ ri ] = ci;
        }

        if( addResidue ){
            previousResname = currentResname;
            previousHetero = currentHetero;
            if( ri > 0 ) addResidueType( ri - 1 );
            residueStore.growIfFull();
            residueStore.resno[ ri ] = resno;
            if( sstruc !== undefined ){
                residueStore.sstruc[ ri ] = sstruc.charCodeAt( 0 );
            }
            if( inscode !== undefined ){
                residueStore.inscode[ ri ] = inscode.charCodeAt( 0 );
            }
            residueStore.atomOffset[ ri ] = ai;
            residueStore.atomCount[ ri ] = 0;
            residueStore.count += 1;
            residueStore.chainIndex[ ri ] = ci;
            chainStore.residueCount[ ci ] += 1;
        }

        atomStore.count += 1;
        atomStore.residueIndex[ ai ] = ri;
        residueStore.atomCount[ ri ] += 1;

        currentModelindex = modelindex;
        currentChainname = chainname;
        currentResname = resname;
        currentResno = resno;
        currentInscode = inscode;
        currentHetero = hetero;

    };

    this.finalize = function(){
        previousResname = currentResname;
        previousHetero = currentHetero;
        if( ri > -1 ) addResidueType( ri );
    };

};


NGL.assignSecondaryStructure = function( structure ){

    if( NGL.debug ) NGL.time( "NGL.assignSecondaryStructure" );

    var chainnames = [];
    structure.eachModel( function( mp ){
        mp.eachChain( function( cp ){
            chainnames.push( cp.chainname );
        } );
    } );

    var chainnamesSorted = chainnames.slice().sort();
    var chainnamesIndex = [];
    chainnamesSorted.forEach( function( c ){
        chainnamesIndex.push( chainnames.indexOf( c ) );
    } );

    // helix assignment

    var helices = structure.helices || [];

    helices = helices.filter( function( h ){
        return NGL.binarySearchIndexOf( chainnamesSorted, h[ 0 ] ) >= 0;
    } );

    helices.sort( function( h1, h2 ){

        var c1 = h1[ 0 ];
        var c2 = h2[ 0 ];
        var r1 = h1[ 1 ];
        var r2 = h2[ 1 ];

        if( c1 === c2 ){
            if( r1 === r2 ){
                return 0;
            }else{
                return r1 < r2 ? -1 : 1;
            }
        }else{
            var idx1 = NGL.binarySearchIndexOf( chainnamesSorted, c1 );
            var idx2 = NGL.binarySearchIndexOf( chainnamesSorted, c2 );
            return chainnamesIndex[ idx1 ] < chainnamesIndex[ idx2 ] ? -1 : 1;
        }

    } );

    var residueStore = structure.residueStore;

    structure.eachModel( function( mp ){

        var i = 0;
        var n = helices.length;
        if( n === 0 ) return;
        var helix = helices[ i ];
        var helixRun = false;
        var done = false;

        mp.eachChain( function( cp ){

            var chainChange = false;

            if( cp.chainname === helix[ 0 ] ){

                var count = cp.residueCount;
                var offset = cp.residueOffset;
                var end = offset + count;

                for( var j = offset; j < end; ++j ){

                    if( residueStore.resno[ j ] === helix[ 1 ] &&  // resnoBeg
                        residueStore.getInscode( j ) === helix[ 2 ]   // inscodeBeg
                    ){
                        helixRun = true;
                    }

                    if( helixRun ){

                        residueStore.sstruc[ j ] = helix[ 6 ];

                        if( residueStore.resno[ j ] === helix[ 4 ] &&  // resnoEnd
                            residueStore.getInscode( j ) === helix[ 5 ]   // inscodeEnd
                        ){

                            helixRun = false
                            i += 1;

                            if( i < n ){
                                // must look at previous residues as
                                // residues may not be ordered by resno
                                j = offset - 1;
                                helix = helices[ i ];
                                chainChange = cp.chainname !== helix[ 0 ];
                            }else{
                                done = true;
                            }

                        }

                    }

                    if( chainChange || done ) return;

                }

            }

        } );

    } );

    // sheet assignment

    var sheets = structure.sheets || [];

    sheets = sheets.filter( function( s ){
        return NGL.binarySearchIndexOf( chainnamesSorted, s[ 0 ] ) >= 0;
    } );

    sheets.sort( function( s1, s2 ){

        var c1 = s1[ 0 ];
        var c2 = s2[ 0 ];

        if( c1 === c2 ) return 0;
        var idx1 = NGL.binarySearchIndexOf( chainnamesSorted, c1 );
        var idx2 = NGL.binarySearchIndexOf( chainnamesSorted, c2 );
        return chainnamesIndex[ idx1 ] < chainnamesIndex[ idx2 ] ? -1 : 1;

    } );

    var strandCharCode = "e".charCodeAt( 0 );
    structure.eachModel( function( mp ){

        var i = 0;
        var n = sheets.length;
        if( n === 0 ) return;
        var sheet = sheets[ i ];
        var sheetRun = false;
        var done = false;

        mp.eachChain( function( cp ){

            var chainChange = false;

            if( cp.chainname === sheet[ 0 ] ){

                var count = cp.residueCount;
                var offset = cp.residueOffset;
                var end = offset + count;

                for( var j = offset; j < end; ++j ){

                    if( residueStore.resno[ j ] === sheet[ 1 ] &&  // resnoBeg
                        residueStore.getInscode( j ) === sheet[ 2 ]   // inscodeBeg
                    ){
                        sheetRun = true;
                    }

                    if( sheetRun ){

                        residueStore.sstruc[ j ] = strandCharCode;

                        if( residueStore.resno[ j ] === sheet[ 4 ] &&  // resnoEnd
                            residueStore.getInscode( j ) === sheet[ 5 ]   // inscodeEnd
                        ){

                            sheetRun = false
                            i += 1;

                            if( i < n ){
                                // must look at previous residues as
                                // residues may not be ordered by resno
                                j = offset - 1;
                                sheet = sheets[ i ];
                                chainChange = cp.chainname !== sheet[ 0 ];
                            }else{
                                done = true;
                            }

                        }

                    }

                    if( chainChange || done ) return;

                }

            }

        } );

    } );

    if( NGL.debug ) NGL.timeEnd( "NGL.assignSecondaryStructure" );

};


NGL.calculateSecondaryStructure = function(){

    // Implementation for proteins based on "pv"
    //
    // assigns secondary structure information based on a simple and very fast
    // algorithm published by Zhang and Skolnick in their TM-align paper.
    // Reference:
    //
    // TM-align: a protein structure alignment algorithm based on the Tm-score
    // (2005) NAR, 33(7) 2302-2309

    var zhangSkolnickSS = function( polymer, i, distances, delta ){

        var structure = polymer.structure;
        var offset = polymer.residueIndexStart;
        var rp1 = structure.getResidueProxy();
        var rp2 = structure.getResidueProxy();
        var ap1 = structure.getAtomProxy();
        var ap2 = structure.getAtomProxy();

        for( var j = Math.max( 0, i - 2 ); j <= i; ++j ){

            for( var k = 2;  k < 5; ++k ){

                if( j + k >= polymer.residueCount ){
                    continue;
                }

                rp1.index = offset + j;
                rp2.index = offset + j + k;
                ap1.index = rp1.traceAtomIndex;
                ap2.index = rp2.traceAtomIndex;

                var d = ap1.distanceTo( ap2 );
                // NGL.log( d )

                if( Math.abs( d - distances[ k - 2 ] ) > delta ){
                    return false;
                }

            }

        }

        return true;

    };

    var isHelical = function( polymer, i ){
        var helixDistances = [ 5.45, 5.18, 6.37 ];
        var helixDelta = 2.1;
        return zhangSkolnickSS( polymer, i, helixDistances, helixDelta );
    };

    var isSheet = function( polymer, i ){
        var sheetDistances = [ 6.1, 10.4, 13.0 ];
        var sheetDelta = 1.42;
        return zhangSkolnickSS( polymer, i, sheetDistances, sheetDelta );
    };

    var proteinPolymer = function( p ){
        var residueStore = p.residueStore;
        var offset = p.residueIndexStart;
        for( var i = 0, il = p.residueCount; i < il; ++i ){
            var sstruc = "c";
            if( isHelical( p, i ) ){
                sstruc = "h";
            }else if( isSheet( p, i ) ){
                sstruc = "s";
            }
            residueStore.sstruc[ offset + i ] = sstruc.charCodeAt( 0 );
        }
    }

    var cgPolymer = function( p ){

        var localAngle = 20;
        var centerDist = 2.0;

        var residueStore = p.residueStore;
        var offset = p.residueIndexStart;

        var helixbundle = new NGL.Helixbundle( p );
        var pos = helixbundle.position;

        var c1 = new THREE.Vector3();
        var c2 = new THREE.Vector3();

        for( var i = 0, il = p.residueCount; i < il; ++i ){

            c1.fromArray( pos.center, i * 3 );
            c2.fromArray( pos.center, i * 3 + 3 );
            var d = c1.distanceTo( c2 );

            if( d < centerDist && d > 1.0 && pos.bending[ i ] < localAngle ){
                residueStore.sstruc[ offset + i ] = "h".charCodeAt( 0 );
                residueStore.sstruc[ offset + i + 1 ] = "h".charCodeAt( 0 );
            }

        }

    }

    return function( structure ){

        if( NGL.debug ) NGL.time( "NGL.Structure.autoSS" );

        structure.eachPolymer( function( p ){

            // assign secondary structure
            if( p.residueCount < 4 ) return;
            if( p.isCg() ){
                cgPolymer( p );
            }else if( p.isProtein() ){
                proteinPolymer( p );
            }else{
                return;
            }

            // set lone secondary structure assignments to "c"
            var prevSstruc = undefined;
            var sstrucCount = 0;
            p.eachResidue( function( r ){
                if( r.sstruc === prevSstruc ){
                    sstrucCount += 1;
                }else{
                    if( sstrucCount === 1 ){
                        r.index -= 1;
                        r.sstruc = "c";
                    }
                    sstrucCount = 1;
                    prevSstruc = r.sstruc;
                }
            } );

        } );

        if( NGL.debug ) NGL.timeEnd( "NGL.Structure.autoSS" );

    }

}();


NGL.calculateChainnames = function( structure ){

    if( NGL.debug ) NGL.time( "NGL.calculateChainnames" );

    var doAutoChainName = true;
    structure.eachChain( function( c ){
        if( c.chainname ) doAutoChainName = false;
    } );

    if( doAutoChainName ){

        // var names = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        //             "abcdefghijklmnopqrstuvwxyz" +
        //             "0123456789";
        var names = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        var n = names.length;

        var modelStore = structure.modelStore;
        var chainStore = structure.chainStore;
        var residueStore = structure.residueStore;

        var addChain = function( mIndex, chainname, rOffset, rCount ){
            var ci = chainStore.count;
            for( var i = 0; i < rCount; ++i ){
                residueStore.chainIndex[ rOffset + i ] = ci;
            }
            chainStore.growIfFull();
            chainStore.modelIndex[ ci ] = mIndex;
            chainStore.setChainname( ci, chainname );
            chainStore.residueOffset[ ci ] = rOffset;
            chainStore.residueCount[ ci ] = rCount;
            chainStore.count += 1;
            modelStore.chainCount[ mIndex ] += 1;
        }

        var ap1 = structure.getAtomProxy();
        var ap2 = structure.getAtomProxy();

        var i = 0;
        var mi = 0;
        var rStart = 0;
        var rEnd = 0;
        var chainData = [];

        if( structure.residueStore.count === 1 ){

            chainData.push( {
                mIndex: 0,
                chainname: "A",
                rStart: 0,
                rCount: 1
            } );

        }else{

            structure.eachResidueN( 2, function( rp1, rp2 ){

                var newChain = false;

                var bbType1 = rp1.backboneType;
                var bbType2 = rp2.backboneType;
                var bbTypeUnk = NGL.UnknownBackboneType;

                rEnd = rp1.index;

                if( rp1.modelIndex !== rp2.modelIndex ){
                    newChain = true;
                }else if( rp1.moleculeType !== rp2.moleculeType ){
                    newChain = true;
                }else if( bbType1 !== bbTypeUnk && bbType1 === bbType2 ){
                    ap1.index = rp1.backboneEndAtomIndex;
                    ap2.index = rp2.backboneStartAtomIndex;
                    if( !ap1.connectedTo( ap2 ) ){
                        newChain = true;
                    }
                }

                if( rp2.index === residueStore.count - 1 ){
                    newChain = true;
                    rEnd = rp2.index;
                }

                if( newChain ){
                    var j = i;
                    var k = 0;
                    var chainname = names[ j % n ];

                    while( j >= n ){
                        j = Math.floor( j / n );
                        chainname += names[ j % n ];
                        k += 1;
                    }

                    chainData.push( {
                        mIndex: mi,
                        chainname: chainname,
                        rStart: rStart,
                        rCount: rEnd - rStart + 1
                    } );

                    i += 1;

                    if( rp1.modelIndex !== rp2.modelIndex ){
                        i = 0;
                        mi += 1;
                    }

                    if( k >= 5 ){
                        NGL.warn( "out of chain names" );
                        i = 0;
                    }

                    rStart = rp2.index;
                    rEnd = rp2.index;

                }

            } );

        }

        //

        chainStore.count = 0;
        chainData.forEach( function( d ){
            addChain( d.mIndex, d.chainname, d.rStart, d.rCount );
        } );

        var chainOffset = 0;
        structure.eachModel( function( mp ){
            modelStore.chainOffset[ mp.index ] = chainOffset;
            modelStore.chainCount[ mp.index ] -= 1;
            chainOffset += modelStore.chainCount[ mp.index ];
        } );

    }

    if( NGL.debug ) NGL.timeEnd( "NGL.calculateChainnames" );

};


NGL.calculateBonds = function( structure ){

    if( NGL.debug ) NGL.time( "NGL.calculateBonds" );

    NGL.calculateBondsWithin( structure );
    NGL.calculateBondsBetween( structure );

    if( NGL.debug ) NGL.timeEnd( "NGL.calculateBonds" );

};


NGL.calculateResidueBonds = function( r ){

    // if( NGL.debug ) NGL.time( "NGL.calculateResidueBonds" );

    var structure = r.structure;
    var a1 = structure.getAtomProxy();
    var a2 = structure.getAtomProxy();

    var count = r.atomCount;
    var offset = r.atomOffset;
    var end = offset + count;
    var end1 = end - 1;

    if( count > 500 ){
        if( NGL.debug ) NGL.warn( "more than 500 atoms, skip residue for auto-bonding", r.qualifiedName() );
        return;
    }

    var atomIndices1 = [];
    var atomIndices2 = [];

    if( count > 50 ){

        var kdtree = new NGL.Kdtree( r, true );
        var radius = r.isCg() ? 1.2 : 2.3;

        for( var i = offset; i < end1; ++i ){
            a1.index = i;
            var maxd = a1.covalent + radius + 0.3;
            var nearestAtoms = kdtree.nearest(
                a1, Infinity, maxd * maxd
            );
            var m = nearestAtoms.length;
            for( var j = 0; j < m; ++j ){
                a2.index = nearestAtoms[ j ].index;
                if( a1.index < a2.index ){
                    if( a1.connectedTo( a2 ) ){
                        atomIndices1.push( a1.index - offset );
                        atomIndices2.push( a2.index - offset );
                    };
                }
            }
        }

    }else{

        for( var i = offset; i < end1; ++i ){
            a1.index = i;
            for( var j = i + 1; j <= end1; ++j ){
                a2.index = j;
                if( a1.connectedTo( a2 ) ){
                    atomIndices1.push( i - offset );
                    atomIndices2.push( j - offset );
                }
            }
        }

    }

    // if( NGL.debug ) NGL.timeEnd( "NGL.calculateResidueBonds" );

    return {
        atomIndices1: atomIndices1,
        atomIndices2: atomIndices2
    };

};


NGL.calculateBondsWithin = function( structure, onlyAddRung ){

    if( NGL.debug ) NGL.time( "NGL.calculateBondsWithin" );

    var bondStore = structure.bondStore;
    var rungBondStore = structure.rungBondStore;
    var rungAtomSet = structure.getAtomSet( false );
    var a1 = structure.getAtomProxy();
    var a2 = structure.getAtomProxy();

    structure.eachResidue( function( r ){

        if( !onlyAddRung ){

            var count = r.atomCount;
            var offset = r.atomOffset;
            var end = offset + count;
            var end1 = end - 1;

            if( count > 500 ){
                NGL.warn( "more than 500 atoms, skip residue for auto-bonding", r.qualifiedName() );
                return;
            }

            var bonds = r.getBonds();
            var atomIndices1 = bonds.atomIndices1;
            var atomIndices2 = bonds.atomIndices2;
            var nn = atomIndices1.length;

            for( var i = 0; i < nn; ++i ){
                a1.index = atomIndices1[ i ] + offset;
                a2.index = atomIndices2[ i ] + offset;
                bondStore.addBond( a1, a2 );
            }

        }

        // get RNA/DNA rung pseudo bonds
        var traceAtomIndex = r.residueType.traceAtomIndex;
        var rungEndAtomIndex = r.residueType.rungEndAtomIndex;
        if( traceAtomIndex !== -1 && rungEndAtomIndex !== -1 ){
            a1.index = r.traceAtomIndex;
            a2.index = r.rungEndAtomIndex;
            rungBondStore.addBond( a1, a2 );
            rungAtomSet.add_unsafe( a1.index );
            rungAtomSet.add_unsafe( a2.index );
        }

    } );

    structure.atomSetDict[ "rung" ] = rungAtomSet;

    if( NGL.debug ) NGL.timeEnd( "NGL.calculateBondsWithin" );

};


NGL.calculateBondsBetween = function( structure, onlyAddBackbone ){

    if( NGL.debug ) NGL.time( "NGL.calculateBondsBetween" );

    var bondStore = structure.bondStore;
    var backboneBondStore = structure.backboneBondStore;
    var backboneAtomSet = structure.getAtomSet( false );
    var ap1 = structure.getAtomProxy();
    var ap2 = structure.getAtomProxy();

    if( backboneBondStore.count === 0 ){
        backboneBondStore.resize( structure.residueStore.count );
    }

    function addBondIfConnected( rp1, rp2 ){
        var bbType1 = rp1.backboneType;
        var bbType2 = rp2.backboneType;
        if( bbType1 !== NGL.UnknownBackboneType && bbType1 === bbType2 ){
            ap1.index = rp1.backboneEndAtomIndex;
            ap2.index = rp2.backboneStartAtomIndex;
            if( ap1.connectedTo( ap2 ) ){
                if( !onlyAddBackbone ){
                    bondStore.addBond( ap1, ap2 );
                }
                ap1.index = rp1.traceAtomIndex;
                ap2.index = rp2.traceAtomIndex;
                backboneBondStore.addBond( ap1, ap2 );
                backboneAtomSet.add_unsafe( ap1.index );
                backboneAtomSet.add_unsafe( ap2.index );
            }
        }
    }

    structure.eachResidueN( 2, addBondIfConnected );

    var rp1 = structure.getResidueProxy();
    var rp2 = structure.getResidueProxy();

    // check for cyclic chains
    structure.eachChain( function( cp ){
        if( cp.residueCount === 0 ) return;
        rp1.index = cp.residueOffset;
        rp2.index = cp.residueOffset + cp.residueCount - 1;
        addBondIfConnected( rp2, rp1 );
    } );

    structure.atomSetDict[ "backbone" ] = backboneAtomSet;

    if( NGL.debug ) NGL.timeEnd( "NGL.calculateBondsBetween" );

};


NGL.buildUnitcellAssembly = function( structure ){

    if( NGL.debug ) NGL.time( "NGL.buildUnitcellAssembly" );

    var uc = structure.unitcell;

    var centerFrac = structure.center.clone().applyMatrix4( uc.cartToFrac );
    var symopDict = NGL.getSymmetryOperations( uc.spacegroup );

    var positionFrac = new THREE.Vector3();
    var centerFracSymop = new THREE.Vector3();
    var positionFracSymop = new THREE.Vector3();

    if( centerFrac.x > 1 ) positionFrac.x -= 1;
    if( centerFrac.x < 0 ) positionFrac.x += 1;
    if( centerFrac.y > 1 ) positionFrac.y -= 1;
    if( centerFrac.y < 0 ) positionFrac.y += 1;
    if( centerFrac.z > 1 ) positionFrac.z -= 1;
    if( centerFrac.z < 0 ) positionFrac.z += 1;

    function getMatrixList( shift ){

        var matrixList = [];

        Object.keys( symopDict ).forEach( function( name ){

            var m = symopDict[ name ].clone();

            centerFracSymop.copy( centerFrac ).applyMatrix4( m );
            positionFracSymop.setFromMatrixPosition( m );
            positionFracSymop.sub( positionFrac );

            if( centerFracSymop.x > 1 ) positionFracSymop.x -= 1;
            if( centerFracSymop.x < 0 ) positionFracSymop.x += 1;
            if( centerFracSymop.y > 1 ) positionFracSymop.y -= 1;
            if( centerFracSymop.y < 0 ) positionFracSymop.y += 1;
            if( centerFracSymop.z > 1 ) positionFracSymop.z -= 1;
            if( centerFracSymop.z < 0 ) positionFracSymop.z += 1;

            if( shift ) positionFracSymop.add( shift );

            m.setPosition( positionFracSymop );
            m.multiplyMatrices( uc.fracToCart, m );
            m.multiply( uc.cartToFrac );

            matrixList.push( m );

        } );

        return matrixList;

    }

    var unitcellAssembly = new NGL.Assembly( "UNITCELL" );
    var unitcellMatrixList = getMatrixList();
    if( structure.biomolDict[ "NCS" ] ){
        var ncsUnitcellMatrixList = [];
        var ncsMatrixList = structure.biomolDict[ "NCS" ].partList[ 0 ].matrixList;
        unitcellMatrixList.forEach( function( sm ){
            ncsMatrixList.forEach( function( nm ){
                ncsUnitcellMatrixList.push( sm.clone().multiply( nm ) );
            } );
        } );
        unitcellAssembly.addPart( ncsUnitcellMatrixList );
    }else{
        unitcellAssembly.addPart( unitcellMatrixList );
    }

    var vec = new THREE.Vector3();
    var supercellAssembly = new NGL.Assembly( "SUPERCELL" );
    var supercellMatrixList = Array.prototype.concat.call(
        getMatrixList(),                         // 555
        getMatrixList( vec.set(  1,  1,  1 ) ),  // 666
        getMatrixList( vec.set( -1, -1, -1 ) ),  // 444

        getMatrixList( vec.set(  1,  0,  0 ) ),  // 655
        getMatrixList( vec.set(  1,  1,  0 ) ),  // 665
        getMatrixList( vec.set(  1,  0,  1 ) ),  // 656
        getMatrixList( vec.set(  0,  1,  0 ) ),  // 565
        getMatrixList( vec.set(  0,  1,  1 ) ),  // 566
        getMatrixList( vec.set(  0,  0,  1 ) ),  // 556

        getMatrixList( vec.set( -1,  0,  0 ) ),  // 455
        getMatrixList( vec.set( -1, -1,  0 ) ),  // 445
        getMatrixList( vec.set( -1,  0, -1 ) ),  // 454
        getMatrixList( vec.set(  0, -1,  0 ) ),  // 545
        getMatrixList( vec.set(  0, -1, -1 ) ),  // 544
        getMatrixList( vec.set(  0,  0, -1 ) ),  // 554

        getMatrixList( vec.set(  1, -1, -1 ) ),  // 644
        getMatrixList( vec.set(  1,  1, -1 ) ),  // 664
        getMatrixList( vec.set(  1, -1,  1 ) ),  // 646
        getMatrixList( vec.set( -1,  1,  1 ) ),  // 466
        getMatrixList( vec.set( -1, -1,  1 ) ),  // 446
        getMatrixList( vec.set( -1,  1, -1 ) ),  // 464

        getMatrixList( vec.set(  0,  1, -1 ) ),  // 564
        getMatrixList( vec.set(  0, -1,  1 ) ),  // 546
        getMatrixList( vec.set(  1,  0, -1 ) ),  // 654
        getMatrixList( vec.set( -1,  0,  1 ) ),  // 456
        getMatrixList( vec.set(  1, -1,  0 ) ),  // 645
        getMatrixList( vec.set( -1,  1,  0 ) )   // 465
    );
    if( structure.biomolDict[ "NCS" ] ){
        var ncsSupercellMatrixList = [];
        var ncsMatrixList = structure.biomolDict[ "NCS" ].partList[ 0 ].matrixList;
        supercellMatrixList.forEach( function( sm ){
            ncsMatrixList.forEach( function( nm ){
                ncsSupercellMatrixList.push( sm.clone().multiply( nm ) );
            } );
        } );
        supercellAssembly.addPart( ncsSupercellMatrixList );
    }else{
        supercellAssembly.addPart( supercellMatrixList );
    }

    structure.biomolDict[ "UNITCELL" ] = unitcellAssembly;
    structure.biomolDict[ "SUPERCELL" ] = supercellAssembly;

    if( NGL.debug ) NGL.timeEnd( "NGL.buildUnitcellAssembly" );

};


NGL.Assembly = function( name ){

    this.name = name || "";
    this.partList = []

};

NGL.Assembly.prototype = {

    constructor: NGL.Assembly,
    type: "Assembly",

    addPart: function( matrixList, chainList ){
        var part = new NGL.AssemblyPart( matrixList, chainList );
        this.partList.push( part );
        return part;
    },

    getAtomCount: function( structure ){

        var atomCount = 0;

        this.partList.forEach( function( part ){
            atomCount += part.getAtomCount( structure );
        } );

        return atomCount;

    },

    getInstanceCount: function(){

        var instanceCount = 0;

        this.partList.forEach( function( part ){
            instanceCount += part.matrixList.length;
        } );

        return instanceCount;

    },

    isIdentity: function( structure ){

        if( this.partList.length !== 1 ) return false;

        var part = this.partList[ 0 ];
        if( part.matrixList.length !== 1 ) return false;

        var identityMatrix = new THREE.Matrix4();
        if( !identityMatrix.equals( part.matrixList[ 0 ] ) ) return false;

        var structureChainList = [];
        structure.eachChain( function( cp ){
            structureChainList.push( cp.chainname );
        } );
        structureChainList = NGL.uniqueArray( structureChainList );
        if( part.chainList.length !== structureChainList.length ) return false;

        return true;

    },

    toJSON: function(){

        var output = {
            name: this.name,
            partList: new Array( this.partList.length )
        };

        this.partList.forEach( function( part, i ){
            output.partList[ i ] = part.toJSON()
        } );

        return output;

    },

    fromJSON: function( input ){

        this.name = input.name;
        this.partList = input.partList;

        this.partList.forEach( function( part, i ){
            this.partList[ i ] = new NGL.AssemblyPart().fromJSON( part );
        }.bind( this ) );

        return this;

    }

};


NGL.AssemblyPart = function( matrixList, chainList ){

    this.matrixList = matrixList || [];
    this.chainList = chainList || [];

};

NGL.AssemblyPart.prototype = {

    constructor: NGL.AssemblyPart,
    type: "AssemblyPart",

    getAtomCount: function( structure ){

        var atomCount = 0;
        var chainList = this.chainList;

        structure.eachChain( function( cp ){
            if( chainList.length === 0 || chainList.indexOf( cp.chainname ) != -1 ){
                atomCount += cp.atomCount;
            }
        } );

        return this.matrixList.length * atomCount;

    },

    getSelection: function(){
        if( this.chainList.length > 0 ){
            var chainList = NGL.uniqueArray( this.chainList );
            var sele = ":" + chainList.join( " OR :" );
            return new NGL.Selection( sele );
        }else{
            return new NGL.Selection( "" );
        }
    },

    getView: function( structure ){
        var selection = this.getSelection();
        if( selection ){
            return structure.getView( selection );
        }else{
            return structure;
        }
    },

    getInstanceList: function(){
        var instanceList = [];
        for ( var j = 0, jl = this.matrixList.length; j < jl; ++j ){
            instanceList.push( {
                id: j + 1,
                name: j,
                matrix: this.matrixList[ j ]
            } );
        }
        return instanceList;
    },

    toJSON: function(){

        var output = {
            matrixList: this.matrixList,
            chainList: this.chainList
        };

        return output;

    },

    fromJSON: function( input ){

        this.matrixList = input.matrixList;
        this.chainList = input.chainList;

        return this;

    }

};


///////////
// Parser

NGL.WorkerRegistry.add( "parse", function( e, callback ){

    if( NGL.debug ) NGL.time( "WORKER parse" );

    var parser = NGL.fromJSON( e.data );

    parser.parse( function(){

        if( NGL.debug ) NGL.timeEnd( "WORKER parse" );

        // no need to return the streamer data
        parser.streamer.dispose();

        callback( parser.toJSON(), parser.getTransferable() );

    } );

} );


NGL.Parser = function( streamer, params ){

    var p = params || {};

    this.streamer = streamer;

    this.name = p.name;
    this.path = p.path;

};

NGL.Parser.prototype = {

    constructor: NGL.Parser,
    type: "",

    __objName: "",

    parse: function( callback ){

        var self = this;

        this.streamer.read( function(){
            self._beforeParse();
            self._parse( function(){
                self._afterParse();
                callback( self[ self.__objName ] );
            } );
        } );

        return this[ this.__objName ];

    },

    parseWorker: function( callback ){

        if( typeof Worker !== "undefined" && typeof importScripts !== 'function' ){

            var worker = new NGL.Worker( "parse" ).post(

                this.toJSON(),

                this.getTransferable(),

                function( e ){

                    worker.terminate();

                    this.fromJSON( e.data );
                    this._afterWorker( callback );

                }.bind( this ),

                function( e ){

                    NGL.warn(
                        "NGL.Parser.parseWorker error - trying without worker", e
                    );
                    worker.terminate();

                    this.parse( callback );

                }.bind( this )

            );

        }else{

            this.parse( callback );

        }

        return this[ this.__objName ];

    },

    _parse: function( callback ){

        NGL.warn( "NGL.Parser._parse not implemented" );
        callback();

    },

    _beforeParse: function(){},

    _afterParse: function(){

        if( NGL.debug ) NGL.log( this[ this.__objName ] );

    },

    _afterWorker: function( callback ){

        if( NGL.debug ) NGL.log( this[ this.__objName ] );
        callback( this[ this.__objName ] );

    },

    toJSON: function(){

        var type = this.type.substr( 0, 1 ).toUpperCase() +
                    this.type.substr( 1 );

        var output = {

            metadata: {
                version: 0.1,
                type: type + 'Parser',
                generator: type + 'ParserExporter'
            },

            streamer: this.streamer.toJSON(),
            name: this.name,
            path: this.path,

        }

        if( typeof this[ this.__objName ].toJSON === "function" ){

            output[ this.__objName ] = this[ this.__objName ].toJSON();

        }else{

            output[ this.__objName ] = this[ this.__objName ];

        }

        return output;

    },

    fromJSON: function( input ){

        this.streamer = NGL.fromJSON( input.streamer );
        this.name = input.name;
        this.path = input.path;

        if( typeof this[ this.__objName ].toJSON === "function" ){

            this[ this.__objName ].fromJSON( input[ this.__objName ] );

        }else{

            this[ this.__objName ] = input[ this.__objName ];

        }

        return this;

    },

    getTransferable: function(){

        var transferable = [];

        transferable = transferable.concat(
            this.streamer.getTransferable()
        );

        if( typeof this[ this.__objName ].toJSON === "function" ){

            transferable = transferable.concat(
                this[ this.__objName ].getTransferable()
            );

        }

        return transferable;

    }

};


////////////////////
// StructureParser

NGL.StructureParser = function( streamer, params ){

    var p = params || {};

    this.firstModelOnly = p.firstModelOnly || false;
    this.asTrajectory = p.asTrajectory || false;
    this.cAlphaOnly = p.cAlphaOnly || false;
    this.reorderAtoms = p.reorderAtoms || false;
    this.dontAutoBond = p.dontAutoBond || false;
    this.doAutoSS = true;

    NGL.Parser.call( this, streamer, p );

    this.structure = new NGL.Structure( this.name, this.path );
    this.structureBuilder = new NGL.StructureBuilder( this.structure );

};

NGL.StructureParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.StructureParser,
    type: "structure",

    __objName: "structure",

    _afterParse: function(){

        if( NGL.debug ) NGL.time( "NGL.StructureParser._afterParse" );

        var s = this.structure;
        s.refresh();

        if( this.reorderAtoms ){
            NGL.reorderAtoms( s );
        }

        // check for chain names
        NGL.calculateChainnames( s );

        if( !this.dontAutoBond ){
            NGL.calculateBonds( s );
        }else if( this.autoBondBetween ){
            NGL.calculateBondsBetween( s );
        }

        // TODO
        // check for secondary structure
        if( this.doAutoSS && s.helices.length === 0 && s.sheets.length === 0 ){
            NGL.calculateSecondaryStructure( s );
        }

        if( s.helices.length > 0 || s.sheets.length > 0 ){
            NGL.assignSecondaryStructure( s );
        }

        this._postProcess();

        if( s.unitcell ){
            NGL.buildUnitcellAssembly( s );
        }

        if( NGL.debug ) NGL.timeEnd( "NGL.StructureParser._afterParse" );
        if( NGL.debug ) NGL.log( this[ this.__objName ] );

    },

    _postProcess: function(){},

    toJSON: function(){

        var output = NGL.Parser.prototype.toJSON.call( this );

        output.firstModelOnly = this.firstModelOnly;
        output.asTrajectory = this.asTrajectory;
        output.cAlphaOnly = this.cAlphaOnly;
        output.reorderAtoms = this.reorderAtoms;
        output.dontAutoBond = this.dontAutoBond;
        output.doAutoSS = this.doAutoSS;

        return output;

    },

    fromJSON: function( input ){

        NGL.Parser.prototype.fromJSON.call( this, input );

        this.firstModelOnly = input.firstModelOnly;
        this.asTrajectory = input.asTrajectory;
        this.cAlphaOnly = input.cAlphaOnly;
        this.reorderAtoms = input.reorderAtoms;
        this.dontAutoBond = input.dontAutoBond;
        this.doAutoSS = input.doAutoSS;

        return this;

    },

} );


NGL.PdbParser = function( streamer, params ){

    NGL.StructureParser.call( this, streamer, params );

};

NGL.PdbParser.prototype = NGL.createObject(

    NGL.StructureParser.prototype, {

    constructor: NGL.PdbParser,
    type: "pdb",

    _parse: function( callback ){

        // http://www.wwpdb.org/documentation/file-format.php

        if( NGL.debug ) NGL.time( "NGL.PdbParser._parse " + this.name );

        var isPqr = this.type === "pqr";
        var reWhitespace = /\s+/;

        var s = this.structure;
        var sb = this.structureBuilder;

        var firstModelOnly = this.firstModelOnly;
        var asTrajectory = this.asTrajectory;
        var cAlphaOnly = this.cAlphaOnly;

        var frames = s.frames;
        var boxes = s.boxes;
        var doFrames = false;
        var currentFrame, currentCoord;

        var id = s.id;
        var title = s.title;

        var atoms = s.atoms;
        var bondSet = s.bondSet;
        var helices = s.helices;
        var sheets = s.sheets;
        var biomolDict = s.biomolDict;
        var currentBiomol;
        var currentPart;
        var currentMatrix;

        var helixTypes = NGL.HelixTypes;

        var line, recordName;
        var serial, chainname, resno, resname, occupancy,
            inscode, atomname, element, hetero, bfactor, altloc;

        var serialDict = {};
        var unitcellDict = {};

        s.hasConnect = false;

        var atomMap = s.atomMap;
        var atomStore = s.atomStore;
        atomStore.resize( Math.round( this.streamer.data.length / 80 ) );

        var ap1 = s.getAtomProxy();
        var ap2 = s.getAtomProxy();

        var idx = 0;
        var modelIdx = 0;
        var pendingStart = true;

        function _parseChunkOfLines( _i, _n, lines ){

            for( var i = _i; i < _n; ++i ){

                line = lines[ i ];
                recordName = line.substr( 0, 6 );

                if( recordName === 'ATOM  ' || recordName === 'HETATM' ){

                    // http://www.wwpdb.org/documentation/file-format-content/format33/sect9.html#ATOM
                    // PQR: Field_name Atom_number Atom_name Residue_name Chain_ID Residue_number X Y Z Charge Radius

                    if( pendingStart ){

                        if( asTrajectory ){

                            if( doFrames ){
                                currentFrame = new Float32Array( atomStore.count * 3 );
                                frames.push( currentFrame );
                            }else{
                                currentFrame = [];
                            }
                            currentCoord = 0;

                        }else{

                            if( !firstModelOnly ) serialDict = {};

                        }

                    }

                    pendingStart = false;

                    if( firstModelOnly && modelIdx > 0 ) continue;

                    if( isPqr ){

                        var ls = line.split( reWhitespace );
                        var dd = ls.length === 10 ? 1 : 0;

                        atomname = ls[ 2 ];
                        if( cAlphaOnly && atomname !== 'CA' ) continue;

                        var x = parseFloat( ls[ 6 - dd ] );
                        var y = parseFloat( ls[ 7 - dd ] );
                        var z = parseFloat( ls[ 8 - dd ] );

                    }else{

                        atomname = line.substr( 12, 4 ).trim();
                        if( cAlphaOnly && atomname !== 'CA' ) continue;

                        var x = parseFloat( line.substr( 30, 8 ) );
                        var y = parseFloat( line.substr( 38, 8 ) );
                        var z = parseFloat( line.substr( 46, 8 ) );

                    }

                    if( asTrajectory ){

                        var j = currentCoord * 3;

                        currentFrame[ j + 0 ] = x;
                        currentFrame[ j + 1 ] = y;
                        currentFrame[ j + 2 ] = z;

                        currentCoord += 1;

                        if( doFrames ) continue;

                    }

                    var element;

                    if( isPqr ){

                        serial = parseInt( ls[ 1 ] );
                        element = "";
                        hetero = ( line[ 0 ] === 'H' ) ? 1 : 0;
                        chainname = dd ? "" : ls[ 4 ];
                        resno = parseInt( ls[ 5 - dd ] );
                        inscode = "";
                        resname = ls[ 3 ];
                        bfactor = parseFloat( ls[ 9 - dd ] );  // charge FIXME should be its own field
                        altloc = "";
                        occupancy = 0.0;
                        // FIXME radius field not supported

                    }else{

                        serial = parseInt( line.substr( 6, 5 ) );
                        element = line.substr( 76, 2 ).trim();
                        hetero = ( line[ 0 ] === 'H' ) ? 1 : 0;
                        chainname = line[ 21 ].trim();
                        resno = parseInt( line.substr( 22, 4 ) );
                        inscode = line[ 26 ].trim();
                        resname = line.substr( 17, 4 ).trim();
                        bfactor = parseFloat( line.substr( 60, 6 ) );
                        altloc = line[ 16 ].trim();
                        occupancy = parseFloat( line.substr( 54, 6 ) );

                    }

                    atomStore.growIfFull();
                    atomStore.atomTypeId[ idx ] = atomMap.add( atomname, element );

                    atomStore.x[ idx ] = x;
                    atomStore.y[ idx ] = y;
                    atomStore.z[ idx ] = z;
                    atomStore.serial[ idx ] = serial;
                    atomStore.bfactor[ idx ] = isNaN( bfactor ) ? 0 : bfactor;
                    atomStore.altloc[ idx ] = altloc.charCodeAt( 0 );
                    atomStore.occupancy[ idx ] = isNaN( occupancy ) ? 0 : occupancy;

                    sb.addAtom( modelIdx, chainname, resname, resno, hetero, undefined, inscode );

                    serialDict[ serial ] = idx;

                    idx += 1;

                }else if( recordName === 'CONECT' ){

                    var from = serialDict[ parseInt( line.substr( 6, 5 ) ) ];
                    var pos = [ 11, 16, 21, 26 ];

                    if( from === undefined ){
                        // NGL.log( "missing CONNECT serial" );
                        continue;
                    }

                    for( var j = 0; j < 4; ++j ){

                        var to = parseInt( line.substr( pos[ j ], 5 ) );
                        if( Number.isNaN( to ) ) continue;
                        to = serialDict[ to ];
                        if( to === undefined ){
                            // NGL.log( "missing CONNECT serial" );
                            continue;
                        }/*else if( to < from ){
                            // likely a duplicate in standard PDB format
                            // but not necessarily, so better remove duplicates
                            // in a pass after parsing (and auto bonding)
                            continue;
                        }*/

                        ap1.index = from;
                        ap2.index = to;

                        s.bondStore.addBond( ap1, ap2 );

                    }

                    s.hasConnect = true;

                }else if( recordName === 'HELIX ' ){

                    var startChain = line[ 19 ].trim();
                    var startResi = parseInt( line.substr( 21, 4 ) );
                    var startIcode = line[ 25 ].trim();
                    var endChain = line[ 31 ].trim();
                    var endResi = parseInt( line.substr( 33, 4 ) );
                    var endIcode = line[ 37 ].trim();
                    var helixType = parseInt( line.substr( 39, 1 ) );
                    helixType = ( helixTypes[ helixType ] || helixTypes[""] ).charCodeAt( 0 );
                    helices.push( [
                        startChain, startResi, startIcode,
                        endChain, endResi, endIcode,
                        helixType
                    ] );

                }else if( recordName === 'SHEET ' ){

                    var startChain = line[ 21 ].trim();
                    var startResi = parseInt( line.substr( 22, 4 ) );
                    var startIcode = line[ 26 ].trim();
                    var endChain = line[ 32 ].trim();
                    var endResi = parseInt( line.substr( 33, 4 ) );
                    var endIcode = line[ 37 ].trim();
                    sheets.push( [
                        startChain, startResi, startIcode,
                        endChain, endResi, endIcode
                    ] );

                }else if( recordName === 'REMARK' && line.substr( 7, 3 ) === '350' ){

                    if( line.substr( 11, 12 ) === "BIOMOLECULE:" ){

                        var name = line.substr( 23 ).trim();
                        if( /^(0|[1-9][0-9]*)$/.test( name ) ) name = "BU" + name;

                        currentBiomol = new NGL.Assembly( name );
                        biomolDict[ name ] = currentBiomol;

                    }else if( line.substr( 13, 5 ) === "BIOMT" ){

                        var ls = line.split( /\s+/ );

                        var row = parseInt( line[ 18 ] ) - 1;
                        var mat = ls[ 3 ].trim();

                        if( row === 0 ){
                            currentMatrix = new THREE.Matrix4();
                            currentPart.matrixList.push( currentMatrix );
                        }

                        var elms = currentMatrix.elements;

                        elms[ 4 * 0 + row ] = parseFloat( ls[ 4 ] );
                        elms[ 4 * 1 + row ] = parseFloat( ls[ 5 ] );
                        elms[ 4 * 2 + row ] = parseFloat( ls[ 6 ] );
                        elms[ 4 * 3 + row ] = parseFloat( ls[ 7 ] );

                    }else if(
                        line.substr( 11, 30 ) === 'APPLY THE FOLLOWING TO CHAINS:' ||
                        line.substr( 11, 30 ) === '                   AND CHAINS:'
                    ){

                        if( line.substr( 11, 5 ) === 'APPLY' ){
                            currentPart = currentBiomol.addPart();
                        }

                        line.substr( 41, 30 ).split( "," ).forEach( function( v ){
                            var c = v.trim();
                            if( c ) currentPart.chainList.push( c );
                        } );

                    }

                }else if( recordName === 'HEADER' ){

                    id = line.substr( 62, 4 );

                }else if( recordName === 'TITLE ' ){

                    title += line.substr( 10, 70 ) + "\n";

                }else if( recordName === 'MODEL ' ){

                    pendingStart = true;

                }else if( recordName === 'ENDMDL' || line.substr( 0, 3 ) === 'END' ){

                    if( pendingStart ) continue;

                    if( asTrajectory && !doFrames ){

                        frames.push( new Float32Array( currentFrame ) );
                        doFrames = true;

                    }

                    modelIdx += 1;
                    pendingStart = true;

                }else if( recordName === 'MTRIX ' ){

                    var ls = line.split( /\s+/ );
                    var mat = ls[ 1 ].trim();

                    if( line[ 5 ] === "1" && mat === "1" ){
                        var name = "NCS";
                        currentBiomol = new NGL.Assembly( name );
                        biomolDict[ name ] = currentBiomol;
                        currentPart = currentBiomol.addPart();
                    }

                    var row = parseInt( line[ 5 ] ) - 1;

                    if( row === 0 ){
                        currentMatrix = new THREE.Matrix4();
                        currentPart.matrixList.push( currentMatrix );
                    }

                    var elms = currentMatrix.elements;

                    elms[ 4 * 0 + row ] = parseFloat( ls[ 2 ] );
                    elms[ 4 * 1 + row ] = parseFloat( ls[ 3 ] );
                    elms[ 4 * 2 + row ] = parseFloat( ls[ 4 ] );
                    elms[ 4 * 3 + row ] = parseFloat( ls[ 5 ] );

                }else if( line.substr( 0, 5 ) === 'ORIGX' ){

                    if( !unitcellDict.origx ){
                        unitcellDict.origx = new THREE.Matrix4();
                    }

                    var ls = line.split( /\s+/ );
                    var row = parseInt( line[ 5 ] ) - 1;
                    var elms = unitcellDict.origx.elements;

                    elms[ 4 * 0 + row ] = parseFloat( ls[ 1 ] );
                    elms[ 4 * 1 + row ] = parseFloat( ls[ 2 ] );
                    elms[ 4 * 2 + row ] = parseFloat( ls[ 3 ] );
                    elms[ 4 * 3 + row ] = parseFloat( ls[ 4 ] );

                }else if( line.substr( 0, 5 ) === 'SCALE' ){

                    if( !unitcellDict.scale ){
                        unitcellDict.scale = new THREE.Matrix4();
                    }

                    var ls = line.split( /\s+/ );
                    var row = parseInt( line[ 5 ] ) - 1;
                    var elms = unitcellDict.scale.elements;

                    elms[ 4 * 0 + row ] = parseFloat( ls[ 1 ] );
                    elms[ 4 * 1 + row ] = parseFloat( ls[ 2 ] );
                    elms[ 4 * 2 + row ] = parseFloat( ls[ 3 ] );
                    elms[ 4 * 3 + row ] = parseFloat( ls[ 4 ] );

                }else if( recordName === 'CRYST1' ){

                    // CRYST1   55.989   55.989   55.989  90.00  90.00  90.00 P 1           1
                    //  7 - 15       Real(9.3)      a (Angstroms)
                    // 16 - 24       Real(9.3)      b (Angstroms)
                    // 25 - 33       Real(9.3)      c (Angstroms)
                    // 34 - 40       Real(7.2)      alpha         alpha (degrees).
                    // 41 - 47       Real(7.2)      beta          beta (degrees).
                    // 48 - 54       Real(7.2)      gamma         gamma (degrees).
                    // 56 - 66       LString        sGroup        Space group.
                    // 67 - 70       Integer        z             Z value.

                    var a = parseFloat( line.substr( 6, 9 ) );
                    var b = parseFloat( line.substr( 15, 9 ) );
                    var c = parseFloat( line.substr( 24, 9 ) );

                    var alpha = parseFloat( line.substr( 33, 7 ) );
                    var beta = parseFloat( line.substr( 40, 7 ) );
                    var gamma = parseFloat( line.substr( 47, 7 ) );

                    var sGroup = line.substr( 55, 11 ).trim();
                    var z = parseInt( line.substr( 66, 4 ) );

                    var box = new Float32Array( 9 );
                    box[ 0 ] = a;
                    box[ 4 ] = b;
                    box[ 8 ] = c;
                    boxes.push( box );

                    if( modelIdx === 0 ){
                        unitcellDict.a = a;
                        unitcellDict.b = b;
                        unitcellDict.c = c;
                        unitcellDict.alpha = alpha;
                        unitcellDict.beta = beta;
                        unitcellDict.gamma = gamma;
                        unitcellDict.spacegroup = sGroup;
                    }

                }

            }

        }

        this.streamer.eachChunkOfLines( function( lines, chunkNo, chunkCount ){
            _parseChunkOfLines( 0, lines.length, lines );
        } );

        sb.finalize();

        if( unitcellDict.a !== undefined ){
            s.unitcell = new NGL.Unitcell(
                unitcellDict.a, unitcellDict.b, unitcellDict.c,
                unitcellDict.alpha, unitcellDict.beta, unitcellDict.gamma,
                unitcellDict.spacegroup, unitcellDict.scale
            );
        }else{
            s.unitcell = undefined;
        }

        if( NGL.debug ) NGL.timeEnd( "NGL.PdbParser._parse " + this.name );
        callback();

    }

} );


NGL.PqrParser = function( streamer, params ){

    NGL.StructureParser.call( this, streamer, params );

    // http://www.poissonboltzmann.org/docs/file-format-info/

};

NGL.PqrParser.prototype = NGL.createObject(

    NGL.PdbParser.prototype, {

    constructor: NGL.PqrParser,
    type: "pqr",

} );


NGL.GroParser = function( streamer, params ){

    NGL.StructureParser.call( this, streamer, params );

    this.structure._doAutoSS = true;
    this.structure._doAutoChainName = true;

};

NGL.GroParser.prototype = NGL.createObject(

    NGL.StructureParser.prototype, {

    constructor: NGL.GroParser,
    type: "gro",

    _parse: function( callback ){

        // http://manual.gromacs.org/current/online/gro.html

        if( NGL.debug ) NGL.time( "NGL.GroParser._parse " + this.name );

        var s = this.structure;
        var sb = this.structureBuilder;

        var firstModelOnly = this.firstModelOnly;
        var asTrajectory = this.asTrajectory;
        var cAlphaOnly = this.cAlphaOnly;

        var frames = s.frames;
        var boxes = s.boxes;
        var doFrames = false;
        var currentFrame, currentCoord;

        var firstLines = this.streamer.peekLines( 3 );

        s.title = firstLines[ 0 ].trim();

        // determine number of decimal places
        var ndec = firstLines[ 2 ].length - firstLines[ 2 ].lastIndexOf( "." ) - 1;
        var lpos = 5 + ndec;
        var xpos = 20;
        var ypos = 20 + lpos;
        var zpos = 20 + 2 * lpos;

        //

        var atomname, resname, element, resno, serial;

        var atomCount = parseInt( firstLines[ 1 ] );
        var modelLineCount = atomCount + 3;

        var atomMap = s.atomMap;
        var atomStore = s.atomStore;
        atomStore.resize( atomCount );

        var idx = 0;
        var modelIdx = 0;
        var lineNo = 0;

        function _parseChunkOfLines( _i, _n, lines ){

            for( var i = _i; i < _n; ++i ){

                ++lineNo;
                var l = lineNo - 1;

                var line = lines[ i ];

                if( !line ) continue;

                if( l % modelLineCount === 0 ){

                    // NGL.log( "title", line )

                    if( asTrajectory ){

                        currentFrame = new Float32Array( atomCount * 3 );
                        frames.push( currentFrame );
                        currentCoord = 0;

                    }

                }else if( l % modelLineCount === 1 ){

                    // NGL.log( "atomCount", line )

                }else if( l % modelLineCount === modelLineCount - 1 ){

                    var str = line.trim().split( /\s+/ );
                    var box = new Float32Array( 9 );
                    box[ 0 ] = parseFloat( str[ 0 ] ) * 10;
                    box[ 4 ] = parseFloat( str[ 1 ] ) * 10;
                    box[ 8 ] = parseFloat( str[ 2 ] ) * 10;
                    boxes.push( box );

                    if( firstModelOnly ){

                        return true;

                    }

                    modelIdx += 1;

                }else{

                    atomname = line.substr( 10, 5 ).trim();
                    if( cAlphaOnly && atomname !== 'CA' ) continue;

                    var x = parseFloat( line.substr( xpos, lpos ) ) * 10;
                    var y = parseFloat( line.substr( ypos, lpos ) ) * 10;
                    var z = parseFloat( line.substr( zpos, lpos ) ) * 10;

                    if( asTrajectory ){

                        var j = currentCoord * 3;

                        currentFrame[ j + 0 ] = x;
                        currentFrame[ j + 1 ] = y;
                        currentFrame[ j + 2 ] = z;

                        currentCoord += 1;

                        if( l > modelLineCount ) continue;

                    }

                    resname = line.substr( 5, 5 ).trim();
                    resno = parseInt( line.substr( 0, 5 ) );
                    serial = parseInt( line.substr( 15, 5 ) );

                    atomStore.growIfFull();
                    atomStore.atomTypeId[ idx ] = atomMap.add( atomname );

                    atomStore.x[ idx ] = x;
                    atomStore.y[ idx ] = y;
                    atomStore.z[ idx ] = z;
                    atomStore.serial[ idx ] = serial;

                    sb.addAtom( modelIdx, "", resname, resno, 0, "l" );

                    idx += 1;

                }

            }

        }

        this.streamer.eachChunkOfLines( function( lines, chunkNo, chunkCount ){
            _parseChunkOfLines( 0, lines.length, lines );
        } );

        sb.finalize();

        s.unitcell = new NGL.Unitcell(
            boxes[ 0 ][ 0 ], boxes[ 0 ][ 4 ], boxes[ 0 ][ 8 ],
            90, 90, 90, "P 1"
        );

        if( NGL.debug ) NGL.timeEnd( "NGL.GroParser._parse " + this.name );
        callback();

    }

} );


NGL.CifParser = function( streamer, params ){

    NGL.StructureParser.call( this, streamer, params );

};

NGL.CifParser.prototype = NGL.createObject(

    NGL.StructureParser.prototype, {

    constructor: NGL.CifParser,
    type: "cif",


    _parse: function( callback ){

        // http://mmcif.wwpdb.org/

        NGL.time( "NGL.CifParser._parse " + this.name );

        var s = this.structure;
        var sb = this.structureBuilder;

        var firstModelOnly = this.firstModelOnly;
        var asTrajectory = this.asTrajectory;
        var cAlphaOnly = this.cAlphaOnly;

        var frames = s.frames;
        var boxes = s.boxes;
        var doFrames = false;
        var currentFrame, currentCoord;

        var title = s.title;
        var atoms = s.atoms;
        var bondSet = s.bondSet;

        var helixTypes = NGL.HelixTypes;

        var line, recordName;
        var altloc, serial, elem, chainname, resno, resname,
            atomname, element, inscode;

        s.hasConnect = false;

        //

        var reWhitespace = /\s+/;
        var reQuotedWhitespace = /'((?:(?!'\s).)*)'|"((?:(?!"\s).)*)"|(\S+)/g;
        var reDoubleQuote = /"/g;

        var cif = {};
        this.cif = cif;

        var pendingString = false;
        var currentString = null;
        var pendingValue = false;
        var pendingLoop = false;
        var loopPointers = [];
        var currentLoopIndex = null;
        var currentCategory = null;
        var currentName = null;
        var first = null;
        var pointerNames = [];

        var auth_asym_id, auth_seq_id,
            label_atom_id, label_comp_id, label_asym_id, label_alt_id,
            group_PDB, id, type_symbol, pdbx_PDB_model_num, pdbx_PDB_ins_code,
            Cartn_x, Cartn_y, Cartn_z, B_iso_or_equiv, occupancy;

        var asymIdDict = {};
        this.asymIdDict = asymIdDict;

        //

        var atomMap = s.atomMap;
        var atomStore = s.atomStore;
        atomStore.resize( this.streamer.data.length / 100 );

        var idx = 0;
        var modelIdx = 0;
        var modelNum;

        function _parseChunkOfLines( _i, _n, lines ){

            for( var i = _i; i < _n; ++i ){

                line = lines[i].trim();

                if( ( !line && !pendingString && !pendingLoop ) || line[0]==="#" ){

                    // NGL.log( "NEW BLOCK" );

                    pendingString = false;
                    pendingLoop = false;
                    pendingValue = false;
                    loopPointers.length = 0;
                    currentLoopIndex = null;
                    currentCategory = null;
                    currentName = null;
                    first = null;
                    pointerNames.length = 0;

                }else if( line.substring( 0, 5 )==="data_" ){

                    var data = line.substring( 5 );

                    // NGL.log( "DATA", data );

                }else if( line[0]===";" ){

                    if( pendingString ){

                        // NGL.log( "STRING END", currentString );

                        if( pendingLoop ){

                            if( currentLoopIndex === loopPointers.length ){
                                currentLoopIndex = 0;
                            }
                            loopPointers[ currentLoopIndex ].push( currentString );
                            currentLoopIndex += 1;

                        }else{

                            cif[ currentCategory ][ currentName ] = currentString;

                        }

                        pendingString = false;
                        currentString = null;

                    }else{

                        // NGL.log( "STRING START" );

                        pendingString = true;
                        currentString = line.substring( 1 );

                    }

                }else if( line==="loop_" ){

                    // NGL.log( "LOOP START" );

                    pendingLoop = true;
                    loopPointers.length = 0;
                    pointerNames.length = 0;
                    currentLoopIndex = 0;

                }else if( line[0]==="_" ){

                    if( pendingLoop ){

                        // NGL.log( "LOOP KEY", line );

                        var ks = line.split(".");
                        var category = ks[ 0 ].substring( 1 );
                        var name = ks[ 1 ];

                        if( ks.length === 1 ){

                            name = false;
                            if( !cif[ category ] ) cif[ category ] = [];
                            loopPointers.push( cif[ category ] );

                        }else{

                            if( !cif[ category ] ) cif[ category ] = {};
                            if( cif[ category ][ name ] ){
                                NGL.warn( category, name, "already exists" );
                            }else{
                                cif[ category ][ name ] = [];
                                loopPointers.push( cif[ category ][ name ] );
                                pointerNames.push( name );
                            }

                        }

                        currentCategory = category;
                        currentName = name;
                        first = true;

                    }else{

                        var ls = line.match( reQuotedWhitespace );
                        var key = ls[ 0 ];
                        var value = ls[ 1 ];
                        var ks = key.split(".");
                        var category = ks[ 0 ].substring( 1 );
                        var name = ks[ 1 ];

                        if( ks.length === 1 ){

                            name = false;
                            if( !cif[ category ] ) cif[ category ] = [];
                            cif[ category ] = value

                        }else{

                            if( !cif[ category ] ) cif[ category ] = {};

                            if( cif[ category ][ name ] ){
                                NGL.warn( category, name, "already exists" );
                            }else{
                                cif[ category ][ name ] = value;
                            }

                        }

                        if( !value ) pendingValue = true;

                        currentCategory = category;
                        currentName = name;

                    }

                }else{

                    if( pendingString ){

                        // NGL.log( "STRING VALUE", line );

                        currentString += " " + line;

                    }else if( pendingLoop ){

                        // NGL.log( "LOOP VALUE", line );

                        if( !line ){

                            continue;

                        }else if( currentCategory==="atom_site" ){

                            var nn = pointerNames.length;

                            var ls = line.split( reWhitespace );
                            var k;

                            if( first ){

                                var names = [
                                    "auth_asym_id", "auth_seq_id",
                                    "label_atom_id", "label_comp_id", "label_asym_id", "label_alt_id",
                                    "group_PDB", "id", "type_symbol", "pdbx_PDB_model_num",
                                    "Cartn_x", "Cartn_y", "Cartn_z", "B_iso_or_equiv",
                                    "pdbx_PDB_ins_code", "occupancy"
                                ];

                                auth_asym_id = pointerNames.indexOf( "auth_asym_id" );
                                auth_seq_id = pointerNames.indexOf( "auth_seq_id" );
                                label_atom_id = pointerNames.indexOf( "label_atom_id" );
                                label_comp_id = pointerNames.indexOf( "label_comp_id" );
                                label_asym_id = pointerNames.indexOf( "label_asym_id" );
                                label_alt_id = pointerNames.indexOf( "label_alt_id" );
                                Cartn_x = pointerNames.indexOf( "Cartn_x" );
                                Cartn_y = pointerNames.indexOf( "Cartn_y" );
                                Cartn_z = pointerNames.indexOf( "Cartn_z" );
                                id = pointerNames.indexOf( "id" );
                                type_symbol = pointerNames.indexOf( "type_symbol" );
                                group_PDB = pointerNames.indexOf( "group_PDB" );
                                B_iso_or_equiv = pointerNames.indexOf( "B_iso_or_equiv" );
                                pdbx_PDB_model_num = pointerNames.indexOf( "pdbx_PDB_model_num" );

                                pdbx_PDB_ins_code = pointerNames.indexOf( "pdbx_PDB_ins_code" );
                                occupancy = pointerNames.indexOf( "occupancy" );

                                first = false;

                                modelNum = parseInt( ls[ pdbx_PDB_model_num ] );

                                if( asTrajectory ){
                                    currentFrame = [];
                                    currentCoord = 0;
                                }

                            }

                            //

                            var _modelNum = parseInt( ls[ pdbx_PDB_model_num ] );

                            if( modelNum !== _modelNum ){

                                if( asTrajectory ){

                                    if( modelIdx === 0 ){
                                        frames.push( new Float32Array( currentFrame ) );
                                    }

                                    currentFrame = new Float32Array( atomStore.count * 3 );
                                    frames.push( currentFrame );
                                    currentCoord = 0;

                                }

                                modelIdx += 1;

                            }

                            modelNum = _modelNum;

                            if( firstModelOnly && modelIdx > 0 ) continue;

                            //

                            var atomname = ls[ label_atom_id ].replace( reDoubleQuote, '' );
                            if( cAlphaOnly && atomname !== 'CA' ) continue;

                            var x = parseFloat( ls[ Cartn_x ] );
                            var y = parseFloat( ls[ Cartn_y ] );
                            var z = parseFloat( ls[ Cartn_z ] );

                            if( asTrajectory ){

                                var j = currentCoord * 3;

                                currentFrame[ j + 0 ] = x;
                                currentFrame[ j + 1 ] = y;
                                currentFrame[ j + 2 ] = z;

                                currentCoord += 1;

                                if( modelIdx > 0 ) continue;

                            }

                            //

                            var resname = ls[ label_comp_id ];
                            var resno = parseInt( ls[ auth_seq_id ] );
                            var inscode = ls[ pdbx_PDB_ins_code ];
                            inscode = ( inscode === '?' ) ? '' : inscode;
                            var chainname = ls[ auth_asym_id ];
                            var hetero = ( ls[ group_PDB ][ 0 ] === 'H' ) ? 1 : 0;

                            //

                            var element = ls[ type_symbol ];
                            var bfactor = parseFloat( ls[ B_iso_or_equiv ] );
                            var occ = parseFloat( ls[ occupancy ] );
                            var altloc = ls[ label_alt_id ];
                            altloc = ( altloc === '.' ) ? '' : altloc;

                            atomStore.growIfFull();
                            atomStore.atomTypeId[ idx ] = atomMap.add( atomname, element );

                            atomStore.x[ idx ] = x;
                            atomStore.y[ idx ] = y;
                            atomStore.z[ idx ] = z;
                            atomStore.serial[ idx ] = parseInt( ls[ id ] );
                            atomStore.bfactor[ idx ] = isNaN( bfactor ) ? 0 : bfactor;
                            atomStore.occupancy[ idx ] = isNaN( occ ) ? 0 : occ;
                            atomStore.altloc[ idx ] = altloc.charCodeAt( 0 );

                            sb.addAtom( modelIdx, chainname, resname, resno, hetero, undefined, inscode );

                            if( NGL.debug ){
                                // check if one-to-many (chainname-asymId) relationship is
                                // actually a many-to-many mapping
                                var assignedChainname = asymIdDict[ ls[ label_asym_id ] ]
                                if( assignedChainname !== undefined && assignedChainname !== chainname ){
                                    NGL.warn( assignedChainname, chainname );
                                }
                            }
                            // chainname mapping: label_asym_id -> auth_asym_id
                            asymIdDict[ ls[ label_asym_id ] ] = chainname;

                            idx += 1;

                        }else{

                            var ls = line.match( reQuotedWhitespace );
                            var nn = ls.length;

                            if( currentLoopIndex === loopPointers.length ){
                                currentLoopIndex = 0;
                            }/*else if( currentLoopIndex + nn > loopPointers.length ){
                                NGL.warn( "cif parsing error, wrong number of loop data entries", nn, loopPointers.length );
                            }*/

                            for( var j = 0; j < nn; ++j ){
                                loopPointers[ currentLoopIndex + j ].push( ls[ j ] );
                            }

                            currentLoopIndex += nn;

                        }

                    }else if( line[0]==="'" && line.substring( line.length-1 )==="'" ){

                        // NGL.log( "NEWLINE STRING", line );

                        var str = line.substring( 1, line.length - 2 );

                        if( currentName === false ){
                            cif[ currentCategory ] = str;
                        }else{
                            cif[ currentCategory ][ currentName ] = str;
                        }

                    }else if( pendingValue ){

                        // NGL.log( "NEWLINE VALUE", line );

                        if( currentName === false ){
                            cif[ currentCategory ] = line.trim();
                        }else{
                            cif[ currentCategory ][ currentName ] = line.trim();
                        }

                    }else{

                        if( NGL.debug ) NGL.log( "NGL.CifParser._parse: unknown state", line );

                    }

                }


            }

        }

        function postProcess(){

            function _ensureArray( dict, field ){

                if( !Array.isArray( dict[ field ] ) ){
                    Object.keys( dict ).forEach( function( key ){
                        dict[ key ] = [ dict[ key ] ];
                    } );
                }

            }

            // get helices
            var sc = cif.struct_conf;

            if( sc ){

                var helices = s.helices;
                var helixTypes = NGL.HelixTypes;

                // ensure data is in lists
                _ensureArray( sc, "id" );

                for( var i = 0, il = sc.beg_auth_seq_id.length; i < il; ++i ){
                    var helixType = parseInt( sc.pdbx_PDB_helix_class[ i ] );
                    if( !Number.isNaN( helixType ) ){
                        var begIcode = sc.pdbx_beg_PDB_ins_code[ i ];
                        var endIcode = sc.pdbx_end_PDB_ins_code[ i ];
                        helices.push( [
                            asymIdDict[ sc.beg_label_asym_id[ i ] ],
                            parseInt( sc.beg_auth_seq_id[ i ] ),
                            begIcode === "?" ? "" : begIcode,
                            asymIdDict[ sc.end_label_asym_id[ i ] ],
                            parseInt( sc.end_auth_seq_id[ i ] ),
                            endIcode === "?" ? "" : endIcode,
                            ( helixTypes[ helixType ] || helixTypes[""] ).charCodeAt( 0 )
                        ] );
                    }
                }

            }

            // get sheets
            var ssr = cif.struct_sheet_range;

            if( ssr ){

                var sheets = s.sheets;

                // ensure data is in lists
                _ensureArray( ssr, "id" );

                for( var i = 0, il = ssr.beg_auth_seq_id.length; i < il; ++i ){
                    var begIcode = ssr.pdbx_beg_PDB_ins_code[ i ];
                    var endIcode = ssr.pdbx_end_PDB_ins_code[ i ];
                    sheets.push( [
                        asymIdDict[ ssr.beg_label_asym_id[ i ] ],
                        parseInt( ssr.beg_auth_seq_id[ i ] ),
                        begIcode === "?" ? "" : begIcode,
                        asymIdDict[ ssr.end_label_asym_id[ i ] ],
                        parseInt( ssr.end_auth_seq_id[ i ] ),
                        endIcode === "?" ? "" : endIcode
                    ] );
                }

            }

        }

        this.streamer.eachChunkOfLines( function( lines, chunkNo, chunkCount ){
            _parseChunkOfLines( 0, lines.length, lines );
        } );

        sb.finalize();

        if( cif.struct && cif.struct.title ){
            s.title = cif.struct.title.trim().replace( /^['"]+|['"]+$/g, "" );
        }

        postProcess();

        if( NGL.debug ) NGL.timeEnd( "NGL.CifParser._parse " + this.name );
        callback();

    },

    _postProcess: function(){

        if( NGL.debug ) NGL.time( "NGL.CifParser._postProcess" );

        var s = this.structure;
        var structure = this.structure;
        var cif = this.cif;
        var asymIdDict = this.asymIdDict;

        function _ensureArray( dict, field ){

            if( !Array.isArray( dict[ field ] ) ){
                Object.keys( dict ).forEach( function( key ){
                    dict[ key ] = [ dict[ key ] ];
                } );
            }

        }

        // biomol & ncs processing
        var operDict = {};
        var biomolDict = s.biomolDict;

        if( cif.pdbx_struct_oper_list ){

            var op = cif.pdbx_struct_oper_list;

            // ensure data is in lists
            _ensureArray( op, "id" );

            op.id.forEach( function( id, i ){

                var m = new THREE.Matrix4();
                var elms = m.elements;

                elms[  0 ] = parseFloat( op[ "matrix[1][1]" ][ i ] );
                elms[  1 ] = parseFloat( op[ "matrix[1][2]" ][ i ] );
                elms[  2 ] = parseFloat( op[ "matrix[1][3]" ][ i ] );

                elms[  4 ] = parseFloat( op[ "matrix[2][1]" ][ i ] );
                elms[  5 ] = parseFloat( op[ "matrix[2][2]" ][ i ] );
                elms[  6 ] = parseFloat( op[ "matrix[2][3]" ][ i ] );

                elms[  8 ] = parseFloat( op[ "matrix[3][1]" ][ i ] );
                elms[  9 ] = parseFloat( op[ "matrix[3][2]" ][ i ] );
                elms[ 10 ] = parseFloat( op[ "matrix[3][3]" ][ i ] );

                elms[  3 ] = parseFloat( op[ "vector[1]" ][ i ] );
                elms[  7 ] = parseFloat( op[ "vector[2]" ][ i ] );
                elms[ 11 ] = parseFloat( op[ "vector[3]" ][ i ] );

                m.transpose();

                operDict[ id ] = m;

            } );

        }

        if( cif.pdbx_struct_assembly_gen ){

            var gen = cif.pdbx_struct_assembly_gen;

            // ensure data is in lists
            _ensureArray( gen, "assembly_id" );

            var getMatrixDict = function( expr ){

                var matDict = {};

                var l = expr.replace( /[\(\)']/g, "" ).split( "," );

                l.forEach( function( e ){

                    if( e.indexOf( "-" ) !== -1 ){

                        var es = e.split( "-" );

                        var j = parseInt( es[ 0 ] );
                        var m = parseInt( es[ 1 ] );

                        for( ; j <= m; ++j ){

                            matDict[ j ] = operDict[ j ];

                        }

                    }else{

                        matDict[ e ] = operDict[ e ];

                    }

                } );

                return matDict;

            }

            gen.assembly_id.forEach( function( id, i ){

                var md = {};
                var oe = gen.oper_expression[ i ];

                if( oe.indexOf( ")(" ) !== -1 ){

                    oe = oe.split( ")(" );

                    var md1 = getMatrixDict( oe[ 0 ] );
                    var md2 = getMatrixDict( oe[ 1 ] );

                    Object.keys( md1 ).forEach( function( k1 ){

                        Object.keys( md2 ).forEach( function( k2 ){

                            var mat = new THREE.Matrix4();

                            mat.multiplyMatrices( md1[ k1 ], md2[ k2 ] );
                            md[ k1 + "x" + k2 ] = mat;

                        } );

                    } );

                }else{

                    md = getMatrixDict( oe );

                }

                var matrixList = [];
                for( var k in md ){
                    matrixList.push( md[ k ] );
                }

                var name = id;
                if( /^(0|[1-9][0-9]*)$/.test( name ) ) name = "BU" + name;

                var chainList = gen.asym_id_list[ i ].split( "," );
                for( var j = 0, jl = chainList.length; j < jl; ++j ){
                    chainList[ j ] = asymIdDict[ chainList[ j ] ];
                }

                if( biomolDict[ name ] === undefined ){
                    biomolDict[ name ] = new NGL.Assembly( name );
                }
                biomolDict[ name ].addPart( matrixList, chainList );

            } );

        }

        // non-crystallographic symmetry operations
        if( cif.struct_ncs_oper ){

            var op = cif.struct_ncs_oper;

            // ensure data is in lists
            _ensureArray( op, "id" );

            var ncsName = "NCS";
            biomolDict[ ncsName ] = new NGL.Assembly( ncsName );
            var ncsPart = biomolDict[ ncsName ].addPart();

            op.id.forEach( function( id, i ){

                var m = new THREE.Matrix4();
                var elms = m.elements;

                elms[  0 ] = parseFloat( op[ "matrix[1][1]" ][ i ] );
                elms[  1 ] = parseFloat( op[ "matrix[1][2]" ][ i ] );
                elms[  2 ] = parseFloat( op[ "matrix[1][3]" ][ i ] );

                elms[  4 ] = parseFloat( op[ "matrix[2][1]" ][ i ] );
                elms[  5 ] = parseFloat( op[ "matrix[2][2]" ][ i ] );
                elms[  6 ] = parseFloat( op[ "matrix[2][3]" ][ i ] );

                elms[  8 ] = parseFloat( op[ "matrix[3][1]" ][ i ] );
                elms[  9 ] = parseFloat( op[ "matrix[3][2]" ][ i ] );
                elms[ 10 ] = parseFloat( op[ "matrix[3][3]" ][ i ] );

                elms[  3 ] = parseFloat( op[ "vector[1]" ][ i ] );
                elms[  7 ] = parseFloat( op[ "vector[2]" ][ i ] );
                elms[ 11 ] = parseFloat( op[ "vector[3]" ][ i ] );

                m.transpose();

                ncsPart.matrixList.push( m );

            } );

        }

        // cell & symmetry
        var unitcellDict = {};

        if( cif.cell ){

            var cell = cif.cell;

            var a = parseFloat( cell.length_a );
            var b = parseFloat( cell.length_b );
            var c = parseFloat( cell.length_c );

            var box = new Float32Array( 9 );
            box[ 0 ] = a;
            box[ 4 ] = b;
            box[ 8 ] = c;
            structure.boxes.push( box );

            unitcellDict.a = a;
            unitcellDict.b = b;
            unitcellDict.c = c;
            unitcellDict.alpha = parseFloat( cell.angle_alpha );
            unitcellDict.beta = parseFloat( cell.angle_beta );
            unitcellDict.gamma = parseFloat( cell.angle_gamma );

        }

        if( cif.symmetry ){

            var symmetry = cif.symmetry;

            var sGroup = symmetry[ "space_group_name_H-M" ];
            if( sGroup[0] === sGroup[ sGroup.length-1 ] &&
                ( sGroup[0] === "'" || sGroup[0] === '"' )
            ){
                sGroup = sGroup.substring( 1, sGroup.length-1 );
            }

            unitcellDict.spacegroup = sGroup;

        }

        // origx
        var origx = new THREE.Matrix4();

        if( cif.database_PDB_matrix ){

            var mat = cif.database_PDB_matrix;
            var elms = origx.elements;

            elms[  0 ] = parseFloat( mat[ "origx[1][1]" ] );
            elms[  1 ] = parseFloat( mat[ "origx[1][2]" ] );
            elms[  2 ] = parseFloat( mat[ "origx[1][3]" ] );

            elms[  4 ] = parseFloat( mat[ "origx[2][1]" ] );
            elms[  5 ] = parseFloat( mat[ "origx[2][2]" ] );
            elms[  6 ] = parseFloat( mat[ "origx[2][3]" ] );

            elms[  8 ] = parseFloat( mat[ "origx[3][1]" ] );
            elms[  9 ] = parseFloat( mat[ "origx[3][2]" ] );
            elms[ 10 ] = parseFloat( mat[ "origx[3][3]" ] );

            elms[  3 ] = parseFloat( mat[ "origx_vector[1]" ] );
            elms[  7 ] = parseFloat( mat[ "origx_vector[2]" ] );
            elms[ 11 ] = parseFloat( mat[ "origx_vector[3]" ] );

            origx.transpose();

            unitcellDict.origx = origx;

        }

        // scale
        var scale = new THREE.Matrix4();

        if( cif.atom_sites ){

            var mat = cif.atom_sites;
            var elms = scale.elements;

            elms[  0 ] = parseFloat( mat[ "fract_transf_matrix[1][1]" ] );
            elms[  1 ] = parseFloat( mat[ "fract_transf_matrix[1][2]" ] );
            elms[  2 ] = parseFloat( mat[ "fract_transf_matrix[1][3]" ] );

            elms[  4 ] = parseFloat( mat[ "fract_transf_matrix[2][1]" ] );
            elms[  5 ] = parseFloat( mat[ "fract_transf_matrix[2][2]" ] );
            elms[  6 ] = parseFloat( mat[ "fract_transf_matrix[2][3]" ] );

            elms[  8 ] = parseFloat( mat[ "fract_transf_matrix[3][1]" ] );
            elms[  9 ] = parseFloat( mat[ "fract_transf_matrix[3][2]" ] );
            elms[ 10 ] = parseFloat( mat[ "fract_transf_matrix[3][3]" ] );

            elms[  3 ] = parseFloat( mat[ "fract_transf_vector[1]" ] );
            elms[  7 ] = parseFloat( mat[ "fract_transf_vector[2]" ] );
            elms[ 11 ] = parseFloat( mat[ "fract_transf_vector[3]" ] );

            scale.transpose();

            unitcellDict.scale = scale;

        }

        if( unitcellDict.a !== undefined ){
            s.unitcell = new NGL.Unitcell(
                unitcellDict.a, unitcellDict.b, unitcellDict.c,
                unitcellDict.alpha, unitcellDict.beta, unitcellDict.gamma,
                unitcellDict.spacegroup, unitcellDict.scale
            );
        }else{
            s.unitcell = undefined;
        }

        // add connections
        var sc = cif.struct_conn;

        if( sc ){

            // ensure data is in lists
            _ensureArray( sc, "id" );

            var reDoubleQuote = /"/g;
            var ap1 = s.getAtomProxy();
            var ap2 = s.getAtomProxy();
            var atomIndicesCache = {};

            for( var i = 0, il = sc.id.length; i < il; ++i ){

                // ignore:
                // hydrog - hydrogen bond
                // mismat - mismatched base pairs
                // saltbr - ionic interaction

                var conn_type_id = sc.conn_type_id[ i ]
                if( conn_type_id === "hydrog" ||
                    conn_type_id === "mismat" ||
                    conn_type_id === "saltbr" ) continue;

                // ignore bonds between symmetry mates
                if( sc.ptnr1_symmetry[ i ] !== "1_555" ||
                    sc.ptnr2_symmetry[ i ] !== "1_555" ) continue;

                // process:
                // covale - covalent bond
                // covale_base -
                //      covalent modification of a nucleotide base
                // covale_phosphate -
                //      covalent modification of a nucleotide phosphate
                // covale_sugar -
                //      covalent modification of a nucleotide sugar
                // disulf - disulfide bridge
                // metalc - metal coordination
                // modres - covalent residue modification

                var inscode1 = sc.pdbx_ptnr1_PDB_ins_code[ i ];
                var altloc1 = sc.pdbx_ptnr1_label_alt_id[ i ];
                var sele1 = (
                    sc.ptnr1_auth_seq_id[ i ] +
                    ( inscode1 === "?" ? "" : ( "^" + inscode1 ) ) +
                    ":" + asymIdDict[ sc.ptnr1_label_asym_id[ i ] ] +
                    "." + sc.ptnr1_label_atom_id[ i ].replace( reDoubleQuote, '' ) +
                    ( altloc1 === "?" ? "" : ( "%" + altloc1 ) )
                );
                var atomIndices1 = atomIndicesCache[ sele1 ];
                if( !atomIndices1 ){
                    var selection1 = new NGL.Selection( sele1 );
                    if( selection1.selection[ "error" ] ){
                        NGL.warn( "invalid selection for connection", sele1 );
                        continue;
                    }
                    atomIndices1 = s.getAtomIndices( selection1 );
                    atomIndicesCache[ sele1 ] = atomIndices1;
                }

                var inscode2 = sc.pdbx_ptnr2_PDB_ins_code[ i ];
                var altloc2 = sc.pdbx_ptnr2_label_alt_id[ i ];
                var sele2 = (
                    sc.ptnr2_auth_seq_id[ i ] +
                    ( inscode2 === "?" ? "" : ( "^" + inscode2 ) ) +
                    ":" + asymIdDict[ sc.ptnr2_label_asym_id[ i ] ] +
                    "." + sc.ptnr2_label_atom_id[ i ].replace( reDoubleQuote, '' ) +
                    ( altloc2 === "?" ? "" : ( "%" + altloc2 ) )
                );
                var atomIndices2 = atomIndicesCache[ sele2 ];
                if( !atomIndices2 ){
                    var selection2 = new NGL.Selection( sele2 );
                    if( selection2.selection[ "error" ] ){
                        NGL.warn( "invalid selection for connection", sele2 );
                        continue;
                    }
                    atomIndices2 = s.getAtomIndices( selection2 );
                    atomIndicesCache[ sele2 ] = atomIndices2;
                }

                // cases with more than one atom per selection
                // - #altloc1 to #altloc2
                // - #model to #model
                // - #altloc1 * #model to #altloc2 * #model

                var k = atomIndices1.length;
                var l = atomIndices2.length;

                if( k > l ){
                    var tmpA = k;
                    k = l;
                    l = tmpA;
                    var tmpB = atomIndices1;
                    atomIndices1 = atomIndices2;
                    atomIndices2 = tmpB;
                }

                // console.log( k, l );

                if( k === 0 || l === 0 ){
                    NGL.warn( "no atoms found for", sele1, sele2 );
                    continue;
                }

                for( var j = 0; j < l; ++j ){

                    ap1.index = atomIndices1[ j % k ];
                    ap2.index = atomIndices2[ j ];

                    if( ap1 && ap2 ){
                        s.bondStore.addBond( ap1, ap2 );
                    }else{
                        NGL.log( "atoms for connection not found" );
                    }

                }

            }

        }

        if( NGL.debug ) NGL.timeEnd( "NGL.CifParser._postProcess" );

    }

} );


NGL.SdfParser = function( streamer, params ){

    NGL.StructureParser.call( this, streamer, params );

};

NGL.SdfParser.prototype = NGL.createObject(

    NGL.StructureParser.prototype, {

    constructor: NGL.SdfParser,
    type: "sdf",

    _parse: function( callback ){

        // https://en.wikipedia.org/wiki/Chemical_table_file#SDF
        // http://download.accelrys.com/freeware/ctfile-formats/ctfile-formats.zip

        if( NGL.debug ) NGL.time( "NGL.SdfParser._parse " + this.name );

        var s = this.structure;
        var sb = this.structureBuilder;

        var firstModelOnly = this.firstModelOnly;
        var asTrajectory = this.asTrajectory;

        var headerLines = this.streamer.peekLines( 2 );

        s.id = headerLines[ 0 ].trim();
        s.title = headerLines[ 1 ].trim();

        var frames = s.frames;
        var boxes = s.boxes;
        var doFrames = false;
        var currentFrame, currentCoord;

        var atomMap = s.atomMap;
        var atomStore = s.atomStore;
        atomStore.resize( Math.round( this.streamer.data.length / 50 ) );

        var ap1 = s.getAtomProxy();
        var ap2 = s.getAtomProxy();

        var idx = 0;
        var lineNo = 0;
        var modelIdx = 0;
        var modelAtomIdxStart = 0;

        var atomCount, bondCount, atomStart, atomEnd, bondStart, bondEnd;

        function _parseChunkOfLines( _i, _n, lines ){

            for( var i = _i; i < _n; ++i ){

                var line = lines[ i ];

                if( line.substr( 0, 4 ) === "$$$$" ){

                    lineNo = -1;
                    ++modelIdx;
                    modelAtomIdxStart = atomStore.count;

                }

                if( lineNo === 3 ){

                    atomCount = parseInt( line.substr( 0, 3 ) );
                    bondCount = parseInt( line.substr( 3, 3 ) );

                    atomStart = 4;
                    atomEnd = atomStart + atomCount;
                    bondStart = atomEnd;
                    bondEnd = bondStart + bondCount;

                    if( asTrajectory ){

                        currentCoord = 0;
                        currentFrame = new Float32Array( atomCount * 3 );
                        frames.push( currentFrame );

                        if( modelIdx > 0 ) doFrames = true;

                    }

                }

                if( lineNo >= atomStart && lineNo < atomEnd ){

                    if( firstModelOnly && modelIdx > 0 ) continue;

                    var x = parseFloat( line.substr( 0, 10 ) );
                    var y = parseFloat( line.substr( 10, 10 ) );
                    var z = parseFloat( line.substr( 20, 10 ) );

                    if( asTrajectory ){

                        var j = currentCoord * 3;

                        currentFrame[ j + 0 ] = x;
                        currentFrame[ j + 1 ] = y;
                        currentFrame[ j + 2 ] = z;

                        currentCoord += 1;

                        if( doFrames ) continue;

                    }

                    var element = line.substr( 31, 3 ).trim();
                    var atomname = element + ( idx + 1 );

                    atomStore.growIfFull();
                    atomStore.atomTypeId[ idx ] = atomMap.add( atomname, element );

                    atomStore.x[ idx ] = x;
                    atomStore.y[ idx ] = y;
                    atomStore.z[ idx ] = z;
                    atomStore.serial[ idx ] = idx;

                    sb.addAtom( modelIdx, "", "HET", 1, 1 );

                    idx += 1;

                }

                if( lineNo >= bondStart && lineNo < bondEnd ){

                    if( firstModelOnly && modelIdx > 0 ) continue;
                    if( asTrajectory && modelIdx > 0 ) continue;

                    ap1.index = parseInt( line.substr( 0, 3 ) ) - 1 + modelAtomIdxStart;
                    ap2.index = parseInt( line.substr( 3, 3 ) ) - 1 + modelAtomIdxStart;
                    var order = parseInt( line.substr( 6, 3 ) );

                    s.bondStore.addBond( ap1, ap2, order );

                }

                ++lineNo;

            };

        };

        this.streamer.eachChunkOfLines( function( lines, chunkNo, chunkCount ){
            _parseChunkOfLines( 0, lines.length, lines );
        } );

        sb.finalize();

        s._dontAutoBond = true;
        s.unitcell = undefined;

        if( NGL.debug ) NGL.timeEnd( "NGL.SdfParser._parse " + this.name );
        callback();

    }

} );


NGL.Mol2Parser = function( streamer, params ){

    NGL.StructureParser.call( this, streamer, params );

};

NGL.Mol2Parser.prototype = NGL.createObject(

    NGL.StructureParser.prototype, {

    constructor: NGL.Mol2Parser,
    type: "mol2",

    _parse: function( callback ){

        // http://www.tripos.com/data/support/mol2.pdf

        if( NGL.debug ) NGL.time( "NGL.Mol2Parser._parse " + this.name );

        var reWhitespace = /\s+/;

        var s = this.structure;
        var sb = this.structureBuilder;

        var firstModelOnly = this.firstModelOnly;
        var asTrajectory = this.asTrajectory;

        var frames = s.frames;
        var boxes = s.boxes;
        var doFrames = false;
        var currentFrame, currentCoord;

        var atomMap = s.atomMap;
        var atomStore = s.atomStore;
        atomStore.resize( Math.round( this.streamer.data.length / 60 ) );

        var idx = 0;
        var moleculeLineNo = 0;
        var modelAtomIdxStart = 0;
        var modelIdx = -1;
        var numAtoms = 0;

        var currentRecordType = 0;
        var moleculeRecordType = 1;
        var atomRecordType = 2;
        var bondRecordType = 3;

        var ap1 = s.getAtomProxy();
        var ap2 = s.getAtomProxy();

        var bondTypes = {
            "1": 1,
            "2": 2,
            "3": 3,
            "am": 1,  // amide
            "ar": 1,  // aromatic
            "du": 1,  // dummy
            "un": 1,  // unknown
            "nc": 0,  // not connected
        };

        function _parseChunkOfLines( _i, _n, lines ){

            for( var i = _i; i < _n; ++i ){

                var line = lines[ i ].trim();

                if( line === "" || line[ 0 ] === "#" ) continue;

                if( line[ 0 ] === "@" ){

                    if( line === "@<TRIPOS>MOLECULE" ){

                        currentRecordType = moleculeRecordType;
                        moleculeLineNo = 0;

                        ++modelIdx;

                    }else if( line === "@<TRIPOS>ATOM" ){

                        currentRecordType = atomRecordType;
                        modelAtomIdxStart = atomStore.count;

                        if( asTrajectory ){

                            currentCoord = 0;
                            currentFrame = new Float32Array( numAtoms * 3 );
                            frames.push( currentFrame );

                            if( modelIdx > 0 ) doFrames = true;

                        }

                    }else if( line === "@<TRIPOS>BOND" ){

                        currentRecordType = bondRecordType;

                    }else{

                        currentRecordType = 0;

                    }

                }else if( currentRecordType === moleculeRecordType ){

                    if( moleculeLineNo === 0 ){

                        s.title = line;
                        s.id = line;

                    }else if( moleculeLineNo === 1 ){

                        var ls = line.split( reWhitespace );
                        numAtoms = parseInt( ls[ 0 ] );
                        // num_atoms [num_bonds [num_subst [num_feat [num_sets]]]]

                    }else if( moleculeLineNo === 2 ){

                        var molType = line;
                        // SMALL, BIOPOLYMER, PROTEIN, NUCLEIC_ACID, SACCHARIDE

                    }else if( moleculeLineNo === 3 ){

                        var chargeType = line;
                        // NO_CHARGES, DEL_RE, GASTEIGER, GAST_HUCK, HUCKEL,
                        // PULLMAN, GAUSS80_CHARGES, AMPAC_CHARGES,
                        // MULLIKEN_CHARGES, DICT_ CHARGES, MMFF94_CHARGES,
                        // USER_CHARGES

                    }else if( moleculeLineNo === 4 ){

                        var statusBits = line;

                    }else if( moleculeLineNo === 5 ){

                        var molComment = line;

                    }

                    ++moleculeLineNo;

                }else if( currentRecordType === atomRecordType ){

                    var ls = line.split( reWhitespace );

                    if( firstModelOnly && modelIdx > 0 ) continue;

                    var x = parseFloat( ls[ 2 ] );
                    var y = parseFloat( ls[ 3 ] );
                    var z = parseFloat( ls[ 4 ] );

                    if( asTrajectory ){

                        var j = currentCoord * 3;

                        currentFrame[ j + 0 ] = x;
                        currentFrame[ j + 1 ] = y;
                        currentFrame[ j + 2 ] = z;

                        currentCoord += 1;

                        if( doFrames ) continue;

                    }

                    var serial = ls[ 0 ];
                    var atomname = ls[ 1 ];
                    var element = ls[ 5 ].split( "." )[ 0 ];
                    var resno = ls[ 6 ] ? parseInt( ls[ 6 ] ) : 1;
                    var resname = ls[ 7 ] ? ls[ 7 ] : "";
                    var bfactor = ls[ 8 ] ? parseFloat( ls[ 8 ] ) : 0.0;

                    atomStore.growIfFull();
                    atomStore.atomTypeId[ idx ] = atomMap.add( atomname, element );

                    atomStore.x[ idx ] = x;
                    atomStore.y[ idx ] = y;
                    atomStore.z[ idx ] = z;
                    atomStore.serial[ idx ] = serial;
                    atomStore.bfactor[ idx ] = bfactor;

                    sb.addAtom( modelIdx, "", resname, resno, 1 );

                    idx += 1;

                }else if( currentRecordType === bondRecordType ){

                    if( firstModelOnly && modelIdx > 0 ) continue;
                    if( asTrajectory && modelIdx > 0 ) continue;

                    var ls = line.split( reWhitespace );

                    // ls[ 0 ] is bond id
                    ap1.index = parseInt( ls[ 1 ] ) - 1 + modelAtomIdxStart;
                    ap2.index = parseInt( ls[ 2 ] ) - 1 + modelAtomIdxStart;
                    var order = bondTypes[ ls[ 3 ] ];

                    s.bondStore.addBond( ap1, ap2, order );

                }

            };

        };

        this.streamer.eachChunkOfLines( function( lines, chunkNo, chunkCount ){
            _parseChunkOfLines( 0, lines.length, lines );
        } );

        sb.finalize();

        s._dontAutoBond = true;
        s.unitcell = undefined;

        if( NGL.debug ) NGL.timeEnd( "NGL.Mol2Parser._parse " + this.name );
        callback();

    }

} );


NGL.MmtfParser = function( streamer, params ){

    NGL.StructureParser.call( this, streamer, params );

    this.dontAutoBond = true;
    this.autoBondBetween = false;
    this.doAutoSS = false;

};

NGL.MmtfParser.prototype = NGL.createObject(

    NGL.StructureParser.prototype, {

    constructor: NGL.MmtfParser,
    type: "mmtf",

    _parse: function( callback ){

        // https://github.com/rcsb/mmtf

        if( NGL.debug ) NGL.time( "NGL.MmtfParser._parse " + this.name );

        var s = this.structure;
        var sd = decodeMmtf( decodeMsgpack( this.streamer.data ) );

        var numBonds, numAtoms, numGroups, numChains, numModels;
        var chainsPerModel;

        if( this.firstModelOnly || this.asTrajectory ){

            numModels = 1;
            numChains = sd.chainsPerModel[ 0 ];

            numGroups = 0;
            for( var i = 0, il = numChains; i < il; ++i ){
                numGroups += sd.groupsPerChain[ i ];
            }

            numAtoms = 0;
            for( var i = 0, il = numGroups; i < il; ++i ){
                var groupData = sd.groupList[ sd.groupTypeList[ i ] ];
                numAtoms += groupData.atomNameList.length;
            }

            numBonds = sd.numBonds;

            chainsPerModel = [ numChains ];

        }else{

            numBonds = sd.numBonds;
            numAtoms = sd.numAtoms;
            numGroups = sd.numGroups;
            numChains = sd.numChains;
            numModels = sd.numModels;

            chainsPerModel = sd.chainsPerModel;

        }

        numBonds += numGroups;  // add numGroups to have space for polymer bonds

        //

        if( this.asTrajectory ){

            for( var i = 0, il = sd.numModels; i < il; ++i ){

                var frame = new Float32Array( numAtoms * 3 );
                var frameAtomOffset = numAtoms * i;

                for( var j = 0; j < numAtoms; ++j ){
                    var j3 = j * 3;
                    var offset = j + frameAtomOffset;
                    frame[ j3     ] = sd.xCoordList[ offset ];
                    frame[ j3 + 1 ] = sd.yCoordList[ offset ];
                    frame[ j3 + 2 ] = sd.zCoordList[ offset ];
                }

                s.frames.push( frame );

            }

        }

        // bondStore
        var bAtomIndex1 = new Uint32Array( numBonds );
        var bAtomIndex2 = new Uint32Array( numBonds );
        var bBondOrder = new Uint8Array( numBonds );

        var aGroupIndex = new Uint32Array( numAtoms );

        var gChainIndex = new Uint32Array( numGroups );
        var gAtomOffset = new Uint32Array( numGroups );
        var gAtomCount = new Uint16Array( numGroups );

        var cModelIndex = new Uint16Array( numChains );
        var cGroupOffset = new Uint32Array( numChains );
        var cGroupCount = new Uint32Array( numChains );

        var mChainOffset = new Uint32Array( numModels );
        var mChainCount = new Uint32Array( numModels );

        // set-up model-chain relations
        var chainOffset = 0;
        for( var i = 0, il = numModels; i < il; ++i ){
            var modelChainCount = chainsPerModel[ i ];
            mChainOffset[ i ] = chainOffset;
            mChainCount[ i ] = modelChainCount;
            for( var j = 0; j < modelChainCount; ++j ){
                cModelIndex[ j + chainOffset ] = i;
            }
            chainOffset += modelChainCount;
        }

        // set-up chain-residue relations
        var groupsPerChain = sd.groupsPerChain;
        var groupOffset = 0;
        for( var i = 0, il = numChains; i < il; ++i ){
            var chainGroupCount = groupsPerChain[ i ];
            cGroupOffset[ i ] = groupOffset;
            cGroupCount[ i ] = chainGroupCount;
            for( var j = 0; j < chainGroupCount; ++j ){
                gChainIndex[ j + groupOffset ] = i;
            }
            groupOffset += chainGroupCount;
        }

        //////
        // get data from group map

        var atomOffset = 0;
        var bondOffset = 0;

        for( var i = 0, il = numGroups; i < il; ++i ){

            var groupData = sd.groupList[ sd.groupTypeList[ i ] ];
            var groupAtomCount = groupData.atomNameList.length;

            var bondAtomList = groupData.bondAtomList;
            var bondOrderList = groupData.bondOrderList;

            for( var j = 0, jl = bondOrderList.length; j < jl; ++j ){
                bAtomIndex1[ bondOffset ] = atomOffset + bondAtomList[ j * 2 ];
                bAtomIndex2[ bondOffset ] = atomOffset + bondAtomList[ j * 2 + 1 ];
                bBondOrder[ bondOffset ] = bondOrderList[ j ];
                bondOffset += 1;
            }

            //

            gAtomOffset[ i ] = atomOffset;
            gAtomCount[ i ] = groupAtomCount;

            for( var j = 0; j < groupAtomCount; ++j ){
                aGroupIndex[ atomOffset ] = i;
                atomOffset += 1;
            }

        }

        // extra bonds

        var bondAtomList = sd.bondAtomList;
        if( bondAtomList ){

            if( sd.bondOrderList ){
                bBondOrder.set( sd.bondOrderList, bondOffset );
            }

            for( var i = 0, il = bondAtomList.length; i < il; i += 2 ){
                var atomIndex1 = bondAtomList[ i ];
                var atomIndex2 = bondAtomList[ i + 1 ];
                if( atomIndex1 < numAtoms && atomIndex2 < numAtoms ){
                    bAtomIndex1[ bondOffset ] = atomIndex1;
                    bAtomIndex2[ bondOffset ] = atomIndex2;
                    bondOffset += 1;
                }
            }

        }

        //

        s.bondStore.length = bBondOrder.length;
        s.bondStore.count = bondOffset;
        s.bondStore.atomIndex1 = bAtomIndex1;
        s.bondStore.atomIndex2 = bAtomIndex2;
        s.bondStore.bondOrder = bBondOrder;

        s.atomStore.length = numAtoms;
        s.atomStore.count = numAtoms;
        s.atomStore.residueIndex = aGroupIndex;
        s.atomStore.atomTypeId = new Uint16Array( numAtoms );
        s.atomStore.x = sd.xCoordList.subarray( 0, numAtoms );
        s.atomStore.y = sd.yCoordList.subarray( 0, numAtoms );
        s.atomStore.z = sd.zCoordList.subarray( 0, numAtoms );
        s.atomStore.serial = sd.atomIdList.subarray( 0, numAtoms );
        s.atomStore.bfactor = sd.bFactorList.subarray( 0, numAtoms );
        s.atomStore.altloc = sd.altLocList.subarray( 0, numAtoms );
        s.atomStore.occupancy = sd.occupancyList.subarray( 0, numAtoms );

        s.residueStore.length = numGroups;
        s.residueStore.count = numGroups;
        s.residueStore.chainIndex = gChainIndex;
        s.residueStore.residueTypeId = sd.groupTypeList;
        s.residueStore.atomOffset = gAtomOffset;
        s.residueStore.atomCount = gAtomCount;
        s.residueStore.resno = sd.groupIdList.subarray( 0, numGroups );
        s.residueStore.sstruc = sd.secStructList.subarray( 0, numGroups );
        s.residueStore.inscode = sd.insCodeList.subarray( 0, numGroups );

        s.chainStore.length = numChains;
        s.chainStore.count = numChains;
        s.chainStore.modelIndex = cModelIndex;
        s.chainStore.residueOffset = cGroupOffset;
        s.chainStore.residueCount = cGroupCount;
        s.chainStore.chainname = sd.chainNameList.subarray( 0, numChains * 4 );

        s.modelStore.length = numModels;
        s.modelStore.count = numModels;
        s.modelStore.chainOffset = mChainOffset;
        s.modelStore.chainCount = mChainCount;

        var sstrucMap = {
            "0": "i".charCodeAt( 0 ),  // pi helix
            "1": "s".charCodeAt( 0 ),  // bend
            "2": "h".charCodeAt( 0 ),  // alpha helix
            "3": "e".charCodeAt( 0 ),  // extended
            "4": "g".charCodeAt( 0 ),  // 3-10 helix
            "5": "b".charCodeAt( 0 ),  // bridge
            "6": "t".charCodeAt( 0 ),  // turn
            "7": "l".charCodeAt( 0 ),  // coil
            "-1": "".charCodeAt( 0 )   // NA
        };

        var hetCompList = [
            "non-polymer", "other", "saccharide", "l-saccharide", "d-saccharide",
            "l-saccharide 1,4 and 1,4 linking", "l-saccharide 1,4 and 1,6 linking",
            "d-saccharide 1,4 and 1,4 linking", "d-saccharide 1,4 and 1,6 linking"
        ];

        var groupTypeDict = {};
        for( var i = 0, il = sd.groupList.length; i < il; ++i ){
            var groupType = sd.groupList[ i ];
            var atomTypeIdList = [];
            for( var j = 0, jl = groupType.atomNameList.length; j < jl; ++j ){
                var element = groupType.elementList[ j ].toUpperCase();
                var atomname = groupType.atomNameList[ j ];
                atomTypeIdList.push( s.atomMap.add( atomname, element ) );
            }
            var hetFlag = hetCompList.indexOf( groupType.chemCompType.toLowerCase() ) !== -1;
            groupTypeDict[ i ] = s.residueMap.add( groupType.groupName, atomTypeIdList, hetFlag );
        }

        for( var i = 0, il = numGroups; i < il; ++i ){
            s.residueStore.residueTypeId[ i ] = groupTypeDict[ s.residueStore.residueTypeId[ i ] ];
        }

        for( var i = 0, il = s.atomStore.count; i < il; ++i ){
            var residueIndex = s.atomStore.residueIndex[ i ];
            var residueType = s.residueMap.list[ s.residueStore.residueTypeId[ residueIndex ] ];
            var resAtomOffset = s.residueStore.atomOffset[ residueIndex ];
            s.atomStore.atomTypeId[ i ] = residueType.atomTypeIdList[ i - resAtomOffset ];
        }

        if( sd.secStructList ){
            var secStructLength = sd.secStructList.length;
            for( var i = 0, il = s.residueStore.count; i < il; ++i ){
                // with ( i % secStructLength ) secStruct entries are reused
                var sstruc = sstrucMap[ s.residueStore.sstruc[ i % secStructLength ] ];
                if( sstruc !== undefined ) s.residueStore.sstruc[ i ] = sstruc;
            }
        }

        //

        if( sd.bioAssemblyList ){
            sd.bioAssemblyList.forEach( function( _assembly, k ){
                var id = k + 1;
                var assembly = new NGL.Assembly( id );
                s.biomolDict[ "BU" + id ] = assembly;
                var chainToPart = {};
                _assembly.transformList.forEach( function( _transform ){
                    var matrix = new THREE.Matrix4().fromArray( _transform.matrix ).transpose();
                    var chainList = _transform.chainIndexList.map( function( chainIndex ){
                        var chainname = "";
                        for( var k = 0; k < 4; ++k ){
                            var code = sd.chainNameList[ chainIndex * 4 + k ];
                            if( code ){
                                chainname += String.fromCharCode( code );
                            }else{
                                break;
                            }
                        }
                        return chainname;
                    } );
                    var part = chainToPart[ chainList ];
                    if( part ){
                        part.matrixList.push( matrix );
                    }else{
                        chainToPart[ chainList ] = assembly.addPart( [ matrix ], chainList );
                    }
                } );
            } );
        }

        if( sd.unitCell && Array.isArray( sd.unitCell ) && sd.unitCell[ 0 ] ){
            s.unitcell = new NGL.Unitcell(
                sd.unitCell[ 0 ], sd.unitCell[ 1 ], sd.unitCell[ 2 ],
                sd.unitCell[ 3 ], sd.unitCell[ 4 ], sd.unitCell[ 5 ],
                sd.spaceGroup
            );
        }else{
            s.unitcell = undefined;
        }

        if( NGL.debug ) NGL.timeEnd( "NGL.MmtfParser._parse " + this.name );

        // calculate backbone bonds
        NGL.calculateBondsBetween( s, true );

        // calculate rung bonds
        NGL.calculateBondsWithin( s, true );

        callback();

    }

} );


//////////////////////
// Trajectory parser

NGL.TrajectoryParser = function( streamer, params ){

    var p = params || {};

    NGL.Parser.call( this, streamer, p );

    this.frames = new NGL.Frames( this.name, this.path );

};

NGL.TrajectoryParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.TrajectoryParser,
    type: "trajectory",

    __objName: "frames"

} );


NGL.DcdParser = function( streamer, params ){

    var p = params || {};

    NGL.TrajectoryParser.call( this, streamer, p );

};

NGL.DcdParser.prototype = NGL.createObject(

    NGL.TrajectoryParser.prototype, {

    constructor: NGL.DcdParser,
    type: "dcd",

    _parse: function( callback ){

        // http://www.ks.uiuc.edu/Research/vmd/plugins/molfile/dcdplugin.html

        // The DCD format is structured as follows
        //   (FORTRAN UNFORMATTED, with Fortran data type descriptions):
        // HDR     NSET    ISTRT   NSAVC   5-ZEROS NATOM-NFREAT    DELTA   9-ZEROS
        // `CORD'  #files  step 1  step    zeroes  (zero)          timestep  (zeroes)
        //                         interval
        // C*4     INT     INT     INT     5INT    INT             DOUBLE  9INT
        // ==========================================================================
        // NTITLE          TITLE
        // INT (=2)        C*MAXTITL
        //                 (=32)
        // ==========================================================================
        // NATOM
        // #atoms
        // INT
        // ==========================================================================
        // X(I), I=1,NATOM         (DOUBLE)
        // Y(I), I=1,NATOM
        // Z(I), I=1,NATOM
        // ==========================================================================

        if( NGL.debug ) NGL.time( "NGL.DcdParser._parse " + this.name );

        var bin = this.streamer.data;
        if( bin instanceof Uint8Array ){
            bin = bin.buffer;
        }
        var dv = new DataView( bin );

        var f = this.frames;
        var coordinates = f.coordinates;
        var boxes = f.boxes;
        var header = {};
        var nextPos = 0;

        // header block

        var intView = new Int32Array( bin, 0, 23 );
        var ef = intView[ 0 ] !== dv.getInt32( 0 );  // endianess flag
        // swap byte order when big endian (84 indicates little endian)
        if( intView[ 0 ] !== 84 ){
            var n = bin.byteLength;
            for( var i = 0; i < n; i+=4 ){
                dv.setFloat32( i, dv.getFloat32( i ), true );
            }
        }
        if( intView[ 0 ] !== 84 ){
            NGL.error( "dcd bad format, header block start" );
        }
        // format indicator, should read 'CORD'
        var formatString = String.fromCharCode(
            dv.getUint8( 4 ), dv.getUint8( 5 ),
            dv.getUint8( 6 ), dv.getUint8( 7 )
        );
        if( formatString !== "CORD" ){
            NGL.error( "dcd bad format, format string" );
        }
        var isCharmm = false;
        var extraBlock = false;
        var fourDims = false;
        // version field in charmm, unused in X-PLOR
        if( intView[ 22 ] !== 0 ){
            isCharmm = true;
            if( intView[ 12 ] !== 0 ) extraBlock = true;
            if( intView[ 13 ] === 1 ) fourDims = true;
        }
        header.NSET = intView[ 2 ];
        header.ISTART = intView[ 3 ];
        header.NSAVC = intView[ 4 ];
        header.NAMNF = intView[ 10 ];
        if( isCharmm ){
            header.DELTA = dv.getFloat32( 44, ef );
        }else{
            header.DELTA = dv.getFloat64( 44, ef );
        }
        if( intView[ 22 ] !== 84 ){
            NGL.error( "dcd bad format, header block end" );
        }
        nextPos = nextPos + 21 * 4 + 8;

        // title block

        var titleLength = dv.getInt32( nextPos, ef );
        var titlePos = nextPos + 1;
        if( ( titleLength - 4 ) % 80 !== 0 ){
            NGL.error( "dcd bad format, title block start" );
        }
        header.TITLE = NGL.Uint8ToString(
            new Uint8Array( bin, titlePos, titleLength )
        );
        if( dv.getInt32( titlePos + titleLength + 4 - 1, ef ) !== titleLength ){
            NGL.error( "dcd bad format, title block end" );
        }
        nextPos = nextPos + titleLength + 8;

        // natom block

        if( dv.getInt32( nextPos, ef ) !== 4 ){
            NGL.error( "dcd bad format, natom block start" );
        }
        header.NATOM = dv.getInt32( nextPos + 4, ef );
        if( dv.getInt32( nextPos + 8, ef ) !== 4 ){
            NGL.error( "dcd bad format, natom block end" );
        }
        nextPos = nextPos + 4 + 8;

        // fixed atoms block

        if( header.NAMNF > 0 ){
            // TODO read coordinates and indices of fixed atoms
            NGL.error( "dcd format with fixed atoms unsupported, aborting" );
            callback();
            return;
        }

        // frames

        var natom = header.NATOM;
        var natom4 = natom * 4;

        for( var i = 0, n = header.NSET; i < n; ++i ){

            if( extraBlock ){
                nextPos += 4;  // block start
                // unitcell: A, alpha, B, beta, gamma, C (doubles)
                var box = new Float32Array( 9 );
                box[ 0 ] = dv.getFloat64( nextPos        , ef );
                box[ 4 ] = dv.getFloat64( nextPos + 2 * 8, ef );
                box[ 8 ] = dv.getFloat64( nextPos + 5 * 8, ef );
                boxes.push( box );
                nextPos += 48;
                nextPos += 4;  // block end
            }

            // xyz coordinates
            var coord = new Float32Array( natom * 3 );
            for( var j = 0; j < 3; ++j ){
                if( dv.getInt32( nextPos, ef ) !== natom4 ){
                    NGL.error( "dcd bad format, coord block start", i, j );
                }
                nextPos += 4;  // block start
                var c = new Float32Array( bin, nextPos, natom );
                for( var k = 0; k < natom; ++k ){
                    coord[ 3 * k + j ] = c[ k ];
                }
                nextPos += natom4;
                if( dv.getInt32( nextPos, ef ) !== natom4 ){
                    NGL.error( "dcd bad format, coord block end", i, j );
                }
                nextPos += 4;  // block end
            }
            coordinates.push( coord );

            if( fourDims ){
                var bytes = dv.getInt32( nextPos, ef );
                nextPos += 4 + bytes + 4;  // block start + skip + block end
            }

        }

        // console.log( header );
        // console.log( header.TITLE );
        // console.log( "isCharmm", isCharmm, "extraBlock", extraBlock, "fourDims", fourDims );

        if( NGL.debug ) NGL.timeEnd( "NGL.DcdParser._parse " + this.name );
        callback();

    },

} );


//////////////////
// Volume parser

NGL.VolumeParser = function( streamer, params ){

    var p = params || {};

    NGL.Parser.call( this, streamer, p );

    this.volume = new NGL.Volume( this.name, this.path );

};

NGL.VolumeParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.VolumeParser,
    type: "volume",

    __objName: "volume",

    _afterParse: function(){

        this.volume.setMatrix( this.getMatrix() );

    },

    getMatrix: function(){

        return new THREE.Matrix4();

    }

} );


NGL.MrcParser = function( streamer, params ){

    NGL.VolumeParser.call( this, streamer, params );

};

NGL.MrcParser.prototype = NGL.createObject(

    NGL.VolumeParser.prototype, {

    constructor: NGL.MrcParser,
    type: "mrc",

    _parse: function( callback ){

        // MRC
        // http://ami.scripps.edu/software/mrctools/mrc_specification.php
        // http://www2.mrc-lmb.cam.ac.uk/research/locally-developed-software/image-processing-software/#image
        // http://bio3d.colorado.edu/imod/doc/mrc_format.txt

        // CCP4 (MAP)
        // http://www.ccp4.ac.uk/html/maplib.html

        // MRC format does not use the skew transformation header records (words 25-37)
        // CCP4 format does not use the ORIGIN header records (words 50-52)

        if( NGL.debug ) NGL.time( "NGL.MrcParser._parse " + this.name );

        var bin = this.streamer.data;

        if( bin instanceof Uint8Array ){
            bin = bin.buffer;
        }

        var v = this.volume;
        var header = {};

        var intView = new Int32Array( bin, 0, 56 );
        var floatView = new Float32Array( bin, 0, 56 );

        var dv = new DataView( bin );

        // 53  MAP         Character string 'MAP ' to identify file type
        header.MAP = String.fromCharCode(
            dv.getUint8( 52 * 4 ), dv.getUint8( 52 * 4 + 1 ),
            dv.getUint8( 52 * 4 + 2 ), dv.getUint8( 52 * 4 + 3 )
        );

        // 54  MACHST      Machine stamp indicating machine type which wrote file
        //                 17 and 17 for big-endian or 68 and 65 for little-endian
        header.MACHST = [ dv.getUint8( 53 * 4 ), dv.getUint8( 53 * 4 + 1 ) ];

        // swap byte order when big endian
        if( header.MACHST[ 0 ] === 17 && header.MACHST[ 1 ] === 17 ){
            var n = bin.byteLength;
            for( var i = 0; i < n; i+=4 ){
                dv.setFloat32( i, dv.getFloat32( i ), true );
            }
        }

        header.NX = intView[ 0 ];  // NC - columns (fastest changing)
        header.NY = intView[ 1 ];  // NR - rows
        header.NZ = intView[ 2 ];  // NS - sections (slowest changing)

        // mode
        //  0 image : signed 8-bit bytes range -128 to 127
        //  1 image : 16-bit halfwords
        //  2 image : 32-bit reals
        //  3 transform : complex 16-bit integers
        //  4 transform : complex 32-bit reals
        //  6 image : unsigned 16-bit range 0 to 65535
        // 16 image: unsigned char * 3 (for rgb data, non-standard)
        //
        // Note: Mode 2 is the normal mode used in the CCP4 programs.
        //       Other modes than 2 and 0 may NOT WORK
        header.MODE = intView[ 3 ];

        // start
        header.NXSTART = intView[ 4 ];  // NCSTART - first column
        header.NYSTART = intView[ 5 ];  // NRSTART - first row
        header.NZSTART = intView[ 6 ];  // NSSTART - first section

        // intervals
        header.MX = intView[ 7 ];  // intervals along x
        header.MY = intView[ 8 ];  // intervals along y
        header.MZ = intView[ 9 ];  // intervals along z

        // cell length (Angstroms in CCP4)
        header.xlen = floatView[ 10 ];
        header.ylen = floatView[ 11 ];
        header.zlen = floatView[ 12 ];

        // cell angle (Degrees)
        header.alpha = floatView[ 13 ];
        header.beta  = floatView[ 14 ];
        header.gamma = floatView[ 15 ];

        // axis correspondence (1,2,3 for X,Y,Z)
        header.MAPC = intView[ 16 ];  // column
        header.MAPR = intView[ 17 ];  // row
        header.MAPS = intView[ 18 ];  // section

        // density statistics
        header.DMIN  = floatView[ 19 ];
        header.DMAX  = floatView[ 20 ];
        header.DMEAN = floatView[ 21 ];

        // space group number 0 or 1 (default=0)
        header.ISPG = intView[ 22 ];

        // number of bytes used for symmetry data (0 or 80)
        header.NSYMBT = intView[ 23 ];

        // Flag for skew transformation, =0 none, =1 if foll
        header.LSKFLG = intView[ 24 ];

        // 26-34  SKWMAT  Skew matrix S (in order S11, S12, S13, S21 etc) if
        //                LSKFLG .ne. 0.
        // 35-37  SKWTRN  Skew translation t if LSKFLG != 0.
        //                Skew transformation is from standard orthogonal
        //                coordinate frame (as used for atoms) to orthogonal
        //                map frame, as Xo(map) = S * (Xo(atoms) - t)

        // 38      future use       (some of these are used by the MSUBSX routines
        //  .          "              in MAPBRICK, MAPCONT and FRODO)
        //  .          "   (all set to zero by default)
        //  .          "
        // 52          "

        // 50-52 origin in X,Y,Z used for transforms
        header.originX = floatView[ 49 ];
        header.originY = floatView[ 50 ];
        header.originZ = floatView[ 51 ];

        // 53  MAP         Character string 'MAP ' to identify file type
        // => see top of this parser

        // 54  MACHST      Machine stamp indicating machine type which wrote file
        // => see top of this parser

        // Rms deviation of map from mean density
        header.ARMS = floatView[ 54 ];

        // 56      NLABL           Number of labels being used
        // 57-256  LABEL(20,10)    10  80 character text labels (ie. A4 format)

        v.header = header;

        // NGL.log( header )

        // FIXME depends on mode
        var data = new Float32Array(
            bin, 256 * 4 + header.NSYMBT,
            header.NX * header.NY * header.NZ
        );

        v.setData( data, header.NX, header.NY, header.NZ );

        if( NGL.debug ) NGL.timeEnd( "NGL.MrcParser._parse " + this.name );
        callback();

    },

    getMatrix: function(){

        var h = this.volume.header;

        var basisX = [
            h.xlen,
            0,
            0
        ];

        var basisY = [
            h.ylen * Math.cos( Math.PI / 180.0 * h.gamma ),
            h.ylen * Math.sin( Math.PI / 180.0 * h.gamma ),
            0
        ];

        var basisZ = [
            h.zlen * Math.cos( Math.PI / 180.0 * h.beta ),
            h.zlen * (
                    Math.cos( Math.PI / 180.0 * h.alpha )
                    - Math.cos( Math.PI / 180.0 * h.gamma )
                    * Math.cos( Math.PI / 180.0 * h.beta )
                ) / Math.sin( Math.PI / 180.0 * h.gamma ),
            0
        ];
        basisZ[ 2 ] = Math.sqrt(
            h.zlen * h.zlen * Math.sin( Math.PI / 180.0 * h.beta ) *
            Math.sin( Math.PI / 180.0 * h.beta ) - basisZ[ 1 ] * basisZ[ 1 ]
        );

        var basis = [ 0, basisX, basisY, basisZ ];
        var nxyz = [ 0, h.MX, h.MY, h.MZ ];
        var mapcrs = [ 0, h.MAPC, h.MAPR, h.MAPS ];

        var matrix = new THREE.Matrix4();

        matrix.set(

            basis[ mapcrs[1] ][0] / nxyz[ mapcrs[1] ],
            basis[ mapcrs[2] ][0] / nxyz[ mapcrs[2] ],
            basis[ mapcrs[3] ][0] / nxyz[ mapcrs[3] ],
            0,

            basis[ mapcrs[1] ][1] / nxyz[ mapcrs[1] ],
            basis[ mapcrs[2] ][1] / nxyz[ mapcrs[2] ],
            basis[ mapcrs[3] ][1] / nxyz[ mapcrs[3] ],
            0,

            basis[ mapcrs[1] ][2] / nxyz[ mapcrs[1] ],
            basis[ mapcrs[2] ][2] / nxyz[ mapcrs[2] ],
            basis[ mapcrs[3] ][2] / nxyz[ mapcrs[3] ],
            0,

            0, 0, 0, 1

        );

        matrix.setPosition( new THREE.Vector3(
            h.originX, h.originY, h.originZ
        ) );

        matrix.multiply( new THREE.Matrix4().makeTranslation(
            h.NXSTART, h.NYSTART, h.NZSTART
        ) );

        return matrix;

    }

} );


NGL.CubeParser = function( streamer, params ){

    // @author Johanna Tiemann <johanna.tiemann@googlemail.com>
    // @author Alexander Rose <alexander.rose@weirdbyte.de>

    NGL.VolumeParser.call( this, streamer, params );

};

NGL.CubeParser.prototype = NGL.createObject(

    NGL.VolumeParser.prototype, {

    constructor: NGL.CubeParser,
    type: "cube",

    _parse: function( callback ){

        // http://paulbourke.net/dataformats/cube/

        if( NGL.debug ) NGL.time( "NGL.CubeParser._parse " + this.name );

        var v = this.volume;
        var headerLines = this.streamer.peekLines( 6 );
        var header = {};
        var reWhitespace = /\s+/;
        var bohrToAngstromFactor = 0.529177210859;

        function headerhelper( k, l ) {
            var field = headerLines[ k ].trim().split( reWhitespace )[ l ];
            return parseFloat( field );
        }

        header.atomCount = Math.abs( headerhelper( 2, 0 ) ); //Number of atoms
        header.originX = headerhelper( 2, 1 ) * bohrToAngstromFactor; //Position of origin of volumetric data
        header.originY = headerhelper( 2, 2 ) * bohrToAngstromFactor;
        header.originZ = headerhelper( 2, 3 ) * bohrToAngstromFactor;
        header.NVX = headerhelper( 3, 0 ); //Number of voxels
        header.NVY = headerhelper( 4, 0 );
        header.NVZ = headerhelper( 5, 0 );
        header.AVX = headerhelper( 3, 1 ) * bohrToAngstromFactor; //Axis vector
        header.AVY = headerhelper( 4, 2 ) * bohrToAngstromFactor;
        header.AVZ = headerhelper( 5, 3 ) * bohrToAngstromFactor;

        var data = new Float32Array( header.NVX * header.NVY * header.NVZ );
        var count = 0;
        var lineNo = 0;

        function _parseChunkOfLines( _i, _n, lines ){

            for( var i = _i; i < _n; ++i ){

                var line = lines[ i ].trim();

                if( line !== "" && lineNo >= header.atomCount + 6 ){

                    line = line.split( reWhitespace );
                    for( var j = 0, lj = line.length; j < lj; ++j ){
                        if ( line.length !==1 ) {
                            data[ count ] = parseFloat( line[ j ] );
                            ++count;
                        };
                    };

                }

                ++lineNo;

            };

        };

        this.streamer.eachChunkOfLines( function( lines, chunkNo, chunkCount ){
            _parseChunkOfLines( 0, lines.length, lines );
        } );

        v.header = header;
        v.setData( data, header.NVZ, header.NVY, header.NVX );

        if( NGL.debug ) NGL.timeEnd( "NGL.CubeParser._parse " + this.name );
        callback();

    },

    getMatrix: function(){

        var h = this.volume.header;
        var matrix = new THREE.Matrix4();

        matrix.multiply(
            new THREE.Matrix4().makeRotationY( THREE.Math.degToRad( 90 ) )
        );

        matrix.multiply(
            new THREE.Matrix4().makeTranslation(
                -h.originZ, h.originY, h.originX
            )
        );

        matrix.multiply(
            new THREE.Matrix4().makeScale(
                -h.AVZ, h.AVY, h.AVX
            )
        );

        return matrix;

    }

} );


NGL.DxParser = function( streamer, params ){

    NGL.VolumeParser.call( this, streamer, params );

};

NGL.DxParser.prototype = NGL.createObject(

    NGL.VolumeParser.prototype, {

    constructor: NGL.DxParser,
    type: "dx",

    _parse: function( callback ){

        // http://www.poissonboltzmann.org/docs/file-format-info/

        if( NGL.debug ) NGL.time( "NGL.DxParser._parse " + this.name );

        var v = this.volume;
        var headerLines = this.streamer.peekLines( 30 );
        var headerInfo = this.parseHeaderLines( headerLines );
        var header = this.volume.header;
        var dataLineStart = headerInfo.dataLineStart;

        var reWhitespace = /\s+/;
        var size = header.nx * header.ny * header.nz;
        var data = new Float32Array( size );
        var count = 0;
        var lineNo = 0;

        function _parseChunkOfLines( _i, _n, lines ){

            for( var i = _i; i < _n; ++i ){

                if( count < size && lineNo > dataLineStart ){

                    var line = lines[ i ].trim();

                    if( line !== "" ){

                        var ls = line.split( reWhitespace );

                        for( var j = 0, lj = ls.length; j < lj; ++j ){
                            data[ count ] = parseFloat( ls[ j ] );
                            ++count;
                        };

                    }

                }

                ++lineNo;

            };

        };

        this.streamer.eachChunkOfLines( function( lines, chunkNo, chunkCount ){
            _parseChunkOfLines( 0, lines.length, lines );
        } );

        v.setData( data, header.nz, header.ny, header.nx );

        if( NGL.debug ) NGL.timeEnd( "NGL.DxParser._parse " + this.name );
        callback();

    },

    parseHeaderLines: function( headerLines ){

        var header = {};
        var reWhitespace = /\s+/;
        var n = headerLines.length;

        var dataLineStart = 0;
        var headerByteCount = 0;
        var deltaLineCount = 0;

        for( var i = 0; i < n; ++i ){

            var line = headerLines[ i ];

            if( line.startsWith( "object 1" ) ){

                var ls = line.split( reWhitespace );

                header.nx = parseInt( ls[ 5 ] );
                header.ny = parseInt( ls[ 6 ] );
                header.nz = parseInt( ls[ 7 ] );

            }else if( line.startsWith( "origin" ) ){

                var ls = line.split( reWhitespace );

                header.xmin = parseFloat( ls[ 1 ] );
                header.ymin = parseFloat( ls[ 2 ] );
                header.zmin = parseFloat( ls[ 3 ] );

            }else if( line.startsWith( "delta" ) ){

                var ls = line.split( reWhitespace );

                if( deltaLineCount === 0 ){
                    header.hx = parseFloat( ls[ 1 ] );
                }else if( deltaLineCount === 1 ){
                    header.hy = parseFloat( ls[ 2 ] );
                }else if( deltaLineCount === 2 ){
                    header.hz = parseFloat( ls[ 3 ] );
                }

                deltaLineCount += 1;

            }else if( line.startsWith( "object 3" ) ){

                dataLineStart = i;
                headerByteCount += line.length + 1;
                break;

            }

            headerByteCount += line.length + 1;

        }

        this.volume.header = header;

        return {
            dataLineStart: dataLineStart,
            headerByteCount: headerByteCount
        }

    },

    getMatrix: function(){

        var h = this.volume.header;
        var matrix = new THREE.Matrix4();

        matrix.multiply(
            new THREE.Matrix4().makeRotationY( THREE.Math.degToRad( 90 ) )
        );

        matrix.multiply(
            new THREE.Matrix4().makeTranslation(
                -h.zmin, h.ymin, h.xmin
            )
        );

        matrix.multiply(
            new THREE.Matrix4().makeScale(
                -h.hz, h.hy, h.hx
            )
        );

        return matrix;

    }

} );


NGL.DxbinParser = function( streamer, params ){

    NGL.DxParser.call( this, streamer, params );

};

NGL.DxbinParser.prototype = NGL.createObject(

    NGL.DxParser.prototype, {

    constructor: NGL.DxbinParser,
    type: "dxbin",

    _parse: function( callback ){

        // https://github.com/Electrostatics/apbs-pdb2pqr/issues/216

        if( NGL.debug ) NGL.time( "NGL.DxbinParser._parse " + this.name );

        var bin = this.streamer.data;
        if( bin instanceof Uint8Array ){
            bin = bin.buffer;
        }

        var headerLines = NGL.Uint8ToLines( new Uint8Array( bin, 0, 1000 ) );
        var headerInfo = this.parseHeaderLines( headerLines );
        var header = this.volume.header;
        var headerByteCount = headerInfo.headerByteCount;

        var size = header.nx * header.ny * header.nz;
        var dv = new DataView( bin );
        var data = new Float32Array( size );

        for( var i = 0; i < size; ++i ){
            data[ i ] = dv.getFloat64( i * 8 + headerByteCount, true );
        }

        this.volume.setData( data, header.nz, header.ny, header.nx );

        if( NGL.debug ) NGL.timeEnd( "NGL.DxbinParser._parse " + this.name );

        callback();

    }

} );


///////////////////
// Surface parser

NGL.SurfaceParser = function( streamer, params ){

    var p = params || {};

    NGL.Parser.call( this, streamer, p );

    this.loader = undefined;
    this.surface = new NGL.Surface( this.name, this.path );

};

NGL.SurfaceParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.SurfaceParser,
    type: "surface",

    __objName: "surface",

    _parse: function( callback ){

        var geometry = this.loader.parse( this.streamer.asText() );

        this.surface.fromGeometry( geometry );

        callback();

    }

} );


NGL.PlyParser = function( streamer, params ){

    var p = params || {};

    NGL.SurfaceParser.call( this, streamer, p );

    this.loader = new THREE.PLYLoader();

};

NGL.PlyParser.prototype = NGL.createObject(

    NGL.SurfaceParser.prototype, {

    constructor: NGL.PlyParser,
    type: "ply"

} );


NGL.ObjParser = function( streamer, params ){

    var p = params || {};

    NGL.SurfaceParser.call( this, streamer, p );

    this.loader = new THREE.OBJLoader();

};

NGL.ObjParser.prototype = NGL.createObject(

    NGL.SurfaceParser.prototype, {

    constructor: NGL.ObjParser,
    type: "obj"

} );


////////////////
// Text parser

NGL.TextParser = function( streamer, params ){

    var p = params || {};

    NGL.Parser.call( this, streamer, p );

    this.text = {

        name: this.name,
        path: this.path,
        data: ""

    };

};

NGL.TextParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.TextParser,
    type: "text",

    __objName: "text",

    _parse: function( callback ){

        this.text.data = this.streamer.asText();

        callback();

    }

} );


///////////////
// Csv parser

NGL.CsvParser = function( streamer, params ){

    var p = params || {};

    NGL.Parser.call( this, streamer, p );

    this.table = {

        name: this.name,
        path: this.path,
        colNames: [],
        data: []

    };

};

NGL.CsvParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.CsvParser,
    type: "csv",

    __objName: "table",

    _parse: function( callback ){

        var data = this.table.data;
        var reDelimiter = /\s*,\s*/;

        this.streamer.eachChunkOfLines( function( chunk, chunkNo, chunkCount ){

            var n = chunk.length;

            for( var i = 0; i < n; ++i ){

                var line = chunk[ i ].trim();
                var values = line.split( reDelimiter );

                if( chunkNo === 0 && i === 0 ){

                    this.table.colNames = values;

                }else if( line ){

                    data.push( values );

                }

            }

        }.bind( this ) );

        callback();

    }

} );


////////////////
// Json parser

NGL.JsonParser = function( streamer, params ){

    var p = params || {};

    NGL.Parser.call( this, streamer, p );

    this.json = {

        name: this.name,
        path: this.path,
        data: {}

    };

};

NGL.JsonParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.JsonParser,
    type: "json",

    __objName: "json",

    _parse: function( callback ){

        if( this.streamer.compressed || this.streamer.binary ){
            this.json.data = JSON.parse( this.streamer.asText() );
        }else{
            this.json.data = this.streamer.data;
        }

        callback();

    }

} );


////////////////
// Xml parser

NGL.XmlParser = function( streamer, params ){

    var p = params || {};

    NGL.Parser.call( this, streamer, p );

    this.xml = {

        name: this.name,
        path: this.path,
        data: {}

    };

};

NGL.XmlParser.prototype = NGL.createObject(

    NGL.Parser.prototype, {

    constructor: NGL.XmlParser,
    type: "xml",

    __objName: "xml",

    _parse: function( callback ){

        // https://github.com/segmentio/xml-parser
        // MIT license

        function parse( xml ){

            xml = xml.trim();

            // strip comments
            xml = xml.replace( /<!--[\s\S]*?-->/g, '' );

            return document();

            function document(){
                return {
                    declaration: declaration(),
                    root: tag()
                }
            }

            function declaration(){
                var m = match(/^<\?xml\s*/);
                if (!m) return;
                // tag
                var node = {
                    attributes: {}
                };
                // attributes
                while (!(eos() || is('?>'))) {
                    var attr = attribute();
                    if (!attr) return node;
                    node.attributes[attr.name] = attr.value;
                }
                match(/\?>\s*/);
                return node;
            }

            function tag(){
                var m = match(/^<([\w-:.]+)\s*/);
                if (!m) return;
                // name
                var node = {
                    name: m[1],
                    attributes: {},
                    children: []
                };
                // attributes
                while (!(eos() || is('>') || is('?>') || is('/>'))) {
                    var attr = attribute();
                    if (!attr) return node;
                    node.attributes[attr.name] = attr.value;
                }
                // self closing tag
                if (match(/^\s*\/>\s*/)) {
                    return node;
                }
                match(/\??>\s*/);
                // content
                node.content = content();
                // children
                var child;
                while (child = tag()) {
                    node.children.push(child);
                }
                // closing
                match(/^<\/[\w-:.]+>\s*/);
                return node;
            }

            function content(){
                var m = match(/^([^<]*)/);
                if (m) return m[1];
                return '';
            }

            function attribute(){
                var m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/);
                if (!m) return;
                return { name: m[1], value: strip(m[2]) }
            }

            function strip( val ){
                return val.replace(/^['"]|['"]$/g, '');
            }

            function match( re ){
                var m = xml.match(re);
                if (!m) return;
                xml = xml.slice(m[0].length);
                return m;
            }

            function eos(){
                return 0 == xml.length;
            }

            function is( prefix ){
                return 0 == xml.indexOf(prefix);
            }

        }

        this.xml.data = parse( this.streamer.asText() );

        callback();

    }

} );

// File:js/ngl/writer.js

/**
 * @file Writer
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


///////////////
// PDB Writer

NGL.PdbWriter = function( structure, params ){

    var p = Object.assign( {}, params );

    var renumberSerial = p.renumberSerial !== undefined ? p.renumberSerial : true;
    var remarks = p.remarks || [];
    if( !Array.isArray( remarks ) ) remarks = [ remarks ];

    var records;

    function writeRecords(){

        records = [];

        writeTitle();
        writeRemarks();
        writeAtoms();

    }

    // http://www.wwpdb.org/documentation/file-format

    // Sample PDB line, the coords X,Y,Z are fields 5,6,7 on each line.
    // ATOM      1  N   ARG     1      29.292  13.212 -12.751  1.00 33.78      1BPT 108

    function DEF( x, y ){
        return x !== undefined ? x : y;
    }

    var atomFormat =
        "ATOM  %5d %-4s %3s %1s%4d    %8.3f%8.3f%8.3f%6.2f%6.2f      %4s%2s";

    var hetatmFormat =
        "HETATM%5d %-4s %3s %1s%4d    %8.3f%8.3f%8.3f%6.2f%6.2f      %4s%2s";

    function writeTitle(){

        // FIXME multiline if title line longer than 80 chars
        records.push( sprintf( "TITEL %-74s", structure.name ) );

    }

    function writeRemarks(){

        remarks.forEach( function( str ){
            records.push( sprintf( "REMARK %-73s", str ) );
        } );

        if( structure.trajectory ){
            records.push( sprintf(
                "REMARK %-73s",
                "Trajectory '" + structure.trajectory.name + "'"
            ) );
            records.push( sprintf(
                "REMARK %-73s",
                "Frame " + structure.trajectory.frame + ""
            ) );
        }

    }

    function writeAtoms(){

        var ia = 1;
        var im = 1;

        structure.eachModel( function( m ){

            records.push( sprintf( "MODEL %-74d", im++ ) );

            m.eachAtom( function( a ){

                var formatString = a.hetero ? hetatmFormat : atomFormat;
                var serial = renumberSerial ? ia : a.serial;

                // Alignment of one-letter atom name such as C starts at column 14,
                // while two-letter atom name such as FE starts at column 13.
                var atomname = a.atomname;
                if( atomname.length === 1 ) atomname = " " + atomname;

                records.push( sprintf(
                    formatString,

                    serial,
                    atomname,
                    a.resname,
                    DEF( a.chainname, " " ),
                    a.resno,
                    a.x, a.y, a.z,
                    DEF( a.occurence, 1.0 ),
                    DEF( a.bfactor, 0.0 ),
                    DEF( a.segid, "" ),
                    DEF( a.element, "" )
                ) );
                ia += 1;

            } );

            records.push( sprintf( "%-80s", "ENDMDL" ) );
            im += 1;

        } );

        records.push( sprintf( "%-80s", "END" ) );

    }

    function getString(){

        writeRecords();
        return records.join( "\n" );

    }

    function getBlob(){

        return new Blob(
            [ getString() ],
            { type: 'text/plain' }
        );

    }

    function download( name, ext ){

        name = name || "structure"
        ext = ext || "pdb";

        var file = name + "." + ext;
        var blob = getBlob();

        NGL.download( blob, file );

    }

    // API

    this.getString = getString;
    this.getBlob = getBlob;
    this.download = download;

};

// File:js/ngl/loader.js

/**
 * @file Loader
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


///////////////
// Datasource

NGL.DatasourceRegistry = {

    sourceDict: {},

    listing: undefined,
    trajectory: undefined,

    __passThrough: {
        getUrl: function( path ){
            return path;
        }
    },

    add: function( name, datasource ){
        name = name.toLowerCase();
        if( name in this.sourceDict ){
            NGL.warn( "overwriting datasource named '" + name + "'" );
        }
        this.sourceDict[ name ] = datasource;
    },

    get: function( name ){
        name = name || "";
        name = name.toLowerCase();
        if( name in this.sourceDict ){
            return this.sourceDict[ name ];
        }else if( [ "http", "https", "ftp" ].indexOf( name ) !== -1 ){
            return this.__passThrough;
        }else if( !name ){
            return this.__passThrough;
        }else{
            NGL.error( "no datasource named '" + name + "' found" );
        }
    }

};


NGL.getDataInfo = function( src ){

    var info = NGL.getFileInfo( src );
    var datasource = NGL.DatasourceRegistry.get( info.protocol );
    var url = datasource.getUrl( info.src );
    var info2 = NGL.getFileInfo( url );
    if( !info2.ext && datasource.getExt ){
        info2.ext = datasource.getExt( src );
    }

    return info2;

};


NGL.StaticDatasource = function( baseUrl ){

    baseUrl = baseUrl || "";

    this.getUrl = function( src ){
        var info = NGL.getFileInfo( src );
        return NGL.getAbsolutePath( baseUrl + info.path );
    };

};


NGL.RcsbDatasource = function(){

    var baseUrl = "http://files.rcsb.org/download/";
    var mmtfBaseUrl = "http://mmtf.rcsb.org/v0/full/";
    var bbMmtfBaseUrl = "http://mmtf.rcsb.org/reduced/";

    this.getUrl = function( src ){
        // valid path are
        // XXXX.pdb, XXXX.pdb.gz, XXXX.cif, XXXX.cif.gz, XXXX.mmtf, XXXX.bb.mmtf
        // XXXX defaults to XXXX.cif
        var info = NGL.getFileInfo( src );
        var file;
        if( [ "pdb", "cif" ].indexOf( info.ext ) !== -1 &&
            ( info.compressed === false || info.compressed === "gz" )
        ){
            return baseUrl + info.path;
        }else if( info.ext === "mmtf" ){
            if( info.base.endsWith( ".bb" ) ){
                return bbMmtfBaseUrl + info.name;
            }else{
                return mmtfBaseUrl + info.name;
            }
        }else if( !info.ext ){
            return baseUrl + info.name + ".cif";
            // return mmtfBaseUrl + info.name + ".mmtf";
        }else{
            console.warn( "unsupported ext", info.ext );
            return mmtfBaseUrl + info.name;
        }
    };

    this.getExt = function( src ){
        var info = NGL.getFileInfo( src );
        if( info.ext === "mmtf" || !info.ext ){
            return "mmtf";
        }
    };

};

NGL.DatasourceRegistry.add(
    "rcsb", new NGL.RcsbDatasource()
);


///////////
// Loader

NGL.Loader = function( src, params ){

    var p = Object.assign( {}, params );

    var binaryExtList = [ "mmtf", "dcd", "mrc", "ccp4", "map", "dxbin" ];
    var binary = binaryExtList.indexOf( p.ext ) !== -1;

    this.compressed = p.compressed || false;
    this.binary = p.binary !== undefined ? p.binary : binary;
    this.name = p.name || "";
    this.ext = p.ext || "";
    this.dir = p.dir || "";
    this.path = p.path || "";
    this.protocol = p.protocol || "";

    this.params = params;

    //

    var streamerParams = {
        compressed: this.compressed,
        binary: this.binary,
        json: this.ext === "json"
    };

    if( ( self.File && src instanceof File ) ||
        ( self.Blob && src instanceof self.Blob )
    ){
        this.streamer = new NGL.FileStreamer( src, streamerParams );
    }else{
        this.streamer = new NGL.NetworkStreamer( src, streamerParams );
    }

    if( typeof p.onProgress === "function" ){
        this.streamer.onprogress = p.onprogress;
    }

};

NGL.Loader.prototype = {

    constructor: NGL.Loader,

    load: function(){

        return new Promise( function( resolve, reject ){

            this.streamer.onerror = reject;

            try{
                this._load( resolve, reject );
            }catch( e ){
                reject( e );
            }

        }.bind( this ) );

    },

    _load: function( resolve, reject ){

        reject( "not implemented" );

    }

};


NGL.ParserLoader = function( src, params ){

    NGL.Loader.call( this, src, params );

    this.useWorker = this.params.useWorker === undefined ? false : this.params.useWorker;

};

NGL.ParserLoader.prototype = NGL.createObject(

    NGL.Loader.prototype, {

    constructor: NGL.ParserLoader,

    _load: function( resolve, reject ){

        var parsersClasses = {

            "gro": NGL.GroParser,
            "pdb": NGL.PdbParser,
            "pdb1": NGL.PdbParser,
            "ent": NGL.PdbParser,
            "pqr": NGL.PqrParser,
            "cif": NGL.CifParser,
            "mcif": NGL.CifParser,
            "mmcif": NGL.CifParser,
            "sdf": NGL.SdfParser,
            "mol2": NGL.Mol2Parser,

            "mmtf": NGL.MmtfParser,

            "dcd": NGL.DcdParser,

            "mrc": NGL.MrcParser,
            "ccp4": NGL.MrcParser,
            "map": NGL.MrcParser,
            "cube": NGL.CubeParser,
            "dx": NGL.DxParser,
            "dxbin": NGL.DxbinParser,

            "ply": NGL.PlyParser,
            "obj": NGL.ObjParser,

            "txt": NGL.TextParser,
            "text": NGL.TextParser,
            "csv": NGL.CsvParser,
            "json": NGL.JsonParser,
            "xml": NGL.XmlParser

        };

        var parser = new parsersClasses[ this.ext ](
            this.streamer, this.params
        );

        if( this.useWorker ){

            parser.parseWorker( resolve );

        }else{

            parser.parse( resolve );

        }

    }

} );


NGL.ScriptLoader = function( src, params ){

    NGL.Loader.call( this, src, params );

};

NGL.ScriptLoader.prototype = NGL.createObject(

    NGL.Loader.prototype, {

    constructor: NGL.ScriptLoader,

    _load: function( resolve, reject ){

        this.streamer.read( function(){

            var text = this.streamer.asText();
            var script = new NGL.Script( text, this.name, this.path );
            resolve( script );

        }.bind( this ) );

    }

} );


NGL.PluginLoader = function( src, params ){

    NGL.Loader.call( this, src, params );

};

NGL.PluginLoader.prototype = NGL.createObject(

    NGL.Loader.prototype, {

    constructor: NGL.PluginLoader,

    _load: function( resolve, reject ){

        var basePath;
        if( this.protocol ){
            basePath = this.protocol + "://" + this.dir;
        }else{
            basePath = this.dir;
        }

        this.streamer.read( function(){

            var manifest = JSON.parse( this.streamer.asText() );
            var promiseList = [];

            manifest.files.map( function( name ){

                promiseList.push(
                    NGL.autoLoad( basePath + name, {
                        ext: "text", useWorker: false
                    } )
                );

            } );

            Promise.all( promiseList ).then( function( dataList ){

                var text = dataList.reduce( function( text, value ){
                    return text + "\n\n" + value.data;
                }, "" );
                text += manifest.source || "";

                var script = new NGL.Script( text, this.name, this.path );
                resolve( script );

            }.bind( this ) );

        }.bind( this ) );

    }

} );


NGL.loaderMap = {

    "gro": NGL.ParserLoader,
    "pdb": NGL.ParserLoader,
    "pdb1": NGL.ParserLoader,
    "ent": NGL.ParserLoader,
    "pqr": NGL.ParserLoader,
    "cif": NGL.ParserLoader,
    "mcif": NGL.ParserLoader,
    "mmcif": NGL.ParserLoader,
    "sdf": NGL.ParserLoader,
    "mol2": NGL.ParserLoader,

    "mmtf":  NGL.ParserLoader,

    "dcd": NGL.ParserLoader,

    "mrc": NGL.ParserLoader,
    "ccp4": NGL.ParserLoader,
    "map": NGL.ParserLoader,
    "cube": NGL.ParserLoader,
    "dx": NGL.ParserLoader,
    "dxbin": NGL.ParserLoader,

    "obj": NGL.ParserLoader,
    "ply": NGL.ParserLoader,

    "txt": NGL.ParserLoader,
    "text": NGL.ParserLoader,
    "csv": NGL.ParserLoader,
    "json": NGL.ParserLoader,
    "xml": NGL.ParserLoader,

    "ngl": NGL.ScriptLoader,
    "plugin": NGL.PluginLoader,

};


NGL.autoLoad = function( file, params ){

    var p = Object.assign( NGL.getDataInfo( file ), params );
    var loader = new NGL.loaderMap[ p.ext ]( p.src, p );

    if( loader ){
        return loader.load();
    }else{
        return Promise.reject( "NGL.autoLoading: ext '" + p.ext + "' unknown" );
    }

};

// File:js/ngl/viewer.js

/**
 * @file Viewer
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


/**
 * [Resources description]
 * @type {Object}
 * @private
 */
NGL.Resources = {

    // shaders
    'shader/CylinderImpostor.vert': null,
    'shader/CylinderImpostor.frag': null,
    'shader/HyperballStickImpostor.vert': null,
    'shader/HyperballStickImpostor.frag': null,
    'shader/Line.vert': null,
    'shader/Line.frag': null,
    // 'shader/LineSprite.vert': null,
    // 'shader/LineSprite.frag': null,
    'shader/Mesh.vert': null,
    'shader/Mesh.frag': null,
    // 'shader/ParticleSprite.vert': null,
    // 'shader/ParticleSprite.frag': null,
    'shader/Point.vert': null,
    'shader/Point.frag': null,
    'shader/Quad.vert': null,
    'shader/Quad.frag': null,
    'shader/Ribbon.vert': null,
    'shader/SDFFont.vert': null,
    'shader/SDFFont.frag': null,
    // 'shader/SphereHalo.vert': null,
    // 'shader/SphereHalo.frag': null,
    'shader/SphereImpostor.vert': null,
    'shader/SphereImpostor.frag': null,

    // shader chunks
    'shader/chunk/dull_interior_fragment.glsl': null,
    'shader/chunk/fog_fragment.glsl': null,
    'shader/chunk/nearclip_fragment.glsl': null,
    'shader/chunk/nearclip_vertex.glsl': null,
    'shader/chunk/opaque_back_fragment.glsl': null,

};


/**
 * [Utils description]
 * @namespace NGL.Utils
 * @type {Object}
 */
NGL.Utils = {

    /**
     * Converted to JavaScript from
     * {@link http://paulbourke.net/geometry/pointlineplane/lineline.c}
     *
     * @param  {THREE.Vector3} p1
     * @param  {THREE.Vector3} p2
     * @param  {THREE.Vector3} p3
     * @param  {THREE.Vector3} p4
     * @return {Array.<THREE.Vector3, THREE.Vector3>}
     */
    lineLineIntersect: function( p1, p2, p3, p4 ){

        var EPS = NGL.EPS;

        var p13 = new THREE.Vector3(),
            p43 = new THREE.Vector3(),
            p21 = new THREE.Vector3();
        var d1343, d4321, d1321, d4343, d2121;
        var denom, numer;

        p13.x = p1.x - p3.x;
        p13.y = p1.y - p3.y;
        p13.z = p1.z - p3.z;
        p43.x = p4.x - p3.x;
        p43.y = p4.y - p3.y;
        p43.z = p4.z - p3.z;
        if( Math.abs(p43.x) < EPS && Math.abs(p43.y) < EPS && Math.abs(p43.z) < EPS )
            return null;

        p21.x = p2.x - p1.x;
        p21.y = p2.y - p1.y;
        p21.z = p2.z - p1.z;
        if( Math.abs(p21.x) < EPS && Math.abs(p21.y) < EPS && Math.abs(p21.z) < EPS )
            return null;

        d1343 = p13.x * p43.x + p13.y * p43.y + p13.z * p43.z;
        d4321 = p43.x * p21.x + p43.y * p21.y + p43.z * p21.z;
        d1321 = p13.x * p21.x + p13.y * p21.y + p13.z * p21.z;
        d4343 = p43.x * p43.x + p43.y * p43.y + p43.z * p43.z;
        d2121 = p21.x * p21.x + p21.y * p21.y + p21.z * p21.z;

        denom = d2121 * d4343 - d4321 * d4321;
        if( Math.abs(denom) < EPS )
            return null;
        numer = d1343 * d4321 - d1321 * d4343;

        var mua = numer / denom;
        var mub = ( d1343 + d4321 * mua ) / d4343;

        var pa = new THREE.Vector3(
            p1.x + mua * p21.x,
            p1.y + mua * p21.y,
            p1.z + mua * p21.z
        );
        var pb = new THREE.Vector3(
            p3.x + mub * p43.x,
            p3.y + mub * p43.y,
            p3.z + mub * p43.z
        );

        return [ pa, pb ];

    },

    circularMean: function(){

        // http://en.wikipedia.org/wiki/Center_of_mass#Systems_with_periodic_boundary_conditions

        // Bai, Linge; Breen, David (2008). Calculating Center of Mass in an Unbounded 2D Environment. Journal of Graphics, GPU, and Game Tools 13 (4): 53–60.

        // http://stackoverflow.com/questions/18166507/using-fft-to-find-the-center-of-mass-under-periodic-boundary-conditions

        var twoPi = 2 * Math.PI;

        return function( array, max, stride, offset, indices ){

            stride = stride || 1;
            offset = offset || 0;

            var n = indices ? indices.length : array.length;
            var angle, i, c;

            var cosMean = 0;
            var sinMean = 0;

            if( indices ){

                for( i = 0; i < n; ++i ){

                    // NGL.log( indices[ i ], stride, offset, indices[ i ] * stride + offset, array.length, array[ indices[ i ] * stride + offset ] );

                    c = ( array[ indices[ i ] * stride + offset ] + max ) % max;

                    angle = ( c / max ) * twoPi - Math.PI;

                    cosMean += Math.cos( angle );
                    sinMean += Math.sin( angle );

                }

            }else{

                for( i = offset; i < n; i += stride ){

                    c = ( array[ i ] + max ) % max;

                    angle = ( c / max ) * twoPi - Math.PI;

                    cosMean += Math.cos( angle );
                    sinMean += Math.sin( angle );

                }

            }

            cosMean /= n;
            sinMean /= n;

            var meanAngle = Math.atan2( sinMean, cosMean );

            var mean = ( meanAngle + Math.PI ) / twoPi * max;

            return mean;

        }

    }(),

    calculateCenterArray: function( array1, array2, center, offset ){

        var n = array1.length;
        center = center || new Float32Array( n );
        offset = offset || 0;

        for( var i = 0; i < n; i+=3 ){

            center[ offset + i + 0 ] = ( array1[ i + 0 ] + array2[ i + 0 ] ) / 2.0;
            center[ offset + i + 1 ] = ( array1[ i + 1 ] + array2[ i + 1 ] ) / 2.0;
            center[ offset + i + 2 ] = ( array1[ i + 2 ] + array2[ i + 2 ] ) / 2.0;

        }

        return center;

    },

    calculateDirectionArray: function( array1, array2 ){

        var n = array1.length;
        var direction = new Float32Array( n );

        for( var i = 0; i < n; i+=3 ){

            direction[ i + 0 ] = array2[ i + 0 ] - array1[ i + 0 ];
            direction[ i + 1 ] = array2[ i + 1 ] - array1[ i + 1 ];
            direction[ i + 2 ] = array2[ i + 2 ] - array1[ i + 2 ];

        }

        return direction;

    },

    positionFromGeometry: function( geometry ){

        var vertices = geometry.vertices;

        var j, v3;
        var n = vertices.length;
        var position = new Float32Array( n * 3 );

        for( var v = 0; v < n; v++ ){

            j = v * 3;
            v3 = vertices[ v ];

            position[ j + 0 ] = v3.x;
            position[ j + 1 ] = v3.y;
            position[ j + 2 ] = v3.z;

        }

        return position;

    },

    colorFromGeometry: function( geometry ){

        var faces = geometry.faces;
        var vn = geometry.vertices.length;

        var j, f, c;
        var n = faces.length;
        var color = new Float32Array( vn * 3 );

        for( var v = 0; v < n; v++ ){

            f = faces[ v ];
            c = f.color;

            j = f.a * 3;
            color[ j + 0 ] = c.r;
            color[ j + 1 ] = c.g;
            color[ j + 2 ] = c.b;

            j = f.b * 3;
            color[ j + 0 ] = c.r;
            color[ j + 1 ] = c.g;
            color[ j + 2 ] = c.b;

            j = f.c * 3;
            color[ j + 0 ] = c.r;
            color[ j + 1 ] = c.g;
            color[ j + 2 ] = c.b;

        }

        return color;

    },

    indexFromGeometry: function( geometry ){

        var faces = geometry.faces;

        var j, f;
        var n = faces.length;
        var TypedArray = n * 3 > 65535 ? Uint32Array : Uint16Array;
        var index = new TypedArray( n * 3 );

        for( var v = 0; v < n; v++ ){

            j = v * 3;
            f = faces[ v ];

            index[ j + 0 ] = f.a;
            index[ j + 1 ] = f.b;
            index[ j + 2 ] = f.c;

        }

        return index;

    },

    normalFromGeometry: function( geometry ){

        var faces = geometry.faces;
        var vn = geometry.vertices.length;

        var j, f, nn, n1, n2, n3;
        var n = faces.length;
        var normal = new Float32Array( vn * 3 );

        for( var v = 0; v < n; v++ ){

            f = faces[ v ];
            nn = f.vertexNormals;
            n1 = nn[ 0 ];
            n2 = nn[ 1 ];
            n3 = nn[ 2 ];

            j = f.a * 3;
            normal[ j + 0 ] = n1.x;
            normal[ j + 1 ] = n1.y;
            normal[ j + 2 ] = n1.z;

            j = f.b * 3;
            normal[ j + 0 ] = n2.x;
            normal[ j + 1 ] = n2.y;
            normal[ j + 2 ] = n2.z;

            j = f.c * 3;
            normal[ j + 0 ] = n3.x;
            normal[ j + 1 ] = n3.y;
            normal[ j + 2 ] = n3.z;

        }

        return normal;

    },

    uniformArray: function( n, a ){

        var array = new Float32Array( n );

        for( var i = 0; i < n; ++i ){

            array[ i ] = a;

        }

        return array;

    },

    uniformArray3: function( n, a, b, c ){

        var array = new Float32Array( n * 3 );

        var j;

        for( var i = 0; i < n; ++i ){

            j = i * 3;

            array[ j + 0 ] = a;
            array[ j + 1 ] = b;
            array[ j + 2 ] = c;

        }

        return array;

    },

    randomColorArray: function( n ){

        var array = new Float32Array( n * 3 );

        var j;

        for( var i = 0; i < n; ++i ){

            j = i * 3;

            array[ j + 0 ] = Math.random();
            array[ j + 1 ] = Math.random();
            array[ j + 2 ] = Math.random();

        }

        return array;

    },

    replicateArray3Entries: function( array, m ){

        var n = array.length / 3;
        var repArr = new Float32Array( n * m * 3 );

        for( var i = 0; i < n; ++i ){

            var v = i * 3;
            var k = i * m * 3;

            var a = array[ v + 0 ];
            var b = array[ v + 1 ];
            var c = array[ v + 2 ];

            for( var j = 0; j < m; ++j ){

                var l = k + j * 3;

                repArr[ l + 0 ] = a;
                repArr[ l + 1 ] = b;
                repArr[ l + 2 ] = c;

            }

        }

        return repArr;

    },

    calculateMeanArray: function( array1, array2 ){

        var n = array1.length;
        var mean = new Float32Array( n );

        for( var i = 0; i < n; i++ ){

            mean[ i ] = ( array1[ i ] + array2[ i ] ) / 2.0;

        }

        return mean;

    },

    calculateMinArray: function( array1, array2 ){

        var n = array1.length;
        var min = new Float32Array( n );

        for( var i = 0; i < n; i++ ){

            min[ i ] = Math.min( array1[ i ],  array2[ i ] );

        }

        return min;

    },

    calculateMeanVector3: function( array ){

        var n = array.length;
        var m = array.length / 3;

        var x = 0;
        var y = 0;
        var z = 0;

        var i;

        for( i = 0; i < n; i += 3 ){

            x += array[ i + 0 ];
            y += array[ i + 1 ];
            z += array[ i + 2 ];

        }

        return new THREE.Vector3( x / m, y / m, z / m );

    },

    isPointOnSegment: function( p, l1, l2 ){

        var len = l1.distanceTo( l2 );

        return p.distanceTo( l1 ) <= len && p.distanceTo( l2 ) <= len;

    },

    pointVectorIntersection: function(){

        var v = new THREE.Vector3();
        var v1 = new THREE.Vector3();

        return function( point, origin, vector ){

            v.copy( vector );
            v1.subVectors( point, origin );
            var distOriginI = Math.cos( v.angleTo( v1 ) ) * v1.length();
            var vectorI = v.normalize().multiplyScalar( distOriginI );
            var pointI = new THREE.Vector3().addVectors( vectorI, origin );

            return pointI;

        }

    }(),

    copyArray: function( src, dst, srcOffset, dstOffset, length ){

        var i;
        var n = length;

        for( i = 0; i < n; ++i ){

            dst[ dstOffset + i ] = src[ srcOffset + i ];

        }

    }

};


NGL.getShader = function(){

    var re = /^(?!\/\/)\s*#include\s+(\S+)/gmi;
    var cache = {};

    function getDefines( defines ){

        if( defines === undefined ) return "";

        var lines = [];

        for ( var name in defines ) {

            var value = defines[ name ];

            if ( value === false ) continue;

            lines.push( '#define ' + name + ' ' + value );

        }

        return lines.join( '\n' ) + "\n";

    }

    //

    return function( name, defines ){

        defines = defines || {};

        var hash = name + "|";
        for( var key in defines ){
            hash += key + ":" + defines[ key ];
        }

        if( !cache[ hash ] ){

            var definesText = getDefines( defines );

            var shaderText = NGL.Resources[ 'shader/' + name ];
            if( !shaderText ){
                throw "empty shader, '" + name + "'";
            }
            shaderText = shaderText.replace( re, function( match, p1 ){

                var path = 'shader/chunk/' + p1 + '.glsl';
                var chunk = NGL.Resources[ path ] || THREE.ShaderChunk[ p1 ];

                return chunk ? chunk : "";

            });

            cache[ hash ] = definesText + shaderText;

        }

        return cache[ hash ];

    }

}();


NGL.trimCanvas = function( canvas, r, g, b, a ){

    var canvasHeight = canvas.height;
    var canvasWidth = canvas.width;

    var ctx = canvas.getContext( '2d' );
    var pixels = ctx.getImageData(0, 0, canvasWidth, canvasHeight ).data;

    var x, y, doBreak;

    doBreak = false;
    for( y = 0; y < canvasHeight; y++ ) {
        for( x = 0; x < canvasWidth; x++ ) {
            var off = ( y * canvasWidth + x ) * 4;
            if( pixels[ off ] !== r || pixels[ off + 1 ] !== g ||
                    pixels[ off + 2 ] !== b || pixels[ off + 3 ] !== a ){
                doBreak = true;
                break;
            }
        }
        if( doBreak ){
            break;
        }
    }
    var topY = y;

    doBreak = false;
    for( x = 0; x < canvasWidth; x++ ) {
        for( y = 0; y < canvasHeight; y++ ) {
            var off = ( y * canvasWidth + x ) * 4;
            if( pixels[ off ] !== r || pixels[ off + 1 ] !== g ||
                    pixels[ off + 2 ] !== b || pixels[ off + 3 ] !== a ){
                doBreak = true;
                break;
            }
        }
        if( doBreak ){
            break;
        }
    }
    var topX = x;

    doBreak = false;
    for( y = canvasHeight-1; y >= 0; y-- ) {
        for( x = canvasWidth-1; x >= 0; x-- ) {
            var off = ( y * canvasWidth + x ) * 4;
            if( pixels[ off ] !== r || pixels[ off + 1 ] !== g ||
                    pixels[ off + 2 ] !== b || pixels[ off + 3 ] !== a ){
                doBreak = true;
                break;
            }
        }
        if( doBreak ){
            break;
        }
    }
    var bottomY = y;

    doBreak = false;
    for( x = canvasWidth-1; x >= 0; x-- ) {
        for( y = canvasHeight-1; y >= 0; y-- ) {
            var off = ( y * canvasWidth + x ) * 4;
            if( pixels[ off ] !== r || pixels[ off + 1 ] !== g ||
                    pixels[ off + 2 ] !== b || pixels[ off + 3 ] !== a ){
                doBreak = true;
                break;
            }
        }
        if( doBreak ){
            break;
        }
    }
    var bottomX = x;

    var trimedCanvas = document.createElement( 'canvas' );
    trimedCanvas.style.display = "hidden";
    document.body.appendChild( trimedCanvas );

    trimedCanvas.width = bottomX - topX;
    trimedCanvas.height = bottomY - topY;

    var trimedCtx = trimedCanvas.getContext( '2d' );

    trimedCtx.drawImage(
        canvas,
        topX, topY,
        trimedCanvas.width, trimedCanvas.height,
        0, 0,
        trimedCanvas.width, trimedCanvas.height
    );

    return trimedCanvas;

}


NGL.JitterVectors = [
    [
        [ 0, 0 ]
    ],
    [
        [ 4, 4 ], [ - 4, - 4 ]
    ],
    [
        [ - 2, - 6 ], [ 6, - 2 ], [ - 6, 2 ], [ 2, 6 ]
    ],
    [
        [ 1, - 3 ], [ - 1, 3 ], [ 5, 1 ], [ - 3, - 5 ],
        [ - 5, 5 ], [ - 7, - 1 ], [ 3, 7 ], [ 7, - 7 ]
    ],
    [
        [ 1, 1 ], [ - 1, - 3 ], [ - 3, 2 ], [ 4, - 1 ],
        [ - 5, - 2 ], [ 2, 5 ], [ 5, 3 ], [ 3, - 5 ],
        [ - 2, 6 ], [ 0, - 7 ], [ - 4, - 6 ], [ - 6, 4 ],
        [ - 8, 0 ], [ 7, - 4 ], [ 6, 7 ], [ - 7, - 8 ]
    ],
    [
        [ - 4, - 7 ], [ - 7, - 5 ], [ - 3, - 5 ], [ - 5, - 4 ],
        [ - 1, - 4 ], [ - 2, - 2 ], [ - 6, - 1 ], [ - 4, 0 ],
        [ - 7, 1 ], [ - 1, 2 ], [ - 6, 3 ], [ - 3, 3 ],
        [ - 7, 6 ], [ - 3, 6 ], [ - 5, 7 ], [ - 1, 7 ],
        [ 5, - 7 ], [ 1, - 6 ], [ 6, - 5 ], [ 4, - 4 ],
        [ 2, - 3 ], [ 7, - 2 ], [ 1, - 1 ], [ 4, - 1 ],
        [ 2, 1 ], [ 6, 2 ], [ 0, 4 ], [ 4, 4 ],
        [ 2, 5 ], [ 7, 5 ], [ 5, 6 ], [ 3, 7 ]
    ]
];
NGL.JitterVectors.forEach( function( offsetList ){
    offsetList.forEach( function( offset ){
        // 0.0625 = 1 / 16
        offset[ 0 ] *= 0.0625;
        offset[ 1 ] *= 0.0625;
    } );
} );


THREE.OrthographicCamera.prototype.setViewOffset = function( fullWidth, fullHeight, x, y, width, height ) {

    this.view = {
        fullWidth: fullWidth,
        fullHeight: fullHeight,
        offsetX: x,
        offsetY: y,
        width: width,
        height: height
    };

    this.updateProjectionMatrix();

};

THREE.OrthographicCamera.prototype.updateProjectionMatrix = function () {

    var dx = ( this.right - this.left ) / ( 2 * this.zoom );
    var dy = ( this.top - this.bottom ) / ( 2 * this.zoom );
    var cx = ( this.right + this.left ) / 2;
    var cy = ( this.top + this.bottom ) / 2;

    var left = cx - dx;
    var right = cx + dx;
    var top = cy + dy;
    var bottom = cy - dy;

    if( this.view ){

        var scaleW = this.zoom / ( this.view.width / this.view.fullWidth );
        var scaleH = this.zoom / ( this.view.height / this.view.fullHeight );

        left += this.view.offsetX / scaleW;
        right = left + this.view.width / scaleW;
        top -= this.view.offsetY / scaleH;
        bottom = top - this.view.height / scaleH;

    }

    this.projectionMatrix.makeOrthographic( left, right, top, bottom, this.near, this.far );

};


//////////
// Stats

NGL.Stats = function(){

    var SIGNALS = signals;

    this.signals = {

        updated: new SIGNALS.Signal(),

    };

    this.begin();

    this.maxDuration = -Infinity;
    this.minDuration = Infinity;
    this.avgDuration = 14;
    this.lastDuration = Infinity;

    this.prevFpsTime = 0;
    this.lastFps = Infinity;
    this.lastFrames = 1;
    this.frames = 0;
    this.count = 0;

}

NGL.Stats.prototype = {

    update: function(){

        this.startTime = this.end();
        this.signals.updated.dispatch();

    },

    begin: function(){

        this.startTime = performance.now();
        this.lastFrames = this.frames;

    },

    end: function(){

        var time = performance.now();

        this.count += 1;
        this.frames += 1;

        this.lastDuration = time - this.startTime;
        this.minDuration = Math.min( this.minDuration, this.lastDuration );
        this.maxDuration = Math.max( this.maxDuration, this.lastDuration );
        this.avgDuration -= this.avgDuration / 30;
        this.avgDuration += this.lastDuration / 30;

        if( time > this.prevFpsTime + 1000 ) {
            this.lastFps = this.frames;
            this.prevFpsTime = time;
            this.frames = 0;
        }

        return time;

    }

};


///////////
// Viewer

/**
 * [Viewer description]
 * @class
 * @param {String} eid
 */
NGL.Viewer = function( eid, params ){

    var SIGNALS = signals;

    this.signals = {

        orientationChanged: new SIGNALS.Signal(),

    };

    if( eid ){
        this.container = document.getElementById( eid );
    }else{
        this.container = document.createElement( 'div' );
    }

    if ( this.container === document ) {
        this.width = window.innerWidth;
        this.height = window.innerHeight;
    } else {
        var box = this.container.getBoundingClientRect();
        this.width = box.width;
        this.height = box.height;
    }

    this.initParams();
    this.initStats();
    // this.holdRendering = true;

    this.initCamera();
    this.initScene();
    if( this.initRenderer() === false ) return;
    this.initControls();
    this.initHelper();

    this._render = this.render.bind( this );
    this._animate = this.animate.bind( this );

    // fog & background
    this.setBackground();
    this.setFog();

    this.boundingBox = new THREE.Box3();
    this.distVector = new THREE.Vector3();

    this.info = {

        memory: {
            programs: 0,
            geometries: 0,
            textures: 0
        },

        render: {
            calls: 0,
            vertices: 0,
            faces: 0,
            points: 0
        }

    };

};

NGL.Viewer.prototype = {

    constructor: NGL.Viewer,

    initParams: function(){

        this.params = {

            fogColor: new THREE.Color( 0x000000 ),
            fogNear: 50,
            fogFar: 100,

            backgroundColor: new THREE.Color( 0x000000 ),

            cameraType: "perspective",
            cameraFov: 40,
            cameraZ: -80, // FIXME initial value should be automatically determined

            clipNear: 0,
            clipFar: 100,
            clipDist: 10,

            spinAxis: null,
            spinAngle: 0.01,

            lightColor: new THREE.Color( 0xdddddd ),
            lightIntensity: 1.0,
            ambientColor: new THREE.Color( 0xdddddd ),
            ambientIntensity: 0.2,

            holdRendering: false,
            sampleLevel: 0

        };

    },

    initCamera: function(){

        var p = this.params;
        var lookAt = new THREE.Vector3( 0, 0, 0 );

        this.perspectiveCamera = new THREE.PerspectiveCamera(
            p.cameraFov, this.width / this.height, 0.1, 10000
        );
        this.perspectiveCamera.position.z = p.cameraZ;
        this.perspectiveCamera.lookAt( lookAt );

        this.orthographicCamera = new THREE.OrthographicCamera(
            this.width / -2, this.width / 2,
            this.height / 2, this.height / -2,
            0.1, 10000
        );
        this.orthographicCamera.position.z = p.cameraZ;
        this.orthographicCamera.lookAt( lookAt );

        if( p.cameraType === "orthographic" ){
            this.camera = this.orthographicCamera;
        }else{  // p.cameraType === "perspective"
            this.camera = this.perspectiveCamera;
        }
        this.camera.updateProjectionMatrix();

    },

    initRenderer: function(){

        try{
            this.renderer = new THREE.WebGLRenderer( {
                preserveDrawingBuffer: true,
                alpha: true,
                antialias: true
            } );
        }catch( e ){
            this.container.innerHTML = NGL.webglErrorMessage;
            return false;
        }
        this.renderer.setPixelRatio( window.devicePixelRatio );
        this.renderer.setSize( this.width, this.height );
        this.renderer.autoClear = false;
        this.renderer.sortObjects = true;

        // var gl = this.renderer.getContext();
        // console.log( gl.getContextAttributes().antialias );
        // console.log( gl.getParameter(gl.SAMPLES) );

        NGL.extensionFragDepth = this.renderer.extensions.get( "EXT_frag_depth" );
        NGL.indexUint16 = !this.renderer.extensions.get( 'OES_element_index_uint' );

        NGL.supportsReadPixelsFloat = (
            ( NGL.browser === "Chrome" &&
                this.renderer.extensions.get( 'OES_texture_float' ) ) ||
            ( this.renderer.extensions.get( 'OES_texture_float' ) &&
                this.renderer.extensions.get( "WEBGL_color_buffer_float" ) )
        );

        this.container.appendChild( this.renderer.domElement );

        // picking texture

        this.renderer.extensions.get( 'OES_texture_float' );
        NGL.supportsHalfFloat = this.renderer.extensions.get( 'OES_texture_half_float' );
        this.renderer.extensions.get( "WEBGL_color_buffer_float" );

        this.pickingTarget = new THREE.WebGLRenderTarget(
            this.width * window.devicePixelRatio,
            this.height * window.devicePixelRatio,
            {
                minFilter: THREE.NearestFilter,
                magFilter: THREE.NearestFilter,
                stencilBuffer: false,
                format: THREE.RGBAFormat,
                type: NGL.supportsReadPixelsFloat ? THREE.FloatType : THREE.UnsignedByteType
            }
        );
        this.pickingTarget.texture.generateMipmaps = false;

        // msaa textures

        this.sampleTarget = new THREE.WebGLRenderTarget(
            this.width * window.devicePixelRatio,
            this.height * window.devicePixelRatio,
            {
                minFilter: THREE.NearestFilter,
                magFilter: THREE.NearestFilter,
                format: THREE.RGBAFormat,
            }
        );

        this.holdTarget = new THREE.WebGLRenderTarget(
            this.width * window.devicePixelRatio,
            this.height * window.devicePixelRatio,
            {
                minFilter: THREE.NearestFilter,
                magFilter: THREE.NearestFilter,
                format: THREE.RGBAFormat,
                type: NGL.supportsHalfFloat ? THREE.HalfFloatType : THREE.FloatType
            }
        );

        this.compositeUniforms = {
            "tForeground": { type: "t", value: null },
            "scale": { type: "f", value: 1.0 }
        };

        this.compositeMaterial = new THREE.ShaderMaterial( {
            uniforms: this.compositeUniforms,
            vertexShader: NGL.getShader( "Quad.vert" ),
            fragmentShader: NGL.getShader( "Quad.frag" ),
            transparent: true,
            blending: THREE.CustomBlending,
            blendSrc: THREE.OneFactor,
            blendDst: THREE.OneFactor,
            blendSrcAlpha: THREE.OneFactor,
            blendDstAlpha: THREE.OneFactor,
            blendEquation: THREE.AddEquation,
            depthTest: false,
            depthWrite: false
        } );

        this.compositeCamera = new THREE.OrthographicCamera( -1, 1, 1, -1, 0, 1 );
        this.compositeScene = new THREE.Scene().add( new THREE.Mesh(
            new THREE.PlaneGeometry( 2, 2 ), this.compositeMaterial
        ) );

    },

    initScene: function(){

        if( !this.scene ){
            this.scene = new THREE.Scene();
        }

        this.rotationGroup = new THREE.Group();
        this.rotationGroup.name = "rotationGroup";
        this.scene.add( this.rotationGroup );

        this.modelGroup = new THREE.Group();
        this.modelGroup.name = "modelGroup";
        this.rotationGroup.add( this.modelGroup );

        this.pickingGroup = new THREE.Group();
        this.pickingGroup.name = "pickingGroup";
        this.rotationGroup.add( this.pickingGroup );

        this.backgroundGroup = new THREE.Group();
        this.backgroundGroup.name = "backgroundGroup";
        this.rotationGroup.add( this.backgroundGroup );

        this.helperGroup = new THREE.Group();
        this.helperGroup.name = "helperGroup";
        this.rotationGroup.add( this.helperGroup );

        // fog

        this.modelGroup.fog = new THREE.Fog();

        // light

        this.pointLight = new THREE.SpotLight(
            this.params.lightColor, this.params.lightIntensity
        );
        this.modelGroup.add( this.pointLight );

        this.ambientLight = new THREE.AmbientLight(
            this.params.ambientLight, this.params.ambientIntensity
        );
        this.modelGroup.add( this.ambientLight );

    },

    initHelper: function(){

        var indices = new Uint16Array( [
            0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6,
            6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7
        ] );
        var positions = new Float32Array( 8 * 3 );

        var bbGeometry = new THREE.BufferGeometry();
        bbGeometry.setIndex( new THREE.BufferAttribute( indices, 1 ) );
        bbGeometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
        var bbMaterial = new THREE.LineBasicMaterial( { color: "skyblue", linewidth: 2 } );

        this.boundingBoxMesh = new THREE.LineSegments( bbGeometry, bbMaterial );
        this.helperGroup.add( this.boundingBoxMesh );

    },

    updateHelper: function(){

        var position = this.boundingBoxMesh.geometry.attributes.position;
        var array = position.array;

        var bb = this.boundingBox;
        var min = bb.min;
        var max = bb.max;

        array[  0 ] = max.x; array[  1 ] = max.y; array[  2 ] = max.z;
        array[  3 ] = min.x; array[  4 ] = max.y; array[  5 ] = max.z;
        array[  6 ] = min.x; array[  7 ] = min.y; array[  8 ] = max.z;
        array[  9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z;
        array[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z;
        array[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z;
        array[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z;
        array[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z;

        position.needsUpdate = true;

        if( !bb.isEmpty() ){
            this.boundingBoxMesh.geometry.computeBoundingSphere();
        }

    },

    initControls: function(){

        function preventDefault( e ){
            e.preventDefault();
        }
        this.renderer.domElement.addEventListener(
            'mousewheel', preventDefault, false
        );
        this.renderer.domElement.addEventListener(  // firefox
            'MozMousePixelScroll', preventDefault, false
        );
        this.renderer.domElement.addEventListener(
            'touchmove', preventDefault, false
        );

        this.controls = new THREE.TrackballControls( this.camera, this.renderer.domElement );
        this.controls.rotateSpeed = 2.0;
        this.controls.zoomSpeed = 1.2;
        this.controls.panSpeed = 0.8;
        this.controls.staticMoving = true;
        // this.controls.dynamicDampingFactor = 0.3;
        this.controls.keys = [ 65, 83, 68 ];

        this.controls.addEventListener(
            'change', this.requestRender.bind( this )
        );

        document.addEventListener(
            'mousemove',
            this.controls.update.bind( this.controls ),
            false
        );
        document.addEventListener(
            'touchmove',
            this.controls.update.bind( this.controls ),
            false
        );

        this.controls.addEventListener(
            'change',
            function(){
                this.signals.orientationChanged.dispatch();
            }.bind( this ),
            false
        );

    },

    initStats: function(){

        this.stats = new NGL.Stats();

    },

    add: function( buffer, instanceList ){

        // NGL.time( "Viewer.add" );

        if( instanceList ){

            instanceList.forEach( function( instance ){

                this.addBuffer( buffer, instance );

            }, this );

        }else{

            this.addBuffer( buffer );

        }

        if( buffer.background ){
            this.backgroundGroup.add( buffer.group );
            this.backgroundGroup.add( buffer.wireframeGroup );
        }else{
            this.modelGroup.add( buffer.group );
            this.modelGroup.add( buffer.wireframeGroup );
        }

        if( buffer.pickable ){
            this.pickingGroup.add( buffer.pickingGroup );
        }

        this.rotationGroup.updateMatrixWorld();
        if( NGL.debug ) this.updateHelper();

        // this.requestRender();

        // NGL.timeEnd( "Viewer.add" );

    },

    addBuffer: function( buffer, instance ){

        // NGL.time( "Viewer.addBuffer" );

        var mesh = buffer.getMesh();
        mesh.userData[ "buffer" ] = buffer;
        if( instance ){
            mesh.applyMatrix( instance.matrix );
            mesh.userData[ "instance" ] = instance;
        }
        buffer.group.add( mesh );

        var wireframeMesh = buffer.getWireframeMesh();
        wireframeMesh.userData[ "buffer" ] = buffer;
        if( instance ){
            // wireframeMesh.applyMatrix( instance.matrix );
            wireframeMesh.matrix.copy( mesh.matrix );
            wireframeMesh.position.copy( mesh.position );
            wireframeMesh.quaternion.copy( mesh.quaternion );
            wireframeMesh.scale.copy( mesh.scale );
            wireframeMesh.userData[ "instance" ] = instance;
        }
        buffer.wireframeGroup.add( wireframeMesh );

        if( buffer.pickable ){

            var pickingMesh = buffer.getPickingMesh();
            pickingMesh.userData[ "buffer" ] = buffer;
            if( instance ){
                // pickingMesh.applyMatrix( instance.matrix );
                pickingMesh.matrix.copy( mesh.matrix );
                pickingMesh.position.copy( mesh.position );
                pickingMesh.quaternion.copy( mesh.quaternion );
                pickingMesh.scale.copy( mesh.scale );
                pickingMesh.userData[ "instance" ] = instance;
            }
            buffer.pickingGroup.add( pickingMesh );

        }

        if( instance ){
            this.updateBoundingBox( buffer.geometry, instance.matrix );
        }else{
            this.updateBoundingBox( buffer.geometry );
        }

        // NGL.timeEnd( "Viewer.addBuffer" );

    },

    remove: function( buffer ){

        this.rotationGroup.children.forEach( function( group ){
            group.remove( buffer.group );
            group.remove( buffer.wireframeGroup );
        } );

        if( buffer.pickable ){
            this.pickingGroup.remove( buffer.pickingGroup );
        }

        this.updateBoundingBox();
        if( NGL.debug ) this.updateHelper();

        // this.requestRender();

    },

    updateBoundingBox: function( geometry, matrix ){

        var gbb;
        var bb = this.boundingBox;

        function updateGeometry( geometry, matrix ){

            if( geometry.attributes.position.count === 0 ) return;

            if( !geometry.boundingBox ){
                geometry.computeBoundingBox();
            }

            if( matrix ){
                gbb = geometry.boundingBox.clone();
                gbb.applyMatrix4( matrix );
            }else{
                gbb = geometry.boundingBox;
            }

            if( gbb.min.equals( gbb.max ) ){
                // mainly to give a single impostor geometry some volume
                // as it is only expanded in the shader on the GPU
                gbb.expandByScalar( 5 );
            }

            bb.expandByPoint( gbb.min );
            bb.expandByPoint( gbb.max );

        }

        function updateNode( node ){

            if( node.geometry !== undefined ){

                var matrix = undefined;
                if( node.userData[ "instance" ] ){
                    matrix = node.userData[ "instance" ].matrix;
                }

                updateGeometry( node.geometry, matrix );

            }

        }

        if( geometry ){

            updateGeometry( geometry, matrix );

        }else{

            bb.makeEmpty();
            this.modelGroup.traverse( updateNode );
            this.backgroundGroup.traverse( updateNode );

        }

        this.controls.maxDistance = bb.size().length() * 10;

    },

    getImage: function(){

        var renderer = this.renderer;

        return new Promise( function( resolve, reject ){
            renderer.domElement.toBlob( resolve, "image/png" );
        } );

    },

    makeImage: function( params ){

        return NGL.makeImage( this, params );

    },

    setLight: function( color, intensity, ambientColor, ambientIntensity ){

        var p = this.params;

        if( color !== undefined ) p.lightColor.set( color );
        if( intensity !== undefined ) p.lightIntensity = intensity;
        if( ambientColor !== undefined ) p.ambientColor.set( ambientColor );
        if( ambientIntensity !== undefined ) p.ambientIntensity = ambientIntensity;

        this.requestRender();

    },

    setFog: function( color, near, far ){

        var p = this.params;

        if( color !== undefined ) p.fogColor.set( color );
        if( near !== undefined ) p.fogNear = near;
        if( far !== undefined ) p.fogFar = far;

        this.requestRender();

    },

    setBackground: function( color ){

        var p = this.params;

        if( color ) p.backgroundColor.set( color );

        this.setFog( p.backgroundColor );
        this.renderer.setClearColor( p.backgroundColor, 0 );
        this.renderer.domElement.style.backgroundColor = p.backgroundColor.getStyle();

        this.requestRender();

    },

    setSampling: function( level ){

        if( level !== undefined ){
            this.params.sampleLevel = level;
            this.sampleLevel = level;
        }

        this.requestRender();

    },

    setCamera: function( type, fov ){

        var p = this.params;

        if( type ) p.cameraType = type;
        if( fov ) p.cameraFov = fov;

        if( p.cameraType === "orthographic" ){
            if( this.camera !== this.orthographicCamera ){
                this.camera = this.orthographicCamera;
                this.camera.position.copy( this.perspectiveCamera.position );
                this.camera.up.copy( this.perspectiveCamera.up );
                this.__updateZoom();
            }
        }else{  // p.cameraType === "perspective"
            if( this.camera !== this.perspectiveCamera ){
                this.camera = this.perspectiveCamera;
                this.camera.position.copy( this.orthographicCamera.position );
                this.camera.up.copy( this.orthographicCamera.up );
            }
        }

        this.perspectiveCamera.fov = p.cameraFov;
        this.controls.object = this.camera;
        this.camera.lookAt( this.controls.target );
        this.camera.updateProjectionMatrix();

        this.requestRender();

    },

    setClip: function( near, far, dist ){

        var p = this.params;

        if( near !== undefined ) p.clipNear = near;
        if( far !== undefined ) p.clipFar = far;
        if( dist !== undefined ) p.clipDist = dist;

        this.requestRender();

    },

    setSpin: function( axis, angle ){

        var p = this.params;

        if( axis !== undefined ) p.spinAxis = axis;
        if( angle !== undefined ) p.spinAngle = angle;

    },

    setSize: function( width, height ){

        this.width = width;
        this.height = height;

        this.perspectiveCamera.aspect = this.width / this.height;
        this.orthographicCamera.left = -this.width / 2;
        this.orthographicCamera.right = this.width / 2;
        this.orthographicCamera.top = this.height / 2;
        this.orthographicCamera.bottom = -this.height / 2;
        this.camera.updateProjectionMatrix();

        this.renderer.setPixelRatio( window.devicePixelRatio );
        this.renderer.setSize( this.width, this.height );

        this.pickingTarget.setSize(
            this.width * window.devicePixelRatio,
            this.height * window.devicePixelRatio
        );
        this.sampleTarget.setSize(
            this.width * window.devicePixelRatio,
            this.height * window.devicePixelRatio
        );
        this.holdTarget.setSize(
            this.width * window.devicePixelRatio,
            this.height * window.devicePixelRatio
        );

        this.controls.handleResize();

        this.requestRender();

    },

    handleResize: function(){

        if( this.container === document ){

            this.setSize( window.innerWidth, window.innerHeight );

        }else{

            var box = this.container.getBoundingClientRect();
            this.setSize( box.width, box.height );

        }

    },

    updateInfo: function( reset ){

        var info = this.info;
        var memory = info.memory;
        var render = info.render;

        if( reset ){

            memory.programs = 0;
            memory.geometries = 0;
            memory.textures = 0;

            render.calls = 0;
            render.vertices = 0;
            render.faces = 0;
            render.points = 0;

        }else{

            var rInfo = this.renderer.info;
            var rMemory = rInfo.memory;
            var rRender = rInfo.render;

            memory.programs = rMemory.programs;
            memory.geometries = rMemory.geometries;
            memory.textures = rMemory.textures;

            render.calls += rRender.calls;
            render.vertices += rRender.vertices;
            render.faces += rRender.faces;
            render.points += rRender.points;

        }

    },

    rotate: function(){

        var eye = new THREE.Vector3();
        var quaternion = new THREE.Quaternion();
        var eyeDirection = new THREE.Vector3();
        var upDirection = new THREE.Vector3();
        var sidewaysDirection = new THREE.Vector3();
        var moveDirection = new THREE.Vector3();

        return function( axis, angle ){

            eye.copy( this.camera.position ).sub( this.controls.target );
            eyeDirection.copy( eye ).normalize();
            upDirection.copy( this.camera.up ).normalize();
            sidewaysDirection.crossVectors( upDirection, eyeDirection ).normalize();

            eyeDirection.setLength( axis.z );
            upDirection.setLength( axis.y );
            sidewaysDirection.setLength( axis.x );
            moveDirection.copy( sidewaysDirection.sub( upDirection ).add( eyeDirection ) );

            quaternion.setFromAxisAngle( moveDirection.normalize(), angle );
            eye.applyQuaternion( quaternion );

            this.camera.up.applyQuaternion( quaternion );
            this.camera.position.addVectors( this.controls.target, eye );
            this.camera.lookAt( this.controls.target );

        }

    }(),

    zoom: function(){

        var eye = new THREE.Vector3();
        var eyeDirection = new THREE.Vector3();

        return function( distance ){

            eye.copy( this.camera.position ).sub( this.controls.target );
            eyeDirection.copy( eye ).normalize();

            eyeDirection.setLength( distance );
            eye.add( eyeDirection );

            this.camera.position.addVectors( this.controls.target, eye );
            this.camera.lookAt( this.controls.target );

            this.__updateZoom();

        }

    }(),

    animate: function(){

        this.controls.update();

        var delta = performance.now() - this.stats.startTime;

        if( delta > 500 && !this.still && this.sampleLevel < 3 && this.sampleLevel !== -1 ){

            var currentSampleLevel = this.sampleLevel;
            this.sampleLevel = 3;
            this._renderPending = true;
            this.render();
            this.still = true;
            this.sampleLevel = currentSampleLevel;
            if( NGL.debug ) NGL.log( "rendered still frame" );

        }

        // spin

        var p = this.params;

        if( p.spinAxis && p.spinAngle ){
            this.rotate( p.spinAxis, p.spinAngle * this.stats.lastDuration / 16 );
            this.requestRender();
        }

        requestAnimationFrame( this._animate );

    },

    pick: function(){

        var pixelBufferFloat = new Float32Array( 4 );
        var pixelBufferUint = new Uint8Array( 4 );

        return function( x, y ){

            x *= window.devicePixelRatio;
            y *= window.devicePixelRatio;

            var gid, object, instance, bondId;
            var pixelBuffer = NGL.supportsReadPixelsFloat ? pixelBufferFloat : pixelBufferUint;

            this.render( true );
            this.renderer.readRenderTargetPixels(
                this.pickingTarget, x, y, 1, 1, pixelBuffer
            );

            if( NGL.supportsReadPixelsFloat ){
                gid =
                    ( ( Math.round( pixelBuffer[0] * 255 ) << 16 ) & 0xFF0000 ) |
                    ( ( Math.round( pixelBuffer[1] * 255 ) << 8 ) & 0x00FF00 ) |
                    ( ( Math.round( pixelBuffer[2] * 255 ) ) & 0x0000FF );
            }else{
                gid =
                    ( pixelBuffer[0] << 16 ) |
                    ( pixelBuffer[1] << 8 ) |
                    ( pixelBuffer[2] );
            }

            object = this.pickingGroup.getObjectById(
                Math.round( pixelBuffer[ 3 ] )
            );

            if( object && object.userData.instance ){
                instance = object.userData.instance;
            }

            if( NGL.debug ){
                var rgba = Array.apply( [], pixelBuffer );
                NGL.log( pixelBuffer );
                NGL.log(
                    "picked color",
                    [
                        ( rgba[0] ).toPrecision(2),
                        ( rgba[1] ).toPrecision(2),
                        ( rgba[2] ).toPrecision(2),
                        ( rgba[3] ).toPrecision(2)
                    ]
                );
                NGL.log( "picked gid", gid );
                NGL.log( "picked instance", instance );
                NGL.log( "picked position", x, y );
                NGL.log( "devicePixelRatio", window.devicePixelRatio );
            }

            return {
                "gid": gid,
                "instance": instance
            };

        };

    }(),

    requestRender: function(){

        if( this._renderPending || this.holdRendering ){
            // NGL.info( "there is still a 'render' call pending" );
            return;
        }

        // start gathering stats anew after inactivity
        if( performance.now() - this.stats.startTime > 22 ){
            this.stats.begin();
            this.still = false;
        }

        this._renderPending = true;

        requestAnimationFrame( function(){
            this.render();
            this.stats.update();
        }.bind( this ) );

    },

    __updateClipping: function(){

        var p = this.params;
        var camera = this.camera;

        // clipping

        var cDist = this.distVector.copy( camera.position )
                        .sub( this.controls.target ).length();
        // console.log( "cDist", cDist )
        if( !cDist ){
            // recover from a broken (NaN) camera position
            camera.position.set( 0, 0, p.cameraZ );
            cDist = Math.abs( p.cameraZ );
        }
        this.cDist = cDist;

        var bRadius = Math.max( 10, this.boundingBox.size( this.distVector ).length() * 0.5 );
        bRadius += this.boundingBox.center( this.distVector )
            .add( this.rotationGroup.position )
            .length();
        if( bRadius === Infinity || bRadius === -Infinity || isNaN( bRadius ) ){
            // console.warn( "something wrong with bRadius" );
            bRadius = 50;
        }
        this.bRadius = bRadius;

        var nearFactor = ( 50 - p.clipNear ) / 50;
        var farFactor = - ( 50 - p.clipFar ) / 50;
        camera.near = Math.max( 0.1, p.clipDist, cDist - ( bRadius * nearFactor ) );
        camera.far = Math.max( 1, cDist + ( bRadius * farFactor ) );

        // fog

        var fogNearFactor = ( 50 - p.fogNear ) / 50;
        var fogFarFactor = - ( 50 - p.fogFar ) / 50;
        var fog = this.modelGroup.fog;
        fog.color.set( p.fogColor );
        fog.near = Math.max( 0.1, cDist - ( bRadius * fogNearFactor ) );
        fog.far = Math.max( 1, cDist + ( bRadius * fogFarFactor ) );

    },

    __updateZoom: function(){

        this.__updateClipping();
        var fov = THREE.Math.degToRad( this.perspectiveCamera.fov );
        var near = this.camera.near;
        var far = this.camera.far;
        var hyperfocus = ( near + far ) / 2;
        var height = 2 * Math.tan( fov / 2 ) * hyperfocus;
        this.orthographicCamera.zoom = this.height / height;

    },

    __updateCamera: function(){

        var camera = this.camera;

        camera.updateMatrix();
        camera.updateMatrixWorld( true );
        camera.matrixWorldInverse.getInverse( camera.matrixWorld );
        camera.updateProjectionMatrix();

        this.updateMaterialUniforms( this.scene, camera );
        this.sortProjectedPosition( this.scene, camera );

    },

    __updateLights: function(){

        var p = this.params;
        var camera = this.camera;

        var pointLight = this.pointLight;
        pointLight.position.copy( camera.position ).multiplyScalar( 100 );
        pointLight.updateMatrixWorld();
        pointLight.color.set( p.lightColor );
        pointLight.intensity = p.lightIntensity;

        var ambientLight = this.ambientLight;
        ambientLight.color.set( p.ambientColor );
        ambientLight.intensity = p.ambientIntensity;

    },

    __renderPickingGroup: function(){

        this.renderer.clearTarget( this.pickingTarget );
        this.renderer.render(
            this.pickingGroup, this.camera, this.pickingTarget
        );
        this.updateInfo();
        this.renderer.setRenderTarget( null );  // back to standard render target

        if( NGL.debug ){
            this.renderer.clear();
            this.renderer.render( this.pickingGroup, this.camera );
            this.renderer.render( this.helperGroup, this.camera );
        }

    },

    __renderModelGroup: function( renderTarget ){

        if( renderTarget ){
            this.renderer.clearTarget( renderTarget );
        }else{
            this.renderer.clear();
        }

        this.renderer.render( this.backgroundGroup, this.camera, renderTarget );
        if( renderTarget ){
            this.renderer.clearTarget( renderTarget, false, true, false );
        }else{
            this.renderer.clearDepth();
        }
        this.updateInfo();

        this.renderer.render( this.modelGroup, this.camera, renderTarget );
        this.updateInfo();

        if( NGL.debug ){
            this.renderer.render( this.helperGroup, this.camera, renderTarget );
        }

    },

    __renderMultiSample: function(){

        // based on the Manual Multi-Sample Anti-Aliasing Render Pass
        // contributed to three.js by bhouston / http://clara.io/
        //
        // This manual approach to MSAA re-renders the scene ones for
        // each sample with camera jitter and accumulates the results.
        // References: https://en.wikipedia.org/wiki/Multisample_anti-aliasing

        var camera = this.camera;
        var offsetList = NGL.JitterVectors[ Math.max( 0, Math.min( this.sampleLevel, 5 ) ) ];

        this.compositeUniforms[ "scale" ].value = 1.0 / offsetList.length;
        this.compositeUniforms[ "tForeground" ].value = this.sampleTarget;
        this.compositeUniforms[ "tForeground" ].needsUpdate = true;
        this.compositeMaterial.needsUpdate = true;

        // this.renderer.setRenderTarget( this.sampleTarget );
        var width = this.sampleTarget.width;
        var height = this.sampleTarget.height;

        // render the scene multiple times, each slightly jitter offset
        // from the last and accumulate the results.
        for ( var i = 0; i < offsetList.length; ++i ){

            var offset = offsetList[ i ];
            camera.setViewOffset(
                width, height, offset[ 0 ], offset[ 1 ], width, height
            );
            this.__updateCamera();

            this.__renderModelGroup( this.sampleTarget );
            this.renderer.render(
                this.compositeScene, this.compositeCamera, this.holdTarget, ( i === 0 )
            );

        }

        this.renderer.setRenderTarget( null );

        this.compositeUniforms[ "scale" ].value = 1.0;
        this.compositeUniforms[ "tForeground" ].value = this.holdTarget;
        this.compositeUniforms[ "tForeground" ].needsUpdate = true;
        this.compositeMaterial.needsUpdate = true;

        this.renderer.clear();
        this.renderer.render( this.compositeScene, this.compositeCamera );

        camera.view = null;

    },

    render: function( picking ){

        if( this._rendering ){
            NGL.warn( "tried to call 'render' from within 'render'" );
            return;
        }

        // NGL.time( "Viewer.render" );

        this._rendering = true;

        this.__updateClipping();
        this.__updateCamera();
        this.__updateLights();

        // render

        this.updateInfo( true );

        if( picking ){

            this.__renderPickingGroup();

        }else if( this.sampleLevel > 0 ){

            this.__renderMultiSample();

        }else{

            this.__renderModelGroup();

        }

        this._rendering = false;
        this._renderPending = false;

        // NGL.timeEnd( "Viewer.render" );
        // NGL.log( this.info.memory, this.info.render );

    },

    updateMaterialUniforms: function(){

        var projectionMatrixInverse = new THREE.Matrix4();
        var projectionMatrixTranspose = new THREE.Matrix4();

        return function( group, camera ){

            var cDist = this.cDist;
            var bRadius = this.bRadius;
            var canvasHeight = this.height;
            var pixelRatio = this.renderer.getPixelRatio();
            var ortho = camera.type === "OrthographicCamera" ? 1.0 : 0.0;

            projectionMatrixInverse.getInverse(
                camera.projectionMatrix
            );

            projectionMatrixTranspose.copy(
                camera.projectionMatrix
            ).transpose();

            group.traverse( function( o ){

                var m = o.material;
                if( !m ) return;

                var u = o.material.uniforms;
                if( !u ) return;

                if( m.clipNear ){
                    var nearFactor = ( 50 - m.clipNear ) / 50;
                    var nearClip = cDist - ( bRadius * nearFactor );
                    u.nearClip.value = nearClip;
                }

                if( u.canvasHeight ){
                    u.canvasHeight.value = canvasHeight;
                }

                if( u.pixelRatio ){
                    u.pixelRatio.value = pixelRatio;
                }

                if( u.projectionMatrixInverse ){
                    u.projectionMatrixInverse.value.copy(
                        projectionMatrixInverse
                    );
                }

                if( u.projectionMatrixTranspose ){
                    u.projectionMatrixTranspose.value.copy(
                        projectionMatrixTranspose
                    );
                }

                if( u.ortho ){
                    u.ortho.value = ortho;
                }

            } );

        }

    }(),

    sortProjectedPosition: function(){

        var lastCall = 0;

        var vertex = new THREE.Vector3();
        var matrix = new THREE.Matrix4();
        var modelViewProjectionMatrix = new THREE.Matrix4();

        return function( scene, camera ){

            // NGL.time( "sort" );

            scene.traverseVisible( function ( o ){

                if( !( o instanceof THREE.Points ) || !o.sortParticles ){
                    return;
                }

                matrix.multiplyMatrices(
                    camera.matrixWorldInverse, o.matrixWorld
                );
                modelViewProjectionMatrix.multiplyMatrices(
                    camera.projectionMatrix, matrix
                );

                var attributes = o.geometry.attributes;
                var n = attributes.position.count;

                if( !o.userData.sortData ){
                    o.userData.sortData = {};
                }

                var sortData = o.userData.sortData;

                if( !sortData.__sortArray ){
                    sortData.__sortArray = new Float32Array( n * 2 );
                }

                var sortArray = sortData.__sortArray;

                for( var i = 0; i < n; ++i ){

                    var i2 = 2 * i;

                    vertex.fromArray( attributes.position.array, i * 3 );
                    vertex.applyProjection( modelViewProjectionMatrix );

                    // negate, so that sorting order is reversed
                    sortArray[ i2 ] = -vertex.z;
                    sortArray[ i2 + 1 ] = i;

                }

                THREE.TypedArrayUtils.quicksortIP( sortArray, 2, 0 );

                var index, indexSrc, indexDst, tmpTab;

                for( var name in attributes ){

                    var attr = attributes[ name ];
                    var array = attr.array;
                    var itemSize = attr.itemSize;

                    if( !sortData[ name ] ){
                        sortData[ name ] = new Float32Array(
                            itemSize * n
                        );
                    }

                    tmpTab = sortData[ name ];
                    sortData[ name ] = array;

                    for( var i = 0; i < n; ++i ){

                        index = sortArray[ i * 2 + 1 ];

                        for( var j = 0; j < itemSize; ++j ){
                            indexSrc = index * itemSize + j;
                            indexDst = i * itemSize + j;
                            tmpTab[ indexDst ] = array[ indexSrc ];
                        }

                    }

                    attributes[ name ].array = tmpTab;
                    attributes[ name ].needsUpdate = true;

                }

            } );

            // NGL.timeEnd( "sort" );

        }

    }(),

    clear: function(){

        NGL.log( "scene cleared" );

        this.scene.remove( this.rotationGroup );

        this.initScene();

        this.renderer.clear();

    },

    centerView: function(){

        var t = new THREE.Vector3();
        var eye = new THREE.Vector3();
        var eyeDirection = new THREE.Vector3();
        var bbSize = new THREE.Vector3();

        return function( zoom, center ){

            center = center || this.boundingBox.center();

            // remove any paning/translation
            this.controls.object.position.sub( this.controls.target );
            this.controls.target.copy( this.controls.target0 );

            // center
            t.copy( center ).multiplyScalar( -1 );
            this.rotationGroup.position.copy( t );
            this.rotationGroup.updateMatrixWorld();

            if( zoom ){

                if( zoom === true ){

                    // automatic zoom that shows
                    // everything inside the bounding box
                    // TODO take extent towards the camera into account

                    this.boundingBox.size( bbSize );
                    var maxSize = Math.max( bbSize.x, bbSize.y, bbSize.z );
                    var minSize = Math.min( bbSize.x, bbSize.y, bbSize.z );
                    // var avgSize = ( bbSize.x + bbSize.y + bbSize.z ) / 3;
                    var objSize = maxSize + ( minSize / 2 );
                    zoom = objSize;

                }

                var fov = THREE.Math.degToRad( this.perspectiveCamera.fov );
                var aspect = this.width / this.height;

                zoom = zoom / 2 / aspect / Math.tan( fov / 2 );
                zoom = Math.max( zoom, 1.2 * this.params.clipDist );

                eye.copy( this.camera.position ).sub( this.controls.target );
                eyeDirection.copy( eye ).normalize();

                eyeDirection.setLength( zoom );
                eye.copy( eyeDirection );

                this.camera.position.addVectors( this.controls.target, eye );

                this.__updateZoom();
            }

            this.requestRender();

            this.signals.orientationChanged.dispatch();

        }

    }(),

    getOrientation: function(){

        return [
            this.camera.position.toArray(),
            this.camera.up.toArray(),
            this.rotationGroup.position.toArray(),
            this.controls.target.toArray()
        ];

    },

    setOrientation: function( orientation ){

        // remove any paning/translation
        this.controls.object.position.sub( this.controls.target );
        this.controls.target.copy( this.controls.target0 );

        this.controls.target.fromArray( orientation[ 3 ] );

        this.rotationGroup.position.fromArray( orientation[ 2 ] );
        this.rotationGroup.updateMatrixWorld();

        this.camera.up.fromArray( orientation[ 1 ] );
        this.camera.position.fromArray( orientation[ 0 ] );

        this.requestRender();

        this.signals.orientationChanged.dispatch();

    }

};


/////////////
// Renderer

NGL.TiledRenderer = function( renderer, camera, viewer, params ){

    var p = params || {};

    this.renderer = renderer;
    this.camera = camera;
    this.viewer = viewer;

    this.factor = p.factor!==undefined ? p.factor : 2;
    this.antialias = p.antialias!==undefined ? p.antialias : false;

    this.onProgress = p.onProgress;
    this.onFinish = p.onFinish;

    this.init();

};

NGL.TiledRenderer.prototype = {

    init: function(){

        if( this.antialias ) this.factor *= 2;
        this.n = this.factor * this.factor;

        // canvas

        var canvas = document.createElement( 'canvas' );
        canvas.style.display = "hidden";
        document.body.appendChild( canvas );

        if( this.antialias ){
            canvas.width = this.viewer.width * this.factor / 2;
            canvas.height = this.viewer.height * this.factor / 2;
        }else{
            canvas.width = this.viewer.width * this.factor;
            canvas.height = this.viewer.height * this.factor;
        }

        this.ctx = canvas.getContext( '2d' );
        this.canvas = canvas;

        this.viewerSampleLevel = this.viewer.sampleLevel;
        this.viewer.sampleLevel = -1;

    },

    renderTile: function( i ){

        var factor = this.factor;

        var x = i % factor;
        var y = Math.floor( i / factor );

        var width = this.viewer.width;
        var height = this.viewer.height;
        var offsetX = x * width;
        var offsetY = y * height;

        this.viewer.camera.setViewOffset(
            width * factor,
            height * factor,
            offsetX,
            offsetY,
            width,
            height
        );

        this.viewer.render();

        if( this.antialias ){
            this.ctx.drawImage(
                this.renderer.domElement,
                Math.floor( offsetX / 2 ),
                Math.floor( offsetY / 2 ),
                Math.ceil( width / 2 ),
                Math.ceil( height / 2 )
            );
        }else{
            this.ctx.drawImage(
                this.renderer.domElement,
                Math.floor( offsetX ),
                Math.floor( offsetY ),
                Math.ceil( width ),
                Math.ceil( height )
            );
        }

        if( typeof this.onProgress === "function" ){
            this.onProgress( i + 1, this.n, false );
        }

    },

    finalize: function(){

        this.viewer.sampleLevel = this.viewerSampleLevel;
        this.viewer.camera.view = null;

        if( typeof this.onFinish === "function" ){
            this.onFinish( this.n + 1, this.n, false );
        }

    },

    render: function(){

        var n = this.n;

        for( var i = 0; i <= n; ++i ){
            if( i === n ){
                this.finalize();
            }else{
                this.renderTile( i );
            }
        }

    },

    renderAsync: function(){

        var n = this.n;
        var renderTile = this.renderTile.bind( this );
        var finalize = this.finalize.bind( this );

        for( var i = 0; i <= n; ++i ){
            setTimeout( function( i ){
                if( i === n ){
                    finalize();
                }else{
                    renderTile( i );
                }
            }, 0, i );
        }

    },

    dispose: function(){

        document.body.removeChild( this.canvas );

    }

};


NGL.makeImage = function( viewer, params ){

    var p = params || {};

    var trim = p.trim!==undefined ? p.trim : false;
    var factor = p.factor!==undefined ? p.factor : 1;
    var antialias = p.antialias!==undefined ? p.antialias : false;
    var transparent = p.transparent!==undefined ? p.transparent : false;

    var renderer = viewer.renderer;
    var camera = viewer.camera;

    var originalClearAlpha = renderer.getClearAlpha();
    var backgroundColor = renderer.getClearColor();

    function setLineWidthAndPixelSize( invert ){
        var _factor = factor;
        if( antialias ) _factor *= 2;
        if( invert ) _factor = 1 / _factor;
        viewer.scene.traverse( function( o ){
            var m = o.material;
            if( m && m.linewidth ){
                m.linewidth *= _factor;
            }
            if( m && m.uniforms && m.uniforms.size ){
                if( m.uniforms.size[ "__seen" ] === undefined ){
                    m.uniforms.size.value *= _factor;
                    m.uniforms.size[ "__seen" ] = true;
                }
            }
        } );
        viewer.scene.traverse( function( o ){
            var m = o.material;
            if( m && m.uniforms && m.uniforms.size ){
                delete m.uniforms.size[ "__seen" ];
            }
        } );
    }

    function trimCanvas( canvas ){
        if( trim ){
            var bg = backgroundColor;
            var r = ( transparent ? 0 : bg.r * 255 ) | 0;
            var g = ( transparent ? 0 : bg.g * 255 ) | 0;
            var b = ( transparent ? 0 : bg.b * 255 ) | 0;
            var a = ( transparent ? 0 : 255 ) | 0;
            return NGL.trimCanvas( canvas, r, g, b, a );
        }else{
            return canvas;
        }
    }

    function onProgress( i, n, finished ){
        if( typeof p.onProgress === "function" ){
            p.onProgress( i, n, finished );
        }
    }

    return new Promise( function( resolve, reject ){

        var tiledRenderer = new NGL.TiledRenderer(
            renderer, camera, viewer,
            {
                factor: factor,
                antialias: antialias,
                onProgress: onProgress,
                onFinish: onFinish
            }
        );

        renderer.setClearAlpha( transparent ? 0 : 1 );
        setLineWidthAndPixelSize();
        tiledRenderer.renderAsync();

        function onFinish( i, n ){
            var canvas = trimCanvas( tiledRenderer.canvas );
            canvas.toBlob(
                function( blob ){
                    renderer.setClearAlpha( originalClearAlpha );
                    setLineWidthAndPixelSize( true );
                    viewer.requestRender();
                    tiledRenderer.dispose();
                    onProgress( n, n, true );
                    resolve( blob );
                },
                "image/png"
            );
        }

    } );

};

// File:js/ngl/buffer.js

/**
 * @file Buffer
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


////////////////
// Buffer Core

NGL.DoubleSidedBuffer = function( buffer ){

    this.size = buffer.size;
    this.side = buffer.side;
    this.wireframe = buffer.wireframe;
    this.visible = buffer.visible;
    this.geometry = buffer.geometry;
    this.pickable = buffer.pickable;
    this.background = buffer.background;

    this.group = new THREE.Group();
    this.wireframeGroup = new THREE.Group();
    this.pickingGroup = new THREE.Group();

    var frontMeshes = [];
    var backMeshes = [];

    var frontBuffer = buffer;
    var backBuffer = new buffer.constructor();

    frontBuffer.makeMaterial();
    backBuffer.makeMaterial();

    backBuffer.geometry = buffer.geometry;
    backBuffer.wireframeGeometry = buffer.wireframeGeometry;
    backBuffer.size = buffer.size;
    backBuffer.attributeSize = buffer.attributeSize;
    backBuffer.pickable = buffer.pickable;
    backBuffer.setParameters( buffer.getParameters() );
    backBuffer.updateShader();

    frontBuffer.setParameters( {
        side: THREE.FrontSide
    } );
    backBuffer.setParameters( {
        side: THREE.BackSide,
        opacity: backBuffer.opacity
    } );

    this.getMesh = function( picking ){

        var front, back;

        if( picking ){
            back = backBuffer.getPickingMesh();
            front = frontBuffer.getPickingMesh();
        }else{
            back = backBuffer.getMesh();
            front = frontBuffer.getMesh();
        }

        frontMeshes.push( front );
        backMeshes.push( back );

        this.setParameters( { side: this.side } );

        return new THREE.Group().add( back, front );

    };

    this.getWireframeMesh = function(){

        return buffer.getWireframeMesh();

    };

    this.getPickingMesh = function(){

        return this.getMesh( true );

    };

    this.setAttributes = function( data ){

        buffer.setAttributes( data );

    };

    this.setParameters = function( data ){

        data = Object.assign( {}, data );

        if( data.side === THREE.FrontSide ){

            frontMeshes.forEach( function( m ){ m.visible = true; } );
            backMeshes.forEach( function( m ){ m.visible = false; } );

        }else if( data.side === THREE.BackSide ){

            frontMeshes.forEach( function( m ){ m.visible = false; } );
            backMeshes.forEach( function( m ){ m.visible = true; } );

        }else if( data.side === THREE.DoubleSide ){

            frontMeshes.forEach( function( m ){ m.visible = true; } );
            backMeshes.forEach( function( m ){ m.visible = true; } );

        }

        if( data.side !== undefined ){
            this.side = data.side;
        }
        delete data.side;

        frontBuffer.setParameters( data );

        if( data.wireframe !== undefined ){
            this.wireframe = data.wireframe;
            this.setVisibility( this.visible );
        }
        delete data.wireframe;

        backBuffer.setParameters( data );

    };

    this.setVisibility = NGL.Buffer.prototype.setVisibility;

    this.dispose = function(){

        frontBuffer.dispose();
        backBuffer.dispose();

    };

};


/**
 * The core buffer class.
 * @class
 * @private
 */
NGL.Buffer = function( position, color, index, pickingColor, params ){

    var p = params || {};

    // required properties:
    // - size
    // - attributeSize
    // - vertexShader
    // - fragmentShader

    this.pickable = false;
    this.dynamic = true;

    this.opaqueBack = p.opaqueBack !== undefined ? p.opaqueBack : false;
    this.dullInterior = p.dullInterior !== undefined ? p.dullInterior : false;
    this.side = p.side !== undefined ? p.side : THREE.DoubleSide;
    this.opacity = p.opacity !== undefined ? p.opacity : 1.0;
    this.clipNear = p.clipNear !== undefined ? p.clipNear : 0;
    this.flatShaded = p.flatShaded !== undefined ? p.flatShaded : false;
    this.background = p.background !== undefined ? p.background : false;
    this.linewidth = p.linewidth !== undefined ? p.linewidth : 1;
    this.wireframe = p.wireframe !== undefined ? p.wireframe : false;
    this.wireframeLinewidth = p.wireframeLinewidth || 1;
    this.roughness = p.roughness !== undefined ? p.roughness : 0.4;
    this.metalness = p.metalness !== undefined ? p.metalness : 0.0;
    this.diffuse = p.diffuse !== undefined ? p.diffuse : 0xffffff;
    this.forceTransparent = p.forceTransparent !== undefined ? p.forceTransparent : false;

    this.geometry = new THREE.BufferGeometry();

    this.addAttributes( {
        "position": { type: "v3", value: position },
        "color": { type: "c", value: color },
    } );

    this.indexVersion = 0;
    this.wireframeIndexVersion = -1;

    if( index ){
        this.geometry.setIndex(
            new THREE.BufferAttribute( index, 1 )
        );
        this.geometry.getIndex().setDynamic( this.dynamic );
    }

    if( pickingColor ){
        this.addAttributes( {
            "pickingColor": { type: "c", value: pickingColor },
        } );
        this.pickable = true;
    }

    this.uniforms = THREE.UniformsUtils.merge( [
        THREE.UniformsLib[ "common" ],
        {
            "fogColor": { value: null },
            "fogNear": { value: 0.0 },
            "fogFar": { value: 0.0 },
            "opacity": { value: this.opacity },
            "nearClip": { value: 0.0 }
        },
        {
            "emissive" : { value: new THREE.Color( 0x000000 ) },
            "roughness": { value: this.roughness },
            "metalness": { value: this.metalness }
        },
        THREE.UniformsLib[ "ambient" ],
        THREE.UniformsLib[ "lights" ]
    ] );

    this.uniforms[ "diffuse" ].value.set( this.diffuse );

    var objectId = new THREE.Uniform( 0.0 )
        .onUpdate( function( object, camera ){
            this.value = NGL.supportsReadPixelsFloat ? object.id : object.id / 255;
        } );

    this.pickingUniforms = {
        "nearClip": { value: 0.0 },
        "objectId": objectId
    };

    this.group = new THREE.Group();
    this.wireframeGroup = new THREE.Group();
    this.pickingGroup = new THREE.Group();

    this.makeWireframeGeometry();

};

NGL.Buffer.prototype = {

    constructor: NGL.Buffer,

    parameters: {

        opaqueBack: { updateShader: true },
        dullInterior: { updateShader: true },
        side: { updateShader: true, property: true },
        opacity: { uniform: true },
        clipNear: { updateShader: true, property: true },
        flatShaded: { updateShader: true },
        background: { updateShader: true },
        linewidth: { property: true },
        wireframe: { updateVisibility: true },
        roughness: { uniform: true },
        metalness: { uniform: true },
        diffuse: { uniform: true },

    },

    get transparent () {

        return this.opacity < 1 || this.forceTransparent;

    },

    makeMaterial: function(){

        this.material = new THREE.ShaderMaterial( {
            uniforms: this.uniforms,
            vertexShader: "",
            fragmentShader: "",
            depthTest: true,
            transparent: this.transparent,
            depthWrite: true,
            lights: true,
            fog: true,
            side: this.side,
            linewidth: this.linewidth
        } );
        this.material.vertexColors = THREE.VertexColors;
        this.material.extensions.derivatives = this.flatShaded;
        this.material.extensions.fragDepth = this.impostor;
        this.material.clipNear = this.clipNear;

        this.wireframeMaterial = new THREE.ShaderMaterial( {
            uniforms: this.uniforms,
            vertexShader: "Line.vert",
            fragmentShader: "Line.frag",
            depthTest: true,
            transparent: this.transparent,
            depthWrite: true,
            lights: false,
            fog: true,
            side: this.side,
            linewidth: this.linewidth
        } );
        this.wireframeMaterial.vertexColors = THREE.VertexColors;
        this.wireframeMaterial.clipNear = this.clipNear;

        this.pickingMaterial = new THREE.ShaderMaterial( {
            uniforms: this.pickingUniforms,
            vertexShader: "",
            fragmentShader: "",
            depthTest: true,
            transparent: false,
            depthWrite: true,
            lights: false,
            fog: false,
            side: this.side,
            linewidth: this.linewidth
        } );
        this.pickingMaterial.vertexColors = THREE.VertexColors;
        this.pickingMaterial.extensions.fragDepth = this.impostor;
        this.pickingMaterial.clipNear = this.clipNear;

        this.updateShader();

    },

    makeWireframeGeometry: function(){

        this.makeWireframeIndex();

        var geometry = this.geometry;
        var wireframeIndex = this.wireframeIndex;
        var wireframeGeometry = new THREE.BufferGeometry();

        wireframeGeometry.attributes = geometry.attributes;
        if( wireframeIndex ){
            wireframeGeometry.setIndex(
                new THREE.BufferAttribute( wireframeIndex, 1 )
                    .setDynamic( this.dynamic )
            );
            wireframeGeometry.setDrawRange( 0, this.wireframeIndexCount );
        }

        this.wireframeGeometry = wireframeGeometry;

    },

    makeWireframeIndex: function(){

        var edges = [];

        function checkEdge( a, b ) {

            if ( a > b ){
                var tmp = a;
                a = b;
                b = tmp;
            }

            var list = edges[ a ];

            if( list === undefined ){
                edges[ a ] = [ b ];
                return true;
            }else if( list.indexOf( b ) === -1 ){
                list.push( b );
                return true;
            }

            return false;

        }

        return function(){

            var index = this.geometry.index;

            if( !this.wireframe ){

                this.wireframeIndex = new Uint16Array( 0 );
                this.wireframeIndexCount = 0;

            }else if( index ){

                var array = index.array;
                var n = array.length;
                if( this.geometry.drawRange.count !== Infinity ){
                    n = this.geometry.drawRange.count;
                }
                var wireframeIndex;
                if( this.wireframeIndex && this.wireframeIndex.length > n * 2 ){
                    wireframeIndex = this.wireframeIndex;
                }else{
                    var count = this.geometry.attributes.position.count;
                    var TypedArray = count > 65535 ? Uint32Array : Uint16Array;
                    wireframeIndex = new TypedArray( n * 2 );
                }

                var j = 0;
                edges.length = 0;

                for( var i = 0; i < n; i += 3 ){

                    var a = array[ i + 0 ];
                    var b = array[ i + 1 ];
                    var c = array[ i + 2 ];

                    if( checkEdge( a, b ) ){
                        wireframeIndex[ j + 0 ] = a;
                        wireframeIndex[ j + 1 ] = b;
                        j += 2;
                    }
                    if( checkEdge( b, c ) ){
                        wireframeIndex[ j + 0 ] = b;
                        wireframeIndex[ j + 1 ] = c;
                        j += 2;
                    }
                    if( checkEdge( c, a ) ){
                        wireframeIndex[ j + 0 ] = c;
                        wireframeIndex[ j + 1 ] = a;
                        j += 2;
                    }

                }

                this.wireframeIndex = wireframeIndex;
                this.wireframeIndexCount = j;
                this.wireframeIndexVersion = this.indexVersion;

            }

        }

    }(),

    updateWireframeIndex: function(){

        this.wireframeGeometry.setDrawRange( 0, Infinity );
        if( this.wireframeIndexVersion < this.indexVersion ) this.makeWireframeIndex();

        if( this.wireframeIndex.length > this.wireframeGeometry.index.array.length ){

            this.wireframeGeometry.setIndex(
                new THREE.BufferAttribute( this.wireframeIndex, 1 )
                    .setDynamic( this.dynamic )
            );

        }else{

            var index = this.wireframeGeometry.getIndex();
            index.set( this.wireframeIndex );
            index.needsUpdate = this.wireframeIndexCount > 0;
            index.updateRange.count = this.wireframeIndexCount;

        }

        this.wireframeGeometry.setDrawRange( 0, this.wireframeIndexCount );

    },

    getRenderOrder: function(){

        var renderOrder = 0;

        if( this instanceof NGL.TextBuffer ){

            renderOrder = 1;

        }else if( this.transparent ){

            if( this instanceof NGL.SurfaceBuffer ){
                renderOrder = 3;
            }else{
                renderOrder = 2;
            }

        }

        return renderOrder;

    },

    getMesh: function(){

        var mesh;

        if( !this.material ) this.makeMaterial();

        if( this.line ){

            mesh = new THREE.LineSegments( this.geometry, this.material );

        }else if( this.point ){

            mesh = new THREE.Points( this.geometry, this.material );
            if( this.sortParticles ) mesh.sortParticles = true;

        }else{

            mesh = new THREE.Mesh( this.geometry, this.material );

        }

        mesh.frustumCulled = false;
        mesh.renderOrder = this.getRenderOrder();

        return mesh;

    },

    getWireframeMesh: function(){

        var mesh;

        if( !this.material ) this.makeMaterial();
        if( !this.wireframeGeometry ) this.makeWireframeGeometry();

        mesh = new THREE.LineSegments(
            this.wireframeGeometry, this.wireframeMaterial
        );

        mesh.frustumCulled = false;
        mesh.renderOrder = this.getRenderOrder();

        return mesh;

    },

    getPickingMesh: function(){

        var mesh;

        if( !this.material ) this.makeMaterial();

        mesh = new THREE.Mesh( this.geometry, this.pickingMaterial );

        mesh.frustumCulled = false;
        mesh.renderOrder = this.getRenderOrder();

        return mesh;

    },

    getShader: function( name, type ){

        return NGL.getShader( name, this.getDefines( type ) );

    },

    getVertexShader: function( type ){

        return this.getShader( this.vertexShader, type );

    },

    getFragmentShader: function( type ){

        return this.getShader( this.fragmentShader, type );

    },

    getDefines: function( type ){

        var defines = {};

        if( this.clipNear ){
            defines[ "NEAR_CLIP" ] = 1;
        }

        if( type === "picking" ){

            defines[ "PICKING" ] = 1;

        }else{

            if( type === "background" || this.background ){
                defines[ "NOLIGHT" ] = 1;
            }
            if( this.flatShaded ){
                defines[ "FLAT_SHADED" ] = 1;
            }
            if( this.opaqueBack ){
                defines[ "OPAQUE_BACK" ] = 1;
            }
            if( this.dullInterior ){
                defines[ "DULL_INTERIOR" ] = 1;
            }

        }

        return defines;

    },

    getParameters: function(){

        var params = {};

        for( var name in this.parameters ){
            params[ name ] = this[ name ];
        }

        return params;

    },

    addUniforms: function( uniforms ){

        this.uniforms = THREE.UniformsUtils.merge(
            [ this.uniforms, uniforms ]
        );

        this.pickingUniforms = THREE.UniformsUtils.merge(
            [ this.pickingUniforms, uniforms ]
        );

    },

    addAttributes: function( attributes ){

        var itemSize = {
            "f": 1, "v2": 2, "v3": 3, "c": 3
        };

        for( var name in attributes ){

            var buf;
            var a = attributes[ name ];

            if( a.value ){

                if( this.attributeSize * itemSize[ a.type ] !== a.value.length ){
                    NGL.error( "attribute value has wrong length", name );
                }

                buf = a.value;

            }else{

                buf = new Float32Array(
                    this.attributeSize * itemSize[ a.type ]
                );

            }

            this.geometry.addAttribute(
                name,
                new THREE.BufferAttribute( buf, itemSize[ a.type ] )
                    .setDynamic( this.dynamic )
            );

        }

    },

    updateRenderOrder: function(){

        var renderOrder = this.getRenderOrder();
        function setRenderOrder( mesh ){
            mesh.renderOrder = renderOrder;
        }

        this.group.children.forEach( setRenderOrder );
        if( this.pickingGroup ){
            this.pickingGroup.children.forEach( setRenderOrder );
        }

    },

    updateShader: function(){

        var m = this.material;
        var wm = this.wireframeMaterial;
        var pm = this.pickingMaterial;

        m.vertexShader = this.getVertexShader();
        m.fragmentShader = this.getFragmentShader();
        m.needsUpdate = true;

        wm.vertexShader = this.getShader( "Line.vert" );
        wm.fragmentShader = this.getShader( "Line.frag" );
        wm.needsUpdate = true;

        pm.vertexShader = this.getVertexShader( "picking" );
        pm.fragmentShader = this.getFragmentShader( "picking" );
        pm.needsUpdate = true;

    },

    setParameters: function( params ){

        if( !params ) return;

        var p = params;
        var tp = this.parameters;

        var propertyData = {};
        var uniformData = {};
        var doShaderUpdate = false;
        var doVisibilityUpdate = false;

        for( var name in p ){

            if( p[ name ] === undefined ) continue;
            if( tp[ name ] === undefined ) continue;

            this[ name ] = p[ name ];

            if( tp[ name ].property ){
                if( tp[ name ].property !== true ){
                    propertyData[ tp[ name ].property ] = p[ name ];
                }else{
                    propertyData[ name ] = p[ name ];
                }
            }

            if( tp[ name ].uniform ){
                if( tp[ name ].uniform !== true ){
                    uniformData[ tp[ name ].uniform ] = p[ name ];
                }else{
                    uniformData[ name ] = p[ name ];
                }
            }

            if( tp[ name ].updateShader ){
                doShaderUpdate = true;
            }

            if( tp[ name ].updateVisibility ){
                doVisibilityUpdate = true;
            }

            if( this.dynamic && name === "wireframe" && p[ name ] === true ){
                this.updateWireframeIndex();
            }

            if( name === "flatShaded" ){
                this.material.extensions.derivatives = this.flatShaded;
            }

            if( name === "forceTransparent" ){
                propertyData[ "transparent" ] = this.transparent;
            }

        }

        this.setProperties( propertyData );
        this.setUniforms( uniformData );
        if( doShaderUpdate ) this.updateShader();
        if( doVisibilityUpdate ) this.setVisibility( this.visible );

    },

    setAttributes: function( data ){

        /**
         * Sets buffer attributes
         * @param {Object} data - An object where the keys are the attribute names
         *      and the values are the attribute data.
         * @example
         * var buffer = new NGL.Buffer();
         * buffer.setAttributes({ attrName: attrData });
         */

        var geometry = this.geometry;
        var attributes = geometry.attributes;

        for( var name in data ){

            var array = data[ name ];
            var length = array.length;

            if( name === "index" ){

                var index = geometry.getIndex();
                geometry.setDrawRange( 0, Infinity );

                if( length > index.array.length ){

                    geometry.setIndex(
                        new THREE.BufferAttribute( array, 1 )
                            .setDynamic( this.dynamic )
                    );

                }else{

                    index.set( array );
                    index.needsUpdate = length > 0;
                    index.updateRange.count = length;
                    geometry.setDrawRange( 0, length );

                }

                this.indexVersion++;
                if( this.wireframe ) this.updateWireframeIndex();

            }else{

                var attribute = attributes[ name ];

                if( length > attribute.array.length ){

                    geometry.addAttribute(
                        name,
                        new THREE.BufferAttribute( array, attribute.itemSize )
                            .setDynamic( this.dynamic )
                    );

                }else{

                    attributes[ name ].set( array );
                    attributes[ name ].needsUpdate = length > 0;
                    attributes[ name ].updateRange.count = length;

                }

            }

        }

    },

    setUniforms: function( data ){

        if( !data ) return;

        var u = this.material.uniforms;
        var wu = this.wireframeMaterial.uniforms;
        var pu = this.pickingMaterial.uniforms;

        for( var name in data ){

            if( name === "opacity" ){
                this.setProperties( { transparent: this.transparent } );
            }

            if( u[ name ] !== undefined ){
                if( u[ name ].value.set ){
                    u[ name ].value.set( data[ name ] );
                }else{
                    u[ name ].value = data[ name ];
                }
            }

            if( wu[ name ] !== undefined ){
                if( wu[ name ].value.set ){
                    wu[ name ].value.set( data[ name ] );
                }else{
                    wu[ name ].value = data[ name ];
                }
            }

            if( pu[ name ] !== undefined ){
                if( pu[ name ].value.set ){
                    pu[ name ].value.set( data[ name ] );
                }else{
                    pu[ name ].value = data[ name ];
                }
            }

        }

    },

    setProperties: function( data ){

        if( !data ) return;

        var m = this.material;
        var wm = this.wireframeMaterial;
        var pm = this.pickingMaterial;

        for( var name in data ){

            if( name === "transparent" ){
                this.updateRenderOrder();
            }

            if( m[ name ] !== undefined ){
                m[ name ] = data[ name ];
            }

            if( wm[ name ] !== undefined ){
                wm[ name ] = data[ name ];
            }

            if( pm[ name ] !== undefined ){
                pm[ name ] = data[ name ];
            }

        }

        m.needsUpdate = true;
        wm.needsUpdate = true;
        pm.needsUpdate = true;

    },

    setVisibility: function( value ){

        this.visible = value;

        if( this.wireframe ){

            this.group.visible = false;
            this.wireframeGroup.visible = value;
            if( this.pickable ){
                this.pickingGroup.visible = false;
            }

        }else{

            this.group.visible = value;
            this.wireframeGroup.visible = false;
            if( this.pickable ){
                this.pickingGroup.visible = value;
            }

        }

    },

    dispose: function(){

        if( this.material ) this.material.dispose();
        if( this.wireframeMaterial ) this.wireframeMaterial.dispose();
        if( this.pickingMaterial ) this.pickingMaterial.dispose();

        this.geometry.dispose();
        if( this.wireframeGeometry ) this.wireframeGeometry.dispose();

    }

};


NGL.MeshBuffer = function( position, color, index, normal, pickingColor, params ){

    var p = params || {};

    this.size = position ? position.length / 3 : 0;
    this.attributeSize = this.size;
    this.vertexShader = 'Mesh.vert';
    this.fragmentShader = 'Mesh.frag';

    NGL.Buffer.call( this, position, color, index, pickingColor, p );

    this.addAttributes( {
        "normal": { type: "v3", value: normal },
    } );

    if( normal === undefined ){
        this.geometry.computeVertexNormals();
    }

};

NGL.MeshBuffer.prototype = Object.create( NGL.Buffer.prototype );

NGL.MeshBuffer.prototype.constructor = NGL.MeshBuffer;


NGL.MappedBuffer = function( params ){

    // required
    // - mapping
    // - mappingType
    // - mappingSize
    // - mappingItemSize
    // - mappingIndices
    // - mappingIndicesSize

    this.size = this.count;
    this.attributeSize = this.count * this.mappingSize;

    var n = this.count * this.mappingIndicesSize;
    var TypedArray = this.attributeSize > 65535 ? Uint32Array : Uint16Array;
    this.index = new TypedArray( n );
    this.makeIndex();

    NGL.Buffer.call( this, null, null, this.index, null, params );

    this.addAttributes( {
        "mapping": { type: this.mappingType, value: null },
    } );

};

NGL.MappedBuffer.prototype = Object.create( NGL.Buffer.prototype );

NGL.MappedBuffer.prototype.constructor = NGL.MappedBuffer;

NGL.MappedBuffer.prototype.setAttributes = function( data ){

    var count = this.count;
    var mappingSize = this.mappingSize;
    var attributes = this.geometry.attributes;

    var a, d, itemSize, array, n, i, j;

    for( var name in data ){

        d = data[ name ];
        a = attributes[ name ];
        itemSize = a.itemSize;
        array = a.array;

        for( var k = 0; k < count; ++k ) {

            n = k * itemSize;
            i = n * mappingSize;

            for( var l = 0; l < mappingSize; ++l ) {

                j = i + ( itemSize * l );

                for( var m = 0; m < itemSize; ++m ) {

                    array[ j + m ] = d[ n + m ];

                }

            }

        }

        a.needsUpdate = true;

    }

};

NGL.MappedBuffer.prototype.makeMapping = function(){

    var count = this.count;
    var mapping = this.mapping;
    var mappingSize = this.mappingSize;
    var mappingItemSize = this.mappingItemSize;

    var aMapping = this.geometry.attributes[ "mapping" ].array;

    for( var v = 0; v < count; v++ ) {

        aMapping.set( mapping, v * mappingItemSize * mappingSize );

    }

};

NGL.MappedBuffer.prototype.makeIndex = function(){

    var count = this.count;
    var mappingSize = this.mappingSize;
    var mappingIndices = this.mappingIndices;
    var mappingIndicesSize = this.mappingIndicesSize;
    var mappingItemSize = this.mappingItemSize;

    var index = this.index;

    var i, ix, it;

    for( var v = 0; v < count; v++ ) {

        i = v * mappingItemSize * mappingSize;
        ix = v * mappingIndicesSize;
        it = v * mappingSize;

        index.set( mappingIndices, ix );

        for( var s = 0; s < mappingIndicesSize; ++s ){
            index[ ix + s ] += it;
        }

    }

};


NGL.QuadBuffer = function( params ){

    this.mapping = new Float32Array([
        -1.0,  1.0,
        -1.0, -1.0,
         1.0,  1.0,
         1.0, -1.0
    ]);

    this.mappingIndices = new Uint16Array([
        0, 1, 2,
        1, 3, 2
    ]);

    this.mappingIndicesSize = 6;
    this.mappingType = "v2";
    this.mappingSize = 4;
    this.mappingItemSize = 2;

    NGL.MappedBuffer.call( this, params );

};

NGL.QuadBuffer.prototype = Object.create( NGL.MappedBuffer.prototype );

NGL.QuadBuffer.prototype.constructor = NGL.QuadBuffer;


NGL.BoxBuffer = function( params ){

    this.mapping = new Float32Array([
        -1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0,  1.0,
        -1.0, -1.0,  1.0,
        -1.0,  1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0,  1.0,  1.0,
        -1.0,  1.0,  1.0
    ]);

    this.mappingIndices = new Uint16Array([
        0, 1, 2,
        0, 2, 3,
        1, 5, 6,
        1, 6, 2,
        4, 6, 5,
        4, 7, 6,
        0, 7, 4,
        0, 3, 7,
        0, 5, 1,
        0, 4, 5,
        3, 2, 6,
        3, 6, 7
    ]);

    this.mappingIndicesSize = 36;
    this.mappingType = "v3";
    this.mappingSize = 8;
    this.mappingItemSize = 3;

    NGL.MappedBuffer.call( this, params );

};

NGL.BoxBuffer.prototype = Object.create( NGL.MappedBuffer.prototype );

NGL.BoxBuffer.prototype.constructor = NGL.BoxBuffer;


NGL.AlignedBoxBuffer = function( params ){

    this.mapping = new Float32Array([
        -1.0,  1.0, -1.0,
        -1.0, -1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0,  1.0,  1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0,  1.0
    ]);

    this.mappingIndices = new Uint16Array([
        0, 1, 2,
        1, 4, 2,
        2, 4, 3,
        4, 5, 3
    ]);

    this.mappingIndicesSize = 12;
    this.mappingType = "v3";
    this.mappingSize = 6;
    this.mappingItemSize = 3;

    NGL.MappedBuffer.call( this, params );

};

NGL.AlignedBoxBuffer.prototype = Object.create( NGL.MappedBuffer.prototype );

NGL.AlignedBoxBuffer.prototype.constructor = NGL.AlignedBoxBuffer;


////////////////////////
// Impostor Primitives

NGL.SphereImpostorBuffer = function( position, color, radius, pickingColor, params ){

    this.impostor = true;
    this.count = position.length / 3;
    this.vertexShader = "SphereImpostor.vert";
    this.fragmentShader = "SphereImpostor.frag";

    NGL.QuadBuffer.call( this, params );

    this.addUniforms( {
        "projectionMatrixInverse": { value: new THREE.Matrix4() },
        "ortho": { value: 0.0 },
    } );

    this.addAttributes( {
        "radius": { type: "f", value: null },
    } );

    this.setAttributes( {
        "position": position,
        "color": color,
        "radius": radius,
    } );

    if( pickingColor ){

        this.addAttributes( {
            "pickingColor": { type: "c", value: null },
        } );

        this.setAttributes( {
            "pickingColor": pickingColor,
        } );

        this.pickable = true;

    }

    this.makeMapping();

};

NGL.SphereImpostorBuffer.prototype = Object.create( NGL.QuadBuffer.prototype );

NGL.SphereImpostorBuffer.prototype.constructor = NGL.SphereImpostorBuffer;


NGL.CylinderImpostorBuffer = function( from, to, color, color2, radius, pickingColor, pickingColor2, params ){

    var p = params || {};

    // Moves the cylinder in camera space to get, for example,
    // one of multiple shifted screen-aligned cylinders.
    this.shift = p.shift !== undefined ? p.shift : 0;

    this.cap = p.cap !== undefined ? p.cap : true;

    this.impostor = true;
    this.count = from.length / 3;
    this.vertexShader = "CylinderImpostor.vert";
    this.fragmentShader = "CylinderImpostor.frag";

    NGL.AlignedBoxBuffer.call( this, p );

    var modelViewMatrixInverse = new THREE.Uniform( new THREE.Matrix4() )
        .onUpdate( function( object, camera ){
            this.value.getInverse( object.modelViewMatrix );
        } );

    this.addUniforms( {
        "modelViewMatrixInverse": modelViewMatrixInverse,
        "shift": { value: this.shift },
        "ortho": { value: 0.0 },
    } );

    this.addAttributes( {
        "position1": { type: "v3", value: null },
        "position2": { type: "v3", value: null },
        "color2": { type: "c", value: null },
        "radius": { type: "f", value: null },
    } );

    this.setAttributes( {
        "position": NGL.Utils.calculateCenterArray( from, to ),

        "position1": from,
        "position2": to,
        "color": color,
        "color2": color2,
        "radius": radius,
    } );

    if( pickingColor ){

        this.addAttributes( {
            "pickingColor": { type: "c", value: null },
            "pickingColor2": { type: "c", value: null },
        } );

        this.setAttributes( {
            "pickingColor": pickingColor,
            "pickingColor2": pickingColor2,
        } );

        this.pickable = true;

    }

    this.makeMapping();

    // FIXME
    // if( this.cap ){
    //     this.material.defines[ "CAP" ] = 1;
    // }

};

NGL.CylinderImpostorBuffer.prototype = Object.create( NGL.AlignedBoxBuffer.prototype );

NGL.CylinderImpostorBuffer.prototype.constructor = NGL.CylinderImpostorBuffer;

NGL.CylinderImpostorBuffer.prototype.getMaterial = function( type ){

    var material = NGL.Buffer.prototype.getMaterial.call( this, type );

    if( this.cap ){
        material.defines[ "CAP" ] = 1;
    }

    return material;

}


NGL.HyperballStickImpostorBuffer = function( position1, position2, color, color2, radius1, radius2, pickingColor, pickingColor2, params ){

    var p = params || {};

    var shrink = p.shrink !== undefined ? p.shrink : 0.14;

    this.impostor = true;
    this.count = position1.length / 3;
    this.vertexShader = "HyperballStickImpostor.vert";
    this.fragmentShader = "HyperballStickImpostor.frag";

    NGL.BoxBuffer.call( this, p );

    var matrix = new THREE.Matrix4();

    function matrixCalc( object, camera ){

        var u = object.material.uniforms;

        if( u.modelViewMatrixInverse ){
            u.modelViewMatrixInverse.value.getInverse(
                object.modelViewMatrix
            );
        }

        if( u.modelViewMatrixInverseTranspose ){
            if( u.modelViewMatrixInverse ){
                u.modelViewMatrixInverseTranspose.value.copy(
                    u.modelViewMatrixInverse.value
                ).transpose();
            }else{
                u.modelViewMatrixInverseTranspose.value
                    .getInverse( object.modelViewMatrix )
                    .transpose();
            }
        }

        if( u.modelViewProjectionMatrix ){
            u.modelViewProjectionMatrix.value.multiplyMatrices(
                camera.projectionMatrix, object.modelViewMatrix
            );
        }

        if( u.modelViewProjectionMatrixInverse ){
            if( u.modelViewProjectionMatrix ){
                matrix.copy(
                    u.modelViewProjectionMatrix.value
                );
                u.modelViewProjectionMatrixInverse.value.getInverse(
                    matrix
                );
            }else{
                matrix.multiplyMatrices(
                    camera.projectionMatrix, object.modelViewMatrix
                );
                u.modelViewProjectionMatrixInverse.value.getInverse(
                    matrix
                );
            }
        }

    }

    var modelViewProjectionMatrix = new THREE.Uniform( new THREE.Matrix4() )
        .onUpdate( matrixCalc );
    var modelViewProjectionMatrixInverse = new THREE.Uniform( new THREE.Matrix4() )
        .onUpdate( matrixCalc );
    var modelViewMatrixInverseTranspose = new THREE.Uniform( new THREE.Matrix4() )
        .onUpdate( matrixCalc );

    this.addUniforms( {
        "modelViewProjectionMatrix": modelViewProjectionMatrix,
        "modelViewProjectionMatrixInverse": modelViewProjectionMatrixInverse,
        "modelViewMatrixInverseTranspose": modelViewMatrixInverseTranspose,
        "shrink": { value: shrink },
    } );

    this.addAttributes( {
        "color": { type: "c", value: null },
        "color2": { type: "c", value: null },
        "radius": { type: "f", value: null },
        "radius2": { type: "f", value: null },
        "position1": { type: "v3", value: null },
        "position2": { type: "v3", value: null },
    } );

    this.setAttributes( {
        "color": color,
        "color2": color2,
        "radius": radius1,
        "radius2": radius2,
        "position1": position1,
        "position2": position2,

        "position": NGL.Utils.calculateCenterArray( position1, position2 ),
    } );

    if( pickingColor ){

        this.addAttributes( {
            "pickingColor": { type: "c", value: null },
            "pickingColor2": { type: "c", value: null },
        } );

        this.setAttributes( {
            "pickingColor": pickingColor,
            "pickingColor2": pickingColor2,
        } );

        this.pickable = true;

    }

    this.makeMapping();

};

NGL.HyperballStickImpostorBuffer.prototype = Object.create( NGL.BoxBuffer.prototype );

NGL.HyperballStickImpostorBuffer.prototype.constructor = NGL.HyperballStickImpostorBuffer;

NGL.HyperballStickImpostorBuffer.prototype.parameters = Object.assign( {

    shrink: { uniform: true }

}, NGL.BoxBuffer.prototype.parameters );


////////////////////////
// Geometry Primitives

NGL.GeometryBuffer = function( position, color, pickingColor, params ){

    var p = params || {};

    // required properties:
    // - geo

    var geo = this.geo;

    var n = position.length / 3;
    var m = geo.vertices.length;
    var o = geo.faces.length;

    this.size = n * m;
    this.positionCount = n;

    this.geoPosition = NGL.Utils.positionFromGeometry( geo );
    this.geoNormal = NGL.Utils.normalFromGeometry( geo );
    this.geoIndex = NGL.Utils.indexFromGeometry( geo );

    this.transformedGeoPosition = new Float32Array( m * 3 );
    this.transformedGeoNormal = new Float32Array( m * 3 );

    this.meshPosition = new Float32Array( this.size * 3 );
    this.meshNormal = new Float32Array( this.size * 3 );
    this.meshColor = new Float32Array( this.size * 3 );
    this.meshPickingColor = new Float32Array( this.size * 3 );

    var TypedArray = this.meshPosition.length / 3 > 65535 ? Uint32Array : Uint16Array;
    this.meshIndex = new TypedArray( n * o * 3 );
    this.makeIndex();

    NGL.MeshBuffer.call(
        this, this.meshPosition, this.meshColor, this.meshIndex,
        this.meshNormal, this.meshPickingColor, p
    );

    this.initNormals = true;

    this.setAttributes( {
        position: position,
        color: color,
        pickingColor: pickingColor
    } );

    this.initNormals = false;

};

NGL.GeometryBuffer.prototype = Object.create( NGL.MeshBuffer.prototype );

NGL.GeometryBuffer.prototype.constructor = NGL.GeometryBuffer;

NGL.GeometryBuffer.prototype.applyPositionTransform = function(){};

NGL.GeometryBuffer.prototype.setAttributes = function(){

    var matrix = new THREE.Matrix4();
    var normalMatrix = new THREE.Matrix3();

    return function( data ){

        var attributes = this.geometry.attributes;

        var position, color, pickingColor;
        var geoPosition, geoNormal;
        var transformedGeoPosition, transformedGeoNormal;
        var meshPosition, meshColor, meshPickingColor, meshNormal;

        if( data[ "position" ] ){
            position = data[ "position" ];
            geoPosition = this.geoPosition;
            meshPosition = this.meshPosition;
            transformedGeoPosition = this.transformedGeoPosition;
            attributes[ "position" ].needsUpdate = true;
        }

        if( data[ "color" ] ){
            color = data[ "color" ];
            meshColor = this.meshColor;
            attributes[ "color" ].needsUpdate = true;
        }

        if( data[ "pickingColor" ] ){
            pickingColor = data[ "pickingColor" ];
            meshPickingColor = this.meshPickingColor;
            attributes[ "pickingColor" ].needsUpdate = true;
        }

        var updateNormals = !!( this.updateNormals && position );
        var initNormals = !!( this.initNormals && position );

        if( updateNormals || initNormals ){
            geoNormal = this.geoNormal;
            meshNormal = this.meshNormal;
            transformedGeoNormal = this.transformedGeoNormal;
            attributes[ "normal" ].needsUpdate = true;
        }

        var n = this.positionCount;
        var m = this.geo.vertices.length;

        for( var i = 0; i < n; ++i ){

            var k = i * m * 3;
            var i3 = i * 3;

            if( position ){

                transformedGeoPosition.set( geoPosition );
                matrix.makeTranslation(
                    position[ i3 ], position[ i3 + 1 ], position[ i3 + 2 ]
                );
                this.applyPositionTransform( matrix, i, i3 );
                matrix.applyToVector3Array( transformedGeoPosition );

                meshPosition.set( transformedGeoPosition, k );

            }

            if( updateNormals ){

                transformedGeoNormal.set( geoNormal );
                normalMatrix.getNormalMatrix( matrix );
                normalMatrix.applyToVector3Array( transformedGeoNormal );

                meshNormal.set( transformedGeoNormal, k );

            }else if( initNormals ){

                meshNormal.set( geoNormal, k );

            }

            if( color ){

                for( var j = 0; j < m; ++j ){

                    var l = k + 3 * j;

                    meshColor[ l     ] = color[ i3     ];
                    meshColor[ l + 1 ] = color[ i3 + 1 ];
                    meshColor[ l + 2 ] = color[ i3 + 2 ];

                }

            }

            if( pickingColor ){

                for( var j = 0; j < m; ++j ){

                    var l = k + 3 * j;

                    meshPickingColor[ l     ] = pickingColor[ i3     ];
                    meshPickingColor[ l + 1 ] = pickingColor[ i3 + 1 ];
                    meshPickingColor[ l + 2 ] = pickingColor[ i3 + 2 ];

                }

            }

        }

    }

}();

NGL.GeometryBuffer.prototype.makeIndex = function(){

    var geoIndex = this.geoIndex;
    var meshIndex = this.meshIndex;

    var n = this.positionCount;
    var m = this.geo.vertices.length;
    var o = this.geo.faces.length;

    var p, i, j, q;
    var o3 = o * 3;

    for( i = 0; i < n; ++i ){

        j = i * o3;
        q = j + o3;

        meshIndex.set( geoIndex, j );
        for( p = j; p < q; ++p ) meshIndex[ p ] += i * m;

    }

};


NGL.SphereGeometryBuffer = function( position, color, radius, pickingColor, params ){

    var detail = params.sphereDetail !== undefined ? params.sphereDetail : 1;

    this.geo = new THREE.IcosahedronGeometry( 1, detail );

    this.setPositionTransform( radius );

    NGL.GeometryBuffer.call( this, position, color, pickingColor, params );

};

NGL.SphereGeometryBuffer.prototype = Object.create( NGL.GeometryBuffer.prototype );

NGL.SphereGeometryBuffer.prototype.constructor = NGL.SphereGeometryBuffer;

NGL.SphereGeometryBuffer.prototype.setPositionTransform = function( radius ){

    var r;
    var scale = new THREE.Vector3();

    this.applyPositionTransform = function( matrix, i ){

        r = radius[ i ];
        scale.set( r, r, r );
        matrix.scale( scale );

    }

};

NGL.SphereGeometryBuffer.prototype.setAttributes = function( data ){

    if( data[ "radius" ] ){
        this.setPositionTransform( data[ "radius" ] );
    }

    NGL.GeometryBuffer.prototype.setAttributes.call( this, data );

}


NGL.CylinderGeometryBuffer = function( from, to, color, color2, radius, pickingColor, pickingColor2, params ){

    var radiusSegments = params.radiusSegments !== undefined ? params.radiusSegments : 10;

    this.updateNormals = true;

    var matrix = new THREE.Matrix4().makeRotationX( Math.PI/ 2  );

    // FIXME params.cap
    this.geo = new THREE.CylinderGeometry( 1, 1, 1, radiusSegments, 1, true );
    this.geo.applyMatrix( matrix );

    var n = from.length;
    var m = radius.length;

    this._position = new Float32Array( n * 2 );
    this._color = new Float32Array( n * 2 );
    this._pickingColor = new Float32Array( n * 2 );
    this._from = new Float32Array( n * 2 );
    this._to = new Float32Array( n * 2 );
    this._radius = new Float32Array( m * 2 );

    this.__center = new Float32Array( n );

    NGL.GeometryBuffer.call(
        this, this._position, this._color, this._pickingColor, params
    );

    this.setPositionTransform( this._from, this._to, this._radius );

    this.setAttributes( {
        "position1": from,
        "position2": to,
        "color": color,
        "color2": color2,
        "radius": radius,
        "pickingColor": pickingColor,
        "pickingColor2": pickingColor2
    } );

};

NGL.CylinderGeometryBuffer.prototype = Object.create( NGL.GeometryBuffer.prototype );

NGL.CylinderImpostorBuffer.prototype.constructor = NGL.CylinderImpostorBuffer;

NGL.CylinderGeometryBuffer.prototype.setPositionTransform = function( from, to, radius ){

    var r;
    var scale = new THREE.Vector3();
    var eye = new THREE.Vector3();
    var target = new THREE.Vector3();
    var up = new THREE.Vector3( 0, 1, 0 );

    this.applyPositionTransform = function( matrix, i, i3 ){

        eye.fromArray( from, i3 );
        target.fromArray( to, i3 );
        matrix.lookAt( eye, target, up );

        r = radius[ i ];
        scale.set( r, r, eye.distanceTo( target ) );
        matrix.scale( scale );

    }

};

NGL.CylinderGeometryBuffer.prototype.setAttributes = function( data ){

    var n = this._position.length / 2;
    var m = this._radius.length / 2;
    var geoData = {};

    if( data[ "position1" ] && data[ "position2" ] ){

        NGL.Utils.calculateCenterArray(
            data[ "position1" ], data[ "position2" ], this.__center
        );
        NGL.Utils.calculateCenterArray(
            data[ "position1" ], this.__center, this._position
        );
        NGL.Utils.calculateCenterArray(
            this.__center, data[ "position2" ], this._position, n
        );

        this._from.set( data[ "position1" ] );
        this._from.set( this.__center, n );

        this._to.set( this.__center );
        this._to.set( data[ "position2" ], n );

        geoData[ "position" ] = this._position;

    }

    if( data[ "color" ] && data[ "color2" ] ){

        this._color.set( data[ "color" ] );
        this._color.set( data[ "color2" ], n );

        geoData[ "color" ] = this._color;

    }

    if( data[ "pickingColor" ] && data[ "pickingColor2" ] ){

        this._pickingColor.set( data[ "pickingColor" ] );
        this._pickingColor.set( data[ "pickingColor2" ], n );

        geoData[ "pickingColor" ] = this._pickingColor;

    }

    if( data[ "radius" ] ){

        this._radius.set( data[ "radius" ] );
        this._radius.set( data[ "radius" ], m );

    }

    if( ( data[ "position1" ] && data[ "position2" ] ) || data[ "radius" ] ){

        this.setPositionTransform( this._from, this._to, this._radius );

    }

    NGL.GeometryBuffer.prototype.setAttributes.call( this, geoData );

}


//////////////////////
// Pixel Primitives

NGL.makePointTexture = function( params ){

    var p = Object.assign( {}, params );

    var width = NGL.defaults( p.width, 256 );
    var height = NGL.defaults( p.height, 256 );
    var center = [ width / 2, height / 2 ];
    var radius = Math.min( width / 2, height / 2 );
    var delta = NGL.defaults( p.delta, 1 / ( radius + 1 ) ) * radius;

    //

    function clamp( value, min, max ){
        return Math.min( Math.max( value, min ), max );
    }

    function distance( x0, y0, x1, y1 ){
        var dx = x1 - x0, dy = y1 - y0;
        return Math.sqrt( dx * dx + dy * dy );
    }

    function smoothStep( edge0, edge1, x ){
        // Scale, bias and saturate x to 0..1 range
        x = clamp( ( x - edge0 ) / ( edge1 - edge0 ), 0, 1 );
        // Evaluate polynomial
        return x * x * ( 3 - 2 * x );
    }

    //

    var x = 0
    var y = 0;
    var data = new Uint8Array( width * height * 4 );

    for ( var i = 0, il = data.length; i < il; i += 4 ) {

        var dist = distance( x, y, center[ 0 ], center[ 1 ] );
        var value = 1 - smoothStep( radius - delta, radius, dist );

        data[ i     ] = value * 255;
        data[ i + 1 ] = value * 255;
        data[ i + 2 ] = value * 255;
        data[ i + 3 ] = value * 255;

        if( ++x === width ){
            x = 0;
            y++;
        }

    }

    var tex = new THREE.DataTexture( data, width, height );
    tex.needsUpdate = true;

    return tex;

};


NGL.PointBuffer = function( position, color, params ){

    var p = params || {};

    this.point = true;
    this.pointSize = p.pointSize !== undefined ? p.pointSize : 1;
    this.sizeAttenuation = p.sizeAttenuation !== undefined ? p.sizeAttenuation : true;
    this.sortParticles = p.sortParticles !== undefined ? p.sortParticles : false;
    this.alphaTest = p.alphaTest !== undefined ? p.alphaTest : 0.5;
    this.useTexture = p.useTexture !== undefined ? p.useTexture : false;
    this.forceTransparent = p.forceTransparent !== undefined ? p.forceTransparent : false;
    this.edgeBleach = p.edgeBleach !== undefined ? p.edgeBleach : 0.0;

    this.size = position.length / 3;
    this.attributeSize = this.size;
    this.vertexShader = 'Point.vert';
    this.fragmentShader = 'Point.frag';

    NGL.Buffer.call( this, position, color, undefined, undefined, p );

    this.addUniforms( {
        "size": { value: this.pointSize },
        "canvasHeight": { value: 1.0 },
        "pixelRatio": { value: 1.0 },
        "map": { value: null },
    } );

};

NGL.PointBuffer.prototype = Object.create( NGL.Buffer.prototype );

NGL.PointBuffer.prototype.constructor = NGL.PointBuffer;

NGL.PointBuffer.prototype.parameters = Object.assign( {

    pointSize: { uniform: "size" },
    sizeAttenuation: { updateShader: true },
    sortParticles: {},
    alphaTest: { updateShader: true },
    useTexture: { updateShader: true },
    forceTransparent: {},
    edgeBleach: { uniform: true },

}, NGL.Buffer.prototype.parameters );

NGL.PointBuffer.prototype.makeMaterial = function(){

    NGL.Buffer.prototype.makeMaterial.call( this );

    this.makeTexture();

    this.material.uniforms.map.value = this.tex;
    this.material.blending = THREE.NormalBlending;
    this.material.needsUpdate = true;

};

NGL.PointBuffer.prototype.makeTexture = function(){

    if( this.tex ) this.tex.dispose();
    this.tex = NGL.makePointTexture( { delta: this.edgeBleach } );

};

NGL.PointBuffer.prototype.getDefines = function( type ){

    var defines = NGL.Buffer.prototype.getDefines.call( this, type );

    if( this.sizeAttenuation ){
        defines[ "USE_SIZEATTENUATION" ] = 1;
    }

    if( this.useTexture ){
        defines[ "USE_MAP" ] = 1;
    }

    if( this.alphaTest > 0 && this.alphaTest <= 1 ){
        defines[ "ALPHATEST" ] = this.alphaTest.toPrecision( 2 );
    }

    return defines;

};

NGL.PointBuffer.prototype.setUniforms = function( data ){

    if( data && data[ "edgeBleach" ] !== undefined ){

        this.makeTexture();
        data[ "map" ] = this.tex;

    }

    NGL.Buffer.prototype.setUniforms.call( this, data );

};

NGL.PointBuffer.prototype.dispose = function(){

    NGL.Buffer.prototype.dispose.call( this );

    if( this.tex ) this.tex.dispose();

};


NGL.LineBuffer = function( from, to, color, color2, params ){

    var p = params || {};

    this.size = from.length / 3;
    this.vertexShader = 'Line.vert';
    this.fragmentShader = 'Line.frag';
    this.line = true;

    var n = this.size;
    var n6 = n * 6;
    var nX = n * 2 * 2;

    this.attributeSize = nX;

    this.linePosition = new Float32Array( nX * 3 );
    this.lineColor = new Float32Array( nX * 3 );

    NGL.Buffer.call(
        this, this.linePosition, this.lineColor, undefined, undefined, p
    );

    this.setAttributes( {
        from: from,
        to: to,
        color: color,
        color2: color2
    } );

};

NGL.LineBuffer.prototype = Object.create( NGL.Buffer.prototype );

NGL.LineBuffer.prototype.constructor = NGL.LineBuffer;

NGL.LineBuffer.prototype.setAttributes = function( data ){

    var from, to, color, color2;
    var aPosition, aColor;

    var attributes = this.geometry.attributes;

    if( data[ "from" ] && data[ "to" ] ){
        from = data[ "from" ];
        to = data[ "to" ];
        aPosition = attributes[ "position" ].array;
        attributes[ "position" ].needsUpdate = true;
    }

    if( data[ "color" ] && data[ "color2" ] ){
        color = data[ "color" ];
        color2 = data[ "color2" ];
        aColor = attributes[ "color" ].array;
        attributes[ "color" ].needsUpdate = true;
    }

    var n = this.size;
    var n6 = n * 6;

    var i, j, i2;
    var x, y, z, x1, y1, z1, x2, y2, z2;

    for( var v = 0; v < n; v++ ){

        j = v * 3;
        i = v * 2 * 3;
        i2 = i + n6;

        if( from && to ){

            x1 = from[ j     ];
            y1 = from[ j + 1 ];
            z1 = from[ j + 2 ];

            x2 = to[ j     ];
            y2 = to[ j + 1 ];
            z2 = to[ j + 2 ];

            x = ( x1 + x2 ) / 2.0;
            y = ( y1 + y2 ) / 2.0;
            z = ( z1 + z2 ) / 2.0;

            aPosition[ i     ] = x1;
            aPosition[ i + 1 ] = y1;
            aPosition[ i + 2 ] = z1;
            aPosition[ i + 3 ] = x;
            aPosition[ i + 4 ] = y;
            aPosition[ i + 5 ] = z;

            aPosition[ i2     ] = x;
            aPosition[ i2 + 1 ] = y;
            aPosition[ i2 + 2 ] = z;
            aPosition[ i2 + 3 ] = x2;
            aPosition[ i2 + 4 ] = y2;
            aPosition[ i2 + 5 ] = z2;

        }

        if( color && color2 ){

            aColor[ i     ] = aColor[ i + 3 ] = color[ j     ];
            aColor[ i + 1 ] = aColor[ i + 4 ] = color[ j + 1 ];
            aColor[ i + 2 ] = aColor[ i + 5 ] = color[ j + 2 ];

            aColor[ i2     ] = aColor[ i2 + 3 ] = color2[ j     ];
            aColor[ i2 + 1 ] = aColor[ i2 + 4 ] = color2[ j + 1 ];
            aColor[ i2 + 2 ] = aColor[ i2 + 5 ] = color2[ j + 2 ];

        }

    }

};


NGL.TraceBuffer = function( position, color, params ){

    var p = params || {};

    this.size = position.length / 3;
    this.vertexShader = 'Line.vert';
    this.fragmentShader = 'Line.frag';
    this.line = true;

    var n = this.size;
    var n1 = n - 1;

    this.attributeSize = n1 * 2;

    this.linePosition = new Float32Array( n1 * 3 * 2 );
    this.lineColor = new Float32Array( n1 * 3 * 2 );

    NGL.Buffer.call(
        this, this.linePosition, this.lineColor, undefined, undefined, p
    );

    this.setAttributes( {
        position: position,
        color: color
    } );

};

NGL.TraceBuffer.prototype = Object.create( NGL.Buffer.prototype );

NGL.TraceBuffer.prototype.constructor = NGL.TraceBuffer;

NGL.TraceBuffer.prototype.setAttributes = function( data ){

    var position, color;
    var linePosition, lineColor;

    var attributes = this.geometry.attributes;

    if( data[ "position" ] ){
        position = data[ "position" ];
        linePosition = attributes[ "position" ].array;
        attributes[ "position" ].needsUpdate = true;
    }

    if( data[ "color" ] ){
        color = data[ "color" ];
        lineColor = attributes[ "color" ].array;
        attributes[ "color" ].needsUpdate = true;
    }

    if( !position && !color ){
        NGL.warn( "NGL.TraceBuffer.prototype.setAttributes no data" );
        return;
    }

    var v, v2;
    var n = this.size;
    var n1 = n - 1;

    for( var i = 0; i < n1; ++i ){

        v = 3 * i;
        v2 = 3 * i * 2;

        if( position ){

            linePosition[ v2     ] = position[ v     ];
            linePosition[ v2 + 1 ] = position[ v + 1 ];
            linePosition[ v2 + 2 ] = position[ v + 2 ];

            linePosition[ v2 + 3 ] = position[ v + 3 ];
            linePosition[ v2 + 4 ] = position[ v + 4 ];
            linePosition[ v2 + 5 ] = position[ v + 5 ];

        }

        if( color ){

            lineColor[ v2     ] = color[ v     ];
            lineColor[ v2 + 1 ] = color[ v + 1 ];
            lineColor[ v2 + 2 ] = color[ v + 2 ];

            lineColor[ v2 + 3 ] = color[ v + 3 ];
            lineColor[ v2 + 4 ] = color[ v + 4 ];
            lineColor[ v2 + 5 ] = color[ v + 5 ];

        }

    }

};


//////////////////////
// Sprite Primitives

// NGL.ParticleSpriteBuffer = function( position, color, radius ){

//     this.count = position.length / 3;
//     this.vertexShader = 'ParticleSprite.vert';
//     this.fragmentShader = 'ParticleSprite.frag';

//     NGL.QuadBuffer.call( this );

//     this.addUniforms({
//         'projectionMatrixInverse': { value: new THREE.Matrix4() },
//     });

//     this.addAttributes( {
//         "radius": { type: "f", value: null },
//     } );

//     this.setAttributes( {
//         "position": position,
//         "color": color,
//         "radius": radius,
//     } );

//     this.material.lights = false;

// };

// NGL.ParticleSpriteBuffer.prototype = Object.create( NGL.QuadBuffer.prototype );

// NGL.ParticleSpriteBuffer.prototype.constructor = NGL.ParticleSpriteBuffer;


NGL.RibbonBuffer = function( position, normal, dir, color, size, pickingColor, params ){

    var p = params || {};

    var n = ( position.length / 3 ) - 1;
    var n4 = n * 4;
    var x = n4 * 3;

    this.meshPosition = new Float32Array( x );
    this.meshColor = new Float32Array( x );
    this.meshNormal = new Float32Array( x );
    this.meshPickingColor = pickingColor ? new Float32Array( x ) : undefined;

    var TypedArray = this.meshPosition.length / 3 > 65535 ? Uint32Array : Uint16Array;
    this.meshIndex = new TypedArray( x );
    this.makeIndex();

    NGL.MeshBuffer.call(
        this, this.meshPosition, this.meshColor, this.meshIndex,
        this.meshNormal, this.meshPickingColor, p
    );

    this.vertexShader = 'Ribbon.vert';
    this.fragmentShader = 'Mesh.frag';

    this.geometry.addAttribute(
        'dir', new THREE.BufferAttribute( new Float32Array( x ), 3 )
    );
    this.geometry.addAttribute(
        'size', new THREE.BufferAttribute( new Float32Array( n4 ), 1 )
    );

    this.setAttributes( {
        position: position,
        normal: normal,
        dir: dir,
        color: color,
        size: size,
        pickingColor: pickingColor
    } );

};

NGL.RibbonBuffer.prototype = Object.create( NGL.MeshBuffer.prototype );

NGL.RibbonBuffer.prototype.constructor = NGL.RibbonBuffer;

NGL.RibbonBuffer.prototype.setAttributes = function( data ){

    var n4 = this.size;
    var n = n4 / 4;

    var attributes = this.geometry.attributes;

    var position, normal, size, dir, color, pickingColor;
    var aPosition, aNormal, aSize, aDir, aColor, aPickingColor;

    if( data[ "position" ] ){
        position = data[ "position" ];
        aPosition = attributes[ "position" ].array;
        attributes[ "position" ].needsUpdate = true;
    }

    if( data[ "normal" ] ){
        normal = data[ "normal" ];
        aNormal = attributes[ "normal" ].array;
        attributes[ "normal" ].needsUpdate = true;
    }

    if( data[ "size" ] ){
        size = data[ "size" ];
        aSize = attributes[ "size" ].array;
        attributes[ "size" ].needsUpdate = true;
    }

    if( data[ "dir" ] ){
        dir = data[ "dir" ];
        aDir = attributes[ "dir" ].array;
        attributes[ "dir" ].needsUpdate = true;
    }

    if( data[ "color" ] ){
        color = data[ "color" ];
        aColor = attributes[ "color" ].array;
        attributes[ "color" ].needsUpdate = true;
    }

    if( data[ "pickingColor" ] ){
        pickingColor = data[ "pickingColor" ];
        aPickingColor = attributes[ "pickingColor" ].array;
        attributes[ "pickingColor" ].needsUpdate = true;
    }

    var v, i, k, p, l, v3;
    var currSize;
    var prevSize = size ? size[ 0 ] : null;

    for( v = 0; v < n; ++v ){

        v3 = v * 3;
        k = v * 3 * 4;
        l = v * 4;

        if( position ){

            aPosition[ k     ] = aPosition[ k + 3 ] = position[ v3     ];
            aPosition[ k + 1 ] = aPosition[ k + 4 ] = position[ v3 + 1 ];
            aPosition[ k + 2 ] = aPosition[ k + 5 ] = position[ v3 + 2 ];

            aPosition[ k + 6 ] = aPosition[ k +  9 ] = position[ v3 + 3 ];
            aPosition[ k + 7 ] = aPosition[ k + 10 ] = position[ v3 + 4 ];
            aPosition[ k + 8 ] = aPosition[ k + 11 ] = position[ v3 + 5 ];

        }

        if( normal ){

            aNormal[ k     ] = aNormal[ k + 3 ] = -normal[ v3     ];
            aNormal[ k + 1 ] = aNormal[ k + 4 ] = -normal[ v3 + 1 ];
            aNormal[ k + 2 ] = aNormal[ k + 5 ] = -normal[ v3 + 2 ];

            aNormal[ k + 6 ] = aNormal[ k +  9 ] = -normal[ v3 + 3 ];
            aNormal[ k + 7 ] = aNormal[ k + 10 ] = -normal[ v3 + 4 ];
            aNormal[ k + 8 ] = aNormal[ k + 11 ] = -normal[ v3 + 5 ];

        }


        for( i = 0; i<4; ++i ){

            p = k + 3 * i;

            if( color ){

                aColor[ p     ] = color[ v3     ];
                aColor[ p + 1 ] = color[ v3 + 1 ];
                aColor[ p + 2 ] = color[ v3 + 2 ];

            }

            if( pickingColor ){

                aPickingColor[ p     ] = pickingColor[ v3     ];
                aPickingColor[ p + 1 ] = pickingColor[ v3 + 1 ];
                aPickingColor[ p + 2 ] = pickingColor[ v3 + 2 ];

            }

        }

        if( size ){

            currSize = size[ v ];

            if( prevSize !== size[ v ] ){

                aSize[ l     ] = prevSize;
                aSize[ l + 1 ] = prevSize;
                aSize[ l + 2 ] = currSize;
                aSize[ l + 3 ] = currSize;

            }else{

                aSize[ l     ] = currSize;
                aSize[ l + 1 ] = currSize;
                aSize[ l + 2 ] = currSize;
                aSize[ l + 3 ] = currSize;

            }

            prevSize = currSize;

        }

        if( dir ){

            aDir[ k     ] = dir[ v3     ];
            aDir[ k + 1 ] = dir[ v3 + 1 ];
            aDir[ k + 2 ] = dir[ v3 + 2 ];

            aDir[ k + 3 ] = -dir[ v3     ];
            aDir[ k + 4 ] = -dir[ v3 + 1 ];
            aDir[ k + 5 ] = -dir[ v3 + 2 ];

            aDir[ k + 6 ] = dir[ v3 + 3 ];
            aDir[ k + 7 ] = dir[ v3 + 4 ];
            aDir[ k + 8 ] = dir[ v3 + 5 ];

            aDir[ k +  9 ] = -dir[ v3 + 3 ];
            aDir[ k + 10 ] = -dir[ v3 + 4 ];
            aDir[ k + 11 ] = -dir[ v3 + 5 ];

        }

    }

};

NGL.RibbonBuffer.prototype.makeIndex = function(){

    var meshIndex = this.meshIndex;
    var n = meshIndex.length / 4 / 3;

    var quadIndices = new Uint16Array([
        0, 1, 2,
        1, 3, 2
    ]);

    var s, v, ix, it;

    for( v = 0; v < n; ++v ){

        ix = v * 6;
        it = v * 4;

        meshIndex.set( quadIndices, ix );
        for( s = 0; s < 6; ++s ){
            meshIndex[ ix + s ] += it;
        }

    }

};


////////////////////
// Mesh Primitives

NGL.TubeMeshBuffer = function( position, normal, binormal, tangent, color, size, pickingColor, params ){

    var p = params || {};

    this.aspectRatio = p.aspectRatio !== undefined ? p.aspectRatio : 1.0;
    this.radialSegments = p.radialSegments !== undefined ? p.radialSegments : 4;
    this.capped = p.capped !== undefined ? p.capped : false;

    this.capVertices = this.capped ? this.radialSegments : 0;
    this.capTriangles = this.capped ? this.radialSegments - 2 : 0;

    var n = position.length / 3;
    var n1 = n - 1;
    var radialSegments1 = this.radialSegments + 1;

    var x = n * this.radialSegments * 3 + 2 * this.capVertices * 3;

    this.size2 = n;

    this.meshPosition = new Float32Array( x );
    this.meshColor = new Float32Array( x );
    this.meshNormal = new Float32Array( x );
    this.meshPickingColor = pickingColor ? new Float32Array( x ) : undefined;

    var xi = n1 * 2 * this.radialSegments * 3 + 2 * this.capTriangles * 3
    var TypedArray = this.meshPosition.length / 3 > 65535 ? Uint32Array : Uint16Array;
    this.meshIndex = new TypedArray( xi );
    this.makeIndex();

    NGL.MeshBuffer.call(
        this, this.meshPosition, this.meshColor, this.meshIndex,
        this.meshNormal, this.meshPickingColor, p
    );

    this.setAttributes( {
        position: position,
        normal: normal,
        binormal: binormal,
        tangent: tangent,
        color: color,
        size: size,
        pickingColor: pickingColor
    } );

}

NGL.TubeMeshBuffer.prototype = Object.create( NGL.MeshBuffer.prototype );

NGL.TubeMeshBuffer.prototype.constructor = NGL.TubeMeshBuffer;

NGL.TubeMeshBuffer.prototype.setAttributes = function(){

    var vTangent = new THREE.Vector3();
    var vMeshNormal = new THREE.Vector3();

    return function( data ){

        var aspectRatio = this.aspectRatio;

        var n = this.size2;
        var n1 = n - 1;
        var capVertices = this.capVertices;
        var radialSegments = this.radialSegments;

        var attributes = this.geometry.attributes;

        var position, normal, binormal, tangent, color, size, pickingColor;
        var meshPosition, meshColor, meshNormal, meshPickingColor

        if( data[ "position" ] ){

            position = data[ "position" ];
            normal = data[ "normal" ];
            binormal = data[ "binormal" ];
            tangent = data[ "tangent" ];
            size = data[ "size" ];

            meshPosition = attributes[ "position" ].array;
            meshNormal = attributes[ "normal" ].array;

            attributes[ "position" ].needsUpdate = true;
            attributes[ "normal" ].needsUpdate = true;

        }

        if( data[ "color" ] ){

            color = data[ "color" ];
            meshColor = attributes[ "color" ].array;
            attributes[ "color" ].needsUpdate = true;

        }

        if( data[ "pickingColor" ] ){

            pickingColor = data[ "pickingColor" ];
            meshPickingColor = attributes[ "pickingColor" ].array;
            attributes[ "pickingColor" ].needsUpdate = true;

        }

        var i, j, k, l, s, t;
        var v, cx, cy;
        var cx1, cy1, cx2, cy2;
        var radius;
        var irs, irs1;

        var normX, normY, normZ;
        var biX, biY, biZ;
        var posX, posY, posZ;

        var cxArr = [];
        var cyArr = [];
        var cx1Arr = [];
        var cy1Arr = [];
        var cx2Arr = [];
        var cy2Arr = [];

        if( position ){

            for( j = 0; j < radialSegments; ++j ){

                v = ( j / radialSegments ) * 2 * Math.PI;

                cxArr[ j ] = aspectRatio * Math.cos( v );
                cyArr[ j ] = Math.sin( v );

                cx1Arr[ j ] = aspectRatio * Math.cos( v - 0.01 );
                cy1Arr[ j ] = Math.sin( v - 0.01 );
                cx2Arr[ j ] = aspectRatio * Math.cos( v + 0.01 );
                cy2Arr[ j ] = Math.sin( v + 0.01 );

            }

        }

        for( i = 0; i < n; ++i ){

            k = i * 3;
            l = k * radialSegments;

            if( position ){

                vTangent.set(
                    tangent[ k ], tangent[ k + 1 ], tangent[ k + 2 ]
                );

                normX = normal[ k     ];
                normY = normal[ k + 1 ];
                normZ = normal[ k + 2 ];

                biX = binormal[ k     ];
                biY = binormal[ k + 1 ];
                biZ = binormal[ k + 2 ];

                posX = position[ k     ];
                posY = position[ k + 1 ];
                posZ = position[ k + 2 ];

                radius = size[ i ];

            }

            for( j = 0; j < radialSegments; ++j ){

                s = l + j * 3

                if( position ){

                    cx = -radius * cxArr[ j ]; // TODO: Hack: Negating it so it faces outside.
                    cy = radius * cyArr[ j ];

                    cx1 = -radius * cx1Arr[ j ];
                    cy1 = radius * cy1Arr[ j ];
                    cx2 = -radius * cx2Arr[ j ];
                    cy2 = radius * cy2Arr[ j ];

                    meshPosition[ s     ] = posX + cx * normX + cy * biX;
                    meshPosition[ s + 1 ] = posY + cx * normY + cy * biY;
                    meshPosition[ s + 2 ] = posZ + cx * normZ + cy * biZ;

                    // TODO half of these are symmetric
                    vMeshNormal.set(
                        // ellipse tangent approximated as vector from/to adjacent points
                        ( cx2 * normX + cy2 * biX ) -
                            ( cx1 * normX + cy1 * biX ),
                        ( cx2 * normY + cy2 * biY ) -
                            ( cx1 * normY + cy1 * biY ),
                        ( cx2 * normZ + cy2 * biZ ) -
                            ( cx1 * normZ + cy1 * biZ )
                    ).cross( vTangent );

                    meshNormal[ s     ] = vMeshNormal.x;
                    meshNormal[ s + 1 ] = vMeshNormal.y;
                    meshNormal[ s + 2 ] = vMeshNormal.z;

                }

                if( color ){

                    meshColor[ s     ] = color[ k     ];
                    meshColor[ s + 1 ] = color[ k + 1 ];
                    meshColor[ s + 2 ] = color[ k + 2 ];

                }

                if( pickingColor ){

                    meshPickingColor[ s     ] = pickingColor[ k     ];
                    meshPickingColor[ s + 1 ] = pickingColor[ k + 1 ];
                    meshPickingColor[ s + 2 ] = pickingColor[ k + 2 ];

                }

            }

        }

        // front cap

        k = 0;
        l = n * 3 * radialSegments;

        for( j = 0; j < radialSegments; ++j ){

            s = k + j * 3;
            t = l + j * 3;

            if( position ){

                meshPosition[ t     ] = meshPosition[ s     ];
                meshPosition[ t + 1 ] = meshPosition[ s + 1 ];
                meshPosition[ t + 2 ] = meshPosition[ s + 2 ];

                meshNormal[ t     ] = tangent[ k     ];
                meshNormal[ t + 1 ] = tangent[ k + 1 ];
                meshNormal[ t + 2 ] = tangent[ k + 2 ];

            }

            if( color ){

                meshColor[ t     ] = meshColor[ s     ];
                meshColor[ t + 1 ] = meshColor[ s + 1 ];
                meshColor[ t + 2 ] = meshColor[ s + 2 ];

            }

            if( pickingColor ){

                meshPickingColor[ t     ] = meshPickingColor[ s     ];
                meshPickingColor[ t + 1 ] = meshPickingColor[ s + 1 ];
                meshPickingColor[ t + 2 ] = meshPickingColor[ s + 2 ];

            }

        }

        // back cap

        k = ( n - 1 ) * 3 * radialSegments;
        l = ( n + 1 ) * 3 * radialSegments;

        for( j = 0; j < radialSegments; ++j ){

            s = k + j * 3;
            t = l + j * 3;

            if( position ){

                meshPosition[ t     ] = meshPosition[ s     ];
                meshPosition[ t + 1 ] = meshPosition[ s + 1 ];
                meshPosition[ t + 2 ] = meshPosition[ s + 2 ];

                meshNormal[ t     ] = tangent[ n1 * 3     ];
                meshNormal[ t + 1 ] = tangent[ n1 * 3 + 1 ];
                meshNormal[ t + 2 ] = tangent[ n1 * 3 + 2 ];

            }

            if( color ){

                meshColor[ t     ] = meshColor[ s     ];
                meshColor[ t + 1 ] = meshColor[ s + 1 ];
                meshColor[ t + 2 ] = meshColor[ s + 2 ];

            }

            if( pickingColor ){

                meshPickingColor[ t     ] = meshPickingColor[ s     ];
                meshPickingColor[ t + 1 ] = meshPickingColor[ s + 1 ];
                meshPickingColor[ t + 2 ] = meshPickingColor[ s + 2 ];

            }

        }

    }

}();

NGL.TubeMeshBuffer.prototype.makeIndex = function(){

    var meshIndex = this.meshIndex;

    var n = this.size2;
    var n1 = n - 1;
    var capTriangles = this.capTriangles;
    var radialSegments = this.radialSegments;
    var radialSegments1 = this.radialSegments + 1;

    var i, k, irs, irs1, l, j;

    for( i = 0; i < n1; ++i ){

        k = i * radialSegments * 3 * 2

        irs = i * radialSegments;
        irs1 = ( i + 1 ) * radialSegments;

        for( j = 0; j < radialSegments; ++j ){

            l = k + j * 3 * 2;

            // meshIndex[ l + 0 ] = irs + ( ( j + 0 ) % radialSegments );
            meshIndex[ l ] = irs + j;
            meshIndex[ l + 1 ] = irs + ( ( j + 1 ) % radialSegments );
            // meshIndex[ l + 2 ] = irs1 + ( ( j + 0 ) % radialSegments );
            meshIndex[ l + 2 ] = irs1 + j;

            // meshIndex[ l + 3 ] = irs1 + ( ( j + 0 ) % radialSegments );
            meshIndex[ l + 3 ] = irs1 + j;
            meshIndex[ l + 4 ] = irs + ( ( j + 1 ) % radialSegments );
            meshIndex[ l + 5 ] = irs1 + ( ( j + 1 ) % radialSegments );

        }

    }

    // capping

    var strip = [ 0 ];

    for( j = 1; j < radialSegments1 / 2; ++j ){

        strip.push( j );
        if( radialSegments - j !== j ){
            strip.push( radialSegments - j );
        }

    }

    // front cap

    l = n1 * radialSegments * 3 * 2;
    k = n * radialSegments;

    for( j = 0; j < strip.length - 2; ++j ){

        if( j % 2 === 0 ){
            meshIndex[ l + j * 3 + 0 ] = k + strip[ j + 0 ];
            meshIndex[ l + j * 3 + 1 ] = k + strip[ j + 1 ];
            meshIndex[ l + j * 3 + 2 ] = k + strip[ j + 2 ];
        }else{
            meshIndex[ l + j * 3 + 0 ] = k + strip[ j + 2 ];
            meshIndex[ l + j * 3 + 1 ] = k + strip[ j + 1 ];
            meshIndex[ l + j * 3 + 2 ] = k + strip[ j + 0 ];
        }

    }

    // back cap

    l = n1 * radialSegments * 3 * 2 + 3 * capTriangles;
    k = n * radialSegments + radialSegments;

    for( j = 0; j < strip.length - 2; ++j ){

        if( j % 2 === 0 ){
            meshIndex[ l + j * 3 + 0 ] = k + strip[ j + 0 ];
            meshIndex[ l + j * 3 + 1 ] = k + strip[ j + 1 ];
            meshIndex[ l + j * 3 + 2 ] = k + strip[ j + 2 ];
        }else{
            meshIndex[ l + j * 3 + 0 ] = k + strip[ j + 2 ];
            meshIndex[ l + j * 3 + 1 ] = k + strip[ j + 1 ];
            meshIndex[ l + j * 3 + 2 ] = k + strip[ j + 0 ];
        }

    }

};


NGL.SurfaceBuffer = function(){

    NGL.MeshBuffer.apply( this, arguments );

}

NGL.SurfaceBuffer.prototype = Object.create( NGL.MeshBuffer.prototype );

NGL.SurfaceBuffer.prototype.constructor = NGL.SurfaceBuffer;


///////////////////
// API Primitives

NGL.SphereBuffer = function( position, color, radius, pickingColor, params ){

    var p = params || {};

    if( !NGL.extensionFragDepth || p.disableImpostor ){

        return new NGL.SphereGeometryBuffer(
            position, color, radius, pickingColor, params
        );

    }else{

        return new NGL.SphereImpostorBuffer(
            position, color, radius, pickingColor, params
        );

    }

};


NGL.CylinderBuffer = function( from, to, color, color2, radius, pickingColor, pickingColor2, params ){

    var p = params || {};

    if( !NGL.extensionFragDepth || p.disableImpostor ){

        // FIXME cap support missing

        return new NGL.CylinderGeometryBuffer(
            from, to, color, color2, radius,
            pickingColor, pickingColor2, params
        );

    }else{

        return new NGL.CylinderImpostorBuffer(
            from, to, color, color2, radius,
            pickingColor, pickingColor2, params
        );

    }

};


NGL.HyperballStickBuffer = function( from, to, color, color2, radius1, radius2, pickingColor, pickingColor2, params ){

    var p = params || {};

    if( !NGL.extensionFragDepth || p.disableImpostor ){

        return new NGL.CylinderGeometryBuffer(
            from, to, color, color2,
            NGL.Utils.calculateMinArray( radius1, radius2 ),
            pickingColor, pickingColor2, params
        );

    }else{

        return new NGL.HyperballStickImpostorBuffer(
            from, to, color, color2,
            radius1, radius2,
            pickingColor, pickingColor2, params
        );

    }

};


////////////////
// Text & Font

NGL.TextAtlas = function( params ){

    // adapted from https://github.com/unconed/mathbox
    // MIT License Copyright (C) 2013+ Steven Wittens and contributors

    var p = Object.assign( {}, params );

    this.font = p.font !== undefined ? p.font : [ 'sans-serif' ];
    this.size = p.size || 36;
    this.style = p.style !== undefined ? p.style : 'normal';
    this.variant = p.variant !== undefined ? p.variant : 'normal';
    this.weight = p.weight !== undefined ? p.weight : 'normal';
    this.outline = p.outline !== undefined ? p.outline : 0;
    this.width = p.width || 1024;
    this.height = p.height || 1024;

    this.gamma = 1;
    if( typeof navigator !== 'undefined' ){
        var ua = navigator.userAgent;
        if( ua.match( /Chrome/ ) && ua.match( /OS X/ ) ){
            this.gamma = 0.5;
        }
    }

    this.mapped = {};
    this.scratchW = 0;
    this.scratchH = 0;
    this.currentX = 0;
    this.currentY = 0;

    this.build( p );

}

NGL.TextAtlas.prototype = {

    build: function( params ){

        // Prepare line-height with room for outline and descenders/ascenders
        var lineHeight = this.size + 2 * this.outline + Math.round( this.size / 4 );
        var maxWidth = this.width / 4;

        // Prepare scratch canvas
        var canvas = document.createElement( "canvas" );
        canvas.width = maxWidth;
        canvas.height = lineHeight;

        // Font string
        var quote = function(str) {
            return "\"" + ( str.replace( /(['"\\])/g, "\\$1" ) ) + "\"";
        };
        var font = this.font.map( quote ).join( ", " );

        var ctx = canvas.getContext( "2d" );
        ctx.font = this.style + " " + this.variant + " " + this.weight + " " + this.size + "px " + this.font;
        ctx.fillStyle = "#FF0000";
        ctx.textAlign = "left";
        ctx.textBaseline = "bottom";
        ctx.lineJoin = "round";

        // document.body.appendChild( canvas );
        // canvas.setAttribute( "style", "position: absolute; top: 0; left: 0; z-index: 100; border: 1px solid red; background: rgba(255,0,255,.25);" );

        var colors = [];
        var dilate = this.outline * 3;
        for( var i = 0; i < dilate; ++i ){
            // 8 rgb levels = 1 step = .5 pixel increase
            var val = Math.max( 0, -i * 8 + 128 - ( !i ) * 8 );
            var hex = ( "00" + val.toString( 16 ) ).slice( -2 );
            colors.push( "#" + hex + hex + hex );
        }
        var scratch = new Uint8Array( maxWidth * lineHeight * 2 );

        this.canvas = canvas;
        this.context = ctx;
        this.lineHeight = lineHeight;
        this.maxWidth = maxWidth;
        this.colors = colors;
        this.scratch = scratch;

        this.data = new Uint8Array( this.width * this.height * 4 );

        this.canvas2 = document.createElement( 'canvas' );
        this.canvas2.width = this.width;
        this.canvas2.height = this.height;
        this.context2 = this.canvas2.getContext( '2d' );
        // document.body.appendChild( this.canvas2 );
        // this.canvas2.setAttribute( "style", "position: absolute; bottom: 0; right: 0; z-index: 100; border: 1px solid green; background: rgba(255,0,255,.25);" );

    },

    map: function( text ){

        if( this.mapped[ text ] === undefined ){

            this.draw( text );

            // ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

            if( this.currentX + this.scratchW > this.width ){
                this.currentX = 0;
                this.currentY += this.scratchH;
            }
            if( this.currentY + this.scratchH > this.height ){
                console.warn( "canvas to small" );
            }

            this.mapped[ text ] = {
                x: this.currentX,
                y: this.currentY,
                w: this.scratchW,
                h: this.scratchH
            };

            this.context2.drawImage(
                this.canvas,
                0, 0,
                this.scratchW, this.scratchH,
                this.currentX, this.currentY,
                this.scratchW, this.scratchH
            );

            this.currentX += this.scratchW;

        }

        return this.mapped[ text ];

    },

    draw: function( text ){

        var h = this.lineHeight;
        var o = this.outline;
        var ctx = this.context;
        var dst = this.scratch;
        var max = this.maxWidth;
        var colors = this.colors;

        // Bottom aligned, take outline into account
        var x = o;
        var y = h - this.outline;

        // Measure text
        var m = ctx.measureText( text );
        var w = Math.min( max, Math.ceil( m.width + 2 * x + 1 ) );

        // Clear scratch area
        ctx.clearRect(0, 0, w, h);

        if( this.outline === 0 ){

            ctx.fillText( text, x, y );
            var imageData = ctx.getImageData( 0, 0, w, h );
            var data = imageData.data;

            var j = 3;  // Skip to alpha channel
            for( var i = 0, il = data.length / 4; i < il; ++i ){
                dst[ i ] = data[ j ];
                j += 4;
            }

        }else{

            ctx.globalCompositeOperation = "source-over";
            // Draw strokes of decreasing width to create
            // nested outlines (absolute distance)
            for( var i = o + 1; i > 0; --i ){
                // Eliminate odd strokes once past > 1px,
                // don't need the detail
                var j = i > 1 ? i * 2 - 2 : i;
                ctx.strokeStyle = colors[ j - 1 ];
                ctx.lineWidth = j;
                ctx.strokeText( text, x, y );
            }
            ctx.globalCompositeOperation = "multiply";
            ctx.fillStyle = "#FF00FF";
            ctx.fillText( text, x, y );
            var imageData = ctx.getImageData( 0, 0, w, h );
            var data = imageData.data;

            var j = 0;
            var gamma = this.gamma;
            for( var i = 0, il = data.length / 4; i < il; ++i ){
                // Get value + mask
                var a = data[ j ];
                var mask = a ? data[ j + 1 ] / a : 1;
                if( gamma === 0.5 ){
                    mask = Math.sqrt( mask );
                }
                mask = Math.min( 1, Math.max( 0, mask ) );

                // Blend between positive/outside and negative/inside
                var b = 256 - a;
                var c = b + ( a - b ) * mask;

                // Clamp (slight expansion to hide errors around the transition)
                dst[ i ] = Math.max( 0, Math.min( 255, c + 2 ) );
                data[ j + 3 ] = dst[ i ];
                j += 4;
            }

        }

        ctx.putImageData( imageData, 0, 0 );
        this.scratchW = w;
        this.scratchH = h;

    },

    dispose: function(){

        // document.body.removeChild( this.canvas );
        // document.body.removeChild( this.canvas2 );

    }

};


NGL.TextBuffer = function( position, size, color, text, params ){

    var p = params || {};
    p.forceTransparent = true;

    this.fontFamily = p.fontFamily !== undefined ? p.fontFamily : "sans-serif";
    this.fontStyle = p.fontStyle !== undefined ? p.fontStyle : "normal";
    this.fontWeight = p.fontWeight !== undefined ? p.fontWeight : "bold";
    this.fontSize = p.fontSize !== undefined ? p.fontSize : 48;
    this.sdf = p.sdf !== undefined ? p.sdf : true;
    this.xOffset = p.xOffset !== undefined ? p.xOffset : 0.0;
    this.yOffset = p.yOffset !== undefined ? p.yOffset : 0.0;
    this.zOffset = p.zOffset !== undefined ? p.zOffset : 0.5;

    var n = position.length / 3;

    var charCount = 0;
    for( var i = 0; i < n; ++i ){
        charCount += text[ i ].length;
    }

    this.text = text;
    this.count = charCount;
    this.positionCount = n;

    this.vertexShader = "SDFFont.vert";
    this.fragmentShader = "SDFFont.frag";

    NGL.QuadBuffer.call( this, p );

    this.addUniforms( {
        "fontTexture"  : { value: null },
        "xOffset": { value: this.xOffset },
        "yOffset": { value: this.yOffset },
        "zOffset": { value: this.zOffset }
    } );

    this.addAttributes( {
        "inputTexCoord": { type: "v2", value: null },
        "inputSize": { type: "f", value: null },
    } );

    this.setAttributes( {
        "position": position,
        "size": size,
        "color": color
    } );

    this.makeTexture();
    this.makeMapping();

};

NGL.TextBuffer.prototype = Object.create( NGL.QuadBuffer.prototype );

NGL.TextBuffer.prototype.constructor = NGL.TextBuffer;

NGL.TextBuffer.prototype.parameters = Object.assign( {

    fontFamily: { uniform: true },
    fontStyle: { uniform: true },
    fontWeight: { uniform: true },
    fontSize: { uniform: true },
    sdf: { updateShader: true, uniform: true },
    xOffset: { uniform: true },
    yOffset: { uniform: true },
    zOffset: { uniform: true }

}, NGL.Buffer.prototype.parameters );

NGL.TextBuffer.prototype.makeMaterial = function(){

    NGL.Buffer.prototype.makeMaterial.call( this );

    this.material.extensions.derivatives = true;
    this.material.lights = false;
    this.material.uniforms.fontTexture.value = this.tex;
    this.material.needsUpdate = true;

};

NGL.TextBuffer.prototype.setAttributes = function( data ){

    var position, size, color;
    var aPosition, inputSize, aColor;

    var text = this.text;
    var attributes = this.geometry.attributes;

    if( data[ "position" ] ){
        position = data[ "position" ];
        aPosition = attributes[ "position" ].array;
        attributes[ "position" ].needsUpdate = true;
    }

    if( data[ "size" ] ){
        size = data[ "size" ];
        inputSize = attributes[ "inputSize" ].array;
        attributes[ "inputSize" ].needsUpdate = true;
    }

    if( data[ "color" ] ){
        color = data[ "color" ];
        aColor = attributes[ "color" ].array;
        attributes[ "color" ].needsUpdate = true;
    }

    var n = this.positionCount;

    var i, j, o;
    var iCharAll = 0;
    var txt, iChar, nChar;

    for( var v = 0; v < n; v++ ) {

        o = 3 * v;
        txt = text[ v ];
        nChar = txt.length;

        for( iChar = 0; iChar < nChar; iChar++, iCharAll++ ) {

            i = iCharAll * 2 * 4;

            for( var m = 0; m < 4; m++ ) {

                j = iCharAll * 4 * 3 + ( 3 * m );

                if( position ){

                    aPosition[ j     ] = position[ o     ];
                    aPosition[ j + 1 ] = position[ o + 1 ];
                    aPosition[ j + 2 ] = position[ o + 2 ];

                }

                if( size ){

                    inputSize[ ( iCharAll * 4 ) + m ] = size[ v ];

                }

                if( color ){

                    aColor[ j     ] = color[ o     ];
                    aColor[ j + 1 ] = color[ o + 1 ];
                    aColor[ j + 2 ] = color[ o + 2 ];

                }

            }

        }

    }

};

NGL.TextBuffer.prototype.makeTexture = function(){

    if( this.tex ) this.tex.dispose();
    if( this.ta ) this.ta.dispose();

    var ta = new NGL.TextAtlas( {
        font: [ this.fontFamily ],
        style: this.fontStyle,
        weight: this.fontWeight,
        size: this.fontSize,
        outline: this.sdf ? 5 : 0
    } );

    for( var i = 0; i < 256; ++i ){
        ta.map( String.fromCharCode( i ) );
    }

    this.ta = ta;

    this.tex = new THREE.CanvasTexture( ta.canvas2 );
    this.tex.flipY = false;
    this.tex.needsUpdate = true;

};

NGL.TextBuffer.prototype.makeMapping = function(){

    var ta = this.ta;
    var text = this.text;

    var inputTexCoord = this.geometry.attributes[ "inputTexCoord" ].array;
    var inputMapping = this.geometry.attributes[ "mapping" ].array;

    var n = this.positionCount;

    var c;
    var i, j, o;
    var iCharAll = 0;
    var txt, xadvance, iChar, nChar;

    for( var v = 0; v < n; v++ ) {

        o = 3 * v;
        txt = text[ v ];
        xadvance = 0;
        nChar = txt.length;

        for( iChar = 0; iChar < nChar; iChar++, iCharAll++ ) {

            c = ta.mapped[ txt[ iChar ] ];
            i = iCharAll * 2 * 4;

            // top left
            inputMapping[ i + 0 ] = xadvance - ta.outline;
            inputMapping[ i + 1 ] = c.h - ta.outline;
            // bottom left
            inputMapping[ i + 2 ] = xadvance - ta.outline;
            inputMapping[ i + 3 ] = 0 - ta.outline;
            // top right
            inputMapping[ i + 4 ] = xadvance + c.w - ta.outline;
            inputMapping[ i + 5 ] = c.h - ta.outline;
            // bottom right
            inputMapping[ i + 6 ] = xadvance + c.w - ta.outline;
            inputMapping[ i + 7 ] = 0 - ta.outline;

            var texWidth = ta.width;
            var texHeight = ta.height;

            var texCoords = [
                c.x/texWidth, c.y/texHeight,             // top left
                c.x/texWidth, (c.y+c.h)/texHeight,       // bottom left
                (c.x+c.w)/texWidth, c.y/texHeight,       // top right
                (c.x+c.w)/texWidth, (c.y+c.h)/texHeight  // bottom right
            ];
            inputTexCoord.set( texCoords, i );

            xadvance += c.w - 2 * ta.outline;

        }

    }

    this.geometry.attributes[ "inputTexCoord" ].needsUpdate = true;
    this.geometry.attributes[ "mapping" ].needsUpdate = true;

};

NGL.TextBuffer.prototype.getDefines = function( type ){

    var defines = NGL.Buffer.prototype.getDefines.call( this, type );

    if( this.sdf ){
        defines[ "SDF" ] = 1;
    }

    return defines;

};

NGL.TextBuffer.prototype.setUniforms = function( data ){

    if( data && (
            data[ "fontFamily" ] !== undefined ||
            data[ "fontStyle" ] !== undefined ||
            data[ "fontWeight" ] !== undefined ||
            data[ "fontSize" ] !== undefined ||
            data[ "sdf" ] !== undefined
        )
    ){

        this.makeTexture();
        this.makeMapping();
        data[ "fontTexture" ] = this.tex;

    }

    NGL.Buffer.prototype.setUniforms.call( this, data );

};

NGL.TextBuffer.prototype.dispose = function(){

    NGL.Buffer.prototype.dispose.call( this );

    if( this.tex ) this.tex.dispose();
    if( this.ta ) this.ta.dispose();

};


///////////
// Helper

NGL.VectorBuffer = function( position, vector, params ){

    var p = params || {};

    this.size = position.length / 3;
    this.vertexShader = 'Line.vert';
    this.fragmentShader = 'Line.frag';
    this.line = true;

    var n = this.size;
    var n2 = n * 2;

    this.attributeSize = n2;

    this.scale = p.scale || 1;
    var color = new THREE.Color( p.color || "grey" );

    this.linePosition = new Float32Array( n2 * 3 );
    this.lineColor = NGL.Utils.uniformArray3( n2, color.r, color.g, color.b );

    NGL.Buffer.call(
        this, this.linePosition, this.lineColor, undefined, undefined, p
    );

    this.setAttributes( {
        position: position,
        vector: vector
    } );

};

NGL.VectorBuffer.prototype = Object.create( NGL.Buffer.prototype );

NGL.VectorBuffer.prototype.constructor = NGL.VectorBuffer;

NGL.VectorBuffer.prototype.setAttributes = function( data ){

    var attributes = this.geometry.attributes;

    var position, vector;
    var aPosition;

    if( data[ "position" ] && data[ "vector" ] ){
        position = data[ "position" ];
        vector = data[ "vector" ];
        aPosition = attributes[ "position" ].array;
        attributes[ "position" ].needsUpdate = true;
    }

    var n = this.size;
    var scale = this.scale;

    var i, j;

    if( data[ "position" ] && data[ "vector" ] ){

        for( var v = 0; v < n; v++ ){

            i = v * 2 * 3;
            j = v * 3;

            aPosition[ i + 0 ] = position[ j + 0 ];
            aPosition[ i + 1 ] = position[ j + 1 ];
            aPosition[ i + 2 ] = position[ j + 2 ];
            aPosition[ i + 3 ] = position[ j + 0 ] + vector[ j + 0 ] * scale;
            aPosition[ i + 4 ] = position[ j + 1 ] + vector[ j + 1 ] * scale;
            aPosition[ i + 5 ] = position[ j + 2 ] + vector[ j + 2 ] * scale;

        }

    }

};

// File:js/ngl/representation.js

/**
 * @file Representation
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


NGL.SideTypes = {};
NGL.SideTypes[ THREE.FrontSide ] = "front";
NGL.SideTypes[ THREE.BackSide ] = "back";
NGL.SideTypes[ THREE.DoubleSide ] = "double";


NGL.makeRepresentation = function( type, object, viewer, params ){

    if( NGL.debug ) NGL.time( "NGL.makeRepresentation " + type );

    var ReprClass;

    if( type === "buffer" ){

        ReprClass = NGL.BufferRepresentation;

    }else if( object instanceof NGL.Structure ){

        ReprClass = NGL.representationTypes[ type ];

        if( !ReprClass ){

            NGL.error(
                "NGL.makeRepresentation: representation type " + type + " unknown"
            );
            return;

        }

    }else if( object instanceof NGL.Surface || object instanceof NGL.Volume ){

        if( type === "surface" ){

            ReprClass = NGL.SurfaceRepresentation;

        }else if( type === "dot" ){

            ReprClass = NGL.DotRepresentation;

        }else{

            NGL.error(
                "NGL.makeRepresentation: representation type " + type + " unknown"
            );
            return;

        }

    }else if( object instanceof NGL.Trajectory ){

        ReprClass = NGL.TrajectoryRepresentation;

    }else{

        NGL.error(
            "NGL.makeRepresentation: object " + object + " unknown"
        );
        return;

    }

    var repr = new ReprClass( object, viewer, params );

    if( NGL.debug ) NGL.timeEnd( "NGL.makeRepresentation " + type );

    return repr;

};


///////////////////
// Representation

NGL.Representation = function( object, viewer, params ){

    this.viewer = viewer;

    this.queue = new NGL.Queue( this.make.bind( this ) );
    this.tasks = new NGL.Counter();
    this.bufferList = [];

    this.init( params );

};

NGL.Representation.prototype = {

    constructor: NGL.Representation,

    type: "",

    parameters: {

        clipNear: {
            type: "range", step: 1, max: 100, min: 0, buffer: true
        },
        flatShaded: {
            type: "boolean", buffer: true
        },
        opacity: {
            type: "range", step: 0.01, max: 1, min: 0, buffer: true
        },
        side: {
            type: "select", options: NGL.SideTypes, buffer: true,
            int: true
        },
        wireframe: {
            type: "boolean", buffer: true
        },
        linewidth: {
            type: "integer", max: 50, min: 1, buffer: true
        },

        colorScheme: {
            type: "select", update: "color",
            options: NGL.ColorMakerRegistry.getTypes()
        },
        colorScale: {
            type: "select", update: "color",
            options: NGL.ColorMakerRegistry.getScales()
        },
        colorValue: {
            type: "color", update: "color"
        },
        colorDomain: {
            type: "hidden", update: "color"
        },
        colorMode: {
            type: "select", update: "color",
            options: NGL.ColorMakerRegistry.getModes()
        },

        roughness: {
            type: "range", step: 0.01, max: 1, min: 0, buffer: true
        },
        metalness: {
            type: "range", step: 0.01, max: 1, min: 0, buffer: true
        },
        diffuse: {
            type: "color", buffer: true
        },

    },

    init: function( params ){

        var p = params || {};

        this.clipNear = p.clipNear !== undefined ? p.clipNear : 0;
        this.flatShaded = p.flatShaded || false;
        this.side = p.side !== undefined ? p.side : THREE.DoubleSide;
        this.opacity = p.opacity !== undefined ? p.opacity : 1.0;
        this.wireframe = p.wireframe || false;
        this.linewidth = p.linewidth || 2;

        this.setColor( p.color, p );

        this.colorScheme = p.colorScheme || "uniform";
        this.colorScale = p.colorScale || "";
        this.colorValue = p.colorValue || 0x909090;
        this.colorDomain = p.colorDomain || "";
        this.colorMode = p.colorMode || "hcl";

        this.visible = p.visible !== undefined ? p.visible : true;
        this.quality = p.quality;

        this.roughness = p.roughness !== undefined ? p.roughness : 0.4;
        this.metalness = p.metalness !== undefined ? p.metalness : 0.0;
        this.diffuse = p.diffuse !== undefined ? p.diffuse : 0xffffff;

    },

    getColorParams: function(){

        return {

            scheme: this.colorScheme,
            scale: this.colorScale,
            value: this.colorValue,
            domain: this.colorDomain,
            mode: this.colorMode,

        };

    },

    getBufferParams: function( p ){

        return Object.assign( {

            clipNear: this.clipNear,
            flatShaded: this.flatShaded,
            opacity: this.opacity,
            side: this.side,
            wireframe: this.wireframe,
            linewidth: this.linewidth,

            roughness: this.roughness,
            metalness: this.metalness,
            diffuse: this.diffuse,

        }, p );

    },

    setColor: function( value, p ){

        var types = Object.keys( NGL.ColorMakerRegistry.getTypes() );

        if( types.indexOf( value ) !== -1 ){

            if( p ){
                p.colorScheme = value;
            }else{
                this.setParameters( { colorScheme: value } );
            }

        }else if( value !== undefined ){

            value = new THREE.Color( value ).getHex();
            if( p ){
                p.colorScheme = "uniform";
                p.colorValue = value;
            }else{
                this.setParameters( {
                    colorScheme: "uniform", colorValue: value
                } );
            }

        }

        return this;

    },

    prepare: false,

    create: function(){

        // this.bufferList.length = 0;

    },

    update: function(){

        this.build();

    },

    build: function( params ){

        if( !this.prepare ){
            if( !params ){
                params = this.getParameters();
                delete params.quality;
            }
            this.tasks.increment();
            this.make( params, function(){} );
            return;
        }

        // don't let tasks accumulate
        if( this.queue.length() > 0 ){

            this.tasks.change( 1 - this.queue.length() );
            this.queue.kill();

        }else{

            this.tasks.increment();

        }

        if( !params ){
            params = this.getParameters();
            delete params.quality;
        }

        this.queue.push( params );

    },

    make: function( params, callback ){

        if( NGL.debug ) NGL.time( "NGL.Representation.make " + this.type );

        if( params && !params.__update ){
            this.init( params );
        }

        var _make = function(){

            if( params.__update ){
                this.update( params.__update );
                this.viewer.requestRender();
                this.tasks.decrement();
                callback();
            }else{
                this.clear();
                this.create();
                if( !this.manualAttach && !this.disposed ){
                    if( NGL.debug ) NGL.time( "NGL.Representation.attach " + this.type );
                    this.attach( function(){
                        if( NGL.debug ) NGL.timeEnd( "NGL.Representation.attach " + this.type );
                        this.tasks.decrement();
                        callback();
                    }.bind( this ) );
                }
            }

            if( NGL.debug ) NGL.timeEnd( "NGL.Representation.make " + this.type );

        }.bind( this );

        if( this.prepare ){
            this.prepare( _make );
        }else{
            _make();
        }

    },

    attach: function( callback ){

        this.setVisibility( this.visible );

        callback();

    },

    setVisibility: function( value, noRenderRequest ){

        this.visible = value;

        this.bufferList.forEach( function( buffer ){

            buffer.setVisibility( value );

        } );

        if( !noRenderRequest ) this.viewer.requestRender();

        return this;

    },

    setParameters: function( params, what, rebuild ){

        var p = params || {};
        var tp = this.parameters;

        what = what || {};
        rebuild = rebuild || false;

        var bufferParams = {};

        for( var name in p ){

            if( p[ name ] === undefined ) continue;
            if( tp[ name ] === undefined ) continue;

            if( tp[ name ].int ) p[ name ] = parseInt( p[ name ] );
            if( tp[ name ].float ) p[ name ] = parseFloat( p[ name ] );

            // no value change
            if( p[ name ] === this[ name ] ) continue;

            this[ name ] = p[ name ];

            // buffer param
            if( tp[ name ].buffer ){
                if( tp[ name ].buffer === true ){
                    bufferParams[ name ] = p[ name ];
                }else{
                    bufferParams[ tp[ name ].buffer ] = p[ name ];
                }
            }

            // mark for update
            if( tp[ name ].update ){
                what[ tp[ name ].update ] = true;
            }

            // mark for rebuild
            if( tp[ name ].rebuild &&
                !( tp[ name ].rebuild === "impostor" &&
                    NGL.extensionFragDepth && !this.disableImpostor )
            ){
                rebuild = true;
            }

        }

        //

        if( rebuild ){

            this.build();

        }else{

            this.bufferList.forEach( function( buffer ){
                buffer.setParameters( bufferParams );
            } );

            if( Object.keys( what ).length ){
                this.update( what );  // update buffer attribute
            }

            this.viewer.requestRender();

        }

        return this;

    },

    getParameters: function(){

        var params = {
            visible: this.visible,
            quality: this.quality
        };

        Object.keys( this.parameters ).forEach( function( name ){
            params[ name ] = this[ name ];
        }, this );

        return params;

    },

    clear: function(){

        this.bufferList.forEach( function( buffer ){

            this.viewer.remove( buffer );
            buffer.dispose();

        }, this );

        this.bufferList.length = 0;

        this.viewer.requestRender();

    },

    dispose: function(){

        this.disposed = true;
        this.queue.kill();
        this.tasks.dispose();
        this.clear();

    }

};


NGL.BufferRepresentation = function( buffer, viewer, params ){

    NGL.Representation.call( this, buffer, viewer, params );

    this.buffer = buffer;

    this.build();

};

NGL.BufferRepresentation.prototype = NGL.createObject(

    NGL.Representation.prototype, {

    constructor: NGL.BufferRepresentation,

    type: "buffer",

    parameters: Object.assign( {

    }, NGL.Representation.prototype.parameters, {

        colorScheme: null,
        colorScale: null,
        colorValue: null,
        colorDomain: null,
        colorMode: null

    } ),

    create: function(){

        this.bufferList.push( this.buffer );

    },

    attach: function( callback ){

        this.bufferList.forEach( function( buffer ){

            this.viewer.add( buffer );

        }, this );

        this.setVisibility( this.visible );

        callback();

    }

} );


/////////////////////////////
// Structure representation

NGL.StructureRepresentation = function( structure, viewer, params ){

    var p = params || {};

    this.dataList = [];

    this.structure = structure;
    this.selection = new NGL.Selection( p.sele );
    this.structureView = this.structure.getView( this.selection );

    NGL.Representation.call( this, structure, viewer, p );

    if( structure.biomolDict ){
        var biomolOptions = {
            "default": "default",
            "": ( structure.unitcell ? "AU" : "FULL" )
        };
        Object.keys( structure.biomolDict ).forEach( function( k ){
            biomolOptions[ k ] = k;
        } );
        this.parameters.assembly = {
            type: "select",
            options: biomolOptions,
            rebuild: true
        };
    }else{
        this.parameters.assembly = null;
    }

    // must come after structureView to ensure selection change signals
    // have already updated the structureView
    this.selection.signals.stringChanged.add( function(){
        this.build();
    }.bind( this ) );

    this.build();

};

NGL.StructureRepresentation.prototype = NGL.createObject(

    NGL.Representation.prototype, {

    constructor: NGL.StructureRepresentation,

    type: "structure",

    parameters: Object.assign( {

        radiusType: {
            type: "select", options: NGL.RadiusFactory.types
        },
        radius: {
            type: "number", precision: 3, max: 10.0, min: 0.001
        },
        scale: {
            type: "number", precision: 3, max: 10.0, min: 0.001
        },
        assembly: null

    }, NGL.Representation.prototype.parameters ),

    defaultScale: {
        "vdw": 1.0,
        "covalent": 1.0,
        "bfactor": 0.01,
        "sstruc": 1.0
    },

    defaultSize: 1.0,

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "element";

        this.radius = p.radius || "vdw";
        this.scale = p.scale || 1.0;
        this.assembly = p.assembly === undefined ? "default" : p.assembly;
        this.defaultAssembly = p.defaultAssembly || "";

        NGL.Representation.prototype.init.call( this, p );

    },

    create: function(){

        if( this.structureView.atomCount === 0 ) return;

        var name = this.assembly === "default" ? this.defaultAssembly : this.assembly;
        var assembly = this.structure.biomolDict[ name ];

        if( assembly ){
            assembly.partList.forEach( function( part, i ){
                var sview = part.getView( this.structureView );
                if( sview.atomCount === 0 ) return;
                var data = this.createData( sview, i );
                if( data ){
                    data.sview = sview;
                    data.instanceList = part.getInstanceList();
                    this.dataList.push( data );
                }
            }, this );
        }else{
            var data = this.createData( this.structureView, 0 );
            if( data ){
                data.sview = this.structureView;
                this.dataList.push( data );
            }
        }

    },

    createData: function( sview ){

        console.error( "createData not implemented" );

    },

    update: function( what ){

        this.dataList.forEach( function( data ){
            if( data.bufferList.length > 0 ){
                this.updateData( what, data );
            }
        }, this );

    },

    updateData: function( what, data ){

        console.error( "updateData not implemented" );

    },

    getColorParams: function(){

        var p = NGL.Representation.prototype.getColorParams.call( this );
        p.structure = this.structure;

        return p;

    },

    getAtomParams: function( what, params ){

        return Object.assign( {
            what: what,
            colorParams: this.getColorParams(),
            radiusParams: { "radius": this.radius, "scale": this.scale }
        }, params );

    },

    getBondParams: function( what, params ){

        return Object.assign( {
            what: what,
            colorParams: this.getColorParams(),
            radiusParams: { "radius": this.radius, "scale": this.scale }
        }, params );

    },

    setSelection: function( string, silent ){

        this.selection.setString( string, silent );

        return this;

    },

    setParameters: function( params, what, rebuild ){

        what = what || {};

        if( params && params[ "radiusType" ] !== undefined ){
            if( params[ "radiusType" ] === "size" ){
                this.radius = this.defaultSize;
            }else{
                this.radius = params[ "radiusType" ];
            }
            what[ "radius" ] = true;
            if( !NGL.extensionFragDepth || this.disableImpostor ){
                rebuild = true;
            }
        }

        if( params && params[ "radius" ] !== undefined ){
            what[ "radius" ] = true;
            if( !NGL.extensionFragDepth || this.disableImpostor ){
                rebuild = true;
            }
        }

        if( params && params[ "scale" ] !== undefined ){
            what[ "radius" ] = true;
            if( !NGL.extensionFragDepth || this.disableImpostor ){
                rebuild = true;
            }
        }

        NGL.Representation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        return this;

    },

    getParameters: function(){

        var params = Object.assign(
            NGL.Representation.prototype.getParameters.call( this ),
            {
                sele: this.selection ? this.selection.string : undefined,
                defaultAssembly: this.defaultAssembly
            }
        );

        return params;

    },

    attach: function( callback ){

        var viewer = this.viewer;
        var bufferList = this.bufferList;

        this.dataList.forEach( function( data ){
            data.bufferList.forEach( function( buffer ){
                bufferList.push( buffer );
                viewer.add( buffer, data.instanceList );
            } )
        } );

        this.setVisibility( this.visible );
        callback();

    },

    clear: function(){

        this.dataList.length = 0;

        NGL.Representation.prototype.clear.call( this );

    },

    dispose: function(){

        this.structureView.dispose();

        delete this.structure;
        delete this.structureView;

        NGL.Representation.prototype.dispose.call( this );

    }

} );


NGL.SpacefillRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.SpacefillRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.SpacefillRepresentation,

    type: "spacefill",

    parameters: Object.assign( {

        sphereDetail: {
            type: "integer", max: 3, min: 0, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};

        if( p.quality === "low" ){
            this.sphereDetail = 0;
        }else if( p.quality === "medium" ){
            this.sphereDetail = 1;
        }else if( p.quality === "high" ){
            this.sphereDetail = 2;
        }else{
            this.sphereDetail = p.sphereDetail !== undefined ? p.sphereDetail : 1;
        }
        this.disableImpostor = p.disableImpostor || false;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    createData: function( sview ){

        var atomData = sview.getAtomData( this.getAtomParams() );

        var sphereBuffer = new NGL.SphereBuffer(
            atomData.position,
            atomData.color,
            atomData.radius,
            atomData.pickingColor,
            this.getBufferParams( {
                sphereDetail: this.sphereDetail,
                dullInterior: true,
                disableImpostor: this.disableImpostor
            } )
        );

        return {
            bufferList: [ sphereBuffer ]
        };

    },

    updateData: function( what, data ){

        var atomData = data.sview.getAtomData( this.getAtomParams( what ) );
        var sphereData = {};

        if( !what || what[ "position" ] ){
            sphereData[ "position" ] = atomData.position;
        }

        if( !what || what[ "color" ] ){
            sphereData[ "color" ] = atomData.color;
        }

        if( !what || what[ "radius" ] ){
            sphereData[ "radius" ] = atomData.radius;
        }

        data.bufferList[ 0 ].setAttributes( sphereData );

    }

} );


NGL.PointRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.PointRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.PointRepresentation,

    type: "point",

    parameters: Object.assign( {

        pointSize: {
            type: "number", precision: 1, max: 100, min: 0, buffer: true
        },
        sizeAttenuation: {
            type: "boolean", buffer: true
        },
        sortParticles: {
            type: "boolean", rebuild: true
        },
        useTexture: {
            type: "boolean", buffer: true
        },
        alphaTest: {
            type: "range", step: 0.001, max: 1, min: 0, buffer: true
        },
        forceTransparent: {
            type: "boolean", buffer: true
        },
        edgeBleach: {
            type: "range", step: 0.001, max: 1, min: 0, buffer: true
        },

    }, NGL.Representation.prototype.parameters, {

        flatShaded: null,
        wireframe: null,
        linewidth: null,

        roughness: null,
        metalness: null

    } ),

    init: function( params ){

        var p = params || {};

        this.pointSize = p.pointSize || 1;
        this.sizeAttenuation = p.sizeAttenuation !== undefined ? p.sizeAttenuation : true;
        this.sortParticles = p.sortParticles !== undefined ? p.sortParticles : false;
        this.useTexture = p.useTexture !== undefined ? p.useTexture : false;
        this.alphaTest = p.alphaTest !== undefined ? p.alphaTest : 0.5;
        this.forceTransparent = p.forceTransparent !== undefined ? p.forceTransparent : false;
        this.edgeBleach = p.edgeBleach !== undefined ? p.edgeBleach : 0.0;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    createData: function( sview ){

        var what = { position: true, color: true };
        var atomData = sview.getAtomData( this.getAtomParams( what ) );

        var pointBuffer = new NGL.PointBuffer(
            atomData.position,
            atomData.color,
            this.getBufferParams( {
                pointSize: this.pointSize,
                sizeAttenuation: this.sizeAttenuation,
                sortParticles: this.sortParticles,
                useTexture: this.useTexture,
                alphaTest: this.alphaTest,
                forceTransparent: this.forceTransparent,
                edgeBleach: this.edgeBleach
            } )
        );

        return {
            bufferList: [ pointBuffer ]
        };

    },

    updateData: function( what, data ){

        var atomData = data.sview.getAtomData( this.getAtomParams( what ) );
        var pointData = {};

        if( !what || what[ "position" ] ){
            pointData[ "position" ] = atomData.position;
        }

        if( !what || what[ "color" ] ){
            pointData[ "color" ] = atomData.color;
        }

        data.bufferList[ 0 ].setAttributes( pointData );

    }

} );


NGL.LabelRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.LabelRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.LabelRepresentation,

    type: "label",

    parameters: Object.assign( {

        labelType: {
            type: "select", options: NGL.LabelFactory.types, rebuild: true
        },
        fontFamily: {
            type: "select", options: {
                "sans-serif": "sans-serif",
                "monospace": "monospace",
                "serif": "serif"
            },
            buffer: true
        },
        fontStyle: {
            type: "select", options: {
                "normal": "normal",
                "italic": "italic"
            },
            buffer: true
        },
        fontWeight: {
            type: "select", options: {
                "normal": "normal",
                "bold": "bold"
            },
            buffer: true
        },
        sdf: {
            type: "boolean", buffer: true
        },
        xOffset: {
            type: "number", precision: 1, max: 20, min: -20, buffer: true
        },
        yOffset: {
            type: "number", precision: 1, max: 20, min: -20, buffer: true
        },
        zOffset: {
            type: "number", precision: 1, max: 20, min: -20, buffer: true
        },

    }, NGL.StructureRepresentation.prototype.parameters, {

        side: null,
        flatShaded: null,
        wireframe: null,
        linewidth: null,

        roughness: null,
        metalness: null,
        diffuse: null,

    } ),

    init: function( params ){

        var p = params || {};

        this.labelType = p.labelType || "res";
        this.labelText = p.labelText || {};
        this.fontFamily = p.fontFamily || "sans-serif";
        this.fontStyle = p.fontStyle || "normal";
        this.fontWeight = p.fontWeight || "bold";
        this.sdf = p.sdf !== undefined ? p.sdf : NGL.browser !== "Firefox";  // FIXME
        this.xOffset = p.xOffset !== undefined ? p.xOffset : 0.0;
        this.yOffset = p.yOffset !== undefined ? p.yOffset : 0.0;
        this.zOffset = p.zOffset !== undefined ? p.zOffset : 0.5;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    createData: function( sview ){

        var what = { position: true, color: true, radius: true };
        var atomData = sview.getAtomData( this.getAtomParams( what ) );

        var text = [];
        var labelFactory = new NGL.LabelFactory(
            this.labelType, this.labelText
        );
        sview.eachSelectedAtom( function( ap ){
            text.push( labelFactory.atomLabel( ap ) );
        } );

        var textBuffer = new NGL.TextBuffer(
            atomData.position,
            atomData.radius,
            atomData.color,
            text,
            this.getBufferParams( {
                fontFamily: this.fontFamily,
                fontStyle: this.fontStyle,
                fontWeight: this.fontWeight,
                sdf: this.sdf,
                xOffset: this.xOffset,
                yOffset: this.yOffset,
                zOffset: this.zOffset
            } )
        );

        return {
            bufferList: [ textBuffer ]
        };

    },

    updateData: function( what, data ){

        var atomData = data.sview.getAtomData( this.getAtomParams( what ) );
        var textData = {};

        if( !what || what[ "position" ] ){
            textData[ "position" ] = atomData.position;
        }

        if( !what || what[ "radius" ] ){
            textData[ "size" ] = atomData.radius;
        }

        if( !what || what[ "color" ] ){
            textData[ "color" ] = atomData.color;
        }

        data.bufferList[ 0 ].setAttributes( textData );

    }

} );


NGL.BallAndStickRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.BallAndStickRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.BallAndStickRepresentation,

    type: "ball+stick",

    defaultSize: 0.15,

    parameters: Object.assign( {

        sphereDetail: {
            type: "integer", max: 3, min: 0, rebuild: "impostor"
        },
        radiusSegments: {
            type: "integer", max: 25, min: 5, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        },
        aspectRatio: {
            type: "number", precision: 1, max: 10.0, min: 1.0
        },
        lineOnly: {
            type: "boolean", rebuild: true
        },
        cylinderOnly: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.radius = p.radius || this.defaultSize;

        if( p.quality === "low" ){
            this.sphereDetail = 0;
            this.radiusSegments = 5;
        }else if( p.quality === "medium" ){
            this.sphereDetail = 1;
            this.radiusSegments = 10;
        }else if( p.quality === "high" ){
            this.sphereDetail = 2;
            this.radiusSegments = 20;
        }else{
            this.sphereDetail = p.sphereDetail !== undefined ? p.sphereDetail : 1;
            this.radiusSegments = p.radiusSegments !== undefined ? p.radiusSegments : 10;
        }
        this.disableImpostor = p.disableImpostor || false;

        this.aspectRatio = p.aspectRatio || 2.0;
        this.lineOnly = p.lineOnly || false;
        this.cylinderOnly = p.cylinderOnly || false;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    getAtomParams: function( what, params ){

        params = Object.assign( {
            radiusParams: { "radius": this.radius, "scale": this.scale * this.aspectRatio }
        }, params );

        return NGL.StructureRepresentation.prototype.getAtomParams.call( this, what, params );

    },

    getAtomData: function( sview, what, params ){

        return sview.getAtomData( this.getAtomParams( what, params ) );

    },

    getBondData: function( sview, what, params ){

        return sview.getBondData( this.getBondParams( what, params ) );

    },

    createData: function( sview ){

        var bufferList = [];

        if( this.lineOnly ){

            var bondData = this.getBondData( sview );

            this.lineBuffer = new NGL.LineBuffer(
                bondData.position1,
                bondData.position2,
                bondData.color1,
                bondData.color2,
                this.getBufferParams()
            );

            bufferList.push( this.lineBuffer );

        }else{

            if( !this.cylinderOnly ){

                var atomData = this.getAtomData( sview );

                var sphereBuffer = new NGL.SphereBuffer(
                    atomData.position,
                    atomData.color,
                    atomData.radius,
                    atomData.pickingColor,
                    this.getBufferParams( {
                        sphereDetail: this.sphereDetail,
                        disableImpostor: this.disableImpostor,
                        dullInterior: true
                    } )
                );

                bufferList.push( sphereBuffer );

            }

            var bondData = this.getBondData( sview );

            var cylinderBuffer = new NGL.CylinderBuffer(
                bondData.position1,
                bondData.position2,
                bondData.color1,
                bondData.color2,
                bondData.radius,
                bondData.pickingColor1,
                bondData.pickingColor2,
                this.getBufferParams( {
                    shift: 0,
                    cap: true,
                    radiusSegments: this.radiusSegments,
                    disableImpostor: this.disableImpostor,
                    dullInterior: true
                } )
            );

            bufferList.push( cylinderBuffer );

        }

        return {
            bufferList: bufferList
        };

    },

    updateData: function( what, data ){

        if( this.lineOnly ){

            var bondData = this.getBondData( data.sview, what );
            var lineData = {};

            if( !what || what[ "position" ] ){
                lineData[ "from" ] = bondData.position1;
                lineData[ "to" ] = bondData.position2;
            }

            if( !what || what[ "color" ] ){
                lineData[ "color" ] = bondData.color1;
                lineData[ "color2" ] = bondData.color2;
            }

            data.bufferList[ 0 ].setAttributes( lineData );

        }else{

            var atomData = this.getAtomData( data.sview, what );
            var bondData = this.getBondData( data.sview, what );
            var sphereData = {};
            var cylinderData = {};

            if( !what || what[ "position" ] ){
                sphereData[ "position" ] = atomData.position;
                cylinderData[ "position" ] = NGL.Utils.calculateCenterArray(
                    bondData.position1, bondData.position2
                );
                cylinderData[ "position1" ] = bondData.position1;
                cylinderData[ "position2" ] = bondData.position2;
            }

            if( !what || what[ "color" ] ){
                sphereData[ "color" ] = atomData.color;
                cylinderData[ "color" ] = bondData.color1;
                cylinderData[ "color2" ] = bondData.color2;
            }

            if( !what || what[ "radius" ] ){
                sphereData[ "radius" ] = atomData.radius;
                cylinderData[ "radius" ] = bondData.radius;
            }

            data.bufferList[ 0 ].setAttributes( sphereData );
            data.bufferList[ 1 ].setAttributes( cylinderData );

        }

    },

    setParameters: function( params ){

        var rebuild = false;
        var what = {};

        if( params && params[ "aspectRatio" ] ){

            what[ "radius" ] = true;
            if( !NGL.extensionFragDepth || this.disableImpostor ){
                rebuild = true;
            }

        }

        NGL.StructureRepresentation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        return this;

    }

} );


NGL.LicoriceRepresentation = function( structure, viewer, params ){

    NGL.BallAndStickRepresentation.call( this, structure, viewer, params );

};

NGL.LicoriceRepresentation.prototype = NGL.createObject(

    NGL.BallAndStickRepresentation.prototype, {

    constructor: NGL.LicoriceRepresentation,

    type: "licorice",

    parameters: Object.assign(
        {}, NGL.BallAndStickRepresentation.prototype.parameters, { aspectRatio: null }
    ),

    init: function( params ){

        var p = params || {};
        p.aspectRatio = 1.0;

        NGL.BallAndStickRepresentation.prototype.init.call( this, p );

    }

} );


NGL.LineRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.LineRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.LineRepresentation,

    type: "line",

    parameters: Object.assign( {

    }, NGL.Representation.prototype.parameters, {

        flatShaded: null,
        side: null,
        wireframe: null,

        roughness: null,
        metalness: null,
        diffuse: null,

    } ),

    init: function( params ){

        var p = params || {};

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    createData: function( sview ){

        var what = { position: true, color: true };
        var bondData = sview.getBondData( this.getBondParams( what ) );

        var lineBuffer = new NGL.LineBuffer(
            bondData.position1,
            bondData.position2,
            bondData.color1,
            bondData.color2,
            this.getBufferParams()
        );

        return {
            bufferList: [ lineBuffer ]
        };

    },

    updateData: function( what, data ){

        var bondData = data.sview.getBondData( this.getBondParams( what ) );
        var lineData = {};

        if( !what || what[ "position" ] ){
            lineData[ "from" ] = bondData.position1;
            lineData[ "to" ] = bondData.position2;
        }

        if( !what || what[ "color" ] ){
            lineData[ "color" ] = bondData.color1;
            lineData[ "color2" ] = bondData.color2;
        }

        data.bufferList[ 0 ].setAttributes( lineData );

    }

} );


NGL.HyperballRepresentation = function( structure, viewer, params ){

    NGL.LicoriceRepresentation.call( this, structure, viewer, params );

    this.defaultScale[ "vdw" ] = 0.2;

};

NGL.HyperballRepresentation.prototype = NGL.createObject(

    NGL.LicoriceRepresentation.prototype, {

    constructor: NGL.HyperballRepresentation,

    type: "hyperball",

    defaultSize: 1.0,

    parameters: Object.assign( {

        shrink: {
            type: "number", precision: 3, max: 1.0, min: 0.001, buffer: true
        }

    }, NGL.LicoriceRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.scale = p.scale || 0.2;
        p.radius = p.radius || "vdw";

        this.shrink = p.shrink || 0.12;

        NGL.LicoriceRepresentation.prototype.init.call( this, p );

    },

    getBondParams: function( what, params ){

        if( !what || what[ "radius" ] ){
            params = Object.assign( { radius2: true }, params );
        }

        return NGL.LicoriceRepresentation.prototype.getBondParams.call( this, what, params );

    },

    createData: function( sview ){

        var atomData = sview.getAtomData( this.getAtomParams() );
        var bondData = sview.getBondData( this.getBondParams() );

        var sphereBuffer = new NGL.SphereBuffer(
            atomData.position,
            atomData.color,
            atomData.radius,
            atomData.pickingColor,
            this.getBufferParams( {
                sphereDetail: this.sphereDetail,
                disableImpostor: this.disableImpostor,
                dullInterior: true
            } )
        );

        this.__center = new Float32Array( sview.bondCount * 3 );

        var stickBuffer = new NGL.HyperballStickBuffer(
            bondData.position1,
            bondData.position2,
            bondData.color1,
            bondData.color2,
            bondData.radius1,
            bondData.radius2,
            bondData.pickingColor1,
            bondData.pickingColor2,
            this.getBufferParams( {
                shrink: this.shrink,
                radiusSegments: this.radiusSegments,
                dullInterior: true
            } ),
            this.disableImpostor
        );

        return {
            bufferList: [ sphereBuffer, stickBuffer ]
        };

    },

    updateData: function( what, data ){

        var atomData = data.sview.getAtomData( this.getAtomParams() );
        var bondData = data.sview.getBondData( this.getBondParams() );
        var sphereData = {};
        var stickData = {};

        if( !what || what[ "position" ] ){
            sphereData[ "position" ] = atomData.position;
            var from = bondData.position1;
            var to = bondData.position2;
            stickData[ "position" ] = NGL.Utils.calculateCenterArray(
                from, to, this.__center
            );
            stickData[ "position1" ] = from;
            stickData[ "position2" ] = to;
        }

        if( !what || what[ "color" ] ){
            sphereData[ "color" ] = atomData.color;
            stickData[ "color" ] = bondData.color1;
            stickData[ "color2" ] = bondData.color2;
        }

        if( !what || what[ "radius" ] ){
            sphereData[ "radius" ] = atomData.radius;
            stickData[ "radius" ] = bondData.radius1;
            stickData[ "radius2" ] = bondData.radius2;
        }

        data.bufferList[ 0 ].setAttributes( sphereData );
        data.bufferList[ 1 ].setAttributes( stickData );

    },

} );


NGL.BackboneRepresentation = function( structure, viewer, params ){

    NGL.BallAndStickRepresentation.call( this, structure, viewer, params );

};

NGL.BackboneRepresentation.prototype = NGL.createObject(

    NGL.BallAndStickRepresentation.prototype, {

    constructor: NGL.BackboneRepresentation,

    type: "backbone",

    defaultSize: 0.25,

    parameters: Object.assign( {

    }, NGL.BallAndStickRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.aspectRatio = p.aspectRatio || 1.0;

        NGL.BallAndStickRepresentation.prototype.init.call( this, p );

    },

    getAtomData: function( sview, what, params ){

        return sview.getBackboneAtomData( this.getAtomParams( what, params ) );

    },

    getBondData: function( sview, what, params ){

        var p = this.getBondParams( what, params );
        p.colorParams.backbone = true;

        return sview.getBackboneBondData( p );

    }

} );


NGL.BaseRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.BaseRepresentation.prototype = NGL.createObject(

    NGL.BallAndStickRepresentation.prototype, {

    constructor: NGL.BaseRepresentation,

    type: "base",

    defaultSize: 0.3,

    parameters: Object.assign( {

    }, NGL.BallAndStickRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.aspectRatio = p.aspectRatio || 1.0;

        NGL.BallAndStickRepresentation.prototype.init.call( this, p );

    },

    getAtomData: function( sview, what, params ){

        return sview.getRungAtomData( this.getAtomParams( what, params ) );

    },

    getBondData: function( sview, what, params ){

        var p = this.getBondParams( what, params );
        p.colorParams.rung = true;

        return sview.getRungBondData( p );

    }

} );


NGL.CartoonRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.CartoonRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.CartoonRepresentation,

    type: "cartoon",

    parameters: Object.assign( {

        aspectRatio: {
            type: "number", precision: 1, max: 10.0, min: 1.0
        },
        subdiv: {
            type: "integer", max: 50, min: 1, rebuild: true
        },
        radialSegments: {
            type: "integer", max: 50, min: 1, rebuild: true
        },
        tension: {
            type: "number", precision: 1, max: 1.0, min: 0.1
        },
        capped: {
            type: "boolean", rebuild: true
        },
        smoothSheet: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "atomindex";
        p.colorScale = p.colorScale || "RdYlBu";
        p.radius = p.radius || "sstruc";
        p.scale = p.scale || 0.7;

        if( p.quality === "low" ){
            this.subdiv = 3;
            this.radialSegments = 6;
        }else if( p.quality === "medium" ){
            this.subdiv = 6;
            this.radialSegments = 10;
        }else if( p.quality === "high" ){
            this.subdiv = 12;
            this.radialSegments = 20;
        }else{
            this.subdiv = p.subdiv || 6;
            this.radialSegments = p.radialSegments || 10;
        }

        this.aspectRatio = p.aspectRatio || 5.0;
        this.tension = p.tension || NaN;
        this.capped = p.capped === undefined ? true : p.capped;
        this.smoothSheet = p.smoothSheet || false;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    getSplineParams: function( params ){

        return Object.assign( {
            subdiv: this.subdiv,
            tension: this.tension,
            directional: this.aspectRatio === 1.0 ? false : true,
            smoothSheet: this.smoothSheet
        }, params );

    },

    getSpline: function( polymer ){

        return new NGL.Spline( polymer, this.getSplineParams() );

    },

    getScale: function( polymer ){

        return polymer.isCg() ? this.scale * this.aspectRatio : this.scale;

    },

    getAspectRatio: function( polymer ){

        return polymer.isCg() ? 1.0 : this.aspectRatio;

    },

    createData: function( sview ){

        var bufferList = [];
        var polymerList = [];

        this.structure.eachPolymer( function( polymer ){

            if( polymer.residueCount < 4 ) return;
            polymerList.push( polymer );

            var spline = this.getSpline( polymer );

            var subPos = spline.getSubdividedPosition();
            var subOri = spline.getSubdividedOrientation();
            var subCol = spline.getSubdividedColor( this.getColorParams() );
            var subSize = spline.getSubdividedSize( this.radius, this.getScale( polymer ) );

            bufferList.push(
                new NGL.TubeMeshBuffer(
                    subPos.position,
                    subOri.normal,
                    subOri.binormal,
                    subOri.tangent,
                    subCol.color,
                    subSize.size,
                    subCol.pickingColor,
                    this.getBufferParams( {
                        radialSegments: this.radialSegments,
                        aspectRatio: this.getAspectRatio( polymer ),
                        capped: this.capped,
                        dullInterior: true
                    } )
                )
            );

        }.bind( this ), sview.getSelection() );

        return {
            bufferList: bufferList,
            polymerList: polymerList
        };

    },

    updateData: function( what, data ){

        if( NGL.debug ) NGL.time( this.type + " repr update" );

        what = what || {};

        for( var i = 0, il = data.polymerList.length; i < il; ++i ){

            var bufferData = {};
            var polymer = data.polymerList[ i ];
            var spline = this.getSpline( polymer );

            data.bufferList[ i ].aspectRatio = this.getAspectRatio( polymer );

            if( what[ "position" ] || what[ "radius" ] ){

                var subPos = spline.getSubdividedPosition();
                var subOri = spline.getSubdividedOrientation();
                var subSize = spline.getSubdividedSize( this.radius, this.getScale( polymer ) );

                bufferData[ "position" ] = subPos.position;
                bufferData[ "normal" ] = subOri.normal;
                bufferData[ "binormal" ] = subOri.binormal;
                bufferData[ "tangent" ] = subOri.tangent;
                bufferData[ "size" ] = subSize.size;

            }

            if( what[ "color" ] ){

                var subCol = spline.getSubdividedColor( this.getColorParams() );

                bufferData[ "color" ] = subCol.color;
                bufferData[ "pickingColor" ] = subCol.pickingColor;

            }

            data.bufferList[ i ].setAttributes( bufferData );

        }

        if( NGL.debug ) NGL.timeEnd( this.type + " repr update" );

    },

    setParameters: function( params ){

        var rebuild = false;
        var what = {};

        if( params && params[ "aspectRatio" ] ){
            what[ "radius" ] = true;
        }

        if( params && params[ "tension" ] ){
            what[ "position" ] = true;
        }

        NGL.StructureRepresentation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        return this;

    }

} );


NGL.TubeRepresentation = function( structure, viewer, params ){

    NGL.CartoonRepresentation.call( this, structure, viewer, params );

};

NGL.TubeRepresentation.prototype = NGL.createObject(

    NGL.CartoonRepresentation.prototype, {

    constructor: NGL.TubeRepresentation,

    type: "tube",

    parameters: Object.assign(
        {}, NGL.CartoonRepresentation.prototype.parameters, { aspectRatio: null }
    ),

    init: function( params ){

        var p = params || {};
        p.aspectRatio = 1.0;
        p.scale = p.scale || 2.0;

        NGL.CartoonRepresentation.prototype.init.call( this, p );

    },

    getSplineParams: function( params ){

        return NGL.CartoonRepresentation.prototype.getSplineParams.call( this, {
            directional: false
        } );

    }

} );


NGL.RibbonRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

    this.defaultScale[ "sstruc" ] *= 3.0;

};

NGL.RibbonRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.RibbonRepresentation,

    type: "ribbon",

    parameters: Object.assign( {

        subdiv: {
            type: "integer", max: 50, min: 1, rebuild: true
        },
        tension: {
            type: "number", precision: 1, max: 1.0, min: 0.1
        },
        smoothSheet: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters, {

        side: null,
        wireframe: null,
        linewidth: null

    } ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "atomindex";
        p.colorScale = p.colorScale || "RdYlBu";
        p.radius = p.radius || "sstruc";
        p.scale = p.scale || 4.0;

        if( p.quality === "low" ){
            this.subdiv = 3;
        }else if( p.quality === "medium" ){
            this.subdiv = 6;
        }else if( p.quality === "high" ){
            this.subdiv = 12;
        }else{
            this.subdiv = p.subdiv || 6;
        }

        this.tension = p.tension || NaN;
        this.smoothSheet = p.smoothSheet || false;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    getSplineParams: function( params ){

        return Object.assign( {
            subdiv: this.subdiv,
            tension: this.tension,
            directional: true,
            smoothSheet: this.smoothSheet
        }, params );

    },

    createData: function( sview ){

        var bufferList = [];
        var polymerList = [];

        this.structure.eachPolymer( function( polymer ){

            if( polymer.residueCount < 4 ) return;
            polymerList.push( polymer );

            var spline = new NGL.Spline( polymer, this.getSplineParams() );
            var subPos = spline.getSubdividedPosition();
            var subOri = spline.getSubdividedOrientation();
            var subCol = spline.getSubdividedColor( this.getColorParams() );
            var subSize = spline.getSubdividedSize( this.radius, this.scale );

            bufferList.push(
                new NGL.RibbonBuffer(
                    subPos.position,
                    subOri.binormal,
                    subOri.normal,
                    subCol.color,
                    subSize.size,
                    subCol.pickingColor,
                    this.getBufferParams()
                )
            );

        }.bind( this ), sview.getSelection() );

        return {
            bufferList: bufferList,
            polymerList: polymerList
        };

    },

    updateData: function( what, data ){

        what = what || {};

        var i = 0;
        var n = data.polymerList.length;

        for( i = 0; i < n; ++i ){

            var bufferData = {};
            var spline = new NGL.Spline( data.polymerList[ i ], this.getSplineParams() );

            if( what[ "position" ] ){
                var subPos = spline.getSubdividedPosition();
                var subOri = spline.getSubdividedOrientation();
                bufferData[ "position" ] = subPos.position;
                bufferData[ "normal" ] = subOri.binormal;
                bufferData[ "dir" ] = subOri.normal;
            }

            if( what[ "radius" ] || what[ "scale" ] ){
                var subSize = spline.getSubdividedSize( this.radius, this.scale );
                bufferData[ "size" ] = subSize.size;
            }

            if( what[ "color" ] ){
                var subCol = spline.getSubdividedColor( this.getColorParams() );
                bufferData[ "color" ] = subCol.color;
            }

            data.bufferList[ i ].setAttributes( bufferData );

        };

    },

    setParameters: function( params ){

        var rebuild = false;
        var what = {};

        if( params && params[ "tension" ] ){
            what[ "position" ] = true;
        }

        NGL.StructureRepresentation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        return this;

    }

} );


NGL.TraceRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.TraceRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.TraceRepresentation,

    type: "trace",

    parameters: Object.assign( {

        subdiv: {
            type: "integer", max: 50, min: 1, rebuild: true
        },
        tension: {
            type: "number", precision: 1, max: 1.0, min: 0.1
        },
        smoothSheet: {
            type: "boolean", rebuild: true
        }

    }, NGL.Representation.prototype.parameters, {

        flatShaded: null,
        side: null,
        wireframe: null

    } ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "atomindex";
        p.colorScale = p.colorScale || "RdYlBu";

        if( p.quality === "low" ){
            this.subdiv = 3;
        }else if( p.quality === "medium" ){
            this.subdiv = 6;
        }else if( p.quality === "high" ){
            this.subdiv = 12;
        }else{
            this.subdiv = p.subdiv || 6;
        }

        this.tension = p.tension || NaN;
        this.smoothSheet = p.smoothSheet || false;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    getSplineParams: function( params ){

        return Object.assign( {
            subdiv: this.subdiv,
            tension: this.tension,
            directional: false,
            smoothSheet: this.smoothSheet
        }, params );

    },

    createData: function( sview ){

        var bufferList = [];
        var polymerList = [];

        this.structure.eachPolymer( function( polymer ){

            if( polymer.residueCount < 4 ) return;
            polymerList.push( polymer );

            var spline = new NGL.Spline( polymer, this.getSplineParams() );
            var subPos = spline.getSubdividedPosition();
            var subCol = spline.getSubdividedColor( this.getColorParams() );

            bufferList.push(
                new NGL.TraceBuffer(
                    subPos.position,
                    subCol.color,
                    this.getBufferParams()
                )
            );

        }.bind( this ), sview.getSelection() );

        return {
            bufferList: bufferList,
            polymerList: polymerList
        };

    },

    updateData: function( what, data ){

        what = what || {};

        var i = 0;
        var n = data.polymerList.length;

        for( i = 0; i < n; ++i ){

            var bufferData = {};
            var spline = new NGL.Spline( data.polymerList[ i ], this.getSplineParams() );

            if( what[ "position" ] ){
                var subPos = spline.getSubdividedPosition();
                bufferData[ "position" ] = subPos.position;
            }

            if( what[ "color" ] ){
                var subCol = spline.getSubdividedColor( this.getColorParams() );
                bufferData[ "color" ] = subCol.color;
            }

            data.bufferList[ i ].setAttributes( bufferData );

        };

    },

    setParameters: function( params ){

        var rebuild = false;
        var what = {};

        if( params && params[ "tension" ] ){
            what[ "position" ] = true;
        }

        NGL.StructureRepresentation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        return this;

    }

} );


NGL.HelixorientRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.HelixorientRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.HelixorientRepresentation,

    type: "helixorient",

    parameters: Object.assign( {

        sphereDetail: {
            type: "integer", max: 3, min: 0, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "sstruc";
        p.radius = p.radius || 0.15;
        p.scale = p.scale || 1.0;

        if( p.quality === "low" ){
            this.sphereDetail = 0;
        }else if( p.quality === "medium" ){
            this.sphereDetail = 1;
        }else if( p.quality === "high" ){
            this.sphereDetail = 2;
        }else{
            this.sphereDetail = p.sphereDetail !== undefined ? p.sphereDetail : 1;
        }
        this.disableImpostor = p.disableImpostor || false;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    createData: function( sview ){

        var bufferList = [];
        var polymerList = [];

        this.structure.eachPolymer( function( polymer ){

            if( polymer.residueCount < 4 ) return;
            polymerList.push( polymer );

            var helixorient = new NGL.Helixorient( polymer );
            var position = helixorient.getPosition();
            var color = helixorient.getColor( this.getColorParams() );
            var size = helixorient.getSize( this.radius, this.scale );

            bufferList.push(

                new NGL.SphereBuffer(
                    position.center,
                    color.color,
                    size.size,
                    color.pickingColor,
                    this.getBufferParams( {
                        sphereDetail: this.sphereDetail,
                        disableImpostor: this.disableImpostor,
                        dullInterior: true
                    } )
                ),

                new NGL.VectorBuffer(
                    position.center,
                    position.axis,
                    this.getBufferParams({
                        color: "skyblue",
                        scale: 1
                    })
                ),

                new NGL.VectorBuffer(
                    position.center,
                    position.resdir,
                    this.getBufferParams({
                        color: "lightgreen",
                        scale: 1
                    })
                )

            );


        }.bind( this ), sview.getSelection() );

        return {
            bufferList: bufferList,
            polymerList: polymerList
        };

    },

    updateData: function( what, data ){

        if( NGL.debug ) NGL.time( this.type + " repr update" );

        what = what || {};

        for( var i = 0, il = data.polymerList.length; i < il; ++i ){

            var j = i * 3;

            var bufferData = {};
            var polymer = data.polymerList[ i ]
            var helixorient = new NGL.Helixorient( polymer );

            if( what[ "position" ] ){

                var position = helixorient.getPosition();

                bufferData[ "position" ] = position.center;

                data.bufferList[ j + 1 ].setAttributes( {
                    "position": position.center,
                    "vector": position.axis,
                } );
                data.bufferList[ j + 2 ].setAttributes( {
                    "position": position.center,
                    "vector": position.resdir,
                } );

            }

            data.bufferList[ j ].setAttributes( bufferData );

        }

        if( NGL.debug ) NGL.timeEnd( this.type + " repr update" );

    }

} );


NGL.RocketRepresentation = function( structure, viewer, params ){

    this.helixbundleList = [];

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.RocketRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.RocketRepresentation,

    type: "rocket",

    parameters: Object.assign( {

        localAngle: {
            type: "integer", max: 180, min: 0, rebuild: true
        },
        centerDist: {
            type: "number", precision: 1, max: 10, min: 0, rebuild: true
        },
        ssBorder: {
            type: "boolean", rebuild: true
        },
        radiusSegments: {
            type: "integer", max: 25, min: 5, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "sstruc";
        p.radius = p.radius || 1.5;
        p.scale = p.scale || 1.0;

        if( p.quality === "low" ){
            this.radiusSegments = 5;
        }else if( p.quality === "medium" ){
            this.radiusSegments = 10;
        }else if( p.quality === "high" ){
            this.radiusSegments = 20;
        }else{
            this.radiusSegments = p.radiusSegments !== undefined ? p.radiusSegments : 10;
        }
        this.disableImpostor = p.disableImpostor || false;

        this.localAngle = p.localAngle || 30;
        this.centerDist = p.centerDist || 2.5;
        this.ssBorder = p.ssBorder === undefined ? false : p.ssBorder;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    createData: function( sview ){

        var length = 0;
        var axisList = [];
        var helixbundleList = [];

        this.structure.eachPolymer( function( polymer ){

            if( polymer.residueCount < 4 || polymer.isNucleic() ) return;

            var helixbundle = new NGL.Helixbundle( polymer );
            var axis = helixbundle.getAxis(
                this.localAngle, this.centerDist, this.ssBorder,
                this.getColorParams(), this.radius, this.scale
            );

            length += axis.size.length;
            axisList.push( axis );
            helixbundleList.push( helixbundle );

        }.bind( this ), sview.getSelection() );

        var axisData = {
            begin: new Float32Array( length * 3 ),
            end: new Float32Array( length * 3 ),
            size: new Float32Array( length ),
            color: new Float32Array( length * 3 ),
            pickingColor: new Float32Array( length * 3 ),
        };

        var offset = 0;

        axisList.forEach( function( axis ){
            axisData.begin.set( axis.begin, offset * 3 );
            axisData.end.set( axis.end, offset * 3 );
            axisData.size.set( axis.size, offset );
            axisData.color.set( axis.color, offset * 3 );
            axisData.pickingColor.set( axis.pickingColor, offset * 3 );
            offset += axis.size.length;
        } );

        var cylinderBuffer = new NGL.CylinderBuffer(
            axisData.begin,
            axisData.end,
            axisData.color,
            axisData.color,
            axisData.size,
            axisData.pickingColor,
            axisData.pickingColor,
            this.getBufferParams( {
                shift: 0,
                cap: true,
                radiusSegments: this.radiusSegments,
                disableImpostor: this.disableImpostor,
                dullInterior: true
            } )
        );

        return {
            bufferList: [ cylinderBuffer ],
            axisList: axisList,
            helixbundleList: helixbundleList,
            axisData: axisData
        };

    },

    updateData: function( what, data ){

        what = what || {};

        if( what[ "position" ] ){
            this.build();
            return;
        }

        var cylinderData = {};

        if( what[ "color" ] || what[ "radius" ] ){

            var offset = 0;

            data.helixbundleList.forEach( function( helixbundle ){

                var axis = helixbundle.getAxis(
                    this.localAngle, this.centerDist, this.ssBorder,
                    this.getColorParams(), this.radius, this.scale
                );
                if( what[ "color" ] ){
                    data.axisData.color.set( axis.color, offset * 3 );
                }
                if( what[ "radius" ] || what[ "scale" ] ){
                    data.axisData.size.set( axis.size, offset );
                }
                offset += axis.size.length;

            }.bind( this ) );

            if( what[ "color" ] ){
                cylinderData[ "color" ] = data.axisData.color;
                cylinderData[ "color2" ] = data.axisData.color;
            }

            if( what[ "radius" ] || what[ "scale" ] ){
                cylinderData[ "radius" ] = data.axisData.size;
            }

        }

        data.bufferList[ 0 ].setAttributes( cylinderData );

    }

} );


NGL.RopeRepresentation = function( structure, viewer, params ){

    NGL.CartoonRepresentation.call( this, structure, viewer, params );

};

NGL.RopeRepresentation.prototype = NGL.createObject(

    NGL.CartoonRepresentation.prototype, {

    constructor: NGL.RopeRepresentation,

    type: "rope",

    parameters: Object.assign( {

        smooth: {
            type: "integer", max: 15, min: 0, rebuild: true
        }

    }, NGL.CartoonRepresentation.prototype.parameters, {
        aspectRatio: null,
        smoothSheet: null
    } ),

    init: function( params ){

        var p = params || {};
        p.aspectRatio = 1.0;
        p.tension = p.tension || 0.5;
        p.scale = p.scale || 5.0;
        p.smoothSheet = false;

        this.smooth = p.smooth === undefined ? 2 : p.smooth;

        NGL.CartoonRepresentation.prototype.init.call( this, p );

    },

    getSpline: function( polymer ){

        var helixorient = new NGL.Helixorient( polymer );

        return new NGL.Spline( polymer, this.getSplineParams( {
            directional: false,
            positionIterator: helixorient.getCenterIterator( this.smooth )
        } ) );

    }

} );


NGL.ContactRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.ContactRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.ContactRepresentation,

    type: "contact",

    defaultSize: 0.25,

    parameters: Object.assign( {

        contactType: {
            type: "select", rebuild: true,
            options: {
                "polar": "polar",
                "polarBackbone": "polar backbone"
            }
        },
        maxDistance: {
            type: "number", precision: 1, max: 10, min: 0.1, rebuild: true
        },
        maxAngle: {
            type: "integer", max: 180, min: 0, rebuild: true
        },
        radiusSegments: {
            type: "integer", max: 25, min: 5, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.radius = p.radius || this.defaultSize;

        if( p.quality === "low" ){
            this.radiusSegments = 5;
        }else if( p.quality === "medium" ){
            this.radiusSegments = 10;
        }else if( p.quality === "high" ){
            this.radiusSegments = 20;
        }else{
            this.radiusSegments = p.radiusSegments !== undefined ? p.radiusSegments : 10;
        }
        this.disableImpostor = p.disableImpostor || false;

        this.contactType = p.contactType || "polarBackbone";
        this.maxDistance = p.maxDistance || 3.5;
        this.maxAngle = p.maxAngle || 40;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    getContactData: function( sview ){

        var contactsFnDict = {
            "polar": NGL.polarContacts,
            "polarBackbone": NGL.polarBackboneContacts
        };

        var contactData = contactsFnDict[ this.contactType ](
            sview, this.maxDistance, this.maxAngle
        );

        return contactData;

    },

    getBondData: function( sview, what, params ){

        return sview.getBondData( this.getBondParams( what, params ) );

    },

    createData: function( sview ){

        var contactData = this.getContactData( sview );

        var bondParams = {
            bondSet: contactData.bondSet,
            bondStore: contactData.bondStore
        };

        var bondData = this.getBondData( sview, undefined, bondParams );

        var cylinderBuffer = new NGL.CylinderBuffer(
            bondData.position1,
            bondData.position2,
            bondData.color1,
            bondData.color2,
            bondData.radius,
            bondData.pickingColor1,
            bondData.pickingColor2,
            this.getBufferParams( {
                shift: 0,
                cap: true,
                radiusSegments: this.radiusSegments,
                disableImpostor: this.disableImpostor,
                dullInterior: true
            } )
        );

        return {
            bufferList: [ cylinderBuffer ],
            bondSet: contactData.bondSet,
            bondStore: contactData.bondStore
        };

    },

    updateData: function( what, data ){

        if( !what || what[ "position" ] ){
            var contactData = this.getContactData( data.sview );
            data.bondSet = contactData.bondSet;
            data.bondStore = contactData.bondStore;
        }

        var bondParams = {
            bondSet: data.bondSet,
            bondStore: data.bondStore
        };

        var bondData = this.getBondData( data.sview, what, bondParams );
        var cylinderData = {};

        if( !what || what[ "position" ] ){

            cylinderData[ "position" ] = NGL.Utils.calculateCenterArray(
                bondData.position1, bondData.position2
            );
            cylinderData[ "position1" ] = bondData.position1;
            cylinderData[ "position2" ] = bondData.position2;
        }

        if( !what || what[ "color" ] ){
            cylinderData[ "color" ] = bondData.color1;
            cylinderData[ "color2" ] = bondData.color2;
        }

        if( !what || what[ "radius" ] ){
            cylinderData[ "radius" ] = bondData.radius;
        }

        data.bufferList[ 0 ].setAttributes( cylinderData );

    }

} );


NGL.MolecularSurfaceRepresentation = function( structure, viewer, params ){

    this.__infoList = [];

    NGL.StructureRepresentation.call( this, structure, viewer, params );

    // TODO find a more direct way
    this.structure.signals.refreshed.add( function(){
        this.__forceNewMolsurf = true;
    }, this );

};

NGL.MolecularSurfaceRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.MolecularSurfaceRepresentation,

    type: "surface",

    parameters: Object.assign( {

        surfaceType: {
            type: "select", rebuild: true,
            options: {
                "vws": "vws",
                "sas": "sas",
                "ms": "ms",
                "ses": "ses"
            }
        },
        probeRadius: {
            type: "number", precision: 1, max: 20, min: 0,
            rebuild: true
        },
        smooth: {
            type: "integer", precision: 1, max: 10, min: 0,
            rebuild: true
        },
        scaleFactor: {
            type: "number", precision: 1, max: 5, min: 0,
            rebuild: true
        },
        cutoff: {
            type: "number", precision: 2, max: 50, min: 0,
            rebuild: true
        },
        background: {
            type: "boolean", rebuild: true  // FIXME
        },
        opaqueBack: {
            type: "boolean", buffer: true
        },
        lowResolution: {
            type: "boolean", rebuild: true
        },
        filterSele: {
            type: "text"
        },
        volume: {
            type: "hidden"
        },
        useWorker: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters, {

        radiusType: null,
        radius: null,
        scale: null

    } ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "uniform";
        p.colorValue = p.colorValue !== undefined ? p.colorValue : 0xDDDDDD;

        this.surfaceType = p.surfaceType !== undefined ? p.surfaceType : "ms";
        this.probeRadius = p.probeRadius !== undefined ? p.probeRadius : 1.4;
        this.smooth = p.smooth !== undefined ? p.smooth : 2;
        this.scaleFactor = p.scaleFactor !== undefined ? p.scaleFactor : 2.0;
        this.cutoff = p.cutoff || 0.0;
        this.background = p.background || false;
        this.opaqueBack = p.opaqueBack !== undefined ? p.opaqueBack : true;
        this.lowResolution = p.lowResolution !== undefined ? p.lowResolution : false;
        this.filterSele = p.filterSele !== undefined ? p.filterSele : "";
        this.volume = p.volume || undefined;
        this.useWorker = p.useWorker !== undefined ? p.useWorker : true;

        NGL.StructureRepresentation.prototype.init.call( this, params );

    },

    prepareData: function( sview, i, callback ){

        var info = this.__infoList[ i ];
        if( !info ){
            info = {};
            this.__infoList[ i ] = info;
        }

        if( !info.molsurf || info.sele !== sview.selection.string ){

            info.sele = sview.selection.string;
            info.molsurf = new NGL.MolecularSurface( sview );

            var p = this.getSurfaceParams();
            var onSurfaceFinish = function( surface ){
                info.surface = surface;
                callback( i );
            };

            if( this.useWorker ){
                info.molsurf.getSurfaceWorker( p, onSurfaceFinish );
            }else{
                onSurfaceFinish( info.molsurf.getSurface( p ) );
            }

        }else{

            callback( i );

        }

    },

    prepare: function( callback ){

        if( this.__forceNewMolsurf || this.__sele !== this.selection.string ||
                this.__surfaceParams !== JSON.stringify( this.getSurfaceParams() ) ){
            this.__infoList.forEach( function( info, i ){
                info.molsurf.dispose();
            }.bind( this ) );
            this.__infoList.length = 0;
        }

        if( this.structureView.atomCount === 0 ){
            callback();
            return
        }

        var after = function(){
            this.__sele = this.selection.string;
            this.__surfaceParams = JSON.stringify( this.getSurfaceParams() );
            this.__forceNewMolsurf = false;
            callback()
        }.bind( this );

        var name = this.assembly === "default" ? this.defaultAssembly : this.assembly;
        var assembly = this.structure.biomolDict[ name ];

        if( assembly ){
            assembly.partList.forEach( function( part, i ){
                var sview = part.getView( this.structureView );
                this.prepareData( sview, i, function( _i ){
                    if( _i === assembly.partList.length - 1 ) after();
                }.bind( this ) );
            }, this );
        }else{
            this.prepareData( this.structureView, 0, after );
        }

    },

    createData: function( sview, i ){

        var info = this.__infoList[ i ];

        var surfaceBuffer = new NGL.SurfaceBuffer(
            info.surface.getPosition(),
            info.surface.getColor( this.getColorParams() ),
            info.surface.getFilteredIndex( this.filterSele, sview ),
            info.surface.getNormal(),
            info.surface.getPickingColor( this.getColorParams() ),
            this.getBufferParams( {
                background: this.background,
                opaqueBack: this.opaqueBack,
                dullInterior: false
            } )
        );
        var doubleSidedBuffer = new NGL.DoubleSidedBuffer( surfaceBuffer );

        return {
            bufferList: [ doubleSidedBuffer ],
            info: info
        };

    },

    updateData: function( what, data ){

        var surfaceData = {};

        if( what[ "position" ] ){
            this.__forceNewMolsurf = true;
            this.build();
            return;
        }

        if( what[ "color" ] ){
            surfaceData[ "color" ] = data.info.surface.getColor( this.getColorParams() );
        }

        if( what[ "index" ] ){
            surfaceData[ "index" ] = data.info.surface.getFilteredIndex( this.filterSele, data.sview );
        }

        data.bufferList[ 0 ].setAttributes( surfaceData );

    },

    setParameters: function( params, what, rebuild ){

        what = what || {};

        if( params && params[ "filterSele" ] ){
            what[ "index" ] = true;
        }

        if( params && params[ "volume" ] !== undefined ){
            what[ "color" ] = true;
        }

        NGL.StructureRepresentation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        return this;

    },

    getSurfaceParams: function( params ){

        var p = Object.assign( {
            type: this.surfaceType,
            probeRadius: this.probeRadius,
            scaleFactor: this.scaleFactor,
            smooth: this.smooth,
            lowRes: this.lowResolution,
            cutoff: this.cutoff,
            useWorker: this.useWorker
        }, params );

        return p;

    },

    getColorParams: function(){

        var p = NGL.StructureRepresentation.prototype.getColorParams.call( this );

        p.volume = this.volume;

        return p;

    },

    clear: function(){

        NGL.StructureRepresentation.prototype.clear.call( this );

    },

    dispose: function(){

        this.__infoList.forEach( function( info, i ){
            info.molsurf.dispose();
        }.bind( this ) );
        this.__infoList.length = 0;

        NGL.StructureRepresentation.prototype.dispose.call( this );

    }

} );


NGL.DistanceRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.DistanceRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.DistanceRepresentation,

    type: "distance",

    defaultSize: 0.15,

    parameters: Object.assign( {

        labelSize: {
            type: "number", precision: 3, max: 10.0, min: 0.001
        },
        labelColor: {
            type: "color"
        },
        labelVisible: {
            type: "boolean"
        },
        atomPair: {
            type: "hidden", rebuild: true
        },
        radiusSegments: {
            type: "integer", max: 25, min: 5, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        }

    }, NGL.StructureRepresentation.prototype.parameters, {
        flatShaded: null,
        assembly: null
    } ),

    init: function( params ){

        var p = params || {};
        p.radius = p.radius || this.defaultSize;

        if( p.quality === "low" ){
            this.radiusSegments = 5;
        }else if( p.quality === "medium" ){
            this.radiusSegments = 10;
        }else if( p.quality === "high" ){
            this.radiusSegments = 20;
        }else{
            this.radiusSegments = p.radiusSegments !== undefined ? p.radiusSegments : 10;
        }
        this.disableImpostor = p.disableImpostor || false;

        this.fontFamily = p.fontFamily || "sans-serif";
        this.fontStyle = p.fontStyle || "normal";
        this.fontWeight = p.fontWeight || "bold";
        this.sdf = p.sdf !== undefined ? p.sdf : NGL.browser !== "Firefox";  // FIXME
        this.labelSize = p.labelSize || 2.0;
        this.labelColor = p.labelColor || 0xFFFFFF;
        this.labelVisible = p.labelVisible !== undefined ? p.labelVisible : true;
        this.atomPair = p.atomPair || [];

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    getDistanceData: function( sview, atomPair ){

        var n = atomPair.length;
        var text = new Array( n );
        var position = new Float32Array( n * 3 );
        var sele1 = new NGL.Selection();
        var sele2 = new NGL.Selection();

        var bondStore = new NGL.BondStore();

        var ap1 = sview.getAtomProxy();
        var ap2 = sview.getAtomProxy();

        var j = 0;

        atomPair.forEach( function( pair, i ){

            i -= j;
            var i3 = i * 3;

            sele1.setString( pair[ 0 ] );
            sele2.setString( pair[ 1 ] );

            var atomIndices1 = sview.getAtomIndices( sele1 );
            var atomIndices2 = sview.getAtomIndices( sele2 );

            if( atomIndices1.length && atomIndices2.length ){

                ap1.index = atomIndices1[ 0 ];
                ap2.index = atomIndices2[ 0 ];

                bondStore.addBond( ap1, ap2, 1 );

                text[ i ] = ap1.distanceTo( ap2 ).toFixed( 2 );

                position[ i3 + 0 ] = ( ap1.x + ap2.x ) / 2;
                position[ i3 + 1 ] = ( ap1.y + ap2.y ) / 2;
                position[ i3 + 2 ] = ( ap1.z + ap2.z ) / 2;

            }else{

                j += 1;

            }

        }, this );

        if( j > 0 ){
            n -= j;
            position = position.subarray( 0, n * 3 );
        }

        var bondSet = new TypedFastBitSet( bondStore.count );
        bondSet.set_all( true );

        return {
            text: text,
            position: position,
            bondSet: bondSet,
            bondStore: bondStore
        };

    },

    getBondData: function( sview, what, params ){

        return sview.getBondData( this.getBondParams( what, params ) );

    },

    create: function(){

        if( this.structureView.atomCount === 0 ) return;

        var n = this.atomPair.length;
        if( n === 0 ) return;

        var distanceData = this.getDistanceData( this.structureView, this.atomPair );

        var c = new THREE.Color( this.labelColor );

        this.textBuffer = new NGL.TextBuffer(
            distanceData.position,
            NGL.Utils.uniformArray( n, this.labelSize ),
            NGL.Utils.uniformArray3( n, c.r, c.g, c.b ),
            distanceData.text,
            this.getBufferParams( {
                fontFamily: this.fontFamily,
                fontStyle: this.fontStyle,
                fontWeight: this.fontWeight,
                sdf: this.sdf,
                opacity: 1.0,
                visible: this.labelVisible
            } )
        );

        var bondParams = {
            bondSet: distanceData.bondSet,
            bondStore: distanceData.bondStore
        };

        var bondData = this.getBondData( this.structureView, undefined, bondParams );

        this.cylinderBuffer = new NGL.CylinderBuffer(
            bondData.position1,
            bondData.position2,
            bondData.color1,
            bondData.color2,
            bondData.radius,
            bondData.pickingColor1,
            bondData.pickingColor2,
            this.getBufferParams( {
                shift: 0,
                cap: true,
                radiusSegments: this.radiusSegments,
                disableImpostor: this.disableImpostor,
                dullInterior: true
            } )
        );

        this.dataList.push( {
            sview: this.structureView,
            bondSet: distanceData.bondSet,
            bondStore: distanceData.bondStore,
            position: distanceData.position,
            bufferList: [ this.textBuffer, this.cylinderBuffer ]
        } );

    },

    updateData: function( what, data ){

        if( !what || what[ "position" ] ){
            var distanceData = this.getDistanceData( data.sview, this.atomPair );
            data.bondSet = distanceData.bondSet;
            data.bondStore = distanceData.bondStore;
            data.position = distanceData.position;
        }

        var bondParams = {
            bondSet: data.bondSet,
            bondStore: data.bondStore
        };

        var bondData = this.getBondData( data.sview, what, bondParams );
        var cylinderData = {};
        var textData = {};
        var n = this.atomPair.length;

        if( what[ "position" ] ){
            textData[ "position" ] = data.position;
            cylinderData[ "position" ] = NGL.Utils.calculateCenterArray(
                bondData.position1, bondData.position2
            );
            cylinderData[ "position1" ] = bondData.position1;
            cylinderData[ "position2" ] = bondData.position2;
        }

        if( what[ "labelSize" ] ){
            textData[ "size" ] = NGL.Utils.uniformArray( n, this.labelSize );
        }

        if( what[ "labelColor" ] ){
            var c = new THREE.Color( this.labelColor );
            textData[ "color" ] = NGL.Utils.uniformArray3( n, c.r, c.g, c.b );
        }

        if( what[ "color" ] ){
            cylinderData[ "color" ] = bondData.color1;
            cylinderData[ "color2" ] = bondData.color2;
        }

        if( what[ "radius" ] || what[ "scale" ] ){
            cylinderData[ "radius" ] = bondData.radius;
        }

        this.textBuffer.setAttributes( textData );
        this.cylinderBuffer.setAttributes( cylinderData );

    },

    setVisibility: function( value, noRenderRequest ){

        NGL.StructureRepresentation.prototype.setVisibility.call(
            this, value, true
        );

        if( this.textBuffer ){

            this.textBuffer.setVisibility(
                this.labelVisible && this.visible
            );

        }

        if( !noRenderRequest ) this.viewer.requestRender();

        return this;

    },

    setParameters: function( params ){

        var rebuild = false;
        var what = {};

        if( params && params[ "labelSize" ] ){

            what[ "labelSize" ] = true;

        }

        if( params && params[ "labelColor" ] ){

            what[ "labelColor" ] = true;

        }

        NGL.StructureRepresentation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        if( params && params[ "labelVisible" ] !== undefined ){

            this.setVisibility( this.visible );

        }

        return this;

    }

} );


NGL.UnitcellRepresentation = function( structure, viewer, params ){

    NGL.StructureRepresentation.call( this, structure, viewer, params );

};

NGL.UnitcellRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.UnitcellRepresentation,

    type: "unitcell",

    parameters: Object.assign( {

        radius: {
            type: "number", precision: 3, max: 10.0, min: 0.001
        },
        sphereDetail: {
            type: "integer", max: 3, min: 0, rebuild: "impostor"
        },
        radiusSegments: {
            type: "integer", max: 25, min: 5, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        }

    }, NGL.Representation.prototype.parameters, {
        assembly: null
    } ),

    init: function( params ){

        var p = params || {};

        p.radius = p.radius === undefined ? 0.5 : p.radius;
        p.colorValue = p.colorValue === undefined ? "orange" : p.colorValue;

        if( p.quality === "low" ){
            this.sphereDetail = 0;
            this.radiusSegments = 5;
        }else if( p.quality === "medium" ){
            this.sphereDetail = 1;
            this.radiusSegments = 10;
        }else if( p.quality === "high" ){
            this.sphereDetail = 2;
            this.radiusSegments = 20;
        }else{
            this.sphereDetail = p.sphereDetail !== undefined ? p.sphereDetail : 1;
            this.radiusSegments = p.radiusSegments !== undefined ? p.radiusSegments : 10;
        }
        this.disableImpostor = p.disableImpostor || false;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    getUnitcellData: function( structure ){

        var c = new THREE.Color( this.colorValue );

        var vertexPosition = new Float32Array( 3 * 8 );
        var vertexColor = NGL.Utils.uniformArray3( 8, c.r, c.g, c.b );
        var vertexRadius = NGL.Utils.uniformArray( 8, this.radius );

        var edgePosition1 = new Float32Array( 3 * 12 );
        var edgePosition2 = new Float32Array( 3 * 12 );
        var edgeColor = NGL.Utils.uniformArray3( 12, c.r, c.g, c.b );
        var edgeRadius = NGL.Utils.uniformArray( 12, this.radius );

        var uc = structure.unitcell;
        var centerFrac = structure.center.clone()
            .applyMatrix4( uc.cartToFrac )
            .floor().multiplyScalar( 2 ).addScalar( 1 );
        var v = new THREE.Vector3();

        var cornerOffset = 0;
        function addCorner( x, y, z ){
            v.set( x, y, z )
                .multiply( centerFrac )
                .applyMatrix4( uc.fracToCart )
                .toArray( vertexPosition, cornerOffset );
            cornerOffset += 3;
        }
        addCorner( 0, 0, 0 );
        addCorner( 1, 0, 0 );
        addCorner( 0, 1, 0 );
        addCorner( 0, 0, 1 );
        addCorner( 1, 1, 0 );
        addCorner( 1, 0, 1 );
        addCorner( 0, 1, 1 );
        addCorner( 1, 1, 1 );

        var edgeOffset = 0;
        function addEdge( a, b ){
            v.fromArray( vertexPosition, a * 3 )
                .toArray( edgePosition1, edgeOffset );
            v.fromArray( vertexPosition, b * 3 )
                .toArray( edgePosition2, edgeOffset );
            edgeOffset += 3;
        }
        addEdge( 0, 1 );
        addEdge( 0, 2 );
        addEdge( 0, 3 );
        addEdge( 1, 4 );
        addEdge( 1, 5 );
        addEdge( 2, 6 );
        addEdge( 3, 5 );
        addEdge( 4, 7 );
        addEdge( 5, 7 );
        addEdge( 2, 4 );
        addEdge( 7, 6 );
        addEdge( 3, 6 );

        return {
            vertexPosition: vertexPosition,
            vertexColor: vertexColor,
            vertexRadius: vertexRadius,
            edgePosition1: edgePosition1,
            edgePosition2: edgePosition2,
            edgeColor: edgeColor,
            edgeRadius: edgeRadius
        };

    },

    create: function(){

        var structure = this.structureView.getStructure();
        if( !structure.unitcell ) return;
        var unitcellData = this.getUnitcellData( structure );

        this.sphereBuffer = new NGL.SphereBuffer(
            unitcellData.vertexPosition,
            unitcellData.vertexColor,
            unitcellData.vertexRadius,
            undefined,
            this.getBufferParams( {
                sphereDetail: this.sphereDetail,
                disableImpostor: this.disableImpostor,
                dullInterior: true
            } )
        );

        this.cylinderBuffer = new NGL.CylinderBuffer(
            unitcellData.edgePosition1,
            unitcellData.edgePosition2,
            unitcellData.edgeColor,
            unitcellData.edgeColor,
            unitcellData.edgeRadius,
            undefined,
            undefined,
            this.getBufferParams( {
                shift: 0,
                cap: true,
                radiusSegments: this.radiusSegments,
                disableImpostor: this.disableImpostor,
                dullInterior: true
            } )
        );

        this.dataList.push( {
            sview: this.structureView,
            bufferList: [ this.sphereBuffer, this.cylinderBuffer ]
        } );

    },

    updateData: function( what, data ){

        var structure = data.sview.getStructure();
        var unitcellData = this.getUnitcellData( structure );
        var sphereData = {};
        var cylinderData = {};

        if( !what || what[ "position" ] ){
            sphereData[ "position" ] = unitcellData.vertexPosition;
            cylinderData[ "position" ] = NGL.Utils.calculateCenterArray(
                unitcellData.position1, unitcellData.position2
            );
            cylinderData[ "position1" ] = unitcellData.edgePosition1;
            cylinderData[ "position2" ] = unitcellData.edgePosition2;
        }

        if( !what || what[ "color" ] ){
            sphereData[ "color" ] = unitcellData.vertexColor;
            cylinderData[ "color" ] = unitcellData.edgeColor;
            cylinderData[ "color2" ] = unitcellData.edgeColor;
        }

        if( !what || what[ "radius" ] ){
            sphereData[ "radius" ] = unitcellData.vertexRadius;
            cylinderData[ "radius" ] = unitcellData.edgeRadius;
        }

        this.sphereBuffer.setAttributes( sphereData );
        this.cylinderBuffer.setAttributes( cylinderData );

    }

} );


//////////////////////////////
// Trajectory representation

NGL.TrajectoryRepresentation = function( trajectory, viewer, params ){

    this.manualAttach = true;

    this.trajectory = trajectory;

    NGL.StructureRepresentation.call(
        this, trajectory.structure, viewer, params
    );

};

NGL.TrajectoryRepresentation.prototype = NGL.createObject(

    NGL.StructureRepresentation.prototype, {

    constructor: NGL.TrajectoryRepresentation,

    type: "",

    parameters: Object.assign( {

        drawLine: {
            type: "boolean", rebuild: true
        },
        drawCylinder: {
            type: "boolean", rebuild: true
        },
        drawPoint: {
            type: "boolean", rebuild: true
        },
        drawSphere: {
            type: "boolean", rebuild: true
        },

        linewidth: {
            type: "integer", max: 20, min: 1, rebuild: true
        },
        pointSize: {
            type: "integer", max: 20, min: 1, rebuild: true
        },
        sizeAttenuation: {
            type: "boolean", rebuild: true
        },
        sort: {
            type: "boolean", rebuild: true
        },

    }, NGL.Representation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "uniform";
        p.colorValue = p.colorValue || 0xDDDDDD;

        this.drawLine = p.drawLine || true;
        this.drawCylinder = p.drawCylinder || false;
        this.drawPoint = p.drawPoint || false;
        this.drawSphere = p.drawSphere || false;

        this.pointSize = p.pointSize || 1;
        this.sizeAttenuation = p.sizeAttenuation !== undefined ? p.sizeAttenuation : false;
        this.sort = p.sort !== undefined ? p.sort : true;

        NGL.StructureRepresentation.prototype.init.call( this, p );

    },

    attach: function( callback ){

        this.bufferList.forEach( function( buffer ){

            this.viewer.add( buffer );

        }, this );

        this.setVisibility( this.visible );

        callback();

    },

    prepare: function( callback ){

        // TODO

        callback();

    },

    create: function(){

        // NGL.log( this.selection )
        // NGL.log( this.atomSet )

        if( this.atomSet.atomCount === 0 ) return;

        var scope = this;

        var index = this.atomSet.atoms[ 0 ].index;

        this.trajectory.getPath( index, function( path ){

            var n = path.length / 3;
            var tc = new THREE.Color( scope.colorValue );

            if( scope.drawSphere ){

                var sphereBuffer = new NGL.SphereBuffer(
                    path,
                    NGL.Utils.uniformArray3( n, tc.r, tc.g, tc.b ),
                    NGL.Utils.uniformArray( n, 0.2 ),
                    NGL.Utils.uniformArray3( n, tc.r, tc.g, tc.b ),
                    scope.getBufferParams( {
                        sphereDetail: scope.sphereDetail,
                        dullInterior: true,
                        disableImpostor: scope.disableImpostor
                    } )
                );

                scope.bufferList.push( sphereBuffer );

            }

            if( scope.drawCylinder ){

                var cylinderBuffer = new NGL.CylinderBuffer(
                    path.subarray( 0, -3 ),
                    path.subarray( 3 ),
                    NGL.Utils.uniformArray3( n - 1, tc.r, tc.g, tc.b ),
                    NGL.Utils.uniformArray3( n - 1, tc.r, tc.g, tc.b ),
                    NGL.Utils.uniformArray( n, 0.05 ),
                    NGL.Utils.uniformArray3( n - 1, tc.r, tc.g, tc.b ),
                    NGL.Utils.uniformArray3( n - 1, tc.r, tc.g, tc.b ),
                    scope.getBufferParams( {
                        shift: 0,
                        cap: true,
                        radiusSegments: scope.radiusSegments,
                        disableImpostor: scope.disableImpostor,
                        dullInterior: true
                    } )

                );

                scope.bufferList.push( cylinderBuffer );

            }

            if( scope.drawPoint ){

                var pointBuffer = new NGL.PointBuffer(
                    path,
                    NGL.Utils.uniformArray3( n, tc.r, tc.g, tc.b ),
                    scope.getBufferParams( {
                        pointSize: scope.pointSize,
                        sizeAttenuation: scope.sizeAttenuation,
                        sort: scope.sort,
                    } )
                );

                scope.bufferList.push( pointBuffer );

            }

            if( scope.drawLine ){

                var lineBuffer = new NGL.LineBuffer(
                    path.subarray( 0, -3 ),
                    path.subarray( 3 ),
                    NGL.Utils.uniformArray3( n - 1, tc.r, tc.g, tc.b ),
                    NGL.Utils.uniformArray3( n - 1, tc.r, tc.g, tc.b ),
                    scope.getBufferParams()
                );

                scope.bufferList.push( lineBuffer );

            }

            scope.attach();

        } );

    }

} );


///////////////////////////
// Surface representation

NGL.SurfaceRepresentation = function( surface, viewer, params ){

    NGL.Representation.call( this, surface, viewer, params );

    if( surface instanceof NGL.Volume ){
        this.surface = undefined;
        this.volume = surface;
    }else{
        this.surface = surface;
        this.volume = undefined;
    }

    this.boxCenter = new THREE.Vector3();
    this.__boxCenter = new THREE.Vector3();
    this.box = new THREE.Box3();
    this.__box = new THREE.Box3();

    this.setBox = ( function(){
        var position = new THREE.Vector3();
        return function(){
            var target = viewer.controls.target;
            var group = viewer.rotationGroup.position;
            position.copy( group ).negate().add( target );
            this.setParameters( { "boxCenter": position } );
        }.bind( this );
    }.bind( this ) )();

    this.viewer.signals.orientationChanged.add(
        this.setBox
    );

    this.build();

};

NGL.SurfaceRepresentation.prototype = NGL.createObject(

    NGL.Representation.prototype, {

    constructor: NGL.SurfaceRepresentation,

    type: "surface",

    parameters: Object.assign( {

        isolevelType: {
            type: "select", options: {
                "value": "value", "sigma": "sigma"
            }
        },
        isolevel: {
            type: "number", precision: 2, max: 1000, min: -1000
        },
        smooth: {
            type: "integer", precision: 1, max: 10, min: 0
        },
        background: {
            type: "boolean", rebuild: true  // FIXME
        },
        opaqueBack: {
            type: "boolean", buffer: true
        },
        boxSize: {
            type: "integer", precision: 1, max: 100, min: 0
        },
        useWorker: {
            type: "boolean", rebuild: true
        }

    }, NGL.Representation.prototype.parameters ),

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "uniform";
        p.colorValue = p.colorValue !== undefined ? p.colorValue : 0xDDDDDD;

        this.isolevelType  = p.isolevelType !== undefined ? p.isolevelType : "sigma";
        this.isolevel = p.isolevel !== undefined ? p.isolevel : 2.0;
        this.smooth = p.smooth !== undefined ? p.smooth : 0;
        this.background = p.background || false;
        this.opaqueBack = p.opaqueBack !== undefined ? p.opaqueBack : true;
        this.boxSize = p.boxSize !== undefined ? p.boxSize : 0;
        this.useWorker = p.useWorker !== undefined ? p.useWorker : true;

        NGL.Representation.prototype.init.call( this, p );

    },

    attach: function( callback ){

        this.bufferList.forEach( function( buffer ){

            this.viewer.add( buffer );

        }, this );

        this.setVisibility( this.visible );

        callback();

    },

    prepare: function( callback ){

        if( this.volume ){

            var isolevel;

            if( this.isolevelType === "sigma" ){
                isolevel = this.volume.getValueForSigma( this.isolevel );
            }else{
                isolevel = this.isolevel;
            }

            if( !this.surface ||
                this.__isolevel !== isolevel ||
                this.__smooth !== this.smooth ||
                this.__boxSize !== this.boxSize ||
                ( this.boxSize > 0 &&
                    !this.__boxCenter.equals( this.boxCenter ) )
            ){
                this.__isolevel = isolevel;
                this.__smooth = this.smooth;
                this.__boxSize = this.boxSize;
                this.__boxCenter.copy( this.boxCenter );
                this.__box.copy( this.box );

                var onSurfaceFinish = function( surface ){
                    this.surface = surface;
                    callback();
                }.bind( this );

                if( this.useWorker ){
                    this.volume.getSurfaceWorker(
                        isolevel, this.smooth, this.boxCenter, this.boxSize,
                        onSurfaceFinish
                    );
                }else{
                    onSurfaceFinish(
                        this.volume.getSurface(
                            isolevel, this.smooth, this.boxCenter, this.boxSize
                        )
                    );
                }
            }else{
                callback();
            }

        }else{
            callback();
        }

    },

    create: function(){

        var surfaceBuffer = new NGL.SurfaceBuffer(
            this.surface.getPosition(),
            this.surface.getColor( this.getColorParams() ),
            this.surface.getIndex(),
            this.surface.getNormal(),
            undefined,  // this.surface.getPickingColor( this.getColorParams() ),
            this.getBufferParams( {
                background: this.background,
                opaqueBack: this.opaqueBack,
                dullInterior: false,
            } )
        );
        var doubleSidedBuffer = new NGL.DoubleSidedBuffer( surfaceBuffer );

        this.bufferList.push( doubleSidedBuffer );

    },

    update: function( what ){

        if( this.bufferList.length === 0 ) return;

        what = what || {};

        var surfaceData = {};

        if( what[ "position" ] ){
            surfaceData[ "position" ] = this.surface.getPosition();
        }

        if( what[ "color" ] ){
            surfaceData[ "color" ] = this.surface.getColor(
                this.getColorParams()
            );
        }

        if( what[ "index" ] ){
            surfaceData[ "index" ] = this.surface.getIndex();
        }

        if( what[ "normal" ] ){
            surfaceData[ "normal" ] = this.surface.getNormal();
        }

        this.bufferList.forEach( function( buffer ){
            buffer.setAttributes( surfaceData );
        } );

    },

    setParameters: function( params, what, rebuild ){

        if( params && params[ "isolevelType" ] !== undefined &&
            this.volume
        ){

            if( this.isolevelType === "value" &&
                params[ "isolevelType" ] === "sigma"
            ){

                this.isolevel = this.volume.getSigmaForValue(
                    this.isolevel
                );

            }else if( this.isolevelType === "sigma" &&
                params[ "isolevelType" ] === "value"
            ){

                this.isolevel = this.volume.getValueForSigma(
                    this.isolevel
                );

            }

            this.isolevelType = params[ "isolevelType" ];

        }

        if( params && params[ "boxCenter" ] ){
            this.boxCenter.copy( params[ "boxCenter" ] );
            delete params[ "boxCenter" ];
        }

        NGL.Representation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        if( this.volume ){
            this.volume.getBox( this.boxCenter, this.boxSize, this.box );
        }

        if( this.surface && (
                params[ "isolevel" ] !== undefined ||
                params[ "smooth" ] !== undefined ||
                params[ "boxSize" ] !== undefined ||
                ( this.boxSize > 0 &&
                    !this.__box.equals( this.box ) )
            )
        ){
            this.build( {
                "__update": {
                    "position": true,
                    "color": true,
                    "index": true,
                    "normal": true
                }
            } );
        }

        return this;

    },

    dispose: function(){

        this.viewer.signals.orientationChanged.remove(
            this.setBox
        );

        NGL.Representation.prototype.dispose.call( this );

    }

} );


NGL.DotRepresentation = function( surface, viewer, params ){

    NGL.Representation.call( this, surface, viewer, params );

    if( surface instanceof NGL.Volume ){
        this.surface = undefined;
        this.volume = surface;
    }else{
        this.surface = surface;
        this.volume = undefined;
    }

    this.build();

};

NGL.DotRepresentation.prototype = NGL.createObject(

    NGL.Representation.prototype, {

    constructor: NGL.DotRepresentation,

    type: "dot",

    parameters: Object.assign( {

        thresholdType: {
            type: "select", rebuild: true, options: {
                "value": "value", "sigma": "sigma"
            }
        },
        thresholdMin: {
            type: "number", precision: 3, max: Infinity, min: -Infinity, rebuild: true
        },
        thresholdMax: {
            type: "number", precision: 3, max: Infinity, min: -Infinity, rebuild: true
        },
        thresholdOut: {
            type: "boolean", rebuild: true
        },
        dotType: {
            type: "select", rebuild: true, options: {
                "": "",
                "sphere": "sphere",
                "point": "point"
            }
        },
        radiusType: {
            type: "select", options: {
                "": "",
                "value": "value",
                "abs-value": "abs-value",
                "value-min": "value-min",
                "deviation": "deviation",
                "size": "size"
            }
        },
        radius: {
            type: "number", precision: 3, max: 10.0, min: 0.001, property: "size"
        },
        scale: {
            type: "number", precision: 3, max: 10.0, min: 0.001
        },
        sphereDetail: {
            type: "integer", max: 3, min: 0, rebuild: "impostor"
        },
        disableImpostor: {
            type: "boolean", rebuild: true
        },

        pointSize: {
            type: "number", precision: 1, max: 100, min: 0, buffer: true
        },
        sizeAttenuation: {
            type: "boolean", buffer: true
        },
        sortParticles: {
            type: "boolean", rebuild: true
        },
        useTexture: {
            type: "boolean", buffer: true
        },
        alphaTest: {
            type: "range", step: 0.001, max: 1, min: 0, buffer: true
        },
        forceTransparent: {
            type: "boolean", buffer: true
        },
        edgeBleach: {
            type: "range", step: 0.001, max: 1, min: 0, buffer: true
        },

    }, NGL.Representation.prototype.parameters, {

        colorScheme: {
            type: "select", update: "color", options: {
                "": "",
                "value": "value",
                "uniform": "uniform",
                // "value-min": "value-min",
                // "deviation": "deviation",
                // "size": "size"
            }
        },

    } ),

    defaultSize: 0.1,

    init: function( params ){

        var p = params || {};
        p.colorScheme = p.colorScheme || "uniform";
        p.colorValue = p.colorValue || 0xDDDDDD;

        if( p.quality === "low" ){
            this.sphereDetail = 0;
        }else if( p.quality === "medium" ){
            this.sphereDetail = 1;
        }else if( p.quality === "high" ){
            this.sphereDetail = 2;
        }else{
            this.sphereDetail = p.sphereDetail || 1;
        }
        this.disableImpostor = p.disableImpostor || false;

        this.thresholdType  = p.thresholdType !== undefined ? p.thresholdType : "sigma";
        this.thresholdMin = p.thresholdMin !== undefined ? p.thresholdMin : 2.0;
        this.thresholdMax = p.thresholdMax !== undefined ? p.thresholdMax : Infinity;
        this.thresholdOut = p.thresholdOut !== undefined ? p.thresholdOut : false;
        this.dotType = p.dotType !== undefined ? p.dotType : "point";
        this.radius = p.radius !== undefined ? p.radius : 0.1;
        this.scale = p.scale !== undefined ? p.scale : 1.0;

        this.pointSize = p.pointSize || 1;
        this.sizeAttenuation = p.sizeAttenuation !== undefined ? p.sizeAttenuation : true;
        this.sortParticles = p.sortParticles !== undefined ? p.sortParticles : false;
        this.useTexture = p.useTexture !== undefined ? p.useTexture : false;
        this.alphaTest = p.alphaTest !== undefined ? p.alphaTest : 0.5;
        this.forceTransparent = p.forceTransparent !== undefined ? p.forceTransparent : false;
        this.edgeBleach = p.edgeBleach !== undefined ? p.edgeBleach : 0.0;

        NGL.Representation.prototype.init.call( this, p );

    },

    attach: function( callback ){

        this.bufferList.forEach( function( buffer ){

            this.viewer.add( buffer );

        }, this );

        this.setVisibility( this.visible );

        callback();

    },

    create: function(){

        var position, color, size, pickingColor;

        if( this.volume ){

            var thresholdMin, thresholdMax;

            if( this.thresholdType === "sigma" ){
                thresholdMin = this.volume.getValueForSigma( this.thresholdMin );
                thresholdMax = this.volume.getValueForSigma( this.thresholdMax );
            }else{
                thresholdMin = this.thresholdMin;
                thresholdMax = this.thresholdMax;
            }
            this.volume.filterData( thresholdMin, thresholdMax, this.thresholdOut );

            position = this.volume.getDataPosition();
            color = this.volume.getDataColor( this.getColorParams() );
            size = this.volume.getDataSize( this.radius, this.scale );
            pickingColor = this.volume.getPickingDataColor( this.getColorParams() );

        }else{

            position = this.surface.getPosition();
            color = this.surface.getColor( this.getColorParams() );
            size = this.surface.getSize( this.radius, this.scale );
            pickingColor = this.surface.getPickingColor( this.getColorParams() );

        }

        if( this.dotType === "sphere" ){

            this.dotBuffer = new NGL.SphereBuffer(
                position,
                color,
                size,
                pickingColor,
                this.getBufferParams( {
                    sphereDetail: this.sphereDetail,
                    disableImpostor: this.disableImpostor,
                    dullInterior: false
                } )
            );

        }else{

            this.dotBuffer = new NGL.PointBuffer(
                position,
                color,
                this.getBufferParams( {
                    pointSize: this.pointSize,
                    sizeAttenuation: this.sizeAttenuation,
                    sortParticles: this.sortParticles,
                    useTexture: this.useTexture,
                    alphaTest: this.alphaTest,
                    forceTransparent: this.forceTransparent,
                    edgeBleach: this.edgeBleach
                } )
            );

        }

        this.bufferList.push( this.dotBuffer );

    },

    update: function( what ){

        if( this.bufferList.length === 0 ) return;

        what = what || {};

        var dotData = {};

        if( what[ "color" ] ){

            if( this.volume ){

                dotData[ "color" ] = this.volume.getDataColor(
                    this.getColorParams()
                );

            }else{

                dotData[ "color" ] = this.surface.getColor(
                    this.getColorParams()
                );

            }

        }

        if( this.dotType === "sphere" && ( what[ "radius" ] || what[ "scale" ] ) ){

            if( this.volume ){

                dotData[ "radius" ] = this.volume.getDataSize(
                    this.radius, this.scale
                );

            }else{

                dotData[ "radius" ] = this.surface.getSize(
                    this.radius, this.scale
                );

            }

        }

        this.dotBuffer.setAttributes( dotData );

    },

    setParameters: function( params, what, rebuild ){

        what = what || {};

        if( params && params[ "thresholdType" ] !== undefined &&
            this.volume instanceof NGL.Volume
        ){

            if( this.thresholdType === "value" &&
                params[ "thresholdType" ] === "sigma"
            ){

                this.thresholdMin = this.volume.getSigmaForValue(
                    this.thresholdMin
                );
                this.thresholdMax = this.volume.getSigmaForValue(
                    this.thresholdMax
                );

            }else if( this.thresholdType === "sigma" &&
                params[ "thresholdType" ] === "value"
            ){

                this.thresholdMin = this.volume.getValueForSigma(
                    this.thresholdMin
                );
                this.thresholdMax = this.volume.getValueForSigma(
                    this.thresholdMax
                );

            }

            this.thresholdType = params[ "thresholdType" ];

        }

        if( params && params[ "radiusType" ] !== undefined ){

            if( params[ "radiusType" ] === "radius" ){
                this.radius = this.defaultSize;
            }else{
                this.radius = params[ "radiusType" ];
            }
            what[ "radius" ] = true;
            if( this.dotType === "sphere" &&
                ( !NGL.extensionFragDepth || this.disableImpostor )
            ){
                rebuild = true;
            }

        }

        if( params && params[ "radius" ] !== undefined ){

            what[ "radius" ] = true;
            if( this.dotType === "sphere" &&
                ( !NGL.extensionFragDepth || this.disableImpostor )
            ){
                rebuild = true;
            }

        }

        if( params && params[ "scale" ] !== undefined ){

            what[ "scale" ] = true;
            if( this.dotType === "sphere" &&
                ( !NGL.extensionFragDepth || this.disableImpostor )
            ){
                rebuild = true;
            }

        }

        NGL.Representation.prototype.setParameters.call(
            this, params, what, rebuild
        );

        return this;

    }

} );


/////////////////////////
// Representation types

( function(){

    NGL.representationTypes = {};
    var reprList = [];

    // find structure representations

    for( var key in NGL ){

        var val = NGL[ key ];

        if( val.prototype instanceof NGL.StructureRepresentation &&
            val.prototype.type
        ){

            reprList.push( val );

        }

    }

    // sort by representation type (i.e. name)

    reprList.sort( function( a, b ){

            return a.prototype.type.localeCompare( b.prototype.type );

    } ).forEach( function( repr ){

        NGL.representationTypes[ repr.prototype.type ] = repr;

    } );

} )();

// File:js/ngl/stage.js

/**
 * @file Stage
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */


//////////
// Stage

NGL.Stage = function( eid, params ){

    var SIGNALS = signals;

    this.signals = {

        parametersChanged: new SIGNALS.Signal(),
        fullscreenChanged: new SIGNALS.Signal(),

        componentAdded: new SIGNALS.Signal(),
        componentRemoved: new SIGNALS.Signal(),

        atomPicked: new SIGNALS.Signal(),
        bondPicked: new SIGNALS.Signal(),
        volumePicked: new SIGNALS.Signal(),
        nothingPicked: new SIGNALS.Signal(),
        onPicking: new SIGNALS.Signal()

    };

    //

    this.tasks = new NGL.Counter();
    this.compList = [];
    this.defaultFileParams = {};

    //

    this.viewer = new NGL.Viewer( eid );
    if( !this.viewer.renderer ) return;

    var p = Object.assign( {
        impostor: true,
        quality: "medium",
        sampleLevel: 0,
        backgroundColor: "black",
        rotateSpeed: 2.0,
        zoomSpeed: 1.2,
        panSpeed: 0.8,
        clipNear: 0,
        clipFar: 100,
        clipDist: 10,
        fogNear: 50,
        fogFar: 100,
        cameraFov: 40,
        cameraType: "perspective",
        lightColor: 0xdddddd,
        lightIntensity: 1.0,
        ambientColor: 0xdddddd,
        ambientIntensity: 0.2
    }, params );
    this.parameters = NGL.deepCopy( NGL.Stage.prototype.parameters );
    this.setParameters( p );  // must come after the viewer has been instantiated

    this.pickingControls = new NGL.PickingControls( this.viewer, this );
    this.viewer.animate();

};

NGL.Stage.prototype = {

    constructor: NGL.Stage,

    parameters: {

        backgroundColor: {
            type: "color"
        },
        quality: {
            type: "select", options: { "low": "low", "medium": "medium", "high": "high" }
        },
        sampleLevel: {
            type: "range", step: 1, max: 5, min: -1
        },
        impostor: {
            type: "boolean"
        },
        rotateSpeed: {
            type: "number", precision: 1, max: 10, min: 0
        },
        zoomSpeed: {
            type: "number", precision: 1, max: 10, min: 0
        },
        panSpeed: {
            type: "number", precision: 1, max: 10, min: 0
        },
        clipNear: {
            type: "range", step: 1, max: 100, min: 0
        },
        clipFar: {
            type: "range", step: 1, max: 100, min: 0
        },
        clipDist: {
            type: "number", precision: 0, max: 200, min: 0
        },
        fogNear: {
            type: "range", step: 1, max: 100, min: 0
        },
        fogFar: {
            type: "range", step: 1, max: 100, min: 0
        },
        cameraType: {
            type: "select", options: { "perspective": "perspective", "orthographic": "orthographic" }
        },
        cameraFov: {
            type: "range", step: 1, max: 120, min: 15
        },
        lightColor: {
            type: "color"
        },
        lightIntensity: {
            type: "number", precision: 2, max: 10, min: 0
        },
        ambientColor: {
            type: "color"
        },
        ambientIntensity: {
            type: "number", precision: 2, max: 10, min: 0
        },

    },

    setParameters: function( params ){

        var p = Object.assign( {}, params );
        var tp = this.parameters;
        var viewer = this.viewer;
        var controls = viewer.controls;

        for( var name in p ){

            if( p[ name ] === undefined ) continue;
            if( !tp[ name ] ) continue;

            if( tp[ name ].int ) p[ name ] = parseInt( p[ name ] );
            if( tp[ name ].float ) p[ name ] = parseFloat( p[ name ] );

            tp[ name ].value = p[ name ];

        }

        // apply parameters
        if( p.quality !== undefined ) this.setQuality( p.quality );
        if( p.impostor !== undefined ) this.setImpostor( p.impostor );
        if( p.rotateSpeed !== undefined ) controls.rotateSpeed = p.rotateSpeed;
        if( p.zoomSpeed !== undefined ) controls.zoomSpeed = p.zoomSpeed;
        if( p.panSpeed !== undefined ) controls.panSpeed = p.panSpeed;
        viewer.setClip( p.clipNear, p.clipFar, p.clipDist );
        viewer.setFog( undefined, p.fogNear, p.fogFar );
        viewer.setCamera( p.cameraType, p.cameraFov );
        viewer.setSampling( p.sampleLevel );
        viewer.setBackground( p.backgroundColor );
        viewer.setLight(
            p.lightColor, p.lightIntensity, p.ambientColor, p.ambientIntensity
        );

        this.signals.parametersChanged.dispatch(
            this.getParameters()
        );

        return this;

    },

    getParameters: function(){

        var params = {};
        for( var name in this.parameters ){
            params[ name ] = this.parameters[ name ].value;
        }
        return params;

    },

    defaultFileRepresentation: function( object ){

        if( object instanceof NGL.StructureComponent ){

            object.setSelection( "/0" );

            var atomCount, instanceCount;
            var structure = object.structure;

            if( structure.biomolDict[ "BU1" ] ){
                var assembly = structure.biomolDict[ "BU1" ];
                atomCount = assembly.getAtomCount( structure );
                instanceCount = assembly.getInstanceCount();
                object.setDefaultAssembly( "BU1" );
            }else{
                atomCount = structure.getModelProxy( 0 ).atomCount;
                instanceCount = 1;
            }

            if( typeof window.orientation !== 'undefined' ){
                atomCount *= 4;
            }

            if( NGL.debug ) console.log( atomCount, instanceCount );

            if( instanceCount > 5 && atomCount > 15000 ){

                var scaleFactor = (
                    Math.min(
                        1.5,
                        Math.max(
                            0.1,
                            2000 / ( atomCount / instanceCount )
                        )
                    )
                );

                object.addRepresentation( "surface", {
                    sele: "polymer",
                    surfaceType: "sas",
                    probeRadius: 0.1,
                    scaleFactor: scaleFactor,
                    colorScheme: "atomindex",
                    colorScale: "RdYlBu",
                    useWorker: false
                } );

            }else if( atomCount > 250000 ){

                object.addRepresentation( "backbone", {
                    lineOnly: true,
                    colorScheme: "atomindex",
                    colorScale: "RdYlBu"
                } );

            }else if( atomCount > 100000 ){

                object.addRepresentation( "backbone", {
                    quality: "low",
                    disableImpostor: true,
                    colorScheme: "atomindex",
                    colorScale: "RdYlBu",
                    scale: 2.0
                } );

            }else if( atomCount > 80000 ){

                object.addRepresentation( "backbone", {
                    colorScheme: "atomindex",
                    colorScale: "RdYlBu",
                    scale: 2.0
                } );

            }else{

                var quality = atomCount < 15000 ? "high" : "medium";

                object.addRepresentation( "cartoon", {
                    color: "atomindex",
                    colorScale: "RdYlBu",
                    scale: 0.7,
                    aspectRatio: 5,
                    quality: quality
                } );
                if( atomCount < 50000 ){
                    object.addRepresentation( "base", {
                        color: "atomindex",
                        colorScale: "RdYlBu",
                        quality: quality
                    } );
                }
                object.addRepresentation( "ball+stick", {
                    sele: "hetero and not ( water or ion )",
                    colorScheme: "element",
                    scale: 2.0,
                    aspectRatio: 1.5,
                    quality: quality
                } );

            }

            this.centerView();

            // add frames as trajectory
            if( object.structure.frames.length ) object.addTrajectory();

        }else if( object instanceof NGL.SurfaceComponent ){

            object.addRepresentation( "surface" );
            this.centerView();

        }

    },

    loadFile: function( path, params ){

        var p = Object.assign( {}, this.defaultFileParams, params );

        // placeholder component
        var component = new NGL.Component( this, p );
        component.name = NGL.getFileInfo( path ).name;
        this.addComponent( component );

        var onLoadFn = function( object ){

            // remove placeholder component
            this.removeComponent( component );

            component = this.addComponentFromObject( object, p );

            if( component instanceof NGL.ScriptComponent ){
                component.run();
            }

            if( p.defaultRepresentation ){
                this.defaultFileRepresentation( component );
            }

            return component;

        }.bind( this );

        var onErrorFn = function( e ){

            component.setStatus( e );
            throw e;

        }

        return NGL.autoLoad( path, p ).then( onLoadFn, onErrorFn );

    },

    addComponent: function( component ){

        if( !component ){

            NGL.warn( "NGL.Stage.addComponent: no component given" );
            return;

        }

        this.compList.push( component );

        this.signals.componentAdded.dispatch( component );

    },

    addComponentFromObject: function( object, params ){

        var component = NGL.makeComponent( this, object, params );

        this.addComponent( component );

        return component;

    },

    removeComponent: function( component ){

        var idx = this.compList.indexOf( component );

        if( idx !== -1 ){

            this.compList.splice( idx, 1 );

        }

        component.dispose();

        this.signals.componentRemoved.dispatch( component );

    },

    removeAllComponents: function( type ){

        this.compList.slice().forEach( function( o, i ){

            if( !type || o instanceof type ){

                this.removeComponent( o );

            }

        }, this );

    },

    handleResize: function(){

        this.viewer.handleResize();

    },

    toggleFullscreen: function( element ){

        if( !document.fullscreenEnabled && !document.mozFullScreenEnabled &&
            !document.webkitFullscreenEnabled && !document.msFullscreenEnabled
        ){
            console.log( "fullscreen mode (currently) not possible" );
            return;
        }

        var self = this;
        element = element || this.viewer.container;
        this.lastFullscreenElement = element;

        //

        function getFullscreenElement(){
            return document.fullscreenElement || document.mozFullScreenElement ||
                document.webkitFullscreenElement || document.msFullscreenElement;
        }

        function resizeElement(){

            if( !getFullscreenElement() && self.lastFullscreenElement ){

                var element = self.lastFullscreenElement;
                element.style.width = element.dataset.normalWidth;
                element.style.height = element.dataset.normalHeight;

                document.removeEventListener( "fullscreenchange", resizeElement );
                document.removeEventListener( "mozfullscreenchange", resizeElement );
                document.removeEventListener( "webkitfullscreenchange", resizeElement );
                document.removeEventListener( "msfullscreenchange", resizeElement );

                self.handleResize();
                self.signals.fullscreenChanged.dispatch( false );

            }

        }

        //

        if( !getFullscreenElement() ){

            element.dataset.normalWidth = element.style.width;
            element.dataset.normalHeight = element.style.height;
            element.style.width = screen.width + "px";
            element.style.height = screen.height + "px";

            if( element.requestFullscreen ){
                element.requestFullscreen();
            }else if( element.msRequestFullscreen ){
                element.msRequestFullscreen();
            }else if( element.mozRequestFullScreen ){
                element.mozRequestFullScreen();
            }else if( element.webkitRequestFullscreen ){
                element.webkitRequestFullscreen();
            }

            document.addEventListener( "fullscreenchange", resizeElement );
            document.addEventListener( "mozfullscreenchange", resizeElement );
            document.addEventListener( "webkitfullscreenchange", resizeElement );
            document.addEventListener( "msfullscreenchange", resizeElement );

            this.handleResize();
            this.signals.fullscreenChanged.dispatch( true );

            // workaround for Safari
            setTimeout( function(){ self.handleResize() }, 100 );

        }else{

            if( document.exitFullscreen ){
                document.exitFullscreen();
            }else if( document.msExitFullscreen ){
                document.msExitFullscreen();
            }else if( document.mozCancelFullScreen ){
                document.mozCancelFullScreen();
            }else if( document.webkitExitFullscreen ){
                document.webkitExitFullscreen();
            }

        }

    },

    centerView: function(){

        if( this.tasks.count > 0 ){

            var centerFn = function( delta, count ){

                if( count === 0 ){

                    this.tasks.signals.countChanged.remove( centerFn, this );

                }

                this.viewer.centerView( true );

            }

            this.tasks.signals.countChanged.add( centerFn, this );

        }

        this.viewer.centerView( true );

    },

    setSpin: function( axis, angle ){

        if( Array.isArray( axis ) ){
            axis = new THREE.Vector3().fromArray( axis );
        }

        this.viewer.setSpin( axis, angle );

    },

    setOrientation: function( orientation ){

        this.tasks.onZeroOnce( function(){

            this.viewer.setOrientation( orientation );

        }, this );

    },

    getOrientation: function(){

        return this.viewer.getOrientation();

    },

    makeImage: function( params ){

        var viewer = this.viewer;
        var tasks = this.tasks;

        return new Promise( function( resolve, reject ){

            function makeImage(){
                tasks.increment();
                viewer.makeImage( params ).then( function( blob ){
                    tasks.decrement();
                    resolve( blob );
                } ).catch( function( e ){
                    tasks.decrement();
                    reject( e );
                } );
            }

            tasks.onZeroOnce( makeImage );

        } );

    },

    setImpostor: function( value ) {

        this.parameters.impostor.value = value;

        var types = [
            "spacefill", "ball+stick", "licorice", "hyperball",
            "backbone", "rocket", "helixorient", "contact", "distance",
            "dot"
        ];

        this.eachRepresentation( function( repr ){

            if( repr instanceof NGL.ScriptComponent ) return;

            if( types.indexOf( repr.getType() ) === -1 ){
                return;
            }

            var p = repr.getParameters();
            p.disableImpostor = !value;
            repr.build( p );

        } );

    },

    setQuality: function( value ) {

        this.parameters.quality.value = value;

        var types = [
            "tube", "cartoon", "ribbon", "trace", "rope"
        ];

        var impostorTypes = [
            "spacefill", "ball+stick", "licorice", "hyperball",
            "backbone", "rocket", "helixorient", "contact", "distance",
            "dot"
        ];

        this.eachRepresentation( function( repr ){

            if( repr instanceof NGL.ScriptComponent ) return;

            var p = repr.getParameters();

            if( types.indexOf( repr.getType() ) === -1 ){

                if( impostorTypes.indexOf( repr.getType() ) === -1 ){
                    return;
                }

                if( NGL.extensionFragDepth && !p.disableImpostor ){
                    repr.repr.quality = value;
                    return;
                }

            }

            p.quality = value;
            repr.build( p );

        } );

    },

    eachComponent: function( callback, type ){

        this.compList.forEach( function( o, i ){

            if( !type || o instanceof type ){

                callback( o, i );

            }

        } );

    },

    eachRepresentation: function( callback, componentType ){

        this.eachComponent( function( comp ){

            comp.reprList.forEach( function( repr ){

                callback( repr, comp );

            } );

        }, componentType );

    },

    getComponentsByName: function( name, componentType ){

        var compList = [];

        this.eachComponent( function( comp ){

            if( name === undefined || comp.name.match( name ) !== null ){
                compList.push( comp );
            }

        }, componentType );

        return new NGL.ComponentCollection( compList );

    },

    getRepresentationsByName: function( name, componentType ){

        var compName, reprName;

        if( typeof name !== "object" ){
            compName = undefined;
            reprName = name;
        }else{
            compName = name.comp;
            reprName = name.repr;
        }

        var reprList = [];

        this.eachRepresentation( function( repr, comp ){

            if( compName !== undefined && comp.name.match( compName ) === null ){
                return;
            }

            if( reprName === undefined || repr.name.match( reprName ) !== null ){
                reprList.push( repr );
            }

        }, componentType );

        return new NGL.RepresentationCollection( reprList );

    },

    getAnythingByName: function( name ){

        var compList = this.getComponentsByName( name ).list;
        var reprList = this.getRepresentationsByName( name ).list;

        return new NGL.Collection( compList.concat( reprList ) );

    },

    dispose: function(){

        this.tasks.dispose();

    }

};


////////////
// Picking

NGL.PickingControls = function( viewer, stage ){

    var position = new THREE.Vector3();

    var mouse = {

        position: new THREE.Vector2(),
        down: new THREE.Vector2(),
        moving: false,
        distance: function(){
            return mouse.position.distanceTo( mouse.down );
        }

    };

    viewer.renderer.domElement.addEventListener( 'mousemove', function( e ){

        e.preventDefault();
        // e.stopPropagation();

        mouse.moving = true;
        mouse.position.x = e.layerX;
        mouse.position.y = e.layerY;

    } );

    viewer.renderer.domElement.addEventListener( 'mousedown', function( e ){

        e.preventDefault();
        // e.stopPropagation();

        mouse.moving = false;
        mouse.down.x = e.layerX;
        mouse.down.y = e.layerY;

    } );

    viewer.renderer.domElement.addEventListener( 'mouseup', function( e ){

        e.preventDefault();
        // e.stopPropagation();

        if( mouse.distance() > 3 || e.which === NGL.RightMouseButton ) return;

        var box = viewer.renderer.domElement.getBoundingClientRect();

        var offsetX = e.clientX - box.left;
        var offsetY = e.clientY - box.top;

        var pickingData = viewer.pick(
            offsetX,
            box.height - offsetY
        );
        var gid = pickingData.gid;
        var instance = pickingData.instance;

        var pickedAtom = undefined;
        var pickedBond = undefined;
        var pickedVolume = undefined;

        var picked = NGL.GidPool.getByGid( gid );

        if( picked && picked.type === "AtomProxy" ){

            pickedAtom = picked;

        }else if( picked && picked.type === "BondProxy" ){

            pickedBond = picked;

        }else if( picked && picked && picked.volume.type === "Volume" ){

            pickedVolume = picked;

        }

        //

        if( ( pickedAtom || pickedBond || pickedVolume ) &&
                e.which === NGL.MiddleMouseButton
        ){

            if( pickedAtom ){

                position.copy( pickedAtom );

            }else if( pickedBond ){

                position.set( 0, 0, 0 )
                    .addVectors( pickedBond.atom1, pickedBond.atom2 )
                    .multiplyScalar( 0.5 );

            }else if( pickedVolume ){

                position.copy( pickedVolume );

            }

            if( instance ){

                position.applyProjection( instance.matrix );

            }

            viewer.centerView( false, position );

        }

        //

        if( pickedAtom ){

            stage.signals.atomPicked.dispatch( pickedAtom );

        }else if( pickedBond ){

            stage.signals.bondPicked.dispatch( pickedBond );

        }else if( pickedVolume ){

            stage.signals.volumePicked.dispatch( pickedVolume );

        }else{

            stage.signals.nothingPicked.dispatch();

        }

        stage.signals.onPicking.dispatch( {

            "atom": pickedAtom,
            "bond": pickedBond,
            "volume": pickedVolume,
            "instance": instance

        } );

        //

        if( NGL.debug ){

            NGL.log( "picked atom", pickedAtom );
            NGL.log( "picked bond", pickedBond );
            NGL.log( "picked volume", pickedVolume );

        }

    } );

};


//////////////
// Component

NGL.makeComponent = function( stage, object, params ){

    var component;

    if( object instanceof NGL.Structure ){

        component = new NGL.StructureComponent( stage, object, params );

    }else if( object instanceof NGL.Surface || object instanceof NGL.Volume ){

        component = new NGL.SurfaceComponent( stage, object, params );

    }else if( object instanceof NGL.Script ){

        component = new NGL.ScriptComponent( stage, object, params );

    }else{

        NGL.warn( "NGL.makeComponent: object type unknown", object );

    }

    return component;

};


NGL.nextComponentId = 0;


NGL.Component = function( stage, params ){

    Object.defineProperty( this, 'id', { value: NGL.nextComponentId++ } );

    var p = params || {};

    this.name = p.name;
    this.uuid = THREE.Math.generateUUID();
    this.visible = p.visible !== undefined ? p.visible : true;

    // construct instance signals
    var signalNames = Object.keys( this.signals );
    this.signals = {};
    signalNames.forEach( function( name ){
        this.signals[ name ] = new signals.Signal();
    }, this );

    this.stage = stage;
    this.viewer = stage.viewer;

    this.reprList = [];

}

NGL.Component.prototype = {

    constructor: NGL.Component,

    type: "component",

    signals: {

        representationAdded: null,
        representationRemoved: null,
        visibilityChanged: null,

        statusChanged: null,
        nameChanged: null,
        disposed: null,

    },

    addRepresentation: function( type, object, params ){

        var p = params || {};
        var sp = this.stage.getParameters();
        p.quality = p.quality || sp.quality;
        p.disableImpostor = NGL.defaults( p.disableImpostor, !sp.impostor );
        p.visible = NGL.defaults( p.visible, true );

        var p2 = Object.assign( {}, p, { visible: this.visible && p.visible } );

        var repr = NGL.makeRepresentation(
            type, object, this.viewer, p2
        );

        var reprComp = new NGL.RepresentationComponent(
            this.stage, repr, p, this
        );

        this.reprList.push( reprComp );

        this.signals.representationAdded.dispatch( reprComp );

        return reprComp;

    },

    addBufferRepresentation: function( buffer, params ){

        return NGL.Component.prototype.addRepresentation.call(
            this, "buffer", buffer, params
        );

    },

    removeRepresentation: function( repr ){

        var idx = this.reprList.indexOf( repr );

        if( idx !== -1 ){

            this.reprList.splice( idx, 1 );

        }

        this.signals.representationRemoved.dispatch( repr );

    },

    updateRepresentations: function( what ){

        this.reprList.forEach( function( repr ){

            repr.update( what );

        } );

        this.stage.viewer.requestRender();

    },

    clearRepresentations: function(){

        // copy via .slice because side effects may change reprList
        this.reprList.slice().forEach( function( repr ){

            repr.dispose();

        } );

    },

    dispose: function(){

        this.clearRepresentations();

        delete this.reprList;

        this.signals.disposed.dispatch();

    },

    setVisibility: function( value ){

        this.visible = value;

        this.eachRepresentation( function( repr ){

            repr.updateVisibility();

        } );

        this.signals.visibilityChanged.dispatch( value );

        return this;

    },

    setStatus: function( value ){

        this.status = value;
        this.signals.statusChanged.dispatch( value );

        return this;

    },

    setName: function( value ){

        this.name = value;
        this.signals.nameChanged.dispatch( value );

        return this;

    },

    getCenter: function(){

        // NGL.warn( "not implemented" )

    },

    eachRepresentation: function( callback ){

        this.reprList.forEach( callback );

    }

};


NGL.StructureComponent = function( stage, structure, params ){

    var p = params || {};
    p.name = p.name !== undefined ? p.name : structure.name;

    NGL.Component.call( this, stage, p );

    this.structure = structure;
    this.trajList = [];
    this.initSelection( p.sele );
    this.setDefaultAssembly( p.assembly || "" );

};

NGL.StructureComponent.prototype = NGL.createObject(

    NGL.Component.prototype, {

    constructor: NGL.StructureComponent,

    type: "structure",

    signals: Object.assign( {

        trajectoryAdded: null,
        trajectoryRemoved: null,
        defaultAssemblyChanged: null

    }, NGL.Component.prototype.signals ),

    initSelection: function( string ){

        this.selection = new NGL.Selection( string );

        this.selection.signals.stringChanged.add( function( string ){

            this.applySelection();

            this.rebuildRepresentations();
            this.rebuildTrajectories();

        }, this );

        this.applySelection();

    },

    applySelection: function(){

        this.structure.setSelection( this.selection );

    },

    setSelection: function( string ){

        this.selection.setString( string );

        return this;

    },

    setDefaultAssembly: function( value ){

        this.defaultAssembly = value;
        this.rebuildRepresentations();
        this.signals.defaultAssemblyChanged.dispatch( value );

    },

    rebuildRepresentations: function(){

        this.reprList.forEach( function( repr ){

            var p = repr.getParameters();
            p.defaultAssembly = this.defaultAssembly;

            repr.build( p );

        }, this );

    },

    rebuildTrajectories: function(){

        this.trajList.slice( 0 ).forEach( function( trajComp ){

            trajComp.trajectory.setStructure( this.structure );

        }, this );

    },

    addRepresentation: function( type, params ){

        var p = params || {};
        p.defaultAssembly = this.defaultAssembly;

        return NGL.Component.prototype.addRepresentation.call(
            this, type, this.structure, p
        );

    },

    addTrajectory: function( trajPath, sele, i ){

        var params = { "i": i };

        var traj = NGL.makeTrajectory(
            trajPath, this.structure, sele
        );

        traj.signals.frameChanged.add( function( value ){

            this.updateRepresentations( { "position": true } );

        }, this );

        var trajComp = new NGL.TrajectoryComponent(
            this.stage, traj, params, this
        );

        this.trajList.push( trajComp );

        this.signals.trajectoryAdded.dispatch( trajComp );

        return trajComp;

    },

    removeTrajectory: function( traj ){

        var idx = this.trajList.indexOf( traj );

        if( idx !== -1 ){

            this.trajList.splice( idx, 1 );

        }

        traj.dispose();

        this.signals.trajectoryRemoved.dispatch( traj );

    },

    dispose: function(){

        // copy via .slice because side effects may change trajList
        this.trajList.slice().forEach( function( traj ){

            traj.dispose();

        } );

        this.trajList = [];
        this.structure.dispose();

        NGL.Component.prototype.dispose.call( this );

    },

    centerView: function( zoom, sele ){

        zoom = zoom !== undefined ? zoom : true;

        var center;

        if( sele ){

            var selection = new NGL.Selection( sele );

            center = this.structure.atomCenter( selection );

            if( zoom ){
                var bb = this.structure.getBoundingBox( selection );
                zoom = bb.size().length();
            }

        }else{

            center = this.structure.center;

            if( zoom ){
                zoom = this.structure.boundingBox.size().length();
            }

        }

        this.viewer.centerView( zoom, center );

        return this;

    },

    getCenter: function(){

        return this.structure.center;

    },

    superpose: function( component, align, sele1, sele2, xsele1, xsele2 ){

        // FIXME does not account for structure.atomBitSet

        NGL.superpose(
            this.structure, component.structure,
            align, sele1, sele2, xsele1, xsele2
        );

        this.updateRepresentations( { "position": true } );

        return this;

    },

    setVisibility: function( value ){

        NGL.Component.prototype.setVisibility.call( this, value );

        this.trajList.forEach( function( traj ){

            // FIXME ???
            traj.setVisibility( value );

        } );

        return this;

    },

} );


NGL.SurfaceComponent = function( stage, surface, params ){

    var p = params || {};
    p.name = p.name !== undefined ? p.name : surface.name;

    NGL.Component.call( this, stage, p );

    this.surface = surface;

};

NGL.SurfaceComponent.prototype = NGL.createObject(

    NGL.Component.prototype, {

    constructor: NGL.SurfaceComponent,

    type: "surface",

    addRepresentation: function( type, params ){

        return NGL.Component.prototype.addRepresentation.call(
            this, type, this.surface, params
        );

    },

    dispose: function(){

        this.surface.dispose();

        NGL.Component.prototype.dispose.call( this );

    },

    centerView: function( zoom ){

        var center = this.surface.center;

        if( zoom ){
            zoom = this.surface.boundingBox.size().length();
        }

        this.viewer.centerView( zoom, center );

    },

} );


NGL.TrajectoryComponent = function( stage, trajectory, params, parent ){

    var p = params || {};
    p.name = p.name !== undefined ? p.name : trajectory.name;

    NGL.Component.call( this, stage, p );

    this.trajectory = trajectory;
    this.parent = parent;
    this.status = "loaded";

    // signals

    trajectory.signals.frameChanged.add( function( i ){

        this.signals.frameChanged.dispatch( i );

    }, this );

    trajectory.signals.playerChanged.add( function( player ){

        this.signals.playerChanged.dispatch( player );

    }, this );

    trajectory.signals.gotNumframes.add( function( n ){

        this.signals.gotNumframes.dispatch( n );

    }, this );

    //

    if( p.i !== undefined ){

        this.setFrame( p.i );

    }

};

NGL.TrajectoryComponent.prototype = NGL.createObject(

    NGL.Component.prototype, {

    constructor: NGL.TrajectoryComponent,

    type: "trajectory",

    signals: Object.assign( {

        frameChanged: null,
        playerChanged: null,
        gotNumframes: null,
        parametersChanged: null

    }, NGL.Component.prototype.signals ),

    addRepresentation: function( type, params ){

        return NGL.Component.prototype.addRepresentation.call(
            this, type, this.trajectory, params
        );

    },

    setFrame: function( i ){

        this.trajectory.setFrame( i );

    },

    setParameters: function( params ){

        this.trajectory.setParameters( params );
        this.signals.parametersChanged.dispatch( params );

        return this;

    },

    dispose: function(){

        this.trajectory.dispose();

        NGL.Component.prototype.dispose.call( this );

    },

    getCenter: function(){}

} );


NGL.ScriptComponent = function( stage, script, params ){

    var p = params || {};
    p.name = p.name !== undefined ? p.name : script.name;

    NGL.Component.call( this, stage, p );

    this.script = script;
    this.status = "loaded";

    this.script.signals.nameChanged.add( function( value ){

        this.setName( value );

    }, this );

};

NGL.ScriptComponent.prototype = NGL.createObject(

    NGL.Component.prototype, {

    constructor: NGL.ScriptComponent,

    type: "script",

    addRepresentation: function( type ){},

    removeRepresentation: function( repr ){},

    run: function(){

        var scope = this;

        this.setStatus( "running" );

        this.script.call( this.stage, function(){

            scope.setStatus( "finished" );

        } );

        this.setStatus( "called" );

    },

    dispose: function(){

        this.signals.disposed.dispatch();

    },

    setVisibility: function( value ){},

    getCenter: function(){}

} );


NGL.RepresentationComponent = function( stage, repr, params, parent ){

    var p = params || {};
    p.name = p.name !== undefined ? p.name : repr.type;

    NGL.Component.call( this, stage, p );

    this.parent = parent;

    this.setRepresentation( repr );

};

NGL.RepresentationComponent.prototype = NGL.createObject(

    NGL.Component.prototype, {

    constructor: NGL.RepresentationComponent,

    type: "representation",

    signals: Object.assign( {

        parametersChanged: null,

    }, NGL.Component.prototype.signals ),

    getType: function(){

        return this.repr.type;

    },

    setRepresentation: function( repr ){

        if( this.repr ){
            this.repr.dispose();
        }

        this.repr = repr;
        // this.name = repr.type;

        this.stage.tasks.listen( this.repr.tasks )

        this.updateVisibility();

    },

    addRepresentation: function( type ){},

    removeRepresentation: function( repr ){},

    dispose: function(){

        if( this.parent ){

            this.parent.removeRepresentation( this );

        }

        this.repr.dispose();

        this.signals.disposed.dispatch();

    },

    setVisibility: function( value ){

        this.visible = value;
        this.updateVisibility();
        this.signals.visibilityChanged.dispatch( this.visible );

        return this;

    },

    updateVisibility: function(){

        if( this.parent ){

            this.repr.setVisibility( this.parent.visible && this.visible );

        }else{

            this.repr.setVisibility( this.visible );

        }

    },

    update: function( what ){

        this.repr.update( what );

        return this;

    },

    build: function( params ){

        this.repr.build( params );

        return this;

    },

    setSelection: function( string ){

        this.repr.setSelection( string );

        return this;

    },

    setParameters: function( params ){

        this.repr.setParameters( params );
        this.signals.parametersChanged.dispatch(
            this.repr.getParameters()
        );

        return this;

    },

    getParameters: function(){

        return this.repr.getParameters();

    },

    setColor: function( value ){

        this.repr.setColor( value );

        return this;

    },

    getCenter: function(){}

} );


///////////////
// Collection

NGL.Collection = function( list ){

    this.list = list || [];

    // remove elements from list when they get disposed

    var n = this.list.length;

    for( var i = 0; i < n; ++i ){

        var elm = this.list[ i ];

        elm.signals.disposed.add( function(){

            this._remove( elm );

        }, this );

    }

};

NGL.Collection.prototype = {

    constructor: NGL.Collection,

    _remove: function( elm ){

        var idx = this.list.indexOf( elm );

        if( idx !== -1 ){

            this.list.splice( idx, 1 );

        }

    },

    _invoke: function( methodName, methodArgs ){

        var n = this.list.length;

        for( var i = 0; i < n; ++i ){

            var elm = this.list[ i ];
            var method = elm[ methodName ];

            if( typeof method === "function" ){

                method.apply( elm, methodArgs );

            }

        }

        return this;

    },

    setVisibility: function( value ){

        return this._invoke( "setVisibility", [ value ] );

    },

    setSelection: function( string ){

        return this._invoke( "setSelection", [ string ] );

    },

    dispose: function(){

        return this._invoke( "dispose" );

    }

};


NGL.ComponentCollection = function( compList ){

    NGL.Collection.call( this, compList );

};

NGL.ComponentCollection.prototype = NGL.createObject(

    NGL.Collection.prototype, {

    constructor: NGL.ComponentCollection,

    addRepresentation: function( name, params ){

        return this._invoke( "addRepresentation", [ name, params ] );

    },

    centerView: function( zoom, sele ){

        return this._invoke( "centerView", [ zoom, sele ] );

    }

} );


NGL.RepresentationCollection = function( reprList ){

    NGL.Collection.call( this, reprList );

};

NGL.RepresentationCollection.prototype = NGL.createObject(

    NGL.Collection.prototype, {

    constructor: NGL.RepresentationCollection,

    setParameters: function( params ){

        return this._invoke( "setParameters", [ params ] );

    },

    setColor: function( color ){

        return this._invoke( "setColor", [ color ] );

    }

} );

// File:shader/CylinderImpostor.vert

NGL.Resources[ 'shader/CylinderImpostor.vert' ] = "// Open-Source PyMOL is Copyright (C) Schrodinger, LLC.\n//\n//  All Rights Reserved\n//\n//  Permission to use, copy, modify, distribute, and distribute modified\n//  versions of this software and its built-in documentation for any\n//  purpose and without fee is hereby granted, provided that the above\n//  copyright notice appears in all copies and that both the copyright\n//  notice and this permission notice appear in supporting documentation,\n//  and that the name of Schrodinger, LLC not be used in advertising or\n//  publicity pertaining to distribution of the software without specific,\n//  written prior permission.\n//\n//  SCHRODINGER, LLC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,\n//  INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN\n//  NO EVENT SHALL SCHRODINGER, LLC BE LIABLE FOR ANY SPECIAL, INDIRECT OR\n//  CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\n//  OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE\n//  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE\n//  USE OR PERFORMANCE OF THIS SOFTWARE.\n\n// Contributions by Alexander Rose\n// - ported to WebGL\n// - dual color\n// - picking color\n// - shift\n\nattribute vec3 mapping;\nattribute vec3 position1;\nattribute vec3 position2;\nattribute float radius;\n\nvarying vec3 axis;\nvarying vec4 base_radius;\nvarying vec4 end_b;\nvarying vec3 U;\nvarying vec3 V;\nvarying vec4 w;\n\n#ifdef PICKING\n    attribute vec3 pickingColor;\n    attribute vec3 pickingColor2;\n    varying vec3 vPickingColor;\n    varying vec3 vPickingColor2;\n#else\n    // attribute vec3 color;\n    attribute vec3 color2;\n    varying vec3 vColor1;\n    varying vec3 vColor2;\n#endif\n\nuniform mat4 modelViewMatrixInverse;\nuniform float shift;\nuniform float ortho;\n\nvoid main(){\n\n    #ifdef PICKING\n        vPickingColor = pickingColor;\n        vPickingColor2 = pickingColor2;\n    #else\n        vColor1 = color;\n        vColor2 = color2;\n    #endif\n\n    // vRadius = radius;\n    base_radius.w = radius;\n\n    vec3 center = position;\n    vec3 dir = normalize( position2 - position1 );\n    float ext = length( position2 - position1 ) / 2.0;\n\n    // using cameraPosition fails on some machines, not sure why\n    // vec3 cam_dir = normalize( cameraPosition - mix( center, vec3( 0.0 ), ortho ) );\n    vec3 cam_dir;\n    if( ortho == 0.0 ){\n        cam_dir = ( modelViewMatrixInverse * vec4( 0, 0, 0, 1 ) ).xyz - center;\n    }else{\n        cam_dir = ( modelViewMatrixInverse * vec4( 0, 0, 1, 0 ) ).xyz;\n    }\n    cam_dir = normalize( cam_dir );\n\n    vec3 ldir;\n\n    float b = dot( cam_dir, dir );\n    end_b.w = b;\n    if( b < 0.0 ) // direction vector looks away, so flip\n        ldir = -ext * dir;\n    else // direction vector already looks in my direction\n        ldir = ext * dir;\n\n    vec3 left = normalize( cross( cam_dir, ldir ) );\n    vec3 leftShift = shift * left * radius;\n    if( b < 0.0 )\n        leftShift *= -1.0;\n    left = radius * left;\n    vec3 up = radius * normalize( cross( left, ldir ) );\n\n    // transform to modelview coordinates\n    axis = normalize( normalMatrix * ldir );\n    U = normalize( normalMatrix * up );\n    V = normalize( normalMatrix * left );\n\n    vec4 base4 = modelViewMatrix * vec4( center - ldir + leftShift, 1.0 );\n    base_radius.xyz = base4.xyz / base4.w;\n\n    vec4 top_position = modelViewMatrix * vec4( center + ldir + leftShift, 1.0 );\n    vec4 end4 = top_position;\n    end_b.xyz = end4.xyz / end4.w;\n\n    w = modelViewMatrix * vec4(\n        center + leftShift + mapping.x*ldir + mapping.y*left + mapping.z*up, 1.0\n    );\n\n    gl_Position = projectionMatrix * w;\n\n    // avoid clipping (1.0 seems to induce flickering with some drivers)\n    gl_Position.z = 0.99;\n\n}";

// File:shader/CylinderImpostor.frag

NGL.Resources[ 'shader/CylinderImpostor.frag' ] = "#define STANDARD\n#define IMPOSTOR\n\n// Open-Source PyMOL is Copyright (C) Schrodinger, LLC.\n//\n//  All Rights Reserved\n//\n//  Permission to use, copy, modify, distribute, and distribute modified\n//  versions of this software and its built-in documentation for any\n//  purpose and without fee is hereby granted, provided that the above\n//  copyright notice appears in all copies and that both the copyright\n//  notice and this permission notice appear in supporting documentation,\n//  and that the name of Schrodinger, LLC not be used in advertising or\n//  publicity pertaining to distribution of the software without specific,\n//  written prior permission.\n//\n//  SCHRODINGER, LLC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,\n//  INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN\n//  NO EVENT SHALL SCHRODINGER, LLC BE LIABLE FOR ANY SPECIAL, INDIRECT OR\n//  CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\n//  OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE\n//  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE\n//  USE OR PERFORMANCE OF THIS SOFTWARE.\n\n// Contributions by Alexander Rose\n// - ported to WebGL\n// - dual color\n// - picking color\n// - custom clipping\n// - three.js lighting\n\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\nuniform float nearClip;\nuniform mat4 projectionMatrix;\nuniform float ortho;\n\nvarying vec3 axis;\nvarying vec4 base_radius;\nvarying vec4 end_b;\nvarying vec3 U;\nvarying vec3 V;\nvarying vec4 w;\n\n#ifdef PICKING\n    uniform float objectId;\n    varying vec3 vPickingColor;\n    varying vec3 vPickingColor2;\n#else\n    varying vec3 vColor1;\n    varying vec3 vColor2;\n    #include common\n    #include fog_pars_fragment\n    #include bsdfs\n    #include ambient_pars\n    #include lights_pars\n    #include lights_physical_pars_fragment\n#endif\n\nbool interior = false;\n\nfloat distSq3( vec3 v3a, vec3 v3b ){\n\n    return (\n        ( v3a.x - v3b.x ) * ( v3a.x - v3b.x ) +\n        ( v3a.y - v3b.y ) * ( v3a.y - v3b.y ) +\n        ( v3a.z - v3b.z ) * ( v3a.z - v3b.z )\n    );\n\n}\n\n// round caps\n// http://sourceforge.net/p/pymol/code/HEAD/tree/trunk/pymol/data/shaders/cylinder.fs\n\n// void main2(void)\n// {\n//     #ifdef PICKING\n//         gl_FragColor = vec4( vPickingColor, 1.0 );\n//     #else\n//         gl_FragColor = vec4( vColor, 1.0 );\n//     #endif\n// }\n\n// Calculate depth based on the given camera position.\nfloat calcDepth( in vec3 cameraPos ){\n    vec2 clipZW = cameraPos.z * projectionMatrix[2].zw + projectionMatrix[3].zw;\n    return 0.5 + 0.5 * clipZW.x / clipZW.y;\n}\n\nfloat calcClip( vec3 cameraPos ){\n    return dot( vec4( cameraPos, 1.0 ), vec4( 0.0, 0.0, 1.0, nearClip - 0.5 ) );\n}\n\nvoid main(){\n\n    vec3 point = w.xyz / w.w;\n\n    // unpacking\n    vec3 base = base_radius.xyz;\n    float vRadius = base_radius.w;\n    vec3 end = end_b.xyz;\n    float b = end_b.w;\n\n    vec3 end_cyl = end;\n    vec3 surface_point = point;\n\n    vec3 ray_target = surface_point;\n    vec3 ray_origin = vec3(0.0);\n    vec3 ray_direction = mix(normalize(ray_origin - ray_target), vec3(0.0, 0.0, 1.0), ortho);\n    mat3 basis = mat3( U, V, axis );\n\n    vec3 diff = ray_target - 0.5 * (base + end_cyl);\n    vec3 P = diff * basis;\n\n    // angle (cos) between cylinder cylinder_axis and ray direction\n    float dz = dot( axis, ray_direction );\n\n    float radius2 = vRadius*vRadius;\n\n    // calculate distance to the cylinder from ray origin\n    vec3 D = vec3(dot(U, ray_direction),\n                dot(V, ray_direction),\n                dz);\n    float a0 = P.x*P.x + P.y*P.y - radius2;\n    float a1 = P.x*D.x + P.y*D.y;\n    float a2 = D.x*D.x + D.y*D.y;\n\n    // calculate a dicriminant of the above quadratic equation\n    float d = a1*a1 - a0*a2;\n    if (d < 0.0)\n        // outside of the cylinder\n        discard;\n\n    float dist = (-a1 + sqrt(d)) / a2;\n\n    // point of intersection on cylinder surface\n    vec3 new_point = ray_target + dist * ray_direction;\n\n    vec3 tmp_point = new_point - base;\n    vec3 _normal = normalize( tmp_point - axis * dot(tmp_point, axis) );\n\n    ray_origin = mix( ray_origin, surface_point, ortho );\n\n    // test front cap\n    float cap_test = dot( new_point - base, axis );\n\n    // to calculate caps, simply check the angle between\n    // the point of intersection - cylinder end vector\n    // and a cap plane normal (which is the cylinder cylinder_axis)\n    // if the angle < 0, the point is outside of cylinder\n    // test front cap\n\n    #ifndef CAP\n        vec3 new_point2 = ray_target + ( (-a1 - sqrt(d)) / a2 ) * ray_direction;\n        vec3 tmp_point2 = new_point2 - base;\n    #endif\n\n    // flat\n    if (cap_test < 0.0)\n    {\n        // ray-plane intersection\n        float dNV = dot(-axis, ray_direction);\n        if (dNV < 0.0)\n            discard;\n        float near = dot(-axis, (base)) / dNV;\n        new_point = ray_direction * near + ray_origin;\n        // within the cap radius?\n        if (dot(new_point - base, new_point-base) > radius2)\n            discard;\n\n        #ifdef CAP\n            _normal = axis;\n        #else\n            _normal = -normalize( tmp_point2 - axis * dot(tmp_point2, axis) );\n        #endif\n    }\n\n    // test end cap\n    cap_test = dot((new_point - end_cyl), axis);\n\n    // flat\n    if( cap_test > 0.0 )\n    {\n        // ray-plane intersection\n        float dNV = dot(axis, ray_direction);\n        if (dNV < 0.0)\n            discard;\n        float near = dot(axis, end_cyl) / dNV;\n        new_point = ray_direction * near + ray_origin;\n        // within the cap radius?\n        if( dot(new_point - end_cyl, new_point-base) > radius2 )\n            discard;\n\n        #ifdef CAP\n            _normal = axis;\n        #else\n            _normal = -normalize( tmp_point2 - axis * dot(tmp_point2, axis) );\n        #endif\n    }\n\n    gl_FragDepthEXT = calcDepth( new_point );\n\n    #ifdef NEAR_CLIP\n        if( calcClip( new_point ) > 0.0 ){\n            dist = (-a1 - sqrt(d)) / a2;\n            new_point = ray_target + dist * ray_direction;\n            if( calcClip( new_point ) > 0.0 )\n                discard;\n            interior = true;\n            gl_FragDepthEXT = calcDepth( new_point );\n            if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = max( 0.0, calcDepth( vec3( - ( nearClip - 0.5 ) ) ) + ( 0.0000001 / vRadius ) );\n            }\n        }else if( gl_FragDepthEXT <= 0.0 ){\n            dist = (-a1 - sqrt(d)) / a2;\n            new_point = ray_target + dist * ray_direction;\n            interior = true;\n            gl_FragDepthEXT = calcDepth( new_point );\n            if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );\n            }\n        }\n    #else\n        if( gl_FragDepthEXT <= 0.0 ){\n            dist = (-a1 - sqrt(d)) / a2;\n            new_point = ray_target + dist * ray_direction;\n            interior = true;\n            gl_FragDepthEXT = calcDepth( new_point );\n            if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );\n            }\n        }\n    #endif\n\n    // this is a workaround necessary for Mac\n    // otherwise the modified fragment won't clip properly\n    if (gl_FragDepthEXT < 0.0)\n        discard;\n    if (gl_FragDepthEXT > 1.0)\n        discard;\n\n    #ifdef PICKING\n\n        if( distSq3( new_point, end_cyl ) < distSq3( new_point, base ) ){\n            if( b < 0.0 ){\n                gl_FragColor = vec4( vPickingColor, objectId );\n            }else{\n                gl_FragColor = vec4( vPickingColor2, objectId );\n            }\n        }else{\n            if( b > 0.0 ){\n                gl_FragColor = vec4( vPickingColor, objectId );\n            }else{\n                gl_FragColor = vec4( vPickingColor2, objectId );\n            }\n        }\n\n    #else\n\n        vec3 vViewPosition = -new_point;\n        vec3 vNormal = _normal;\n        vec3 vColor;\n\n        if( distSq3( new_point, end_cyl ) < distSq3( new_point, base ) ){\n            if( b < 0.0 ){\n                vColor = vColor1;\n            }else{\n                vColor = vColor2;\n            }\n        }else{\n            if( b > 0.0 ){\n                vColor = vColor1;\n            }else{\n                vColor = vColor2;\n            }\n        }\n\n        vec4 diffuseColor = vec4( diffuse, opacity );\n        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n        vec3 totalEmissiveLight = emissive;\n\n        #include color_fragment\n        #include roughnessmap_fragment\n        #include metalnessmap_fragment\n\n        vec3 normal = normalize( vNormal );  // don't use #include normal_fragment\n        if( interior ){\n            normal = vec3( 0.0, 0.0, 0.4 );\n        }\n\n        #include lights_physical_fragment\n        #include lights_template\n\n        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;\n\n        gl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\n        #include premultiplied_alpha_fragment\n        #include tonemapping_fragment\n        #include encodings_fragment\n        #include fog_fragment\n\n    #endif\n\n}";

// File:shader/HyperballStickImpostor.vert

NGL.Resources[ 'shader/HyperballStickImpostor.vert' ] = "// Copyright (C) 2010-2011 by\n// Laboratoire de Biochimie Theorique (CNRS),\n// Laboratoire d'Informatique Fondamentale d'Orleans (Universite d'Orleans), (INRIA) and\n// Departement des Sciences de la Simulation et de l'Information (CEA).\n//\n// License: CeCILL-C license (http://www.cecill.info/)\n//\n// Contact: Marc Baaden\n// E-mail: baaden@smplinux.de\n// Webpage: http://hyperballs.sourceforge.net\n\n// Contributions by Alexander Rose\n// - ported to WebGL\n// - dual color\n// - picking color\n\nattribute vec3 mapping;\nattribute float radius;\nattribute float radius2;\nattribute vec3 position1;\nattribute vec3 position2;\n\nvarying mat4 matrix_near;\nvarying vec4 prime1;\nvarying vec4 prime2;\nvarying float vRadius;\nvarying float vRadius2;\n\n#ifdef PICKING\n    attribute vec3 pickingColor;\n    attribute vec3 pickingColor2;\n    varying vec3 vPickingColor;\n    varying vec3 vPickingColor2;\n#else\n    // attribute vec3 color;\n    attribute vec3 color2;\n    varying vec3 vColor1;\n    varying vec3 vColor2;\n#endif\n\nuniform float shrink;\nuniform mat4 modelViewProjectionMatrix;\nuniform mat4 modelViewProjectionMatrixInverse;\n\nvoid main(){\n\n    vRadius = radius;\n    vRadius2 = radius2;\n\n    vec4 spaceposition;\n    vec3 position_atom1;\n    vec3 position_atom2;\n    vec4 vertex_position;\n\n    #ifdef PICKING\n        vPickingColor = pickingColor;\n        vPickingColor2 = pickingColor2;\n    #else\n        vColor1 = color;\n        vColor2 = color2;\n    #endif\n\n    float radius1 = radius;\n\n    position_atom1 = position1;\n    position_atom2 = position2;\n\n    float distance = distance( position_atom1, position_atom2 );\n\n    spaceposition.z = mapping.z * distance;\n\n    if (radius1 > radius2) {\n        spaceposition.y = mapping.y * 1.5 * radius1;\n        spaceposition.x = mapping.x * 1.5 * radius1;\n    } else {\n        spaceposition.y = mapping.y * 1.5 * radius2;\n        spaceposition.x = mapping.x * 1.5 * radius2;\n    }\n    spaceposition.w = 1.0;\n\n    vec4 e3 = vec4( 1.0 );\n    vec3 e1, e1_temp, e2, e2_temp;\n\n    // Calculation of bond direction: e3\n    e3.xyz = normalize(position_atom1-position_atom2);\n\n    // little hack to avoid some problems of precision due to graphic card limitation using float: To improve soon\n    if (e3.z == 0.0) { e3.z = 0.0000000000001;}\n    if ( (position_atom1.x - position_atom2.x) == 0.0) { position_atom1.x += 0.001;}\n    if ( (position_atom1.y - position_atom2.y) == 0.0) { position_atom1.y += 0.001;}\n    if ( (position_atom1.z - position_atom2.z) == 0.0) { position_atom1.z += 0.001;}\n\n    // Focus calculation\n    vec4 focus = vec4( 1.0 );\n    focus.x = ( position_atom1.x*position_atom1.x - position_atom2.x*position_atom2.x +\n        ( radius2*radius2 - radius1*radius1 )*e3.x*e3.x/shrink )/(2.0*(position_atom1.x - position_atom2.x));\n    focus.y = ( position_atom1.y*position_atom1.y - position_atom2.y*position_atom2.y +\n        ( radius2*radius2 - radius1*radius1 )*e3.y*e3.y/shrink )/(2.0*(position_atom1.y - position_atom2.y));\n    focus.z = ( position_atom1.z*position_atom1.z - position_atom2.z*position_atom2.z +\n        ( radius2*radius2 - radius1*radius1 )*e3.z*e3.z/shrink )/(2.0*(position_atom1.z - position_atom2.z));\n\n    // e1 calculation\n    e1.x = 1.0;\n    e1.y = 1.0;\n    e1.z = ( (e3.x*focus.x + e3.y*focus.y + e3.z*focus.z) - e1.x*e3.x - e1.y*e3.y)/e3.z;\n    e1_temp = e1 - focus.xyz;\n    e1 = normalize(e1_temp);\n\n    // e2 calculation\n    e2_temp = e1.yzx * e3.zxy - e1.zxy * e3.yzx;\n    e2 = normalize(e2_temp);\n\n    //ROTATION:\n    // final form of change of basis matrix:\n    mat3 R= mat3( e1.xyz, e2.xyz, e3.xyz );\n    // Apply rotation and translation to the bond primitive\n    vertex_position.xyz = R * spaceposition.xyz;\n    vertex_position.w = 1.0;\n\n    // TRANSLATION:\n    vertex_position.x += (position_atom1.x+position_atom2.x) / 2.0;\n    vertex_position.y += (position_atom1.y+position_atom2.y) / 2.0;\n    vertex_position.z += (position_atom1.z+position_atom2.z) / 2.0;\n\n    // New position\n    gl_Position = modelViewProjectionMatrix * vertex_position;\n\n    vec4 i_near, i_far;\n\n    // Calculate near from position\n    vec4 near = gl_Position;\n    near.z = 0.0 ;\n    near = modelViewProjectionMatrixInverse * near;\n    i_near = near;\n\n    // Calculate far from position\n    vec4 far = gl_Position;\n    far.z = far.w ;\n    i_far = modelViewProjectionMatrixInverse * far;\n\n    prime1 = vec4( position_atom1 - (position_atom1 - focus.xyz)*shrink, 1.0 );\n    prime2 = vec4( position_atom2 - (position_atom2 - focus.xyz)*shrink, 1.0 );\n\n    float Rsquare = (radius1*radius1/shrink) - (\n                        (position_atom1.x - focus.x)*(position_atom1.x - focus.x) +\n                        (position_atom1.y - focus.y)*(position_atom1.y - focus.y) +\n                        (position_atom1.z - focus.z)*(position_atom1.z - focus.z)\n                    );\n\n    focus.w = Rsquare;\n\n    matrix_near = mat4( i_near, i_far, focus, e3 );\n\n    // avoid clipping\n    gl_Position.z = 1.0;\n\n}";

// File:shader/HyperballStickImpostor.frag

NGL.Resources[ 'shader/HyperballStickImpostor.frag' ] = "#define STANDARD\n#define IMPOSTOR\n\n// Copyright (C) 2010-2011 by\n// Laboratoire de Biochimie Theorique (CNRS),\n// Laboratoire d'Informatique Fondamentale d'Orleans (Universite d'Orleans), (INRIA) and\n// Departement des Sciences de la Simulation et de l'Information (CEA).\n//\n// License: CeCILL-C license (http://www.cecill.info/)\n//\n// Contact: Marc Baaden\n// E-mail: baaden@smplinux.de\n// Webpage: http://hyperballs.sourceforge.net\n\n// Contributions by Alexander Rose\n// - ported to WebGL\n// - dual color\n// - picking color\n// - custom clipping\n// - three.js lighting\n\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\nuniform float nearClip;\nuniform float shrink;\nuniform mat4 modelViewMatrix;\nuniform mat4 modelViewProjectionMatrix;\nuniform mat4 modelViewMatrixInverseTranspose;\nuniform mat4 projectionMatrix;\n\nvarying mat4 matrix_near;\nvarying vec4 prime1;\nvarying vec4 prime2;\nvarying float vRadius;\nvarying float vRadius2;\n\n#ifdef PICKING\n    uniform float objectId;\n    varying vec3 vPickingColor;\n    varying vec3 vPickingColor2;\n#else\n    varying vec3 vColor1;\n    varying vec3 vColor2;\n    #include common\n    #include fog_pars_fragment\n    #include bsdfs\n    #include ambient_pars\n    #include lights_pars\n    #include lights_physical_pars_fragment\n#endif\n\nbool interior = false;\n\nfloat calcClip( vec4 cameraPos ){\n    return dot( cameraPos, vec4( 0.0, 0.0, 1.0, nearClip - 0.5 ) );\n}\n\nfloat calcClip( vec3 cameraPos ){\n    return calcClip( vec4( cameraPos, 1.0 ) );\n}\n\nfloat calcDepth( in vec3 cameraPos ){\n    vec2 clipZW = cameraPos.z * projectionMatrix[2].zw + projectionMatrix[3].zw;\n    return 0.5 + 0.5 * clipZW.x / clipZW.y;\n}\n\nstruct Ray {\n    vec3 origin ;\n    vec3 direction ;\n};\n\nbool cutoff_plane (vec3 M, vec3 cutoff, vec3 x3){\n    float a = x3.x;\n    float b = x3.y;\n    float c = x3.z;\n    float d = -x3.x*cutoff.x-x3.y*cutoff.y-x3.z*cutoff.z;\n    float l = a*M.x+b*M.y+c*M.z+d;\n    if (l<0.0) {return true;}\n    else{return false;}\n}\n\nvec3 isect_surf(Ray r, mat4 matrix_coef){\n    vec4 direction = vec4(r.direction, 0.0);\n    vec4 origin = vec4(r.origin, 1.0);\n    float a = dot(direction,(matrix_coef*direction));\n    float b = dot(origin,(matrix_coef*direction));\n    float c = dot(origin,(matrix_coef*origin));\n    float delta =b*b-a*c;\n    gl_FragColor.a = 1.0;\n    if (delta<0.0){\n        discard;\n        // gl_FragColor.a = 0.5;\n    }\n    float t1 =(-b-sqrt(delta))/a;\n\n    // Second solution not necessary if you don't want\n    // to see inside spheres and cylinders, save some fps\n    //float t2 = (-b+sqrt(delta)) / a  ;\n    //float t =(t1<t2) ? t1 : t2;\n\n    return r.origin+t1*r.direction;\n}\n\nvec3 isect_surf2(Ray r, mat4 matrix_coef){\n    vec4 direction = vec4(r.direction, 0.0);\n    vec4 origin = vec4(r.origin, 1.0);\n    float a = dot(direction,(matrix_coef*direction));\n    float b = dot(origin,(matrix_coef*direction));\n    float c = dot(origin,(matrix_coef*origin));\n    float delta =b*b-a*c;\n    gl_FragColor.a = 1.0;\n    if (delta<0.0){\n        discard;\n        // gl_FragColor.a = 0.5;\n    }\n    float t2 =(-b+sqrt(delta))/a;\n\n    return r.origin+t2*r.direction;\n}\n\nRay primary_ray(vec4 near1, vec4 far1){\n    vec3 near=near1.xyz/near1.w;\n    vec3 far=far1.xyz/far1.w;\n    return Ray(near,far-near);\n}\n\nfloat update_z_buffer(vec3 M, mat4 ModelViewP){\n    float  depth1;\n    vec4 Ms=(ModelViewP*vec4(M,1.0));\n    return depth1=(1.0+Ms.z/Ms.w)/2.0;\n}\n\nvoid main(){\n\n    float radius = max( vRadius, vRadius2 );\n\n    vec4 i_near, i_far, focus;\n    vec3 e3, e1, e1_temp, e2;\n\n    i_near = vec4(matrix_near[0][0],matrix_near[0][1],matrix_near[0][2],matrix_near[0][3]);\n    i_far  = vec4(matrix_near[1][0],matrix_near[1][1],matrix_near[1][2],matrix_near[1][3]);\n    focus = vec4(matrix_near[2][0],matrix_near[2][1],matrix_near[2][2],matrix_near[2][3]);\n    e3 = vec3(matrix_near[3][0],matrix_near[3][1],matrix_near[3][2]);\n\n    e1.x = 1.0;\n    e1.y = 1.0;\n    e1.z = ( (e3.x*focus.x + e3.y*focus.y + e3.z*focus.z) - e1.x*e3.x - e1.y*e3.y)/e3.z;\n    e1_temp = e1 - focus.xyz;\n    e1 = normalize(e1_temp);\n\n    e2 = normalize(cross(e1,e3));\n\n    vec4 equation = focus;\n\n    float shrinkfactor = shrink;\n    float t1 = -1.0/(1.0-shrinkfactor);\n    float t2 = 1.0/(shrinkfactor);\n    // float t3 = 2.0/(shrinkfactor);\n\n    vec4 colonne1, colonne2, colonne3, colonne4;\n    mat4 mat;\n\n    vec3 equation1 = vec3(t2,t2,t1);\n\n    float A1 = - e1.x*equation.x - e1.y*equation.y - e1.z*equation.z;\n    float A2 = - e2.x*equation.x - e2.y*equation.y - e2.z*equation.z;\n    float A3 = - e3.x*equation.x - e3.y*equation.y - e3.z*equation.z;\n\n    float A11 = equation1.x*e1.x*e1.x +  equation1.y*e2.x*e2.x + equation1.z*e3.x*e3.x;\n    float A21 = equation1.x*e1.x*e1.y +  equation1.y*e2.x*e2.y + equation1.z*e3.x*e3.y;\n    float A31 = equation1.x*e1.x*e1.z +  equation1.y*e2.x*e2.z + equation1.z*e3.x*e3.z;\n    float A41 = equation1.x*e1.x*A1   +  equation1.y*e2.x*A2   + equation1.z*e3.x*A3;\n\n    float A22 = equation1.x*e1.y*e1.y +  equation1.y*e2.y*e2.y + equation1.z*e3.y*e3.y;\n    float A32 = equation1.x*e1.y*e1.z +  equation1.y*e2.y*e2.z + equation1.z*e3.y*e3.z;\n    float A42 = equation1.x*e1.y*A1   +  equation1.y*e2.y*A2   + equation1.z*e3.y*A3;\n\n    float A33 = equation1.x*e1.z*e1.z +  equation1.y*e2.z*e2.z + equation1.z*e3.z*e3.z;\n    float A43 = equation1.x*e1.z*A1   +  equation1.y*e2.z*A2   + equation1.z*e3.z*A3;\n\n    float A44 = equation1.x*A1*A1 +  equation1.y*A2*A2 + equation1.z*A3*A3 - equation.w;\n\n    colonne1 = vec4(A11,A21,A31,A41);\n    colonne2 = vec4(A21,A22,A32,A42);\n    colonne3 = vec4(A31,A32,A33,A43);\n    colonne4 = vec4(A41,A42,A43,A44);\n\n    mat = mat4(colonne1,colonne2,colonne3,colonne4);\n\n    // Ray calculation using near and far\n    Ray ray = primary_ray(i_near,i_far) ;\n\n    // Intersection between ray and surface for each pixel\n    vec3 M;\n    M = isect_surf(ray, mat);\n\n    // cut the extremities of bonds to superimpose bond and spheres surfaces\n    if (cutoff_plane(M, prime1.xyz, -e3) || cutoff_plane(M, prime2.xyz, e3)){ discard; }\n\n    // Transform normal to model space to view-space\n    vec4 M1 = vec4(M,1.0);\n    vec4 M2 =  mat*M1;\n    // vec3 _normal = normalize( ( modelViewMatrixInverseTranspose * M2 ).xyz );\n    vec3 _normal = ( modelViewMatrixInverseTranspose * M2 ).xyz;\n\n    // Recalculate the depth in function of the new pixel position\n    gl_FragDepthEXT = update_z_buffer(M, modelViewProjectionMatrix) ;\n\n    #ifdef NEAR_CLIP\n        if( calcClip( modelViewMatrix * vec4( M, 1.0 ) ) > 0.0 ){\n            M = isect_surf2(ray, mat);\n            if( calcClip( modelViewMatrix * vec4( M, 1.0 ) ) > 0.0 )\n                discard;\n            interior = true;\n            gl_FragDepthEXT = update_z_buffer(M, modelViewProjectionMatrix) ;\n            if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = max( 0.0, calcDepth( vec3( - ( nearClip - 0.5 ) ) ) + ( 0.0000001 / radius ) );\n            }\n        }else if( gl_FragDepthEXT <= 0.0 ){\n            M = isect_surf2(ray, mat);\n            interior = true;\n            gl_FragDepthEXT = update_z_buffer(M, modelViewProjectionMatrix);\n            if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = 0.0 + ( 0.0000001 / radius );\n            }\n        }\n    #else\n        if( gl_FragDepthEXT <= 0.0 ){\n            M = isect_surf2(ray, mat);\n            interior = true;\n            gl_FragDepthEXT = update_z_buffer(M, modelViewProjectionMatrix) ;\n            if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = 0.0 + ( 0.0000001 / radius );\n            }\n        }\n    #endif\n\n    // cut the extremities of bonds to superimpose bond and spheres surfaces\n    if (cutoff_plane(M, prime1.xyz, -e3) || cutoff_plane(M, prime2.xyz, e3)){ discard; }\n\n    if (gl_FragDepthEXT < 0.0)\n        discard;\n    if (gl_FragDepthEXT > 1.0)\n        discard;\n\n    // Mix the color bond in function of the two atom colors\n    float distance_ratio = ((M.x-prime2.x)*e3.x + (M.y-prime2.y)*e3.y +(M.z-prime2.z)*e3.z) /\n                                distance(prime2.xyz,prime1.xyz);\n\n    #ifdef PICKING\n\n        if( distance_ratio > 0.5 ){\n            gl_FragColor = vec4( vPickingColor, objectId );\n        }else{\n            gl_FragColor = vec4( vPickingColor2, objectId );\n        }\n\n    #else\n\n        vec3 vViewPosition = -( modelViewMatrix * vec4( M, 1.0 ) ).xyz;\n        vec3 vNormal = _normal;\n        vec3 vColor;\n\n        if( distance_ratio>0.5 ){\n            vColor = vColor1;\n        }else{\n            vColor = vColor2;\n        }\n\n        vec4 diffuseColor = vec4( diffuse, opacity );\n        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n        vec3 totalEmissiveLight = emissive;\n\n        #include color_fragment\n        #include roughnessmap_fragment\n        #include metalnessmap_fragment\n\n        vec3 normal = normalize( vNormal );  // don't use #include normal_fragment\n        if( interior ){\n            normal = vec3( 0.0, 0.0, 0.4 );\n        }\n\n        #include lights_physical_fragment\n        #include lights_template\n\n        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;\n\n        gl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\n        #include premultiplied_alpha_fragment\n        #include tonemapping_fragment\n        #include encodings_fragment\n        #include fog_fragment\n\n    #endif\n\n}";

// File:shader/Line.vert

NGL.Resources[ 'shader/Line.vert' ] = "uniform float nearClip;\n\nvarying vec3 vViewPosition;\n\n#include color_pars_vertex\n\nvoid main(){\n\n    #include color_vertex\n    #include begin_vertex\n    #include project_vertex\n\n    vViewPosition = -mvPosition.xyz;\n\n    #include nearclip_vertex\n\n}";

// File:shader/Line.frag

NGL.Resources[ 'shader/Line.frag' ] = "uniform float opacity;\nuniform float nearClip;\n\nvarying vec3 vViewPosition;\n\n#include common\n#include color_pars_fragment\n#include fog_pars_fragment\n\nvoid main(){\n\n    #include nearclip_fragment\n\n    gl_FragColor = vec4( vColor, opacity );\n\n    #include premultiplied_alpha_fragment\n    #include tonemapping_fragment\n    #include encodings_fragment\n    #include fog_fragment\n\n}";

// File:shader/Mesh.vert

NGL.Resources[ 'shader/Mesh.vert' ] = "#define STANDARD\n\nuniform float nearClip;\n\n#if defined( NEAR_CLIP ) || ( !defined( PICKING ) && !defined( NOLIGHT ) )\n    varying vec3 vViewPosition;\n#endif\n\n#if defined( PICKING )\n    attribute vec3 pickingColor;\n    varying vec3 vPickingColor;\n#elif defined( NOLIGHT )\n    varying vec3 vColor;\n#else\n    #include color_pars_vertex\n    #ifndef FLAT_SHADED\n        varying vec3 vNormal;\n    #endif\n#endif\n\n#include common\n\nvoid main(){\n\n    #if defined( PICKING )\n        vPickingColor = pickingColor;\n    #elif defined( NOLIGHT )\n        vColor = color;\n    #else\n        #include color_vertex\n        #include beginnormal_vertex\n        #include defaultnormal_vertex\n        #ifndef FLAT_SHADED  // Normal computed with derivatives when FLAT_SHADED\n            vNormal = normalize( transformedNormal );\n        #endif\n    #endif\n\n    #include begin_vertex\n    #include project_vertex\n\n    #if defined( NEAR_CLIP ) || ( !defined( PICKING ) && !defined( NOLIGHT ) )\n        vViewPosition = -mvPosition.xyz;\n    #endif\n\n    #include nearclip_vertex\n\n}";

// File:shader/Mesh.frag

NGL.Resources[ 'shader/Mesh.frag' ] = "#define STANDARD\n\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\nuniform float nearClip;\n\n#if defined( NEAR_CLIP ) || ( !defined( PICKING ) && !defined( NOLIGHT ) )\n    varying vec3 vViewPosition;\n#endif\n\n#if defined( PICKING )\n    uniform float objectId;\n    varying vec3 vPickingColor;\n#elif defined( NOLIGHT )\n    varying vec3 vColor;\n#else\n    #ifndef FLAT_SHADED\n        varying vec3 vNormal;\n    #endif\n    #include common\n    #include color_pars_fragment\n    #include fog_pars_fragment\n    #include bsdfs\n    #include ambient_pars\n    #include lights_pars\n    #include lights_physical_pars_fragment\n#endif\n\nvoid main(){\n\n    #include nearclip_fragment\n\n    #if defined( PICKING )\n\n        gl_FragColor = vec4( vPickingColor, objectId );\n\n    #elif defined( NOLIGHT )\n\n        gl_FragColor = vec4( vColor, opacity );\n\n    #else\n\n        vec4 diffuseColor = vec4( diffuse, opacity );\n        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n        vec3 totalEmissiveLight = emissive;\n\n        #include color_fragment\n        #include roughnessmap_fragment\n        #include metalnessmap_fragment\n        #include normal_fragment\n\n        #include dull_interior_fragment\n\n        #include lights_physical_fragment\n        #include lights_template\n\n        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;\n\n        gl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\n        #include premultiplied_alpha_fragment\n        #include tonemapping_fragment\n        #include encodings_fragment\n        #include fog_fragment\n\n        #include opaque_back_fragment\n\n    #endif\n\n}";

// File:shader/Point.vert

NGL.Resources[ 'shader/Point.vert' ] = "uniform float nearClip;\nuniform float size;\nuniform float canvasHeight;\nuniform float pixelRatio;\n\nvarying vec3 vViewPosition;\n\n#include color_pars_vertex\n#include common\n\nvoid main(){\n\n    #include color_vertex\n    #include begin_vertex\n    #include project_vertex\n\n    #ifdef USE_SIZEATTENUATION\n        gl_PointSize = size * pixelRatio * ( ( canvasHeight / 2.0 ) / -mvPosition.z );\n    #else\n        gl_PointSize = size * pixelRatio;\n    #endif\n\n    vViewPosition = -mvPosition.xyz;\n\n    #include nearclip_vertex\n\n}";

// File:shader/Point.frag

NGL.Resources[ 'shader/Point.frag' ] = "uniform vec3 diffuse;\nuniform float opacity;\n\nvarying vec3 vViewPosition;\n\n#ifdef USE_MAP\n    uniform sampler2D map;\n#endif\n\n#include common\n#include color_pars_fragment\n#include fog_pars_fragment\n\nvoid main(){\n\n    vec3 outgoingLight = vec3( 0.0 );\n    vec4 diffuseColor = vec4( diffuse, 1.0 );\n\n    #ifdef USE_MAP\n        diffuseColor *= texture2D( map, vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y ) );\n    #endif\n\n    #include color_fragment\n    #include alphatest_fragment\n\n    outgoingLight = diffuseColor.rgb;\n\n    gl_FragColor = vec4( outgoingLight, diffuseColor.a * opacity );\n\n    #include premultiplied_alpha_fragment\n    #include tonemapping_fragment\n    #include encodings_fragment\n    #include fog_fragment\n\n}";

// File:shader/Quad.vert

NGL.Resources[ 'shader/Quad.vert' ] = "varying vec2 vUv;\n\nvoid main() {\n\n    vUv = uv;\n    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n}";

// File:shader/Quad.frag

NGL.Resources[ 'shader/Quad.frag' ] = "varying vec2 vUv;\n\nuniform sampler2D tForeground;\nuniform float scale;\n\nvoid main() {\n\n    vec4 foreground = texture2D( tForeground, vUv );\n    gl_FragColor = foreground * scale;\n\n}";

// File:shader/Ribbon.vert

NGL.Resources[ 'shader/Ribbon.vert' ] = "#define STANDARD\n\nuniform float nearClip;\n\n#if defined( NEAR_CLIP ) || !defined( PICKING )\n    varying vec3 vViewPosition;\n#endif\n\nattribute vec3 dir;\nattribute float size;\n\n#ifdef PICKING\n    attribute vec3 pickingColor;\n    varying vec3 vPickingColor;\n#else\n    #include color_pars_vertex\n    #ifndef FLAT_SHADED\n        varying vec3 vNormal;\n    #endif\n#endif\n\n#include common\n\nvoid main(void){\n\n    #ifdef PICKING\n        vPickingColor = pickingColor;\n    #else\n        #include color_vertex\n        #include beginnormal_vertex\n        #include defaultnormal_vertex\n        #ifndef FLAT_SHADED  // Normal computed with derivatives when FLAT_SHADED\n            vNormal = normalize( transformedNormal );\n        #endif\n    #endif\n\n    #include begin_vertex\n\n    transformed += normalize( dir ) * size;\n\n    #include project_vertex\n\n    #if defined( NEAR_CLIP ) || !defined( PICKING )\n        vViewPosition = -mvPosition.xyz;\n    #endif\n\n    #include nearclip_vertex\n\n}";

// File:shader/SDFFont.vert

NGL.Resources[ 'shader/SDFFont.vert' ] = "uniform float nearClip;\nuniform float xOffset;\nuniform float yOffset;\nuniform float zOffset;\n\nvarying vec3 vViewPosition;\nvarying vec2 texCoord;\n\nattribute vec2 mapping;\nattribute vec2 inputTexCoord;\nattribute float inputSize;\n\n#include color_pars_vertex\n#include common\n\nvoid main(void){\n\n    #include color_vertex\n    texCoord = inputTexCoord;\n\n    vec4 cameraPos = ( modelViewMatrix * vec4( position, 1.0 ) );\n    vec4 cameraCornerPos = vec4( cameraPos.xyz, 1.0 );\n    cameraCornerPos.xy += mapping * inputSize * 0.01;\n    cameraCornerPos.x += xOffset;\n    cameraCornerPos.y += yOffset;\n    cameraCornerPos.xyz += normalize( -cameraCornerPos.xyz ) * zOffset;\n\n    gl_Position = projectionMatrix * cameraCornerPos;\n\n    vViewPosition = -cameraCornerPos.xyz;\n\n    #include nearclip_vertex\n\n}";

// File:shader/SDFFont.frag

NGL.Resources[ 'shader/SDFFont.frag' ] = "uniform sampler2D fontTexture;\nuniform float opacity;\n\nvarying vec3 vViewPosition;\nvarying vec2 texCoord;\n\n#include common\n#include color_pars_fragment\n#include fog_pars_fragment\n\n#ifdef SDF\n    const float smoothness = 16.0;\n#else\n    const float smoothness = 256.0;\n#endif\nconst float gamma = 2.2;\n\nvoid main(){\n\n    // retrieve signed distance\n    float sdf = texture2D( fontTexture, texCoord ).a;\n\n    // perform adaptive anti-aliasing of the edges\n    float w = clamp(\n        smoothness * ( abs( dFdx( texCoord.x ) ) + abs( dFdy( texCoord.y ) ) ),\n        0.0,\n        0.5\n    );\n    float a = smoothstep( 0.5 - w, 0.5 + w, sdf );\n\n    // gamma correction for linear attenuation\n    a = pow( a, 1.0 / gamma );\n    if( a < 0.2 ) discard;\n    a *= opacity;\n\n    vec3 outgoingLight = vColor;\n\n    gl_FragColor = vec4( outgoingLight, a );\n\n    #include premultiplied_alpha_fragment\n    #include tonemapping_fragment\n    #include encodings_fragment\n    #include fog_fragment\n\n}";

// File:shader/SphereImpostor.vert

NGL.Resources[ 'shader/SphereImpostor.vert' ] = "uniform mat4 projectionMatrixInverse;\nuniform float nearClip;\n\nvarying float vRadius;\nvarying float vRadiusSq;\nvarying vec3 vPoint;\nvarying vec3 vPointViewPosition;\n\nattribute vec2 mapping;\nattribute float radius;\n\n#ifdef PICKING\n    attribute vec3 pickingColor;\n    varying vec3 vPickingColor;\n#else\n    #include color_pars_vertex\n#endif\n\nconst mat4 D = mat4(\n    1.0, 0.0, 0.0, 0.0,\n    0.0, 1.0, 0.0, 0.0,\n    0.0, 0.0, 1.0, 0.0,\n    0.0, 0.0, 0.0, -1.0\n);\n\nmat4 transpose( in mat4 inMatrix ) {\n    vec4 i0 = inMatrix[0];\n    vec4 i1 = inMatrix[1];\n    vec4 i2 = inMatrix[2];\n    vec4 i3 = inMatrix[3];\n\n    mat4 outMatrix = mat4(\n        vec4(i0.x, i1.x, i2.x, i3.x),\n        vec4(i0.y, i1.y, i2.y, i3.y),\n        vec4(i0.z, i1.z, i2.z, i3.z),\n        vec4(i0.w, i1.w, i2.w, i3.w)\n    );\n    return outMatrix;\n}\n\n//------------------------------------------------------------------------------\n// Compute point size and center using the technique described in:\n// \"GPU-Based Ray-Casting of Quadratic Surfaces\"\n// by Christian Sigg, Tim Weyrich, Mario Botsch, Markus Gross.\n//\n// Code based on\n/*=========================================================================\n\n Program:   Visualization Toolkit\n Module:    Quadrics_fs.glsl and Quadrics_vs.glsl\n\n Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen\n All rights reserved.\n See Copyright.txt or http://www.kitware.com/Copyright.htm for details.\n\n This software is distributed WITHOUT ANY WARRANTY; without even\n the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n PURPOSE.  See the above copyright notice for more information.\n\n =========================================================================*/\n\n// .NAME Quadrics_fs.glsl and Quadrics_vs.glsl\n// .SECTION Thanks\n// <verbatim>\n//\n//  This file is part of the PointSprites plugin developed and contributed by\n//\n//  Copyright (c) CSCS - Swiss National Supercomputing Centre\n//                EDF - Electricite de France\n//\n//  John Biddiscombe, Ugo Varetto (CSCS)\n//  Stephane Ploix (EDF)\n//\n// </verbatim>\n//\n// Contributions by Alexander Rose\n// - ported to WebGL\n// - adapted to work with quads\nvoid ComputePointSizeAndPositionInClipCoordSphere(){\n\n    vec2 xbc;\n    vec2 ybc;\n\n    mat4 T = mat4(\n        radius, 0.0, 0.0, 0.0,\n        0.0, radius, 0.0, 0.0,\n        0.0, 0.0, radius, 0.0,\n        position.x, position.y, position.z, 1.0\n    );\n\n    mat4 R = transpose( projectionMatrix * modelViewMatrix * T );\n    float A = dot( R[ 3 ], D * R[ 3 ] );\n    float B = -2.0 * dot( R[ 0 ], D * R[ 3 ] );\n    float C = dot( R[ 0 ], D * R[ 0 ] );\n    xbc[ 0 ] = ( -B - sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );\n    xbc[ 1 ] = ( -B + sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );\n    float sx = abs( xbc[ 0 ] - xbc[ 1 ] ) * 0.5;\n\n    A = dot( R[ 3 ], D * R[ 3 ] );\n    B = -2.0 * dot( R[ 1 ], D * R[ 3 ] );\n    C = dot( R[ 1 ], D * R[ 1 ] );\n    ybc[ 0 ] = ( -B - sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );\n    ybc[ 1 ] = ( -B + sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );\n    float sy = abs( ybc[ 0 ] - ybc[ 1 ]  ) * 0.5;\n\n    gl_Position.xy = vec2( 0.5 * ( xbc.x + xbc.y ), 0.5 * ( ybc.x + ybc.y ) );\n    gl_Position.xy -= mapping * vec2( sx, sy );\n    gl_Position.xy *= gl_Position.w;\n\n}\n\nvoid main(void){\n\n    #ifdef PICKING\n        vPickingColor = pickingColor;\n    #else\n        #include color_vertex\n    #endif\n\n    vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\n    mvPosition.z -= radius;  // avoid clipping, added again in fragment shader\n\n    gl_Position = projectionMatrix * vec4( mvPosition.xyz, 1.0 );\n    ComputePointSizeAndPositionInClipCoordSphere();\n\n    vRadius = radius;\n    vRadiusSq = radius * radius;\n    vec4 vPoint4 = projectionMatrixInverse * gl_Position;\n    vPoint = vPoint4.xyz / vPoint4.w;\n    vPointViewPosition = -mvPosition.xyz / mvPosition.w;\n\n}";

// File:shader/SphereImpostor.frag

NGL.Resources[ 'shader/SphereImpostor.frag' ] = "#define STANDARD\n#define IMPOSTOR\n\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\nuniform float nearClip;\nuniform mat4 projectionMatrix;\nuniform float ortho;\n\n// uniform vec3 specular;\n// uniform float shininess;\n\nvarying float vRadius;\nvarying float vRadiusSq;\nvarying vec3 vPoint;\nvarying vec3 vPointViewPosition;\n\n#ifdef PICKING\n    uniform float objectId;\n    varying vec3 vPickingColor;\n#else\n    #include common\n    #include color_pars_fragment\n    #include fog_pars_fragment\n    #include bsdfs\n    #include ambient_pars\n    #include lights_pars\n    // #include lights_phong_pars_fragment\n    #include lights_physical_pars_fragment\n#endif\n\nbool flag2 = false;\nbool interior = false;\nvec3 cameraPos;\nvec3 cameraNormal;\n\n// vec4 poly_color = gl_Color;\n//   if(uf_use_border_hinting == 1.0)\n//   {\n//     vec3 wc_eye_dir = normalize(wc_sp_pt);\n//     float n_dot_e   = abs(dot(wc_sp_nrml,wc_eye_dir));\n//     float alpha     = max(uf_border_color_start_cosine - n_dot_e,0.0)/uf_border_color_start_cosine;\n//     poly_color      = mix(gl_Color,uf_border_color,0.75*alpha);\n//   }\n//   color += (diff + amb)*poly_color + spec*gl_FrontMaterial.specular;\n\n// Calculate depth based on the given camera position.\nfloat calcDepth( in vec3 cameraPos ){\n    vec2 clipZW = cameraPos.z * projectionMatrix[2].zw + projectionMatrix[3].zw;\n    return 0.5 + 0.5 * clipZW.x / clipZW.y;\n}\n\nfloat calcClip( vec3 cameraPos ){\n    return dot( vec4( cameraPos, 1.0 ), vec4( 0.0, 0.0, 1.0, nearClip - 0.5 ) );\n}\n\nbool Impostor( out vec3 cameraPos, out vec3 cameraNormal ){\n\n    vec3 cameraSpherePos = -vPointViewPosition;\n    cameraSpherePos.z += vRadius;\n\n    vec3 rayOrigin = mix( vec3( 0.0, 0.0, 0.0 ), vPoint, ortho );\n    vec3 rayDirection = mix( normalize( vPoint ), vec3( 0.0, 0.0, 1.0 ), ortho );\n    vec3 cameraSphereDir = mix( cameraSpherePos, rayOrigin - cameraSpherePos, ortho );\n\n    float B = dot( rayDirection, cameraSphereDir );\n    float det = B * B + vRadiusSq - dot( cameraSphereDir, cameraSphereDir );\n\n    if( det < 0.0 ){\n        discard;\n        return false;\n    }else{\n        float sqrtDet = sqrt( det );\n        float posT = mix( B + sqrtDet, B + sqrtDet, ortho );\n        float negT = mix( B - sqrtDet, sqrtDet - B, ortho );\n\n        cameraPos = rayDirection * negT + rayOrigin;\n\n        #ifdef NEAR_CLIP\n            if( calcDepth( cameraPos ) <= 0.0 ){\n                cameraPos = rayDirection * posT + rayOrigin;\n                interior = true;\n                return false;\n            }else if( calcClip( cameraPos ) > 0.0 ){\n                cameraPos = rayDirection * posT + rayOrigin;\n                interior = true;\n                flag2 = true;\n                return false;\n            }else{\n                cameraNormal = normalize( -cameraPos - cameraSpherePos );\n            }\n        #else\n            if( calcDepth( cameraPos ) <= 0.0 ){\n                cameraPos = rayDirection * posT + rayOrigin;\n                interior = true;\n                return false;\n            }else{\n                cameraNormal = normalize( cameraPos - cameraSpherePos );\n            }\n        #endif\n\n        return true;\n    }\n\n    return false; // ensure that each control flow has a return\n\n}\n\nvoid main(void){\n\n    // vec3 specular = vec3( 1.0, 1.0, 1.0 );\n    // float specularStrength = 1.0;\n    // float shininess = 1.0;\n\n    bool flag = Impostor( cameraPos, cameraNormal );\n\n    #ifdef NEAR_CLIP\n        if( calcClip( cameraPos ) > 0.0 )\n            discard;\n    #endif\n\n    // FIXME not compatible with custom clipping plane\n    //Set the depth based on the new cameraPos.\n    gl_FragDepthEXT = calcDepth( cameraPos );\n    if( !flag ){\n\n        // clamp to near clipping plane and add a tiny value to\n        // make spheres with a greater radius occlude smaller ones\n        #ifdef NEAR_CLIP\n            if( flag2 ){\n                gl_FragDepthEXT = max( 0.0, calcDepth( vec3( - ( nearClip - 0.5 ) ) ) + ( 0.0000001 / vRadius ) );\n            }else if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );\n            }\n        #else\n            if( gl_FragDepthEXT >= 0.0 ){\n                gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );\n            }\n        #endif\n\n    }\n\n    // bugfix (mac only?)\n    if (gl_FragDepthEXT < 0.0)\n        discard;\n    if (gl_FragDepthEXT > 1.0)\n        discard;\n\n    #ifdef PICKING\n\n        gl_FragColor = vec4( vPickingColor, objectId );\n\n    #else\n\n        // vec3 specColor = vColor;  // vec3( 1.0, 1.0, 1.0 );\n        // vec3 lightDir = vec3( 0.0, 0.0, 1.0 );\n        // vec3 vNormal = cameraNormal;\n\n        // float lambertian = max(dot(lightDir,vNormal), 0.0);\n        // float specular = 0.0;\n\n        // if(lambertian > 0.0) {\n\n        //     vec3 reflectDir = reflect(-lightDir, vNormal);\n        //     vec3 viewDir = normalize(-cameraPos);\n\n        //     float specAngle = max(dot(reflectDir, viewDir), 0.0);\n        //     specular = pow(specAngle, 4.0);\n\n        //     // the exponent controls the shininess (try mode 2)\n        //     specular = pow(specAngle, 16.0);\n\n        //     // according to the rendering equation we would need to multiply\n        //     // with the the \"lambertian\", but this has little visual effect\n        //     specular *= lambertian;\n\n\n        // }\n\n        // gl_FragColor = vec4( lambertian*vColor + specular*specColor, opacity );\n\n        //\n\n        vec3 vNormal = cameraNormal;\n        vec3 vViewPosition = -cameraPos;\n\n        vec4 diffuseColor = vec4( diffuse, opacity );\n        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n        vec3 totalEmissiveLight = emissive;\n\n        #include color_fragment\n        #include roughnessmap_fragment\n        #include metalnessmap_fragment\n        #include normal_fragment\n        if( interior ){\n            normal = vec3( 0.0, 0.0, 0.4 );\n        }\n\n        // #include lights_phong_fragment\n        #include lights_physical_fragment\n        #include lights_template\n\n        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;\n\n        gl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\n        #include premultiplied_alpha_fragment\n        #include tonemapping_fragment\n        #include encodings_fragment\n        #include fog_fragment\n\n    #endif\n\n}";

// File:shader/chunk/dull_interior_fragment.glsl

NGL.Resources[ 'shader/chunk/dull_interior_fragment.glsl' ] = "#ifdef DULL_INTERIOR\n    if( gl_FrontFacing == false ){\n        normal = vec3( 0.0, 0.0, 0.4 );\n    }\n#endif";

// File:shader/chunk/fog_fragment.glsl

NGL.Resources[ 'shader/chunk/fog_fragment.glsl' ] = "#ifdef USE_FOG\n\n	// #if defined( USE_LOGDEPTHBUF_EXT ) || defined( IMPOSTOR )\n	//\n	// 	float depth = gl_FragDepthEXT / gl_FragCoord.w;\n	//\n	// #else\n	//\n	// 	float depth = gl_FragCoord.z / gl_FragCoord.w;\n	//\n	// #endif\n\n	float depth = length( vViewPosition );\n\n	#ifdef FOG_EXP2\n\n		float fogFactor = whiteCompliment( exp2( - fogDensity * fogDensity * depth * depth * LOG2 ) );\n\n	#else\n\n		float fogFactor = smoothstep( fogNear, fogFar, depth );\n\n	#endif\n\n	gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n\n#endif";

// File:shader/chunk/nearclip_fragment.glsl

NGL.Resources[ 'shader/chunk/nearclip_fragment.glsl' ] = "#ifdef NEAR_CLIP\n    if( vViewPosition.z < nearClip )\n        discard;\n#endif";

// File:shader/chunk/nearclip_vertex.glsl

NGL.Resources[ 'shader/chunk/nearclip_vertex.glsl' ] = "#ifdef NEAR_CLIP\n    if( vViewPosition.z < nearClip )\n        gl_Position.z = 2.0 * gl_Position.w;  // move out of [ -w, +w ]\n#endif";

// File:shader/chunk/opaque_back_fragment.glsl

NGL.Resources[ 'shader/chunk/opaque_back_fragment.glsl' ] = "#ifdef OPAQUE_BACK\n    #ifdef FLIP_SIDED\n        if( gl_FrontFacing == true ){\n            gl_FragColor.a = 1.0;\n        }\n    #else\n        if( gl_FrontFacing == false ){\n            gl_FragColor.a = 1.0;\n        }\n    #endif\n#endif";

