var $NGL_shaderTextHash = {};

$NGL_shaderTextHash['SphereImpostor.frag'] = ["#define STANDARD",
"#define IMPOSTOR",
"",
"uniform vec3 diffuse;",
"uniform vec3 emissive;",
"uniform float roughness;",
"uniform float metalness;",
"uniform float opacity;",
"uniform float nearClip;",
"uniform mat4 projectionMatrix;",
"uniform float ortho;",
"",
"varying float vRadius;",
"varying float vRadiusSq;",
"varying vec3 vPoint;",
"varying vec3 vPointViewPosition;",
"",
"#ifdef PICKING",
"    uniform float objectId;",
"    varying vec3 vPickingColor;",
"#else",
"    #include common",
"    #include color_pars_fragment",
"    #include fog_pars_fragment",
"    #include bsdfs",
"    #include lights_pars_begin",
"    #include lights_physical_pars_fragment",
"#endif",
"",
"bool flag2 = false;",
"bool interior = false;",
"vec3 cameraPos;",
"vec3 cameraNormal;",
"",
"// Calculate depth based on the given camera position.",
"float calcDepth( in vec3 cameraPos ){",
"    vec2 clipZW = cameraPos.z * projectionMatrix[2].zw + projectionMatrix[3].zw;",
"    return 0.5 + 0.5 * clipZW.x / clipZW.y;",
"}",
"",
"float calcClip( vec3 cameraPos ){",
"    return dot( vec4( cameraPos, 1.0 ), vec4( 0.0, 0.0, 1.0, nearClip - 0.5 ) );",
"}",
"",
"bool Impostor( out vec3 cameraPos, out vec3 cameraNormal ){",
"",
"    vec3 cameraSpherePos = -vPointViewPosition;",
"    cameraSpherePos.z += vRadius;",
"",
"    vec3 rayOrigin = mix( vec3( 0.0, 0.0, 0.0 ), vPoint, ortho );",
"    vec3 rayDirection = mix( normalize( vPoint ), vec3( 0.0, 0.0, 1.0 ), ortho );",
"    vec3 cameraSphereDir = mix( cameraSpherePos, rayOrigin - cameraSpherePos, ortho );",
"",
"    float B = dot( rayDirection, cameraSphereDir );",
"    float det = B * B + vRadiusSq - dot( cameraSphereDir, cameraSphereDir );",
"",
"    if( det < 0.0 ){",
"        discard;",
"        return false;",
"    }",
"        float sqrtDet = sqrt( det );",
"        float posT = mix( B + sqrtDet, B + sqrtDet, ortho );",
"        float negT = mix( B - sqrtDet, sqrtDet - B, ortho );",
"",
"        cameraPos = rayDirection * negT + rayOrigin;",
"",
"        #ifdef NEAR_CLIP",
"if( calcDepth( cameraPos ) <= 0.0 ){",
"    cameraPos = rayDirection * posT + rayOrigin;",
"    interior = true;",
"    return false;",
"}else if( calcClip( cameraPos ) > 0.0 ){",
"    cameraPos = rayDirection * posT + rayOrigin;",
"    interior = true;",
"    flag2 = true;",
"    return false;",
"}else{",
"    cameraNormal = normalize( cameraPos - cameraSpherePos );",
"}",
"        #else",
"if( calcDepth( cameraPos ) <= 0.0 ){",
"    cameraPos = rayDirection * posT + rayOrigin;",
"    interior = true;",
"    return false;",
"}else{",
"    cameraNormal = normalize( cameraPos - cameraSpherePos );",
"}",
"        #endif",
"",
"        cameraNormal = normalize( cameraPos - cameraSpherePos );",
"        cameraNormal *= float(!interior) * 2.0 - 1.0;",
"         return !interior;",
"",
"}",
"",
"void main(void){",
"",
"    bool flag = Impostor( cameraPos, cameraNormal );",
"",
"    #ifdef NEAR_CLIP",
"        if( calcClip( cameraPos ) > 0.0 )",
"            discard;",
"    #endif",
"",
"    // FIXME not compatible with custom clipping plane",
"    //Set the depth based on the new cameraPos.",
"    gl_FragDepthEXT = calcDepth( cameraPos );",
"    if( !flag ){",
"",
"        // clamp to near clipping plane and add a tiny value to",
"        // make spheres with a greater radius occlude smaller ones",
"        #ifdef NEAR_CLIP",
"if( flag2 ){",
"    gl_FragDepthEXT = max( 0.0, calcDepth( vec3( - ( nearClip - 0.5 ) ) ) + ( 0.0000001 / vRadius ) );",
"}else if( gl_FragDepthEXT >= 0.0 ){",
"    gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );",
"}",
"        #else",
"if( gl_FragDepthEXT >= 0.0 ){",
"    gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );",
"}",
"        #endif",
"",
"    }",
"",
"    // bugfix (mac only?)",
"    if (gl_FragDepthEXT < 0.0)",
"        discard;",
"    if (gl_FragDepthEXT > 1.0)",
"        discard;",
"",
"    #ifdef PICKING",
"",
"        gl_FragColor = vec4( vPickingColor, objectId );",
"",
"    #else",
"",
"        vec3 vNormal = cameraNormal;",
"        vec3 vViewPosition = -cameraPos;",
"",
"        vec4 diffuseColor = vec4( diffuse, opacity );",
"        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );",
"        vec3 totalEmissiveLight = emissive;",
"",
"        #include color_fragment",
"        #include roughnessmap_fragment",
"        #include metalnessmap_fragment",
"",
"        // don't use include normal_fragment",
"        vec3 normal = normalize( vNormal );",
"",
"        #include lights_physical_fragment",
"        //include lights_template",
"        #include lights_fragment_begin",
"        #include lights_fragment_end",
"",
"        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;",
"",
"        gl_FragColor = vec4( outgoingLight, diffuseColor.a );",
"        //gl_FragColor = vec4( reflectedLight.directSpecular, diffuseColor.a );",
"",
"        #include premultiplied_alpha_fragment",
"        #include tonemapping_fragment",
"        #include encodings_fragment",
"        //include fog_fragment",
"        #ifdef USE_FOG",
"            #ifdef USE_LOGDEPTHBUF_EXT",
"                float depth = gl_FragDepthEXT / gl_FragCoord.w;",
"            #else",
"                float depth = gl_FragCoord.z / gl_FragCoord.w;",
"            #endif",
"            #ifdef FOG_EXP2",
"                float fogFactor = whiteCompliment( exp2( - fogDensity * fogDensity * depth * depth * LOG2 ) );",
"            #else",
"                float fogFactor = smoothstep( fogNear, fogFar, depth );",
"            #endif",
"            gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );",
"        #endif",
"",
"    #endif",
"",
"}"
].join("\n");

$NGL_shaderTextHash['SphereImpostor.vert'] = ["uniform mat4 projectionMatrixInverse;",
"uniform float nearClip;",
"",
"varying float vRadius;",
"varying float vRadiusSq;",
"varying vec3 vPoint;",
"varying vec3 vPointViewPosition;",
"varying float fogDepth;",
"varying float fogNear;",
"varying float fogFar;",
"",
"attribute vec2 mapping;",
"//attribute vec3 position;",
"attribute float radius;",
"",
"#ifdef PICKING",
"    #include unpack_clr",
"    attribute float primitiveId;",
"    varying vec3 vPickingColor;",
"#else",
"    #include color_pars_vertex",
"#endif",
"",
"//include matrix_scale",
"float matrixScale( in mat4 m ){",
"    vec4 r = m[ 0 ];",
"    return sqrt( r[ 0 ] * r[ 0 ] + r[ 1 ] * r[ 1 ] + r[ 2 ] * r[ 2 ] );",
"}",
"",
"const mat4 D = mat4(",
"    1.0, 0.0, 0.0, 0.0,",
"    0.0, 1.0, 0.0, 0.0,",
"    0.0, 0.0, 1.0, 0.0,",
"    0.0, 0.0, 0.0, -1.0",
");",
"",
"mat4 transposeTmp( in mat4 inMatrix ) {",
"    vec4 i0 = inMatrix[0];",
"    vec4 i1 = inMatrix[1];",
"    vec4 i2 = inMatrix[2];",
"    vec4 i3 = inMatrix[3];",
"",
"    mat4 outMatrix = mat4(",
"        vec4(i0.x, i1.x, i2.x, i3.x),",
"        vec4(i0.y, i1.y, i2.y, i3.y),",
"        vec4(i0.z, i1.z, i2.z, i3.z),",
"        vec4(i0.w, i1.w, i2.w, i3.w)",
"    );",
"    return outMatrix;",
"}",
"",
"//------------------------------------------------------------------------------",
"// Compute point size and center using the technique described in:",
"// 'GPU-Based Ray-Casting of Quadratic Surfaces'",
"// by Christian Sigg, Tim Weyrich, Mario Botsch, Markus Gross.",
"//",
"// Code based on",
"/*=========================================================================",
"",
" Program:   Visualization Toolkit",
" Module:    Quadrics_fs.glsl and Quadrics_vs.glsl",
"",
" Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen",
" All rights reserved.",
" See Copyright.txt or http://www.kitware.com/Copyright.htm for details.",
"",
" This software is distributed WITHOUT ANY WARRANTY; without even",
" the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR",
" PURPOSE.  See the above copyright notice for more information.",
"",
" =========================================================================*/",
"",
"// .NAME Quadrics_fs.glsl and Quadrics_vs.glsl",
"// .SECTION Thanks",
"// <verbatim>",
"//",
"//  This file is part of the PointSprites plugin developed and contributed by",
"//",
"//  Copyright (c) CSCS - Swiss National Supercomputing Centre",
"//                EDF - Electricite de France",
"//",
"//  John Biddiscombe, Ugo Varetto (CSCS)",
"//  Stephane Ploix (EDF)",
"//",
"// </verbatim>",
"//",
"// Contributions by Alexander Rose",
"// - ported to WebGL",
"// - adapted to work with quads",
"void ComputePointSizeAndPositionInClipCoordSphere(){",
"",
"    vec2 xbc;",
"    vec2 ybc;",
"",
"    mat4 T = mat4(",
"        radius, 0.0, 0.0, 0.0,",
"        0.0, radius, 0.0, 0.0,",
"        0.0, 0.0, radius, 0.0,",
"        position.x, position.y, position.z, 1.0",
"    );",
"",
"    mat4 R = transposeTmp( projectionMatrix * modelViewMatrix * T );",
"    float A = dot( R[ 3 ], D * R[ 3 ] );",
"    float B = -2.0 * dot( R[ 0 ], D * R[ 3 ] );",
"    float C = dot( R[ 0 ], D * R[ 0 ] );",
"    xbc[ 0 ] = ( -B - sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    xbc[ 1 ] = ( -B + sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    float sx = abs( xbc[ 0 ] - xbc[ 1 ] ) * 0.5;",
"",
"    A = dot( R[ 3 ], D * R[ 3 ] );",
"    B = -2.0 * dot( R[ 1 ], D * R[ 3 ] );",
"    C = dot( R[ 1 ], D * R[ 1 ] );",
"    ybc[ 0 ] = ( -B - sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    ybc[ 1 ] = ( -B + sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    float sy = abs( ybc[ 0 ] - ybc[ 1 ]  ) * 0.5;",
"",
"    gl_Position.xy = vec2( 0.5 * ( xbc.x + xbc.y ), 0.5 * ( ybc.x + ybc.y ) );",
"    gl_Position.xy -= mapping * vec2( sx, sy );",
"    gl_Position.xy *= gl_Position.w;",
"",
"}",
"",
"void main(void){",
"",
"    #ifdef PICKING",
"        vPickingColor = unpackColor( primitiveId );",
"    #else",
"        #include color_vertex",
"    #endif",
"",
"    vRadius = radius * matrixScale( modelViewMatrix );",
"",
"    vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
"    // avoid clipping, added again in fragment shader",
"    mvPosition.z -= vRadius;",
"",
"    gl_Position = projectionMatrix * vec4( mvPosition.xyz, 1.0 );",
"    ComputePointSizeAndPositionInClipCoordSphere();",
"",
"",
"    vRadiusSq = vRadius * vRadius;",
"    vec4 vPoint4 = projectionMatrixInverse * gl_Position;",
"    vPoint = vPoint4.xyz / vPoint4.w;",
"    vPointViewPosition = -mvPosition.xyz / mvPosition.w;",
"",
"}"
].join("\n");

$NGL_shaderTextHash['CylinderImpostor.frag'] = ["#define STANDARD",
"#define IMPOSTOR",
"",
"// Open-Source PyMOL is Copyright (C) Schrodinger, LLC.",
"//",
"//  All Rights Reserved",
"//",
"//  Permission to use, copy, modify, distribute, and distribute modified",
"//  versions of this software and its built-in documentation for any",
"//  purpose and without fee is hereby granted, provided that the above",
"//  copyright notice appears in all copies and that both the copyright",
"//  notice and this permission notice appear in supporting documentation,",
"//  and that the name of Schrodinger, LLC not be used in advertising or",
"//  publicity pertaining to distribution of the software without specific,",
"//  written prior permission.",
"//",
"//  SCHRODINGER, LLC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,",
"//  INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN",
"//  NO EVENT SHALL SCHRODINGER, LLC BE LIABLE FOR ANY SPECIAL, INDIRECT OR",
"//  CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS",
"//  OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE",
"//  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE",
"//  USE OR PERFORMANCE OF THIS SOFTWARE.",
"",
"// Contributions by Alexander Rose",
"// - ported to WebGL",
"// - dual color",
"// - pk color",
"// - custom clipping",
"// - three.js lighting",
"",
"uniform vec3 diffuse;",
"uniform vec3 emissive;",
"uniform float roughness;",
"uniform float metalness;",
"uniform float opacity;",
"uniform float nearClip;",
"uniform mat4 projectionMatrix;",
"uniform float ortho;",
"",
"varying vec3 axis;",
"varying vec4 base_radius;",
"varying vec4 end_b;",
"varying vec3 U;",
"varying vec3 V;",
"varying vec4 w;",
"",
"#ifdef PICKING",
"    uniform float objectId;",
"    varying vec3 vPickingColor;",
"#else",
"    varying vec3 vColor1;",
"    varying vec3 vColor2;",
"    #include common",
"    #include fog_pars_fragment",
"    #include bsdfs",
"    #include lights_pars_begin",
"    #include lights_physical_pars_fragment",
"#endif",
"",
"bool interior = false;",
"",
"float distSq3( vec3 v3a, vec3 v3b ){",
"    return (",
"        ( v3a.x - v3b.x ) * ( v3a.x - v3b.x ) +",
"        ( v3a.y - v3b.y ) * ( v3a.y - v3b.y ) +",
"        ( v3a.z - v3b.z ) * ( v3a.z - v3b.z )",
"    );",
"}",
"",
"// Calculate depth based on the given camera position.",
"float calcDepth( in vec3 cameraPos ){",
"    vec2 clipZW = cameraPos.z * projectionMatrix[2].zw + projectionMatrix[3].zw;",
"    return 0.5 + 0.5 * clipZW.x / clipZW.y;",
"}",
"",
"float calcClip( vec3 cameraPos ){",
"    return dot( vec4( cameraPos, 1.0 ), vec4( 0.0, 0.0, 1.0, nearClip - 0.5 ) );",
"}",
"",
"void main(){",
"",
"    vec3 point = w.xyz / w.w;",
"",
"    // unpacking",
"    vec3 base = base_radius.xyz;",
"    float vRadius = base_radius.w;",
"    vec3 end = end_b.xyz;",
"    float b = end_b.w;",
"",
"    vec3 end_cyl = end;",
"    vec3 surface_point = point;",
"",
"    vec3 ray_target = surface_point;",
"    vec3 ray_origin = vec3(0.0);",
"    vec3 ray_direction = mix(normalize(ray_origin - ray_target), vec3(0.0, 0.0, 1.0), ortho);",
"    mat3 basis = mat3( U, V, axis );",
"",
"    vec3 diff = ray_target - 0.5 * (base + end_cyl);",
"    vec3 P = diff * basis;",
"",
"    // angle (cos) between cylinder cylinder_axis and ray direction",
"    float dz = dot( axis, ray_direction );",
"",
"    float radius2 = vRadius*vRadius;",
"",
"    // calculate distance to the cylinder from ray origin",
"    vec3 D = vec3(dot(U, ray_direction),",
"                dot(V, ray_direction),",
"                dz);",
"    float a0 = P.x*P.x + P.y*P.y - radius2;",
"    float a1 = P.x*D.x + P.y*D.y;",
"    float a2 = D.x*D.x + D.y*D.y;",
"",
"    // calculate a dicriminant of the above quadratic equation",
"    float d = a1*a1 - a0*a2;",
"    if (d < 0.0)",
"        // outside of the cylinder",
"        discard;",
"",
"    float dist = (-a1 + sqrt(d)) / a2;",
"",
"    // point of intersection on cylinder surface",
"    vec3 new_point = ray_target + dist * ray_direction;",
"",
"    vec3 tmp_point = new_point - base;",
"    vec3 _normal = normalize( tmp_point - axis * dot(tmp_point, axis) );",
"",
"    ray_origin = mix( ray_origin, surface_point, ortho );",
"",
"    // test caps",
"    float front_cap_test = dot( tmp_point, axis );",
"    float end_cap_test = dot((new_point - end_cyl), axis);",
"",
"    // to calculate caps, simply check the angle between",
"    // the point of intersection - cylinder end vector",
"    // and a cap plane normal (which is the cylinder cylinder_axis)",
"    // if the angle < 0, the point is outside of cylinder",
"    // test front cap",
"",
"    #ifndef CAP",
"        vec3 new_point2 = ray_target + ( (-a1 - sqrt(d)) / a2 ) * ray_direction;",
"        vec3 tmp_point2 = new_point2 - base;",
"    #endif",
"",
"    // flat",
"    if (front_cap_test < 0.0)",
"    {",
"        // ray-plane intersection",
"        float dNV = dot(-axis, ray_direction);",
"        if (dNV < 0.0)",
"            discard;",
"        float near = dot(-axis, (base)) / dNV;",
"        vec3 front_point = ray_direction * near + ray_origin;",
"        // within the cap radius?",
"        if (dot(front_point - base, front_point-base) > radius2)",
"            discard;",
"",
"        #ifdef CAP",
"            new_point = front_point;",
"            _normal = axis;",
"        #else",
"            new_point = ray_target + ( (-a1 - sqrt(d)) / a2 ) * ray_direction;",
"            dNV = dot(-axis, ray_direction);",
"            near = dot(axis, end_cyl) / dNV;",
"            new_point2 = ray_direction * near + ray_origin;",
"            if (dot(new_point2 - end_cyl, new_point2-base) < radius2)",
"                discard;",
"            interior = true;",
"        #endif",
"    }",
"",
"    // test end cap",
"",
"",
"    // flat",
"    if( end_cap_test > 0.0 )",
"    {",
"        // ray-plane intersection",
"        float dNV = dot(axis, ray_direction);",
"        if (dNV < 0.0)",
"            discard;",
"        float near = dot(axis, end_cyl) / dNV;",
"        vec3 end_point = ray_direction * near + ray_origin;",
"        // within the cap radius?",
"        if( dot(end_point - end_cyl, end_point-base) > radius2 )",
"            discard;",
"",
"        #ifdef CAP",
"            new_point = end_point;",
"            _normal = axis;",
"        #else",
"            new_point = ray_target + ( (-a1 - sqrt(d)) / a2 ) * ray_direction;",
"            dNV = dot(-axis, ray_direction);",
"            near = dot(-axis, (base)) / dNV;",
"            new_point2 = ray_direction * near + ray_origin;",
"            if (dot(new_point2 - base, new_point2-base) < radius2)",
"                discard;",
"            interior = true;",
"        #endif",
"    }",
"",
"    gl_FragDepthEXT = calcDepth( new_point );",
"",
"    #ifdef NEAR_CLIP",
"        if( calcClip( new_point ) > 0.0 ){",
"            dist = (-a1 - sqrt(d)) / a2;",
"            new_point = ray_target + dist * ray_direction;",
"            if( calcClip( new_point ) > 0.0 )",
"                discard;",
"            interior = true;",
"            gl_FragDepthEXT = calcDepth( new_point );",
"            if( gl_FragDepthEXT >= 0.0 ){",
"                gl_FragDepthEXT = max( 0.0, calcDepth( vec3( - ( nearClip - 0.5 ) ) ) + ( 0.0000001 / vRadius ) );",
"            }",
"        }else if( gl_FragDepthEXT <= 0.0 ){",
"            dist = (-a1 - sqrt(d)) / a2;",
"            new_point = ray_target + dist * ray_direction;",
"            interior = true;",
"            gl_FragDepthEXT = calcDepth( new_point );",
"            if( gl_FragDepthEXT >= 0.0 ){",
"                gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );",
"            }",
"        }",
"    #else",
"        if( gl_FragDepthEXT <= 0.0 ){",
"            dist = (-a1 - sqrt(d)) / a2;",
"            new_point = ray_target + dist * ray_direction;",
"            interior = true;",
"            gl_FragDepthEXT = calcDepth( new_point );",
"            if( gl_FragDepthEXT >= 0.0 ){",
"                gl_FragDepthEXT = 0.0 + ( 0.0000001 / vRadius );",
"            }",
"        }",
"    #endif",
"",
"    // this is a workaround necessary for Mac",
"    // otherwise the modified fragment won't clip properly",
"    if (gl_FragDepthEXT < 0.0)",
"        discard;",
"    if (gl_FragDepthEXT > 1.0)",
"        discard;",
"",
"    #ifdef PICKING",
"",
"        gl_FragColor = vec4( vPickingColor, objectId );",
"",
"    #else",
"",
"        vec3 vViewPosition = -new_point;",
"        vec3 vNormal = _normal;",
"        vec3 vColor;",
"",
"        if( distSq3( new_point, end_cyl ) < distSq3( new_point, base ) ){",
"            if( b < 0.0 ){",
"                vColor = vColor1;",
"            }else{",
"                vColor = vColor2;",
"            }",
"        }else{",
"            if( b > 0.0 ){",
"                vColor = vColor1;",
"            }else{",
"                vColor = vColor2;",
"            }",
"        }",
"",
"        vec4 diffuseColor = vec4( diffuse, opacity );",
"        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );",
"        vec3 totalEmissiveLight = emissive;",
"",
"        #include color_fragment",
"     //ifdef USE_COLOR",
"     //diffuseColor.r *= vColor[0];",
"     //diffuseColor.g *= vColor[1];",
"     //diffuseColor.b *= vColor[2];",
"     //endif",
"        #include roughnessmap_fragment",
"        #include metalnessmap_fragment",
"",
"        // don't use include normal_fragment",
"        vec3 normal = normalize( vNormal );",
"",
"        #include lights_physical_fragment",
"        //include lights_template",
"        #include lights_fragment_begin",
"        #include lights_fragment_end",
"",
"        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;",
"",
"        gl_FragColor = vec4( outgoingLight, diffuseColor.a );",
"        //gl_FragColor = vec4( reflectedLight.directSpecular, diffuseColor.a );",
"",
"        #include premultiplied_alpha_fragment",
"        #include tonemapping_fragment",
"        #include encodings_fragment",
"        //include fog_fragment",
"        #ifdef USE_FOG",
"            #ifdef USE_LOGDEPTHBUF_EXT",
"                float depth = gl_FragDepthEXT / gl_FragCoord.w;",
"            #else",
"                float depth = gl_FragCoord.z / gl_FragCoord.w;",
"            #endif",
"            #ifdef FOG_EXP2",
"                float fogFactor = whiteCompliment( exp2( - fogDensity * fogDensity * depth * depth * LOG2 ) );",
"            #else",
"                float fogFactor = smoothstep( fogNear, fogFar, depth );",
"            #endif",
"            gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );",
"        #endif",
"",
"    #endif",
"",
"}"
].join("\n");

$NGL_shaderTextHash['CylinderImpostor.vert'] = ["// Open-Source PyMOL is Copyright (C) Schrodinger, LLC.",
"//",
"//  All Rights Reserved",
"//",
"//  Permission to use, copy, modify, distribute, and distribute modified",
"//  versions of this software and its built-in documentation for any",
"//  purpose and without fee is hereby granted, provided that the above",
"//  copyright notice appears in all copies and that both the copyright",
"//  notice and this permission notice appear in supporting documentation,",
"//  and that the name of Schrodinger, LLC not be used in advertising or",
"//  publicity pertaining to distribution of the software without specific,",
"//  written prior permission.",
"//",
"//  SCHRODINGER, LLC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,",
"//  INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN",
"//  NO EVENT SHALL SCHRODINGER, LLC BE LIABLE FOR ANY SPECIAL, INDIRECT OR",
"//  CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS",
"//  OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE",
"//  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE",
"//  USE OR PERFORMANCE OF THIS SOFTWARE.",
"",
"// Contributions by Alexander Rose",
"// - ported to WebGL",
"// - dual color",
"// - pk color",
"// - shift",
"",
"attribute vec3 mapping;",
"attribute vec3 position1;",
"attribute vec3 position2;",
"attribute float radius;",
"",
"varying vec3 axis;",
"varying vec4 base_radius;",
"varying vec4 end_b;",
"varying vec3 U;",
"varying vec3 V;",
"varying vec4 w;",
"varying float fogDepth;",
"varying float fogNear;",
"varying float fogFar;",
"",
"#ifdef PICKING",
"    #include unpack_clr",
"    attribute float primitiveId;",
"    varying vec3 vPickingColor;",
"#else",
"    //attribute vec3 color;",
"    attribute vec3 color2;",
"    varying vec3 vColor1;",
"    varying vec3 vColor2;",
"#endif",
"",
"uniform mat4 modelViewMatrixInverse;",
"uniform float ortho;",
"",
"//include matrix_scale",
"float matrixScale( in mat4 m ){",
"    vec4 r = m[ 0 ];",
"    return sqrt( r[ 0 ] * r[ 0 ] + r[ 1 ] * r[ 1 ] + r[ 2 ] * r[ 2 ] );",
"}",
"",
"void main(){",
"",
"    #ifdef PICKING",
"        vPickingColor = unpackColor( primitiveId );",
"    #else",
"        vColor1 = color;",
"        vColor2 = color2;",
"    #endif",
"",
"    // vRadius = radius;",
"    base_radius.w = radius * matrixScale( modelViewMatrix );",
"",
"    //vec3 center = position;",
"    vec3 center = ( position2 + position1 ) / 2.0;",
"    vec3 dir = normalize( position2 - position1 );",
"    float ext = length( position2 - position1 ) / 2.0;",
"",
"    // using cameraPosition fails on some machines, not sure why",
"    // vec3 cam_dir = normalize( cameraPosition - mix( center, vec3( 0.0 ), ortho ) );",
"    vec3 cam_dir;",
"    if( ortho == 0.0 ){",
"        cam_dir = ( modelViewMatrixInverse * vec4( 0, 0, 0, 1 ) ).xyz - center;",
"    }else{",
"        cam_dir = ( modelViewMatrixInverse * vec4( 0, 0, 1, 0 ) ).xyz;",
"    }",
"    cam_dir = normalize( cam_dir );",
"",
"    vec3 ldir;",
"",
"    float b = dot( cam_dir, dir );",
"    end_b.w = b;",
"    // direction vector looks away, so flip",
"    if( b < 0.0 )",
"        ldir = -ext * dir;",
"    // direction vector already looks in my direction",
"    else",
"        ldir = ext * dir;",
"",
"    vec3 left = normalize( cross( cam_dir, ldir ) );",
"    left = radius * left;",
"    vec3 up = radius * normalize( cross( left, ldir ) );",
"",
"    // transform to modelview coordinates",
"    axis = normalize( normalMatrix * ldir );",
"    U = normalize( normalMatrix * up );",
"    V = normalize( normalMatrix * left );",
"",
"    vec4 base4 = modelViewMatrix * vec4( center - ldir, 1.0 );",
"    base_radius.xyz = base4.xyz / base4.w;",
"",
"    vec4 top_position = modelViewMatrix * vec4( center + ldir, 1.0 );",
"    vec4 end4 = top_position;",
"    end_b.xyz = end4.xyz / end4.w;",
"",
"    w = modelViewMatrix * vec4(",
"        center + mapping.x*ldir + mapping.y*left + mapping.z*up, 1.0",
"    );",
"",
"    gl_Position = projectionMatrix * w;",
"",
"    // avoid clipping (1.0 seems to induce flickering with some drivers)",
"    gl_Position.z = 0.99;",
"",
"}"
].join("\n");

$NGL_shaderTextHash['SphereInstancing.frag'] = $NGL_shaderTextHash['SphereImpostor.frag'];

$NGL_shaderTextHash['SphereInstancing.vert'] = ["uniform mat4 projectionMatrixInverse;",
"uniform float nearClip;",
"",
"varying float vRadius;",
"varying float vRadiusSq;",
"varying vec3 vPoint;",
"varying vec3 vPointViewPosition;",
"varying float fogDepth;",
"varying float fogNear;",
"varying float fogFar;",
"",
"attribute vec2 mapping;",
"//attribute vec3 position;",
"attribute float radius;",
"attribute vec4 matrix1;",
"attribute vec4 matrix2;",
"attribute vec4 matrix3;",
"attribute vec4 matrix4;",
"",
"#ifdef PICKING",
"    #include unpack_clr",
"    attribute float primitiveId;",
"    varying vec3 vPickingColor;",
"#else",
"    #include color_pars_vertex",
"#endif",
"",
"//include matrix_scale",
"float matrixScale( in mat4 m ){",
"    vec4 r = m[ 0 ];",
"    return sqrt( r[ 0 ] * r[ 0 ] + r[ 1 ] * r[ 1 ] + r[ 2 ] * r[ 2 ] );",
"}",
"",
"const mat4 D = mat4(",
"    1.0, 0.0, 0.0, 0.0,",
"    0.0, 1.0, 0.0, 0.0,",
"    0.0, 0.0, 1.0, 0.0,",
"    0.0, 0.0, 0.0, -1.0",
");",
"",
"mat4 transposeTmp( in mat4 inMatrix ) {",
"    vec4 i0 = inMatrix[0];",
"    vec4 i1 = inMatrix[1];",
"    vec4 i2 = inMatrix[2];",
"    vec4 i3 = inMatrix[3];",
"",
"    mat4 outMatrix = mat4(",
"        vec4(i0.x, i1.x, i2.x, i3.x),",
"        vec4(i0.y, i1.y, i2.y, i3.y),",
"        vec4(i0.z, i1.z, i2.z, i3.z),",
"        vec4(i0.w, i1.w, i2.w, i3.w)",
"    );",
"    return outMatrix;",
"}",
"",
"//------------------------------------------------------------------------------",
"// Compute point size and center using the technique described in:",
"// 'GPU-Based Ray-Casting of Quadratic Surfaces'",
"// by Christian Sigg, Tim Weyrich, Mario Botsch, Markus Gross.",
"//",
"// Code based on",
"/*=========================================================================",
"",
" Program:   Visualization Toolkit",
" Module:    Quadrics_fs.glsl and Quadrics_vs.glsl",
"",
" Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen",
" All rights reserved.",
" See Copyright.txt or http://www.kitware.com/Copyright.htm for details.",
"",
" This software is distributed WITHOUT ANY WARRANTY; without even",
" the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR",
" PURPOSE.  See the above copyright notice for more information.",
"",
" =========================================================================*/",
"",
"// .NAME Quadrics_fs.glsl and Quadrics_vs.glsl",
"// .SECTION Thanks",
"// <verbatim>",
"//",
"//  This file is part of the PointSprites plugin developed and contributed by",
"//",
"//  Copyright (c) CSCS - Swiss National Supercomputing Centre",
"//                EDF - Electricite de France",
"//",
"//  John Biddiscombe, Ugo Varetto (CSCS)",
"//  Stephane Ploix (EDF)",
"//",
"// </verbatim>",
"//",
"// Contributions by Alexander Rose",
"// - ported to WebGL",
"// - adapted to work with quads",
"void ComputePointSizeAndPositionInClipCoordSphere(vec4 updatePosition){",
"",
"    vec2 xbc;",
"    vec2 ybc;",
"",
"    mat4 T = mat4(",
"        radius, 0.0, 0.0, 0.0,",
"        0.0, radius, 0.0, 0.0,",
"        0.0, 0.0, radius, 0.0,",
"        updatePosition.x, updatePosition.y, updatePosition.z, 1.0",
"    );",
"",
"    mat4 R = transposeTmp( projectionMatrix * modelViewMatrix * T );",
"    float A = dot( R[ 3 ], D * R[ 3 ] );",
"    float B = -2.0 * dot( R[ 0 ], D * R[ 3 ] );",
"    float C = dot( R[ 0 ], D * R[ 0 ] );",
"    xbc[ 0 ] = ( -B - sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    xbc[ 1 ] = ( -B + sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    float sx = abs( xbc[ 0 ] - xbc[ 1 ] ) * 0.5;",
"",
"    A = dot( R[ 3 ], D * R[ 3 ] );",
"    B = -2.0 * dot( R[ 1 ], D * R[ 3 ] );",
"    C = dot( R[ 1 ], D * R[ 1 ] );",
"    ybc[ 0 ] = ( -B - sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    ybc[ 1 ] = ( -B + sqrt( B * B - 4.0 * A * C ) ) / ( 2.0 * A );",
"    float sy = abs( ybc[ 0 ] - ybc[ 1 ]  ) * 0.5;",
"",
"    gl_Position.xy = vec2( 0.5 * ( xbc.x + xbc.y ), 0.5 * ( ybc.x + ybc.y ) );",
"    gl_Position.xy -= mapping * vec2( sx, sy );",
"    gl_Position.xy *= gl_Position.w;",
"",
"}",
"",
"  mat4 computeMat(vec4 v1, vec4 v2, vec4 v3, vec4 v4) {",
"    return mat4(",
"      v1.x, v1.y, v1.z, v1.w,",
"      v2.x, v2.y, v2.z, v2.w,",
"      v3.x, v3.y, v3.z, v3.w,",
"      v4.x, v4.y, v4.z, v4.w",
"    );",
"  }",
"",
"void main(void){",
"",
"    #ifdef PICKING",
"        vPickingColor = unpackColor( primitiveId );",
"    #else",
"        #include color_vertex",
"    #endif",
"",
"    vRadius = radius * matrixScale( modelViewMatrix );",
"",
"    mat4 matrix = computeMat(matrix1, matrix2, matrix3, matrix4);",
"    vec4 updatePosition = matrix * vec4(position, 1.0);",
"",
"//    vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
"    vec4 mvPosition = modelViewMatrix * vec4( updatePosition.xyz, 1.0 );",
"    // avoid clipping, added again in fragment shader",
"    mvPosition.z -= vRadius;",
"",
"//    gl_Position = projectionMatrix * vec4( mvPosition.xyz, 1.0 );",
"    gl_Position = projectionMatrix * vec4( mvPosition.xyz, 1.0 );",
"    ComputePointSizeAndPositionInClipCoordSphere(updatePosition);",
"",
"",
"    vRadiusSq = vRadius * vRadius;",
"    vec4 vPoint4 = projectionMatrixInverse * gl_Position;",
"    vPoint = vPoint4.xyz / vPoint4.w;",
"    vPointViewPosition = -mvPosition.xyz / mvPosition.w;",
"",
"}"
].join("\n");

$NGL_shaderTextHash['CylinderInstancing.frag'] = $NGL_shaderTextHash['CylinderImpostor.frag'];
$NGL_shaderTextHash['CylinderInstancing.vert'] = ["// Open-Source PyMOL is Copyright (C) Schrodinger, LLC.",
"//",
"//  All Rights Reserved",
"//",
"//  Permission to use, copy, modify, distribute, and distribute modified",
"//  versions of this software and its built-in documentation for any",
"//  purpose and without fee is hereby granted, provided that the above",
"//  copyright notice appears in all copies and that both the copyright",
"//  notice and this permission notice appear in supporting documentation,",
"//  and that the name of Schrodinger, LLC not be used in advertising or",
"//  publicity pertaining to distribution of the software without specific,",
"//  written prior permission.",
"//",
"//  SCHRODINGER, LLC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,",
"//  INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN",
"//  NO EVENT SHALL SCHRODINGER, LLC BE LIABLE FOR ANY SPECIAL, INDIRECT OR",
"//  CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS",
"//  OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE",
"//  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE",
"//  USE OR PERFORMANCE OF THIS SOFTWARE.",
"",
"// Contributions by Alexander Rose",
"// - ported to WebGL",
"// - dual color",
"// - pk color",
"// - shift",
"",
"attribute vec3 mapping;",
"attribute vec3 position1;",
"attribute vec3 position2;",
"attribute float radius;",
"attribute vec4 matrix1;",
"attribute vec4 matrix2;",
"attribute vec4 matrix3;",
"attribute vec4 matrix4;",
"",
"varying vec3 axis;",
"varying vec4 base_radius;",
"varying vec4 end_b;",
"varying vec3 U;",
"varying vec3 V;",
"varying vec4 w;",
"varying float fogDepth;",
"varying float fogNear;",
"varying float fogFar;",
"",
"#ifdef PICKING",
"    #include unpack_clr",
"    attribute float primitiveId;",
"    varying vec3 vPickingColor;",
"#else",
"    //attribute vec3 color;",
"    attribute vec3 color2;",
"    varying vec3 vColor1;",
"    varying vec3 vColor2;",
"#endif",
"",
"uniform mat4 modelViewMatrixInverse;",
"uniform float ortho;",
"",
"//include matrix_scale",
"float matrixScale( in mat4 m ){",
"    vec4 r = m[ 0 ];",
"    return sqrt( r[ 0 ] * r[ 0 ] + r[ 1 ] * r[ 1 ] + r[ 2 ] * r[ 2 ] );",
"}",
"",
"  mat4 computeMat(vec4 v1, vec4 v2, vec4 v3, vec4 v4) {",
"    return mat4(",
"      v1.x, v1.y, v1.z, v1.w,",
"      v2.x, v2.y, v2.z, v2.w,",
"      v3.x, v3.y, v3.z, v3.w,",
"      v4.x, v4.y, v4.z, v4.w",
"    );",
"  }",
"",
"void main(){",
"",
"    #ifdef PICKING",
"        vPickingColor = unpackColor( primitiveId );",
"    #else",
"        vColor1 = color;",
"        vColor2 = color2;",
"    #endif",
"",
"    // vRadius = radius;",
"    base_radius.w = radius * matrixScale( modelViewMatrix );",
"",
"    //vec3 center = ( position2 + position1 ) / 2.0;",
"",
"    mat4 matrix = computeMat(matrix1, matrix2, matrix3, matrix4);",
"    vec4 updatePosition1 = matrix * vec4(position1, 1.0);",
"    vec4 updatePosition2 = matrix * vec4(position2, 1.0);",
"    vec3 center = ( updatePosition2.xyz + updatePosition1.xyz ) / 2.0;",
"",
"    //vec3 dir = normalize( position2 - position1 );",
"    vec3 dir = normalize( updatePosition2.xyz - updatePosition1.xyz );",
"    float ext = length( position2 - position1 ) / 2.0;",
"",
"    // using cameraPosition fails on some machines, not sure why",
"    // vec3 cam_dir = normalize( cameraPosition - mix( center, vec3( 0.0 ), ortho ) );",
"    vec3 cam_dir;",
"    if( ortho == 0.0 ){",
"        cam_dir = ( modelViewMatrixInverse * vec4( 0, 0, 0, 1 ) ).xyz - center;",
"    }else{",
"        cam_dir = ( modelViewMatrixInverse * vec4( 0, 0, 1, 0 ) ).xyz;",
"    }",
"    cam_dir = normalize( cam_dir );",
"",
"    vec3 ldir;",
"",
"    float b = dot( cam_dir, dir );",
"    end_b.w = b;",
"    // direction vector looks away, so flip",
"    if( b < 0.0 )",
"        ldir = -ext * dir;",
"    // direction vector already looks in my direction",
"    else",
"        ldir = ext * dir;",
"",
"    vec3 left = normalize( cross( cam_dir, ldir ) );",
"    left = radius * left;",
"    vec3 up = radius * normalize( cross( left, ldir ) );",
"",
"    // transform to modelview coordinates",
"    axis = normalize( normalMatrix * ldir );",
"    U = normalize( normalMatrix * up );",
"    V = normalize( normalMatrix * left );",
"",
"    vec4 base4 = modelViewMatrix * vec4( center - ldir, 1.0 );",
"    base_radius.xyz = base4.xyz / base4.w;",
"",
"    vec4 top_position = modelViewMatrix * vec4( center + ldir, 1.0 );",
"    vec4 end4 = top_position;",
"    end_b.xyz = end4.xyz / end4.w;",
"",
"    w = modelViewMatrix * vec4(",
"        center + mapping.x*ldir + mapping.y*left + mapping.z*up, 1.0",
"    );",
"",
"    gl_Position = projectionMatrix * w;",
"",
"    // avoid clipping (1.0 seems to induce flickering with some drivers)",
"    gl_Position.z = 0.99;",
"",
"}"
].join("\n");

$NGL_shaderTextHash['Instancing.frag'] = ["#define STANDARD",
"uniform vec3 diffuse;",
"uniform vec3 emissive;",
"uniform float roughness;",
"uniform float metalness;",
"uniform float opacity;",
"uniform float nearClip;",
"uniform float clipRadius;",
"uniform mat4 projectionMatrix;",
"uniform float ortho;",
"varying float bCylinder;",
"",
"#if defined( NEAR_CLIP ) || defined( RADIUS_CLIP ) || ( !defined( PICKING ) && !defined( NOLIGHT ) )",
"    varying vec3 vViewPosition;",
"#endif",
"",
"#if defined( RADIUS_CLIP )",
"    varying vec3 vClipCenter;",
"#endif",
"",
"#if defined( PICKING )",
"    uniform float objectId;",
"    varying vec3 vPickingColor;",
"#elif defined( NOLIGHT )",
"    varying vec3 vColor;",
"#else",
"    #ifndef FLAT_SHADED",
"        varying vec3 vNormal;",
"    #endif",
"    #include common",
"    #include color_pars_fragment",
"    #include fog_pars_fragment",
"    #include bsdfs",
"    #include lights_pars_begin",
"    #include lights_physical_pars_fragment",
"#endif",
"",
"void main(){",
"    #include nearclip_fragment",
"    #include radiusclip_fragment",
"",
"    #if defined( PICKING )",
"",
"        gl_FragColor = vec4( vPickingColor, objectId );",
"",
"    #elif defined( NOLIGHT )",
"",
"        gl_FragColor = vec4( vColor, opacity );",
"",
"    #else",
"",
"        vec4 diffuseColor = vec4( diffuse, opacity );",
"        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );",
"        vec3 totalEmissiveLight = emissive;",
"",
"        #include color_fragment",
"        #include roughnessmap_fragment",
"        #include metalnessmap_fragment",
"        #include normal_flip",
"        #include normal_fragment_begin",
"",
"        //include dull_interior_fragment",
"",
"        #include lights_physical_fragment",
"        //include lights_template",
"        #include lights_fragment_begin",
"        #include lights_fragment_end",
"",
"        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveLight;",
"",
"        #include interior_fragment",
"",
"        gl_FragColor = vec4( outgoingLight, diffuseColor.a );",
"",
"        #include premultiplied_alpha_fragment",
"        #include tonemapping_fragment",
"        #include encodings_fragment",
"        #include fog_fragment",
"",
"        #include opaque_back_fragment",
"",
"    #endif",
"",
"}"
].join("\n");

$NGL_shaderTextHash['Instancing.vert'] = ["#define STANDARD",
"",
"uniform mat4 projectionMatrixInverse;",
"uniform float nearClip;",
"uniform vec3 clipCenter;",
"attribute vec4 matrix1;",
"attribute vec4 matrix2;",
"attribute vec4 matrix3;",
"attribute vec4 matrix4;",
"attribute float cylinder;",
"varying float bCylinder;",
"",
"#if defined( NEAR_CLIP ) || defined( RADIUS_CLIP ) || ( !defined( PICKING ) && !defined( NOLIGHT ) )",
"    varying vec3 vViewPosition;",
"#endif",
"",
"#if defined( RADIUS_CLIP )",
"    varying vec3 vClipCenter;",
"#endif",
"",
"#if defined( PICKING )",
"    #include unpack_color",
"    attribute float primitiveId;",
"    varying vec3 vPickingColor;",
"#elif defined( NOLIGHT )",
"    varying vec3 vColor;",
"#else",
"    #include color_pars_vertex",
"    #ifndef FLAT_SHADED",
"        varying vec3 vNormal;",
"    #endif",
"#endif",
"",
"#include common",
"",
"  mat4 computeMat(vec4 v1, vec4 v2, vec4 v3, vec4 v4) {",
"    return mat4(",
"      v1.x, v1.y, v1.z, v1.w,",
"      v2.x, v2.y, v2.z, v2.w,",
"      v3.x, v3.y, v3.z, v3.w,",
"      v4.x, v4.y, v4.z, v4.w",
"    );",
"  }",
"",
"void main(){",
"    bCylinder = cylinder;",
"",
"    mat4 matrix = computeMat(matrix1, matrix2, matrix3, matrix4);",
"    vec4 updatePosition = matrix * vec4(position, 1.0);",
"",
"    #if defined( PICKING )",
"        vPickingColor = unpackColor( primitiveId );",
"    #elif defined( NOLIGHT )",
"        vColor = color;",
"    #else",
"        #include color_vertex",
"        //include beginnormal_vertex",
"        //vec3 objectNormal = vec3( normal );",
"        vec3 objectNormal = vec3(matrix * vec4(normal,0.0));",
"        #include defaultnormal_vertex",
"        // Normal computed with derivatives when FLAT_SHADED",
"        #ifndef FLAT_SHADED",
"            vNormal = normalize( transformedNormal );",
"        #endif",
"    #endif",
"",
"    //include begin_vertex",
"    vec3 transformed = updatePosition.xyz;",
"    //include project_vertex",
"    vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );",
"    gl_Position = projectionMatrix * mvPosition;",
"",
"    #if defined( NEAR_CLIP ) || defined( RADIUS_CLIP ) || ( !defined( PICKING ) && !defined( NOLIGHT ) )",
"        vViewPosition = -mvPosition.xyz;",
"    #endif",
"",
"    #if defined( RADIUS_CLIP )",
"        vClipCenter = -( modelViewMatrix * vec4( clipCenter, 1.0 ) ).xyz;",
"    #endif",
"",
"    #include nearclip_vertex",
"",
"}"
].join("\n");

/* Projector.js from http://threejs.org/
 * @author mrdoob / http://mrdoob.com/
 * @author supereggbert / http://www.paulbrunt.co.uk/
 * @author julianwa / https://github.com/julianwa
 */



THREE.RenderableObject = function () {
    "use strict";

    this.id = 0;

    this.object = null;
    this.z = 0;

};

//

THREE.RenderableFace = function () {
    "use strict";

    this.id = 0;

    this.v1 = new THREE.RenderableVertex();
    this.v2 = new THREE.RenderableVertex();
    this.v3 = new THREE.RenderableVertex();

    this.normalModel = new THREE.Vector3();

    this.vertexNormalsModel = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
    this.vertexNormalsLength = 0;

    this.color = new THREE.Color();
    this.material = null;
    this.uvs = [ new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2() ];

    this.z = 0;

};

//

THREE.RenderableVertex = function () {
    "use strict";

    this.position = new THREE.Vector3();
    this.positionWorld = new THREE.Vector3();
    this.positionScreen = new THREE.Vector4();

    this.visible = true;

};

THREE.RenderableVertex.prototype.copy = function ( vertex ) {
    "use strict";

    this.positionWorld.copy( vertex.positionWorld );
    this.positionScreen.copy( vertex.positionScreen );

};

//

THREE.RenderableLine = function () {
    "use strict";

    this.id = 0;

    this.v1 = new THREE.RenderableVertex();
    this.v2 = new THREE.RenderableVertex();

    this.vertexColors = [ new THREE.Color(), new THREE.Color() ];
    this.material = null;

    this.z = 0;

};

//

THREE.RenderableSprite = function () {
    "use strict";

    this.id = 0;

    this.object = null;

    this.x = 0;
    this.y = 0;
    this.z = 0;

    this.rotation = 0;
    this.scale = new THREE.Vector2();

    this.material = null;

};

//

THREE.Projector = function () {
    "use strict";

    var _object, _objectCount, _objectPool = [], _objectPoolLength = 0,
    _vertex, _vertexCount, _vertexPool = [], _vertexPoolLength = 0,
    _face, _faceCount, _facePool = [], _facePoolLength = 0,
    _line, _lineCount, _linePool = [], _linePoolLength = 0,
    _sprite, _spriteCount, _spritePool = [], _spritePoolLength = 0,

    _renderData = { objects: [], lights: [], elements: [] },

    _vA = new THREE.Vector3(),
    _vB = new THREE.Vector3(),
    _vC = new THREE.Vector3(),

    _vector3 = new THREE.Vector3(),
    _vector4 = new THREE.Vector4(),

    _clipBox = new THREE.Box3( new THREE.Vector3( - 1, - 1, - 1 ), new THREE.Vector3( 1, 1, 1 ) ),
    _boundingBox = new THREE.Box3(),
    _pnts3 = new Array( 3 ),
    _pnts4 = new Array( 4 ),

    _viewMatrix = new THREE.Matrix4(),
    _viewProjectionMatrix = new THREE.Matrix4(),

    _modelMatrix,
    _modelViewProjectionMatrix = new THREE.Matrix4(),

    _normalMatrix = new THREE.Matrix3(),

    _frustum = new THREE.Frustum(),

    _clippedVertex1PositionScreen = new THREE.Vector4(),
    _clippedVertex2PositionScreen = new THREE.Vector4();

    //

    this.projectVector = function ( vector, camera ) {

        console.warn( 'THREE.Projector: .projectVector() is now vector.project().' );
        vector.project( camera );

    };

    this.unprojectVector = function ( vector, camera ) {

        console.warn( 'THREE.Projector: .unprojectVector() is now vector.unproject().' );
        vector.unproject( camera );

    };

    this.pkRay = function ( vector, camera ) {

        console.error( 'THREE.Projector: .pkRay() is now raycaster.setFromCamera().' );

    };

    //

    var RenderList = function () {

        var normals = [];
        var uvs = [];

        var object = null;
        var material = null;

        var normalMatrix = new THREE.Matrix3();

        var setObject = function ( value ) {

            object = value;
            material = object.material;

            normalMatrix.getNormalMatrix( object.matrixWorld );

            normals.length = 0;
            uvs.length = 0;

        };

        var projectVertex = function ( vertex ) {

            var position = vertex.position;
            var positionWorld = vertex.positionWorld;
            var positionScreen = vertex.positionScreen;

            positionWorld.copy( position ).applyMatrix4( _modelMatrix );
            positionScreen.copy( positionWorld ).applyMatrix4( _viewProjectionMatrix );

            var invW = 1 / positionScreen.w;

            positionScreen.x *= invW;
            positionScreen.y *= invW;
            positionScreen.z *= invW;

            vertex.visible = positionScreen.x >= - 1 && positionScreen.x <= 1 &&
                     positionScreen.y >= - 1 && positionScreen.y <= 1 &&
                     positionScreen.z >= - 1 && positionScreen.z <= 1;

        };

        var pushVertex = function ( x, y, z ) {

            _vertex = getNextVertexInPool();
            _vertex.position.set( x, y, z );

            projectVertex( _vertex );

        };

        var pushNormal = function ( x, y, z ) {

            normals.push( x, y, z );

        };

        var pushUv = function ( x, y ) {

            uvs.push( x, y );

        };

        var checkTriangleVisibility = function ( v1, v2, v3 ) {

            if ( v1.visible === true || v2.visible === true || v3.visible === true ) return true;

            _pnts3[ 0 ] = v1.positionScreen;
            _pnts3[ 1 ] = v2.positionScreen;
            _pnts3[ 2 ] = v3.positionScreen;

            return _clipBox.isIntersectionBox( _boundingBox.setFromPoints( _pnts3 ) );

        };

        var checkBackfaceCulling = function ( v1, v2, v3 ) {

            return ( ( v3.positionScreen.x - v1.positionScreen.x ) *
                    ( v2.positionScreen.y - v1.positionScreen.y ) -
                    ( v3.positionScreen.y - v1.positionScreen.y ) *
                    ( v2.positionScreen.x - v1.positionScreen.x ) ) < 0;

        };

        var pushLine = function ( a, b ) {

            var v1 = _vertexPool[ a ];
            var v2 = _vertexPool[ b ];

            _line = getNextLineInPool();

            _line.id = object.id;
            _line.v1.copy( v1 );
            _line.v2.copy( v2 );
            _line.z = ( v1.positionScreen.z + v2.positionScreen.z ) / 2;

            _line.material = object.material;

            _renderData.elements.push( _line );

        };

        var pushTriangle = function ( a, b, c ) {

            var v1 = _vertexPool[ a ];
            var v2 = _vertexPool[ b ];
            var v3 = _vertexPool[ c ];

            if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return;

            if ( material.side === THREE.DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) {

                _face = getNextFaceInPool();

                _face.id = object.id;
                _face.v1.copy( v1 );
                _face.v2.copy( v2 );
                _face.v3.copy( v3 );
                _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;

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

                    var offset = arguments[ i ] * 3;
                    var normal = _face.vertexNormalsModel[ i ];

                    normal.set( normals[ offset ], normals[ offset + 1 ], normals[ offset + 2 ] );
                    normal.applyMatrix3( normalMatrix ).normalize();

                    var offset2 = arguments[ i ] * 2;

                    var uv = _face.uvs[ i ];
                    uv.set( uvs[ offset2 ], uvs[ offset2 + 1 ] );

                }

                _face.vertexNormalsLength = 3;

                _face.material = object.material;

                _renderData.elements.push( _face );

            }

        };

        return {
            setObject: setObject,
            projectVertex: projectVertex,
            checkTriangleVisibility: checkTriangleVisibility,
            checkBackfaceCulling: checkBackfaceCulling,
            pushVertex: pushVertex,
            pushNormal: pushNormal,
            pushUv: pushUv,
            pushLine: pushLine,
            pushTriangle: pushTriangle
        }

    };

    var renderList = new RenderList();

    this.projectScene = function ( scene, camera, sortObjects, sortElements ) {

        _faceCount = 0;
        _lineCount = 0;
        _spriteCount = 0;

        _renderData.elements.length = 0;

        if ( scene.autoUpdate === true ) scene.updateMatrixWorld();
        if ( camera.parent === undefined ) camera.updateMatrixWorld();

        //_viewMatrix.copy( camera.matrixWorldInverse.getInverse( camera.matrixWorld ) );
        _viewMatrix.copy( camera.matrixWorldInverse.copy(camera.matrixWorld).invert() );
        _viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );

        _frustum.setFromMatrix( _viewProjectionMatrix );

        //

        _objectCount = 0;

        _renderData.objects.length = 0;
        _renderData.lights.length = 0;

        scene.traverseVisible( function ( object ) {

            if ( object instanceof THREE.Light ) {

                _renderData.lights.push( object );

            } else if ( object instanceof THREE.Mesh || object instanceof THREE.Line || object instanceof THREE.Sprite ) {

                if ( object.material.visible === false ) return;

                if ( object.frustumCulled === false || _frustum.intersectsObject( object ) === true ) {

                    _object = getNextObjectInPool();
                    _object.id = object.id;
                    _object.object = object;

                    _vector3.setFromMatrixPosition( object.matrixWorld );
                    _vector3.applyProjection( _viewProjectionMatrix );
                    _object.z = _vector3.z;

                    _renderData.objects.push( _object );

                }

            }

        } );

        if ( sortObjects === true ) {

            _renderData.objects.sort( painterSort );

        }

        //

        for ( var o = 0, ol = _renderData.objects.length; o < ol; o ++ ) {

            var object = _renderData.objects[ o ].object;
            var geometry = object.geometry;

            renderList.setObject( object );

            _modelMatrix = object.matrixWorld;

            _vertexCount = 0;

            if ( object instanceof THREE.Mesh ) {

                if ( geometry instanceof THREE.BufferGeometry ) {

                    var attributes = geometry.attributes;
                    var offsets = geometry.offsets;

                    if ( attributes.position === undefined ) continue;

                    var positions = attributes.position.array;

                    for ( var i = 0, l = positions.length; i < l; i += 3 ) {

                        renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );

                    }

                    if ( attributes.normal !== undefined ) {

                        var normals = attributes.normal.array;

                        for ( var i = 0, l = normals.length; i < l; i += 3 ) {

                            renderList.pushNormal( normals[ i ], normals[ i + 1 ], normals[ i + 2 ] );

                        }

                    }

                    if ( attributes.uv !== undefined ) {

                        var uvs = attributes.uv.array;

                        for ( var i = 0, l = uvs.length; i < l; i += 2 ) {

                            renderList.pushUv( uvs[ i ], uvs[ i + 1 ] );

                        }

                    }

                    if ( attributes.index !== undefined ) {

                        var indices = attributes.index.array;

                        if ( offsets.length > 0 ) {

                            for ( var o = 0; o < offsets.length; o ++ ) {

                                var offset = offsets[ o ];
                                var index = offset.index;

                                for ( var i = offset.start, l = offset.start + offset.count; i < l; i += 3 ) {

                                    renderList.pushTriangle( indices[ i ] + index, indices[ i + 1 ] + index, indices[ i + 2 ] + index );

                                }

                            }

                        } else {

                            for ( var i = 0, l = indices.length; i < l; i += 3 ) {

                                renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] );

                            }

                        }

                    } else {

                        for ( var i = 0, l = positions.length / 3; i < l; i += 3 ) {

                            renderList.pushTriangle( i, i + 1, i + 2 );

                        }

                    }

                }
                /*
                else if ( geometry instanceof THREE.Geometry ) {

                    var vertices = geometry.vertices;
                    var faces = geometry.faces;
                    var faceVertexUvs = geometry.faceVertexUvs[ 0 ];

                    _normalMatrix.getNormalMatrix( _modelMatrix );

                    var isFaceMaterial = object.material instanceof THREE.MeshFaceMaterial;
                    var objectMaterials = isFaceMaterial === true ? object.material : null;

                    for ( var v = 0, vl = vertices.length; v < vl; v ++ ) {

                        var vertex = vertices[ v ];
                        renderList.pushVertex( vertex.x, vertex.y, vertex.z );

                    }

                    for ( var f = 0, fl = faces.length; f < fl; f ++ ) {

                        var face = faces[ f ];

                        var material = isFaceMaterial === true
                             ? objectMaterials.materials[ face.materialIndex ]
                             : object.material;

                        if ( material === undefined ) continue;

                        var side = material.side;

                        var v1 = _vertexPool[ face.a ];
                        var v2 = _vertexPool[ face.b ];
                        var v3 = _vertexPool[ face.c ];

                        if ( material.morphTargets === true ) {

                            var morphTargets = geometry.morphTargets;
                            var morphInfluences = object.morphTargetInfluences;

                            var v1p = v1.position;
                            var v2p = v2.position;
                            var v3p = v3.position;

                            _vA.set( 0, 0, 0 );
                            _vB.set( 0, 0, 0 );
                            _vC.set( 0, 0, 0 );

                            for ( var t = 0, tl = morphTargets.length; t < tl; t ++ ) {

                                var influence = morphInfluences[ t ];

                                if ( influence === 0 ) continue;

                                var targets = morphTargets[ t ].vertices;

                                _vA.x += ( targets[ face.a ].x - v1p.x ) * influence;
                                _vA.y += ( targets[ face.a ].y - v1p.y ) * influence;
                                _vA.z += ( targets[ face.a ].z - v1p.z ) * influence;

                                _vB.x += ( targets[ face.b ].x - v2p.x ) * influence;
                                _vB.y += ( targets[ face.b ].y - v2p.y ) * influence;
                                _vB.z += ( targets[ face.b ].z - v2p.z ) * influence;

                                _vC.x += ( targets[ face.c ].x - v3p.x ) * influence;
                                _vC.y += ( targets[ face.c ].y - v3p.y ) * influence;
                                _vC.z += ( targets[ face.c ].z - v3p.z ) * influence;

                            }

                            v1.position.add( _vA );
                            v2.position.add( _vB );
                            v3.position.add( _vC );

                            renderList.projectVertex( v1 );
                            renderList.projectVertex( v2 );
                            renderList.projectVertex( v3 );

                        }

                        if ( renderList.checkTriangleVisibility( v1, v2, v3 ) === false ) continue;

                        var visible = renderList.checkBackfaceCulling( v1, v2, v3 );

                        if ( side !== THREE.DoubleSide ) {
                            if ( side === THREE.FrontSide && visible === false ) continue;
                            if ( side === THREE.BackSide && visible === true ) continue;
                        }

                        _face = getNextFaceInPool();

                        _face.id = object.id;
                        _face.v1.copy( v1 );
                        _face.v2.copy( v2 );
                        _face.v3.copy( v3 );

                        _face.normalModel.copy( face.normal );

                        if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) {

                            _face.normalModel.negate();

                        }

                        _face.normalModel.applyMatrix3( _normalMatrix ).normalize();

                        var faceVertexNormals = face.vertexNormals;

                        for ( var n = 0, nl = Math.min( faceVertexNormals.length, 3 ); n < nl; n ++ ) {

                            var normalModel = _face.vertexNormalsModel[ n ];
                            normalModel.copy( faceVertexNormals[ n ] );

                            if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) {

                                normalModel.negate();

                            }

                            normalModel.applyMatrix3( _normalMatrix ).normalize();

                        }

                        _face.vertexNormalsLength = faceVertexNormals.length;

                        var vertexUvs = faceVertexUvs[ f ];

                        if ( vertexUvs !== undefined ) {

                            for ( var u = 0; u < 3; u ++ ) {

                                _face.uvs[ u ].copy( vertexUvs[ u ] );

                            }

                        }

                        _face.color = face.color;
                        _face.material = material;

                        _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;

                        _renderData.elements.push( _face );

                    }

                }
                */

            } else if ( object instanceof THREE.Line ) {

                if ( geometry instanceof THREE.BufferGeometry ) {

                    var attributes = geometry.attributes;

                    if ( attributes.position !== undefined ) {

                        var positions = attributes.position.array;

                        for ( var i = 0, l = positions.length; i < l; i += 3 ) {

                            renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );

                        }

                        if ( attributes.index !== undefined ) {

                            var indices = attributes.index.array;

                            for ( var i = 0, l = indices.length; i < l; i += 2 ) {

                                renderList.pushLine( indices[ i ], indices[ i + 1 ] );

                            }

                        } else {

                            var step = object.mode === THREE.LinePieces ? 2 : 1;

                            for ( var i = 0, l = ( positions.length / 3 ) - 1; i < l; i += step ) {

                                renderList.pushLine( i, i + 1 );

                            }

                        }

                    }

                }
                /*
                else if ( geometry instanceof THREE.Geometry ) {

                    _modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix );

                    var vertices = object.geometry.vertices;

                    if ( vertices.length === 0 ) continue;

                    v1 = getNextVertexInPool();
                    v1.positionScreen.copy( vertices[ 0 ] ).applyMatrix4( _modelViewProjectionMatrix );

                    // Handle LineStrip and LinePieces
                    var step = object.mode === THREE.LinePieces ? 2 : 1;

                    for ( var v = 1, vl = vertices.length; v < vl; v ++ ) {

                        v1 = getNextVertexInPool();
                        v1.positionScreen.copy( vertices[ v ] ).applyMatrix4( _modelViewProjectionMatrix );

                        if ( ( v + 1 ) % step > 0 ) continue;

                        v2 = _vertexPool[ _vertexCount - 2 ];

                        _clippedVertex1PositionScreen.copy( v1.positionScreen );
                        _clippedVertex2PositionScreen.copy( v2.positionScreen );

                        if ( clipLine( _clippedVertex1PositionScreen, _clippedVertex2PositionScreen ) === true ) {

                            // Perform the perspective divide
                            _clippedVertex1PositionScreen.multiplyScalar( 1 / _clippedVertex1PositionScreen.w );
                            _clippedVertex2PositionScreen.multiplyScalar( 1 / _clippedVertex2PositionScreen.w );

                            _line = getNextLineInPool();

                            _line.id = object.id;
                            _line.v1.positionScreen.copy( _clippedVertex1PositionScreen );
                            _line.v2.positionScreen.copy( _clippedVertex2PositionScreen );

                            _line.z = Math.max( _clippedVertex1PositionScreen.z, _clippedVertex2PositionScreen.z );

                            _line.material = object.material;

                            if ( object.material.vertexColors === THREE.VertexColors ) {

                                _line.vertexColors[ 0 ].copy( object.geometry.colors[ v ] );
                                _line.vertexColors[ 1 ].copy( object.geometry.colors[ v - 1 ] );

                            }

                            _renderData.elements.push( _line );

                        }

                    }

                }
                */

            } else if ( object instanceof THREE.Sprite ) {

                _vector4.set( _modelMatrix.elements[ 12 ], _modelMatrix.elements[ 13 ], _modelMatrix.elements[ 14 ], 1 );
                _vector4.applyMatrix4( _viewProjectionMatrix );

                var invW = 1 / _vector4.w;

                _vector4.z *= invW;

                if ( _vector4.z >= - 1 && _vector4.z <= 1 ) {

                    _sprite = getNextSpriteInPool();
                    _sprite.id = object.id;
                    _sprite.x = _vector4.x * invW;
                    _sprite.y = _vector4.y * invW;
                    _sprite.z = _vector4.z;
                    _sprite.object = object;

                    _sprite.rotation = object.rotation;

                    _sprite.scale.x = object.scale.x * Math.abs( _sprite.x - ( _vector4.x + camera.projectionMatrix.elements[ 0 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 12 ] ) );
                    _sprite.scale.y = object.scale.y * Math.abs( _sprite.y - ( _vector4.y + camera.projectionMatrix.elements[ 5 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 13 ] ) );

                    _sprite.material = object.material;

                    _renderData.elements.push( _sprite );

                }

            }

        }

        if ( sortElements === true ) {

            _renderData.elements.sort( painterSort );

        }

        return _renderData;

    };

    // Pools

    function getNextObjectInPool() {

        if ( _objectCount === _objectPoolLength ) {

            var object = new THREE.RenderableObject();
            _objectPool.push( object );
            _objectPoolLength ++;
            _objectCount ++;
            return object;

        }

        return _objectPool[ _objectCount ++ ];

    }

    function getNextVertexInPool() {

        if ( _vertexCount === _vertexPoolLength ) {

            var vertex = new THREE.RenderableVertex();
            _vertexPool.push( vertex );
            _vertexPoolLength ++;
            _vertexCount ++;
            return vertex;

        }

        return _vertexPool[ _vertexCount ++ ];

    }

    function getNextFaceInPool() {

        if ( _faceCount === _facePoolLength ) {

            var face = new THREE.RenderableFace();
            _facePool.push( face );
            _facePoolLength ++;
            _faceCount ++;
            return face;

        }

        return _facePool[ _faceCount ++ ];


    }

    function getNextLineInPool() {

        if ( _lineCount === _linePoolLength ) {

            var line = new THREE.RenderableLine();
            _linePool.push( line );
            _linePoolLength ++;
            _lineCount ++
            return line;

        }

        return _linePool[ _lineCount ++ ];

    }

    function getNextSpriteInPool() {

        if ( _spriteCount === _spritePoolLength ) {

            var sprite = new THREE.RenderableSprite();
            _spritePool.push( sprite );
            _spritePoolLength ++;
            _spriteCount ++
            return sprite;

        }

        return _spritePool[ _spriteCount ++ ];

    }

    //

    function painterSort( a, b ) {

        if ( a.z !== b.z ) {

            return b.z - a.z;

        } else if ( a.id !== b.id ) {

            return a.id - b.id;

        } else {

            return 0;

        }

    }

    function clipLine( s1, s2 ) {

        var alpha1 = 0, alpha2 = 1,

        // Calculate the boundary coordinate of each vertex for the near and far clip planes,
        // Z = -1 and Z = +1, respectively.
        bc1near =  s1.z + s1.w,
        bc2near =  s2.z + s2.w,
        bc1far =  - s1.z + s1.w,
        bc2far =  - s2.z + s2.w;

        if ( bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0 ) {

            // Both vertices lie entirely within all clip planes.
            return true;

        } else if ( ( bc1near < 0 && bc2near < 0 ) || ( bc1far < 0 && bc2far < 0 ) ) {

            // Both vertices lie entirely outside one of the clip planes.
            return false;

        } else {

            // The line segment spans at least one clip plane.

            if ( bc1near < 0 ) {

                // v1 lies outside the near plane, v2 inside
                alpha1 = Math.max( alpha1, bc1near / ( bc1near - bc2near ) );

            } else if ( bc2near < 0 ) {

                // v2 lies outside the near plane, v1 inside
                alpha2 = Math.min( alpha2, bc1near / ( bc1near - bc2near ) );

            }

            if ( bc1far < 0 ) {

                // v1 lies outside the far plane, v2 inside
                alpha1 = Math.max( alpha1, bc1far / ( bc1far - bc2far ) );

            } else if ( bc2far < 0 ) {

                // v2 lies outside the far plane, v2 inside
                alpha2 = Math.min( alpha2, bc1far / ( bc1far - bc2far ) );

            }

            if ( alpha2 < alpha1 ) {

                // The line segment spans two boundaries, but is outside both of them.
                // (This can't happen when we're only clipping against just near/far but good
                //  to leave the check here for future usage if other clip planes are added.)
                return false;

            } else {

                // Update the s1 and s2 vertices to match the clipped line segment.
                s1.lerp( s2, alpha1 );
                s2.lerp( s1, 1 - alpha2 );

                return true;

            }

        }

    }

};

/* TrackballControls.js from http://threejs.org/
 * @author Eberhard Graether / http://egraether.com/
 * @author Mark Lundin  / http://mark-lundin.com
 * modified by Jiyao Wang
 */



THREE.TrackballControls = function ( object, domElement, icn3d ) {
    "use strict";

    var _this = this;

    this.STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };

    this.object = object;
    this.domElement = ( domElement !== undefined ) ? domElement : document;

    // API
    this.enabled = true;

    this.screen = { left: 0, top: 0, width: 0, height: 0 };

    this.rotateSpeed = 1.0;
    this.zoomSpeed = 1.2;
    this.panSpeed = 0.3;

    this.noRotate = false;
    this.noZoom = false;
    this.noPan = false;
    this.noRoll = false;

    this.staticMoving = false;
    this.dynamicDampingFactor = 0.2;

    this.minDistance = 0;
    this.maxDistance = Infinity;

    this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];

    // internals

    this.target = new THREE.Vector3();

    var EPS = 0.000001;

    var lastPosition = new THREE.Vector3();

    this._state = this.STATE.NONE;
    var _prevState = this.STATE.NONE;

    var _eye = new THREE.Vector3();

    this._rotateStart = new THREE.Vector3();
    this._rotateEnd = new THREE.Vector3();

    this._zoomStart = new THREE.Vector2();
    this._zoomEnd = new THREE.Vector2();

    var _touchZoomDistanceStart = 0;
    var _touchZoomDistanceEnd = 0;

    this._panStart = new THREE.Vector2();
    this._panEnd = new THREE.Vector2();

    // for reset

    this.target0 = this.target.clone();
    this.position0 = this.object.position.clone();
    this.up0 = this.object.up.clone();

    // events

    var changeEvent = { type: 'change' };
    var startEvent = { type: 'start'};
    var endEvent = { type: 'end'};


    // methods

    this.handleResize = function () {

        if ( this.domElement === document ) {

            this.screen.left = 0;
            this.screen.top = 0;
            this.screen.width = window.innerWidth;
            this.screen.height = window.innerHeight;

        } else {

            var box = this.domElement.getBoundingClientRect();
            // adjustments come from similar code in the jquery offset() function
            var d = this.domElement.ownerDocument.documentElement;
            this.screen.left = box.left + window.pageXOffset - d.clientLeft;
            this.screen.top = box.top + window.pageYOffset - d.clientTop;
            this.screen.width = box.width;
            this.screen.height = box.height;

        }

    };

    this.handleEvent = function ( event ) {

        if ( typeof this[ event.type ] === 'function' ) {

            this[ event.type ]( event );

        }

    };

    var getMouseOnScreen = ( function () {

        var vector = new THREE.Vector2();

        return function ( pageX, pageY ) {

            vector.set(
                ( pageX - _this.screen.left ) / _this.screen.width,
                ( pageY - _this.screen.top ) / _this.screen.height
            );

            return vector;

        };

    }() );

    var getMouseProjectionOnBall = ( function () {

        var vector = new THREE.Vector3();
        var objectUp = new THREE.Vector3();
        var mouseOnBall = new THREE.Vector3();

        return function ( pageX, pageY ) {

            mouseOnBall.set(
                ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / (_this.screen.width*.5),
                ( _this.screen.height * 0.5 + _this.screen.top - pageY ) / (_this.screen.height*.5),
                0.0
            );

            var length = mouseOnBall.length();

            if ( _this.noRoll ) {

                if ( length < Math.SQRT1_2 ) {

                    mouseOnBall.z = Math.sqrt( 1.0 - length*length );

                } else {

                    mouseOnBall.z = .5 / length;

                }

            } else if ( length > 1.0 ) {

                mouseOnBall.normalize();

            } else {

                mouseOnBall.z = Math.sqrt( 1.0 - length * length );

            }

            _eye.copy( _this.object.position ).sub( _this.target );

            vector.copy( _this.object.up ).setLength( mouseOnBall.y )
            vector.add( objectUp.copy( _this.object.up ).cross( _eye ).setLength( mouseOnBall.x ) );
            vector.add( _eye.setLength( mouseOnBall.z ) );

            return vector;

        };

    }() );

    this.rotateCamera = (function(quaternionIn, bUpdate){

        var axis = new THREE.Vector3(),
            quaternion = new THREE.Quaternion();


        return function (quaternionIn, bUpdate) {

            var angle;
            if(quaternionIn === undefined) {
              angle = Math.acos( _this._rotateStart.dot( _this._rotateEnd ) / _this._rotateStart.length() / _this._rotateEnd.length() );
            }

            //var angle = Math.acos( _this._rotateStart.dot( _this._rotateEnd ) / _this._rotateStart.length() / _this._rotateEnd.length() );

            if ( angle || quaternionIn !== undefined) {
                if(quaternionIn === undefined) {
                  axis.crossVectors( _this._rotateStart, _this._rotateEnd ).normalize();

                  angle *= _this.rotateSpeed;

                  quaternion.setFromAxisAngle( axis, -angle );
                }
                else {
                  quaternion.copy(quaternionIn);
                }

                // order matters in quaernion multiplication: http://www.cprogramming.com/tutorial/3d/quaternions.html
                if(icn3d !== undefined && icn3d.quaternion !== undefined && (bUpdate === undefined || bUpdate === true)) {
                    icn3d.quaternion.multiplyQuaternions(quaternion, icn3d.quaternion);
                }

                _eye.applyQuaternion( quaternion );
                _this.object.up.applyQuaternion( quaternion );

                _this._rotateEnd.applyQuaternion( quaternion );

                if ( _this.staticMoving ) {

                    _this._rotateStart.copy( _this._rotateEnd );

                } else {

                    quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) );
                    _this._rotateStart.applyQuaternion( quaternion );

                }
            }

        }

    }());

    this.zoomCamera = function (zoomFactor, bUpdate) {
        if ( _this._state === _this.STATE.TOUCH_ZOOM_PAN ) {

            var factor;

            if(zoomFactor !== undefined) {
              factor = zoomFactor;
            }
            else {

              factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
              _touchZoomDistanceStart = _touchZoomDistanceEnd;
            }

            _eye.multiplyScalar( factor );

            if(icn3d !== undefined && icn3d._zoomFactor !== undefined && (bUpdate === undefined || bUpdate === true)) {
                icn3d._zoomFactor *= factor;
                icn3d.fogCls.setFog();
            }

        } else {

            var factor;

            if(zoomFactor !== undefined) {
              factor = zoomFactor;
            }
            else {
              factor = 1.0 + ( _this._zoomEnd.y - _this._zoomStart.y ) * _this.zoomSpeed;
            }

            if(icn3d !== undefined && icn3d._zoomFactor !== undefined && (bUpdate === undefined || bUpdate === true)) {
                icn3d._zoomFactor *= factor;
                icn3d.fogCls.setFog();
            }

            //if ( factor !== 1.0 && factor > 0.0 ) {
            if ( factor !== 1.0 ) {

                _eye.multiplyScalar( factor );

                if ( _this.staticMoving ) {

                    _this._zoomStart.copy( _this._zoomEnd );

                } else {

                    _this._zoomStart.y += ( _this._zoomEnd.y - _this._zoomStart.y ) * this.dynamicDampingFactor;
                }
            }

        }

    };

    this.panCamera = (function(mouseChangeIn, bUpdate){

        var mouseChange = new THREE.Vector2(),
            objectUp = new THREE.Vector3(),
            pan = new THREE.Vector3();

        return function (mouseChangeIn, bUpdate) {

            if(mouseChangeIn !== undefined) {
              mouseChange = mouseChangeIn;

              if(icn3d !== undefined && icn3d.mouseChange !== undefined && (bUpdate === undefined || bUpdate === true)) icn3d.mouseChange.add(mouseChangeIn);
            }
            else {
              mouseChange.copy( _this._panEnd ).sub( _this._panStart );

              if(icn3d !== undefined && icn3d.mouseChange !== undefined && (bUpdate === undefined || bUpdate === true)) icn3d.mouseChange.add( _this._panEnd ).sub( _this._panStart );
            }

            if ( mouseChange.lengthSq() ) {
                mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );

                pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
                pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );

                _this.object.position.add( pan );
                _this.target.add( pan );

                if ( _this.staticMoving ) {

                    _this._panStart.copy( _this._panEnd );

                } else {

                    _this._panStart.add( mouseChange.subVectors( _this._panEnd, _this._panStart ).multiplyScalar( _this.dynamicDampingFactor ) );

                }

            }
        }

    }());

    this.checkDistances = function () {

        if ( !_this.noZoom || !_this.noPan ) {

            if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) {

                _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) );

            }

            if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {

                _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );

            }

        }

    };

    this.update = function (para) {

        _eye.subVectors( _this.object.position, _this.target );

        if ( !_this.noRotate ) {

            if(para !== undefined && para.quaternion !== undefined) {
              _this.rotateCamera(para.quaternion, para.update);
            }
            else {
              _this.rotateCamera();
            }

        }

        if ( !_this.noZoom ) {

            if(para !== undefined && para._zoomFactor !== undefined) {
              _this.zoomCamera(para._zoomFactor, para.update);
            }
            else {
              _this.zoomCamera();
            }

        }

        if ( !_this.noPan ) {

            if(para !== undefined && para.mouseChange !== undefined) {
              _this.panCamera(para.mouseChange, para.update);
            }
            else {
              _this.panCamera();
            }

        }

        _this.object.position.addVectors( _this.target, _eye );

        _this.checkDistances();

        _this.object.lookAt( _this.target );

        if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) {

            _this.dispatchEvent( changeEvent );

            lastPosition.copy( _this.object.position );

        }

    };

    this.reset = function () {

        _this._state = _this.STATE.NONE;
        _prevState = _this.STATE.NONE;

        _this.target.copy( _this.target0 );
        _this.object.position.copy( _this.position0 );
        _this.object.up.copy( _this.up0 );

        _eye.subVectors( _this.object.position, _this.target );

        _this.object.lookAt( _this.target );

        _this.dispatchEvent( changeEvent );

        lastPosition.copy( _this.object.position );

    };

    // listeners

    function keydown( event ) {
//console.log("keydown");

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        window.removeEventListener( 'keydown', keydown );

        _prevState = _this._state;


        if ( _this._state !== _this.STATE.NONE ) {

            return;

        } else if ( event.keyCode === _this.keys[ _this.STATE.ROTATE ] &&  !_this.noRotate) {

            _this._state = _this.STATE.ROTATE;

        } else if ( (event.keyCode === _this.keys[ _this.STATE.ZOOM ]) && !_this.noZoom ) {

            _this._state = _this.STATE.ZOOM;

        } else if ( (event.keyCode === _this.keys[ _this.STATE.PAN ]) && !_this.noPan ) {

            _this._state = _this.STATE.PAN;

        }


    }

    function keyup( event ) {
//console.log("keyup");

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        _this._state = _prevState;

        window.addEventListener( 'keydown', keydown, false );

    }

    function mousedown( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        if ( _this._state === _this.STATE.NONE ) {

            _this._state = event.button;

        }

        if ( _this._state === _this.STATE.ROTATE && !_this.noRotate ) {

            _this._rotateStart.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) );
            _this._rotateEnd.copy( _this._rotateStart );

        } else if ( _this._state === _this.STATE.ZOOM && !_this.noZoom ) {

            _this._zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
            _this._zoomEnd.copy(_this._zoomStart);

        } else if ( _this._state === _this.STATE.PAN && !_this.noPan ) {

            _this._panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
            _this._panEnd.copy(_this._panStart)

        }

        document.addEventListener( 'mousemove', mousemove, false );
        document.addEventListener( 'mouseup', mouseup, false );

        _this.dispatchEvent( startEvent );

    }

    function mousemove( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        if ( _this._state === _this.STATE.ROTATE && !_this.noRotate ) {

//console.log("ROTATE");
            _this._rotateEnd.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) );

        } else if ( _this._state === _this.STATE.ZOOM && !_this.noZoom ) {

            _this._zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );

        } else if ( _this._state === _this.STATE.PAN && !_this.noPan ) {

            _this._panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );

        }

    }

    function mouseup( event ) {
        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        _this._state = _this.STATE.NONE;

        document.removeEventListener( 'mousemove', mousemove );
        document.removeEventListener( 'mouseup', mouseup );
        _this.dispatchEvent( endEvent );

    }

    function mousewheel( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        var delta = 0;

        if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9

            delta = event.wheelDelta / 40;

        } else if ( event.detail ) { // Firefox

            delta = - event.detail / 3;

        }

        //_this._zoomStart.y += delta * 0.01;
        //_this._zoomStart.y = delta * 0.01;
        _this._zoomStart.y = delta * 0.005;
        _this.dispatchEvent( startEvent );
        _this.dispatchEvent( endEvent );

    }

    function touchstart( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        switch ( event.touches.length ) {
            case 1:
                _this._state = _this.STATE.TOUCH_ROTATE;
                _this._rotateStart.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
                _this._rotateEnd.copy( _this._rotateStart );
                break;

            case 2:
                _this._state = _this.STATE.TOUCH_ZOOM_PAN;
                var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
                var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
                _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );

                var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
                var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
                _this._panStart.copy( getMouseOnScreen( x, y ) );
                _this._panEnd.copy( _this._panStart );
                break;

            default:
                _this._state = _this.STATE.NONE;

        }
        _this.dispatchEvent( startEvent );


    }

    function touchmove( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        switch ( event.touches.length ) {

            case 1:
                _this._rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
                break;

            case 2:
                var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
                var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
                _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );

                var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
                var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
                _this._panEnd.copy( getMouseOnScreen( x, y ) );
                break;

            default:
                _this._state = _this.STATE.NONE;

        }

    }

    function touchend( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        switch ( event.touches.length ) {

            case 1:
                _this._rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
                _this._rotateStart.copy( _this._rotateEnd );
                break;

            case 2:
                _touchZoomDistanceStart = _touchZoomDistanceEnd = 0;

                var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
                var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
                _this._panEnd.copy( getMouseOnScreen( x, y ) );
                _this._panStart.copy( _this._panEnd );
                break;

        }

        _this._state = _this.STATE.NONE;
        _this.dispatchEvent( endEvent );

    }

    if(Object.keys(window).length >= 2) {
        this.domElement.addEventListener( 'contextmn', function ( event ) {
            //event.preventDefault();
        }, false );

        this.domElement.addEventListener( 'mousedown', mousedown, false );

        this.domElement.addEventListener( 'mousewheel', mousewheel, false );
        this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox

        this.domElement.addEventListener( 'touchstart', touchstart, false );
        this.domElement.addEventListener( 'touchend', touchend, false );
        this.domElement.addEventListener( 'touchmove', touchmove, false );

        if(Object.keys(window).length >= 2) window.addEventListener( 'keydown', keydown, false );
        if(Object.keys(window).length >= 2) window.addEventListener( 'keyup', keyup, false );
    }

    this.handleResize();

    // force an update at start
    this.update();

};

THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;

/* OrthographicTrackballControls.js from http://threejs.org/
 * @author Eberhard Graether / http://egraether.com/
 * @author Mark Lundin  / http://mark-lundin.com
 * @author Patrick Fuller / http://patrick-fuller.com
 * modified by Jiyao Wang
 */



THREE.OrthographicTrackballControls = function ( object, domElement, icn3d ) { var me = this, ic = me.icn3d; "use strict";
    var _this = this;
    var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };

    this.object = object;
    this.domElement = ( domElement !== undefined ) ? domElement : document;

    // API
    this.enabled = true;

    this.screen = { left: 0, top: 0, width: 0, height: 0 };

    // JW: the rotation speed of orthographic should be much less than that of perspective
    //this.rotateSpeed = 1.0;
    this.rotateSpeed = 0.5;
    this.zoomSpeed = 1.2;

    var zoomSpeedAdjust = 0.01;
    this.zoomSpeed *= zoomSpeedAdjust;

    //this.panSpeed = 0.3;
    this.panSpeed = 0.03;

    this.noRotate = false;
    this.noZoom = false;
    this.noPan = false;
    this.noRoll = false;

    this.staticMoving = false;
    this.dynamicDampingFactor = 0.2;

    this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];

    // internals

    this.target = new THREE.Vector3();

    var EPS = 0.000001;

    var lastPosition = new THREE.Vector3();

    this._state = STATE.NONE;
    var _prevState = STATE.NONE;

    var _eye = new THREE.Vector3();

    this._rotateStart = new THREE.Vector3();
    this._rotateEnd = new THREE.Vector3();

    this._zoomStart = new THREE.Vector2();
    this._zoomEnd = new THREE.Vector2();
    var _zoomFactor = 1;

    var _touchZoomDistanceStart = 0;
    var _touchZoomDistanceEnd = 0;

    this._panStart = new THREE.Vector2();
    this._panEnd = new THREE.Vector2();

    // for reset

    this.target0 = this.target.clone();
    this.position0 = this.object.position.clone();
    this.up0 = this.object.up.clone();

    this.left0 = this.object.left;
    this.right0 = this.object.right;
    this.top0 = this.object.top;
    this.bottom0 = this.object.bottom;
    this.center0 = new THREE.Vector2((this.left0 + this.right0) / 2.0, (this.top0 + this.bottom0) / 2.0);

    // events

    var changeEvent = { type: 'change' };
    var startEvent = { type: 'start'};
    var endEvent = { type: 'end'};


    // methods

    this.handleResize = function () {

        if ( this.domElement === document ) {

            this.screen.left = 0;
            this.screen.top = 0;
            this.screen.width = window.innerWidth;
            this.screen.height = window.innerHeight;

        } else {

            var box = this.domElement.getBoundingClientRect();
            // adjustments come from similar code in the jquery offset() function
            var d = this.domElement.ownerDocument.documentElement;
            this.screen.left = box.left + window.pageXOffset - d.clientLeft;
            this.screen.top = box.top + window.pageYOffset - d.clientTop;
            this.screen.width = box.width;
            this.screen.height = box.height;
        }

        this.left0 = this.object.left;
        this.right0 = this.object.right;
        this.top0 = this.object.top;
        this.bottom0 = this.object.bottom;
        this.center0.set((this.left0 + this.right0) / 2.0, (this.top0 + this.bottom0) / 2.0);

    };

    this.handleEvent = function ( event ) {

        if ( typeof this[ event.type ] === 'function' ) {

            this[ event.type ]( event );

        }

    };

    var getMouseOnScreen = ( function () {

        var vector = new THREE.Vector2();

        return function ( pageX, pageY ) {

            vector.set(
                ( pageX - _this.screen.left ) / _this.screen.width,
                ( pageY - _this.screen.top ) / _this.screen.height
            );

            return vector;

        };

    }() );

    var getMouseProjectionOnBall = ( function () {

        var vector = new THREE.Vector3();
        var objectUp = new THREE.Vector3();
        var mouseOnBall = new THREE.Vector3();

        return function ( pageX, pageY ) {

            mouseOnBall.set(
                ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / (_this.screen.width*.5),
                ( _this.screen.height * 0.5 + _this.screen.top - pageY ) / (_this.screen.height*.5),
                0.0
            );

            var length = mouseOnBall.length();

            if ( _this.noRoll ) {

                if ( length < Math.SQRT1_2 ) {

                    mouseOnBall.z = Math.sqrt( 1.0 - length*length );

                } else {

                    mouseOnBall.z = .5 / length;

                }

            } else if ( length > 1.0 ) {

                mouseOnBall.normalize();

            } else {

                mouseOnBall.z = Math.sqrt( 1.0 - length * length );

            }

            _eye.copy( _this.object.position ).sub( _this.target );

            vector.copy( _this.object.up ).setLength( mouseOnBall.y )
            vector.add( objectUp.copy( _this.object.up ).cross( _eye ).setLength( mouseOnBall.x ) );
            vector.add( _eye.setLength( mouseOnBall.z ) );

            return vector;

        };

    }() );

    this.rotateCamera = (function(quaternionIn, bUpdate){

        var axis = new THREE.Vector3(),
            quaternion = new THREE.Quaternion();

        return function (quaternionIn, bUpdate) {

            var angle;
            if(quaternionIn === undefined) {
              angle = Math.acos( _this._rotateStart.dot( _this._rotateEnd ) / _this._rotateStart.length() / _this._rotateEnd.length() );
            }

            //var angle = Math.acos( _this._rotateStart.dot( _this._rotateEnd ) / _this._rotateStart.length() / _this._rotateEnd.length() );

            if ( angle || quaternionIn !== undefined) {
                if(quaternionIn === undefined) {
                  axis.crossVectors( _this._rotateStart, _this._rotateEnd ).normalize();

                  angle *= _this.rotateSpeed;

                  quaternion.setFromAxisAngle( axis, -angle );
                }
                else {
                  quaternion.copy(quaternionIn);
                }

                // order matters in quaernion multiplication: http://www.cprogramming.com/tutorial/3d/quaternions.html
                if(icn3d !== undefined && icn3d.quaternion !== undefined && (bUpdate === undefined || bUpdate === true)) icn3d.quaternion.multiplyQuaternions(quaternion, icn3d.quaternion);

                _eye.applyQuaternion( quaternion );
                _this.object.up.applyQuaternion( quaternion );

                _this._rotateEnd.applyQuaternion( quaternion );

                if ( _this.staticMoving ) {

                    _this._rotateStart.copy( _this._rotateEnd );

                } else {

                    quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) );
                    _this._rotateStart.applyQuaternion( quaternion );

                }

            }
        }

    }());

    this.zoomCamera = function (zoomFactor, bUpdate) {

        var factor;
        if ( _this._state === STATE.TOUCH_ZOOM_PAN ) {

            if(zoomFactor !== undefined) {
              factor = zoomFactor;
            }
            else {

              factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
              _touchZoomDistanceStart = _touchZoomDistanceEnd;
            }

        } else {

            if(zoomFactor !== undefined) {
              factor = zoomFactor;
            }
            else {

              factor = 1.0 + ( _this._zoomEnd.y - _this._zoomStart.y ) * _this.zoomSpeed / zoomSpeedAdjust;
            }
        }

        if(icn3d !== undefined && icn3d._zoomFactor !== undefined && (bUpdate === undefined || bUpdate === true)) icn3d._zoomFactor *= factor;

        //if ( factor !== 1.0 && factor > 0.0 ) {
        if ( factor !== 1.0 ) {

            //_zoomFactor *= factor;
            _zoomFactor = factor;

            _this.object.left = _zoomFactor * _this.left0 + ( 1 - _zoomFactor ) *  _this.center0.x;
            _this.object.right = _zoomFactor * _this.right0 + ( 1 - _zoomFactor ) *  _this.center0.x;
            _this.object.top = _zoomFactor * _this.top0 + ( 1 - _zoomFactor ) *  _this.center0.y;
            _this.object.bottom = _zoomFactor * _this.bottom0 + ( 1 - _zoomFactor ) *  _this.center0.y;

            if ( _this.staticMoving ) {

                _this._zoomStart.copy( _this._zoomEnd );

            } else {

                _this._zoomStart.y += ( _this._zoomEnd.y - _this._zoomStart.y ) * this.dynamicDampingFactor;

            }

        }

    };

    this.panCamera = (function(mouseChangeIn, bUpdate){

        var mouseChange = new THREE.Vector2(),
            objectUp = new THREE.Vector3(),
            pan = new THREE.Vector3();

        return function (mouseChangeIn, bUpdate) {

            if(mouseChangeIn !== undefined) {
              mouseChange = mouseChangeIn;

              if(icn3d !== undefined && icn3d.mouseChange !== undefined && (bUpdate === undefined || bUpdate === true)) icn3d.mouseChange.add(mouseChangeIn);
            }
            else {
              mouseChange.copy( _this._panEnd ).sub( _this._panStart );

              if(icn3d !== undefined && icn3d.mouseChange !== undefined && (bUpdate === undefined || bUpdate === true)) icn3d.mouseChange.add( _this._panEnd ).sub( _this._panStart );
            }

            if ( mouseChange.lengthSq() ) {

                mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );

                pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
                pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );

                _this.object.position.add( pan );
                _this.target.add( pan );

                if ( _this.staticMoving ) {

                    _this._panStart.copy( _this._panEnd );

                } else {

                    _this._panStart.add( mouseChange.subVectors( _this._panEnd, _this._panStart ).multiplyScalar( _this.dynamicDampingFactor ) );

                }

            }
        }

    }());

    this.update = function (para) {

        _eye.subVectors( _this.object.position, _this.target );

        if ( !_this.noRotate ) {

            if(para !== undefined && para.quaternion !== undefined) {
              _this.rotateCamera(para.quaternion, para.update);
            }
            else {
              _this.rotateCamera();
            }

        }

        if ( !_this.noZoom ) {

            if(para !== undefined && para._zoomFactor !== undefined) {
              _this.zoomCamera(para._zoomFactor, para.update);
            }
            else {
              _this.zoomCamera();
            }

            _this.object.updateProjectionMatrix();

        }

        if ( !_this.noPan ) {

            if(para !== undefined && para.mouseChange !== undefined) {
              _this.panCamera(para.mouseChange, para.update);
            }
            else {
              _this.panCamera();
            }

        }

        _this.object.position.addVectors( _this.target, _eye );

        _this.object.lookAt( _this.target );

        if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) {

            _this.dispatchEvent( changeEvent );

            lastPosition.copy( _this.object.position );

        }

    };

    this.reset = function () {

        _this._state = STATE.NONE;
        _prevState = STATE.NONE;

        _this.target.copy( _this.target0 );
        _this.object.position.copy( _this.position0 );
        _this.object.up.copy( _this.up0 );

        _eye.subVectors( _this.object.position, _this.target );

        _this.object.left = _this.left0;
        _this.object.right = _this.right0;
        _this.object.top = _this.top0;
        _this.object.bottom = _this.bottom0;

        _this.object.lookAt( _this.target );

        _this.dispatchEvent( changeEvent );

        lastPosition.copy( _this.object.position );

    };

    // listeners

    function keydown( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        window.removeEventListener( 'keydown', keydown );

        _prevState = _this._state;

        if ( _this._state !== STATE.NONE ) {

            return;

        } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) {

            _this._state = STATE.ROTATE;

        } else if ( (event.keyCode === _this.keys[ STATE.ZOOM ]) && !_this.noZoom ) {

            _this._state = STATE.ZOOM;

        } else if ( (event.keyCode === _this.keys[ STATE.PAN ]) && !_this.noPan ) {

            _this._state = STATE.PAN;

        }

    }

    function keyup( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        _this._state = _prevState;

        window.addEventListener( 'keydown', keydown, false );

    }

    function mousedown( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        if ( _this._state === STATE.NONE ) {

            _this._state = event.button;

        }

        if ( _this._state === STATE.ROTATE && !_this.noRotate ) {

            _this._rotateStart.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) );
            _this._rotateEnd.copy( _this._rotateStart );

        } else if ( _this._state === STATE.ZOOM && !_this.noZoom ) {

            _this._zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
            _this._zoomEnd.copy(_this._zoomStart);

        } else if ( _this._state === STATE.PAN && !_this.noPan ) {

            _this._panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
            _this._panEnd.copy(_this._panStart)

        }

        document.addEventListener( 'mousemove', mousemove, false );
        document.addEventListener( 'mouseup', mouseup, false );

        _this.dispatchEvent( startEvent );

    }

    function mousemove( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        if ( _this._state === STATE.ROTATE && !_this.noRotate ) {

            _this._rotateEnd.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) );

        } else if ( _this._state === STATE.ZOOM && !_this.noZoom ) {

            _this._zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );

        } else if ( _this._state === STATE.PAN && !_this.noPan ) {

            _this._panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );

        }

    }

    function mouseup( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        _this._state = STATE.NONE;

        document.removeEventListener( 'mousemove', mousemove );
        document.removeEventListener( 'mouseup', mouseup );
        _this.dispatchEvent( endEvent );

    }

    function mousewheel( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        var delta = 0;

        if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9

            delta = event.wheelDelta / 40;

        } else if ( event.detail ) { // Firefox

            delta = - event.detail / 3;

        }

        //_this._zoomStart.y += delta * 0.01;
        _this._zoomStart.y = delta * 0.01;
        _this.dispatchEvent( startEvent );
        _this.dispatchEvent( endEvent );

    }

    function touchstart( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        switch ( event.touches.length ) {

            case 1:
                _this._state = STATE.TOUCH_ROTATE;
                _this._rotateStart.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
                _this._rotateEnd.copy( _this._rotateStart );
                break;

            case 2:
                _this._state = STATE.TOUCH_ZOOM_PAN;
                var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
                var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
                _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );

                var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
                var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
                _this._panStart.copy( getMouseOnScreen( x, y ) );
                _this._panEnd.copy( _this._panStart );
                break;

            default:
                _this._state = STATE.NONE;

        }
        _this.dispatchEvent( startEvent );


    }

    function touchmove( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        //event.preventDefault();
        event.stopPropagation();

        switch ( event.touches.length ) {

            case 1:
                _this._rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
                break;

            case 2:
                var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
                var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
                _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );

                var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
                var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
                _this._panEnd.copy( getMouseOnScreen( x, y ) );
                break;

            default:
                _this._state = STATE.NONE;

        }

    }

    function touchend( event ) {

        if ( _this.enabled === false || Object.keys(window).length < 2)  return;

        switch ( event.touches.length ) {

            case 1:
                _this._rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
                _this._rotateStart.copy( _this._rotateEnd );
                break;

            case 2:
                _touchZoomDistanceStart = _touchZoomDistanceEnd = 0;

                var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
                var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
                _this._panEnd.copy( getMouseOnScreen( x, y ) );
                _this._panStart.copy( _this._panEnd );
                break;

        }

        _this._state = STATE.NONE;
        _this.dispatchEvent( endEvent );

    }

    if(Object.keys(window).length >= 2) {
        this.domElement.addEventListener( 'contextmn', function ( event ) {
            //event.preventDefault();
        }, false );

        this.domElement.addEventListener( 'mousedown', mousedown, false );

        this.domElement.addEventListener( 'mousewheel', mousewheel, false );
        this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox

        this.domElement.addEventListener( 'touchstart', touchstart, false );
        this.domElement.addEventListener( 'touchend', touchend, false );
        this.domElement.addEventListener( 'touchmove', touchmove, false );

        window.addEventListener( 'keydown', keydown, false );
        window.addEventListener( 'keyup', keyup, false );
    }

    this.handleResize();

    // force an update at start
    this.update();

};

THREE.OrthographicTrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
THREE.OrthographicTrackballControls.prototype.constructor = THREE.OrthographicTrackballControls;

/*
! function(r, t) {
    "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd ? define(["exports"], t) : t(r.MMTF = r.MMTF || {})
}(this, function(r) {
    "use strict";
*/

var MMTF = {};

MMTF = initIcn3dpyMMTF(MMTF);

function initIcn3dpyMMTF(r) {
function t(r,t,n){for(var e=(r.byteLength,0),i=n.length;i>e;e++){var o=n.charCodeAt(e);if(128>o)r.setUint8(t++,o>>>0&127|0);else if(2048>o)r.setUint8(t++,o>>>6&31|192),r.setUint8(t++,o>>>0&63|128);else if(65536>o)r.setUint8(t++,o>>>12&15|224),r.setUint8(t++,o>>>6&63|128),r.setUint8(t++,o>>>0&63|128);else{if(!(1114112>o))throw new Error("bad codepoint "+o);r.setUint8(t++,o>>>18&7|240),r.setUint8(t++,o>>>12&63|128),r.setUint8(t++,o>>>6&63|128),r.setUint8(t++,o>>>0&63|128)}}}function n(r){for(var t=0,n=0,e=r.length;e>n;n++){var i=r.charCodeAt(n);if(128>i)t+=1;else if(2048>i)t+=2;else if(65536>i)t+=3;else{if(!(1114112>i))throw new Error("bad codepoint "+i);t+=4}}return t}function e(r,i,o){var a=typeof r;if("string"===a){var u=n(r);if(32>u)return i.setUint8(o,160|u),t(i,o+1,r),1+u;if(256>u)return i.setUint8(o,217),i.setUint8(o+1,u),t(i,o+2,r),2+u;if(65536>u)return i.setUint8(o,218),i.setUint16(o+1,u),t(i,o+3,r),3+u;if(4294967296>u)return i.setUint8(o,219),i.setUint32(o+1,u),t(i,o+5,r),5+u}if(r instanceof Uint8Array){var u=r.byteLength,s=new Uint8Array(i.buffer);if(256>u)return i.setUint8(o,196),i.setUint8(o+1,u),s.set(r,o+2),2+u;if(65536>u)return i.setUint8(o,197),i.setUint16(o+1,u),s.set(r,o+3),3+u;if(4294967296>u)return i.setUint8(o,198),i.setUint32(o+1,u),s.set(r,o+5),5+u}if("number"===a){if(!isFinite(r))throw new Error("Number not finite: "+r);if(Math.floor(r)!==r)return i.setUint8(o,203),i.setFloat64(o+1,r),9;if(r>=0){if(128>r)return i.setUint8(o,r),1;if(256>r)return i.setUint8(o,204),i.setUint8(o+1,r),2;if(65536>r)return i.setUint8(o,205),i.setUint16(o+1,r),3;if(4294967296>r)return i.setUint8(o,206),i.setUint32(o+1,r),5;throw new Error("Number too big 0x"+r.toString(16))}if(r>=-32)return i.setInt8(o,r),1;if(r>=-128)return i.setUint8(o,208),i.setInt8(o+1,r),2;if(r>=-32768)return i.setUint8(o,209),i.setInt16(o+1,r),3;if(r>=-2147483648)return i.setUint8(o,210),i.setInt32(o+1,r),5;throw new Error("Number too small -0x"+(-r).toString(16).substr(1))}if(null===r)return i.setUint8(o,192),1;if("boolean"===a)return i.setUint8(o,r?195:194),1;if("object"===a){var u,f=0,c=Array.isArray(r);if(c)u=r.length;else{var d=Object.keys(r);u=d.length}if(16>u?(i.setUint8(o,u|(c?144:128)),f=1):65536>u?(i.setUint8(o,c?220:222),i.setUint16(o+1,u),f=3):4294967296>u&&(i.setUint8(o,c?221:223),i.setUint32(o+1,u),f=5),c)for(var l=0;u>l;l++)f+=e(r[l],i,o+f);else for(var l=0;u>l;l++){var g=d[l];f+=e(g,i,o+f),f+=e(r[g],i,o+f)}return f}throw new Error("Unknown type "+a)}function i(r){var t=typeof r;if("string"===t){var e=n(r);if(32>e)return 1+e;if(256>e)return 2+e;if(65536>e)return 3+e;if(4294967296>e)return 5+e}if(r instanceof Uint8Array){var e=r.byteLength;if(256>e)return 2+e;if(65536>e)return 3+e;if(4294967296>e)return 5+e}if("number"===t){if(Math.floor(r)!==r)return 9;if(r>=0){if(128>r)return 1;if(256>r)return 2;if(65536>r)return 3;if(4294967296>r)return 5;throw new Error("Number too big 0x"+r.toString(16))}if(r>=-32)return 1;if(r>=-128)return 2;if(r>=-32768)return 3;if(r>=-2147483648)return 5;throw new Error("Number too small -0x"+r.toString(16).substr(1))}if("boolean"===t||null===r)return 1;if("object"===t){var e,o=0;if(Array.isArray(r)){e=r.length;for(var a=0;e>a;a++)o+=i(r[a])}else{var u=Object.keys(r);e=u.length;for(var a=0;e>a;a++){var s=u[a];o+=i(s)+i(r[s])}}if(16>e)return 1+o;if(65536>e)return 3+o;if(4294967296>e)return 5+o;throw new Error("Array or object too long 0x"+e.toString(16))}throw new Error("Unknown type "+t)}function o(r){var t=new ArrayBuffer(i(r)),n=new DataView(t);return e(r,n,0),new Uint8Array(t)}function a(r,t,n){return t?new r(t.buffer,t.byteOffset,t.byteLength/(n||1)):void 0}function u(r){return a(DataView,r)}function s(r){return a(Uint8Array,r)}function f(r){return a(Int8Array,r)}function c(r){return a(Int32Array,r,4)}function d(r){return a(Float32Array,r,4)}function l(r,t){var n=r.length/2;t||(t=new Int16Array(n));for(var e=0,i=0;n>e;++e,i+=2)t[e]=r[i]<<8^r[i+1]<<0;return t}function g(r,t){var n=r.length;t||(t=new Uint8Array(2*n));for(var e=u(t),i=0;n>i;++i)e.setInt16(2*i,r[i]);return s(t)}function v(r,t){var n=r.length/4;t||(t=new Int32Array(n));for(var e=0,i=0;n>e;++e,i+=4)t[e]=r[i]<<24^r[i+1]<<16^r[i+2]<<8^r[i+3]<<0;return t}function L(r,t){var n=r.length;t||(t=new Uint8Array(4*n));for(var e=u(t),i=0;n>i;++i)e.setInt32(4*i,r[i]);return s(t)}function h(r,t){var n=r.length;t||(t=new Float32Array(n/4));for(var e=u(t),i=u(r),o=0,a=0,s=n/4;s>o;++o,a+=4)e.setFloat32(a,i.getFloat32(a),!0);return t}function y(r,t,n){var e=r.length,i=1/t;n||(n=new Float32Array(e));for(var o=0;e>o;++o)n[o]=r[o]*i;return n}function m(r,t,n){var e=r.length;n||(n=new Int32Array(e));for(var i=0;e>i;++i)n[i]=Math.round(r[i]*t);return n}function p(r,t){var n,e;if(!t){var i=0;for(n=0,e=r.length;e>n;n+=2)i+=r[n+1];t=new r.constructor(i)}var o=0;for(n=0,e=r.length;e>n;n+=2)for(var a=r[n],u=r[n+1],s=0;u>s;++s)t[o]=a,++o;return t}function U(r){if(0===r.length)return new Int32Array;var t,n,e=2;for(t=1,n=r.length;n>t;++t)r[t-1]!==r[t]&&(e+=2);var i=new Int32Array(e),o=0,a=1;for(t=1,n=r.length;n>t;++t)r[t-1]!==r[t]?(i[o]=r[t-1],i[o+1]=a,a=1,o+=2):++a;return i[o]=r[r.length-1],i[o+1]=a,i}function b(r,t){var n=r.length;t||(t=new r.constructor(n)),n&&(t[0]=r[0]);for(var e=1;n>e;++e)t[e]=r[e]+t[e-1];return t}function I(r,t){var n=r.length;t||(t=new r.constructor(n)),t[0]=r[0];for(var e=1;n>e;++e)t[e]=r[e]-r[e-1];return t}function w(r,t){var n,e,i=r instanceof Int8Array?127:32767,o=-i-1,a=r.length;if(!t){var u=0;for(n=0;a>n;++n)r[n]<i&&r[n]>o&&++u;t=new Int32Array(u)}for(n=0,e=0;a>n;){for(var s=0;r[n]===i||r[n]===o;)s+=r[n],++n;s+=r[n],++n,t[e]=s,++e}return t}function C(r,t){var n,e=t?127:32767,i=-e-1,o=r.length,a=0;for(n=0;o>n;++n){var u=r[n];0===u?++a:u>0?(a+=Math.ceil(u/e),u%e===0&&(a+=1)):(a+=Math.ceil(u/i),u%i===0&&(a+=1))}var s=t?new Int8Array(a):new Int16Array(a),f=0;for(n=0;o>n;++n){var u=r[n];if(u>=0)for(;u>=e;)s[f]=e,++f,u-=e;else for(;i>=u;)s[f]=i,++f,u-=i;s[f]=u,++f}return s}function A(r,t){return b(p(r),t)}function x(r){return U(I(r))}function M(r,t,n){return y(p(r,c(n)),t,n)}function F(r,t){return U(m(r,t))}function S(r,t,n){return y(b(r,c(n)),t,n)}function E(r,t,n){return I(m(r,t),n)}function N(r,t,n){return y(w(r,c(n)),t,n)}function O(r,t,n){var e=w(r,c(n));return S(e,t,d(e))}function T(r,t,n){return C(E(r,t),n)}function k(r){var t=u(r),n=t.getInt32(0),e=t.getInt32(4),i=r.subarray(8,12),r=r.subarray(12);return[n,r,e,i]}function j(r,t,n,e){var i=new ArrayBuffer(12+e.byteLength),o=new Uint8Array(i),a=new DataView(i);return a.setInt32(0,r),a.setInt32(4,t),n&&o.set(n,8),o.set(e,12),o}function q(r){var t=r.length,n=s(r);return j(2,t,void 0,n)}function D(r){var t=r.length,n=L(r);return j(4,t,void 0,n)}function P(r,t){var n=r.length/t,e=L([t]),i=s(r);return j(5,n,e,i)}function z(r){var t=r.length,n=L(U(r));return j(6,t,void 0,n)}function B(r){var t=r.length,n=L(x(r));return j(8,t,void 0,n)}function V(r,t){var n=r.length,e=L([t]),i=L(F(r,t));return j(9,n,e,i)}function G(r,t){var n=r.length,e=L([t]),i=g(T(r,t));return j(10,n,e,i)}function R(r){var t={};return rr.forEach(function(n){void 0!==r[n]&&(t[n]=r[n])}),r.bondAtomList&&(t.bondAtomList=D(r.bondAtomList)),r.bondOrderList&&(t.bondOrderList=q(r.bondOrderList)),t.xCoordList=G(r.xCoordList,1e3),t.yCoordList=G(r.yCoordList,1e3),t.zCoordList=G(r.zCoordList,1e3),r.bFactorList&&(t.bFactorList=G(r.bFactorList,100)),r.atomIdList&&(t.atomIdList=B(r.atomIdList)),r.altLocList&&(t.altLocList=z(r.altLocList)),r.occupancyList&&(t.occupancyList=V(r.occupancyList,100)),t.groupIdList=B(r.groupIdList),t.groupTypeList=D(r.groupTypeList),r.secStructList&&(t.secStructList=q(r.secStructList)),r.insCodeList&&(t.insCodeList=z(r.insCodeList)),r.sequenceIndexList&&(t.sequenceIndexList=B(r.sequenceIndexList)),t.chainIdList=P(r.chainIdList,4),r.chainNameList&&(t.chainNameList=P(r.chainNameList,4)),t}function H(r){function t(r){for(var t={},n=0;r>n;n++){var e=o();t[e]=o()}return t}function n(t){var n=r.subarray(a,a+t);return a+=t,n}function e(t){var n=r.subarray(a,a+t);a+=t;var e=65535;if(t>e){for(var i=[],o=0;o<n.length;o+=e)i.push(String.fromCharCode.apply(null,n.subarray(o,o+e)));return i.join("")}return String.fromCharCode.apply(null,n)}function i(r){for(var t=new Array(r),n=0;r>n;n++)t[n]=o();return t}function o(){var o,s,f=r[a];if(0===(128&f))return a++,f;if(128===(240&f))return s=15&f,a++,t(s);if(144===(240&f))return s=15&f,a++,i(s);if(160===(224&f))return s=31&f,a++,e(s);if(224===(224&f))return o=u.getInt8(a),a++,o;switch(f){case 192:return a++,null;case 194:return a++,!1;case 195:return a++,!0;case 196:return s=u.getUint8(a+1),a+=2,n(s);case 197:return s=u.getUint16(a+1),a+=3,n(s);case 198:return s=u.getUint32(a+1),a+=5,n(s);case 202:return o=u.getFloat32(a+1),a+=5,o;case 203:return o=u.getFloat64(a+1),a+=9,o;case 204:return o=r[a+1],a+=2,o;case 205:return o=u.getUint16(a+1),a+=3,o;case 206:return o=u.getUint32(a+1),a+=5,o;case 208:return o=u.getInt8(a+1),a+=2,o;case 209:return o=u.getInt16(a+1),a+=3,o;case 210:return o=u.getInt32(a+1),a+=5,o;case 217:return s=u.getUint8(a+1),a+=2,e(s);case 218:return s=u.getUint16(a+1),a+=3,e(s);case 219:return s=u.getUint32(a+1),a+=5,e(s);case 220:return s=u.getUint16(a+1),a+=3,i(s);case 221:return s=u.getUint32(a+1),a+=5,i(s);case 222:return s=u.getUint16(a+1),a+=3,t(s);case 223:return s=u.getUint32(a+1),a+=5,t(s)}throw new Error("Unknown type 0x"+f.toString(16))}var a=0,u=new DataView(r.buffer);return o()}function W(r,t,n,e){switch(r){case 1:return h(t);case 2:return f(t);case 3:return l(t);case 4:return v(t);case 5:return s(t);case 6:return p(v(t),new Uint8Array(n));case 7:return p(v(t));case 8:return A(v(t));case 9:return M(v(t),v(e)[0]);case 10:return O(l(t),v(e)[0]);case 11:return y(l(t),v(e)[0]);case 12:return N(l(t),v(e)[0]);case 13:return N(f(t),v(e)[0]);case 14:return w(l(t));case 15:return w(f(t))}}function X(r,t){t=t||{};var n=t.ignoreFields,e={};return nr.forEach(function(t){var i=n?-1!==n.indexOf(t):!1,o=r[t];i||void 0===o||(o instanceof Uint8Array?e[t]=W.apply(null,k(o)):e[t]=o)}),e}function J(r){return String.fromCharCode.apply(null,r).replace(/\0/g,"")}function K(r,t,n){n=n||{};var e,i,o,a,u,s,f=n.firstModelOnly,c=t.onModel,d=t.onChain,l=t.onGroup,g=t.onAtom,v=t.onBond,L=0,h=0,y=0,m=0,p=0,U=-1,b=r.chainNameList,I=r.secStructList,w=r.insCodeList,C=r.sequenceIndexList,A=r.atomIdList,x=r.bFactorList,M=r.altLocList,F=r.occupancyList,S=r.bondAtomList,E=r.bondOrderList;for(e=0,i=r.chainsPerModel.length;i>e&&!(f&&L>0);++e){var N=r.chainsPerModel[L];for(c&&c({chainCount:N,modelIndex:L}),o=0;N>o;++o){var O=r.groupsPerChain[h];if(d){var T=J(r.chainIdList.subarray(4*h,4*h+4)),k=null;b&&(k=J(b.subarray(4*h,4*h+4))),d({groupCount:O,chainIndex:h,modelIndex:L,chainId:T,chainName:k})}for(a=0;O>a;++a){var j=r.groupList[r.groupTypeList[y]],q=j.atomNameList.length;if(l){var D=null;I&&(D=I[y]);var P=null;r.insCodeList&&(P=String.fromCharCode(w[y]));var z=null;C&&(z=C[y]),l({atomCount:q,groupIndex:y,chainIndex:h,modelIndex:L,groupId:r.groupIdList[y],groupType:r.groupTypeList[y],groupName:j.groupName,singleLetterCode:j.singleLetterCode,chemCompType:j.chemCompType,secStruct:D,insCode:P,sequenceIndex:z})}for(u=0;q>u;++u){if(g){var B=null;A&&(B=A[m]);var V=null;x&&(V=x[m]);var G=null;M&&(G=String.fromCharCode(M[m]));var R=null;F&&(R=F[m]),g({atomIndex:m,groupIndex:y,chainIndex:h,modelIndex:L,atomId:B,element:j.elementList[u],atomName:j.atomNameList[u],formalCharge:j.formalChargeList[u],xCoord:r.xCoordList[m],yCoord:r.yCoordList[m],zCoord:r.zCoordList[m],bFactor:V,altLoc:G,occupancy:R})}m+=1}if(v){var H=j.bondAtomList;for(u=0,s=j.bondOrderList.length;s>u;++u)v({atomIndex1:m-q+H[2*u],atomIndex2:m-q+H[2*u+1],bondOrder:j.bondOrderList[u]})}y+=1}h+=1}if(p=U+1,U=m-1,v&&S)for(u=0,s=S.length;s>u;u+=2){var W=S[u],X=S[u+1];(W>=p&&U>=W||X>=p&&U>=X)&&v({atomIndex1:W,atomIndex2:X,bondOrder:E?E[u/2]:null})}L+=1}}function Q(r){return o(R(r))}function Y(r,t){r instanceof ArrayBuffer&&(r=new Uint8Array(r));var n;return n=r instanceof Uint8Array?H(r):r,X(n,t)}function Z(r,t,n,e){function i(){try{var r=Y(o.response);n(r)}catch(t){e(t)}}var o=new XMLHttpRequest;o.addEventListener("load",i,!0),o.addEventListener("error",e,!0),o.responseType="arraybuffer",o.open("GET",t+r.toUpperCase()),o.send()}function $(r,t,n){Z(r,or,t,n)}function _(r,t,n){Z(r,ar,t,n)}

//var rr=["mmtfVersion","mmtfProducer","unitCell","spaceGroup","structureId","title","depositionDate","releaseDate","experimentalMethods","resolution","rFree","rWork","bioAssemblyList","ncsOperatorList","entityList","groupList","numBonds","numAtoms","numGroups","numChains","numModels","groupsPerChain","chainsPerModel"],tr=["xCoordList","yCoordList","zCoordList","groupIdList","groupTypeList","chainIdList","bFactorList","atomIdList","altLocList","occupancyList","secStructList","insCodeList","sequenceIndexList","chainNameList","bondAtomList","bondOrderList"],nr=rr.concat(tr),er="v1.0.1",ir="//mmtf.rcsb.org/v1.0/",or=ir+"full/",ar=ir+"reduced/";r.encode=Q,r.decode=Y,r.traverse=K,r.fetch=$,r.fetchReduced=_,r.version=er,r.fetchUrl=or,r.fetchReducedUrl=ar,r.encodeMsgpack=o,r.encodeMmtf=R,r.decodeMsgpack=H,r.decodeMmtf=X

    var rr = ["mmtfVersion", "mmtfProducer", "unitCell", "spaceGroup", "structureId", "title", "depositionDate", "releaseDate", "experimentalMethods", "resolution", "rFree", "rWork", "bioAssemblyList", "ncsOperatorList", "entityList", "groupList", "numBonds", "numAtoms", "numGroups", "numChains", "numModels", "groupsPerChain", "chainsPerModel"],
        tr = ["xCoordList", "yCoordList", "zCoordList", "groupIdList", "groupTypeList", "chainIdList", "bFactorList", "atomIdList", "altLocList", "occupancyList", "secStructList", "insCodeList", "sequenceIndexList", "chainNameList", "bondAtomList", "bondOrderList"],
        nr = rr.concat(tr),
        er = "v1.0.1",
        ir = "//mmtf.rcsb.org/v1.0/",
        or = ir + "full/",
        ar = ir + "reduced/";
    r.encode = Q, r.decode = Y, r.traverse = K, r.fetch = $, r.fetchReduced = _, r.version = er, r.fetchUrl = or, r.fetchReducedUrl = ar, r.encodeMsgpack = o, r.encodeMmtf = R, r.decodeMsgpack = H, r.decodeMmtf = X

    return r;
}

//});

/*
 * ==========================================================
 *  COLOR PICKER PLUGIN 1.3.9
 * ==========================================================
 * Author: Taufik Nurrohman <https://github.com/tovic>
 * License: MIT
 * ----------------------------------------------------------
 */

(function(win, doc, NS) {

    var instance = '__instance__',
        first = 'firstChild',
        delay = setTimeout;

    function is_set(x) {
        return typeof x !== "undefined";
    }

    function is_string(x) {
        return typeof x === "string";
    }

    function is_object(x) {
        return typeof x === "object";
    }

    function object_length(x) {
        return Object.keys(x).length;
    }

    function edge(a, b, c) {
        if (a < b) return b;
        if (a > c) return c;
        return a;
    }

    function num(i, j) {
        return parseInt(i, j || 10);
    }

    function round(i) {
        return Math.round(i);
    }

    // [h, s, v] ... 0 <= h, s, v <= 1
    function HSV2RGB(a) {
        var h = +a[0],
            s = +a[1],
            v = +a[2],
            r, g, b, i, f, p, q, t;
        i = Math.floor(h * 6);
        f = h * 6 - i;
        p = v * (1 - s);
        q = v * (1 - f * s);
        t = v * (1 - (1 - f) * s);
        i = i || 0;
        q = q || 0;
        t = t || 0;
        switch (i % 6) {
            case 0:
                r = v, g = t, b = p;
                break;
            case 1:
                r = q, g = v, b = p;
                break;
            case 2:
                r = p, g = v, b = t;
                break;
            case 3:
                r = p, g = q, b = v;
                break;
            case 4:
                r = t, g = p, b = v;
                break;
            case 5:
                r = v, g = p, b = q;
                break;
        }
        return [round(r * 255), round(g * 255), round(b * 255)];
    }

    function HSV2HEX(a) {
        return RGB2HEX(HSV2RGB(a));
    }

    // [r, g, b] ... 0 <= r, g, b <= 255
    function RGB2HSV(a) {
        var r = +a[0],
            g = +a[1],
            b = +a[2],
            max = Math.max(r, g, b),
            min = Math.min(r, g, b),
            d = max - min,
            h, s = (max === 0 ? 0 : d / max),
            v = max / 255;
        switch (max) {
            case min:
                h = 0;
                break;
            case r:
                h = (g - b) + d * (g < b ? 6 : 0);
                h /= 6 * d;
                break;
            case g:
                h = (b - r) + d * 2;
                h /= 6 * d;
                break;
            case b:
                h = (r - g) + d * 4;
                h /= 6 * d;
                break;
        }
        return [h, s, v];
    }

    function RGB2HEX(a) {
        var s = +a[2] | (+a[1] << 8) | (+a[0] << 16);
        s = '000000' + s.toString(16);
        return s.slice(-6);
    }

    // rrggbb or rgb
    function HEX2HSV(s) {
        return RGB2HSV(HEX2RGB(s));
    }

    function HEX2RGB(s) {
        if (s.length === 3) {
            s = s.replace(/./g, '$&$&');
        }
        return [num(s[0] + s[1], 16), num(s[2] + s[3], 16), num(s[4] + s[5], 16)];
    }

    // convert range from `0` to `360` and `0` to `100` in color into range from `0` to `1`
    function _2HSV_pri(a) {
        return [+a[0] / 360, +a[1] / 100, +a[2] / 100];
    }

    // convert range from `0` to `1` into `0` to `360` and `0` to `100` in color
    function _2HSV_pub(a) {
        return [round(+a[0] * 360), round(+a[1] * 100), round(+a[2] * 100)];
    }

    // convert range from `0` to `255` in color into range from `0` to `1`
    function _2RGB_pri(a) {
        return [+a[0] / 255, +a[1] / 255, +a[2] / 255];
    }

    // *
    function parse(x) {
        if (is_object(x)) return x;
        var rgb = /\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/i.exec(x),
            hsv = /\s*hsv\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)\s*$/i.exec(x),
            hex = x[0] === '#' && x.match(/^#([\da-f]{3}|[\da-f]{6})$/i);
        if (hex) {
            return HEX2HSV(x.slice(1));
        } else if (hsv) {
            return _2HSV_pri([+hsv[1], +hsv[2], +hsv[3]]);
        } else if (rgb) {
            return RGB2HSV([+rgb[1], +rgb[2], +rgb[3]]);
        }
        return [0, 1, 1]; // default is red
    }

    (function($) {

        // plugin version
        $.version = '1.3.9';

        // collect all instance(s)
        $[instance] = {};

        // plug to all instance(s)
        $.each = function(fn, t) {
            return delay(function() {
                var ins = $[instance], i;
                for (i in ins) {
                    fn(ins[i], i, ins);
                }
            }, t === 0 ? 0 : (t || 1)), $;
        };

        // static method(s)
        $.parse = parse;
        $._HSV2RGB = HSV2RGB;
        $._HSV2HEX = HSV2HEX;
        $._RGB2HSV = RGB2HSV;
        $._HEX2HSV = HEX2HSV;
        $._HEX2RGB = function(a) {
            return _2RGB_pri(HEX2RGB(a));
        };
        $.HSV2RGB = function(a) {
            return HSV2RGB(_2HSV_pri(a));
        };
        $.HSV2HEX = function(a) {
            return HSV2HEX(_2HSV_pri(a));
        };
        $.RGB2HSV = function(a) {
            return _2HSV_pub(RGB2HSV(a));
        };
        $.RGB2HEX = RGB2HEX;
        $.HEX2HSV = function(s) {
            return _2HSV_pub(HEX2HSV(s));
        };
        $.HEX2RGB = HEX2RGB;

    })(win[NS] = function(target, events, parent) {

        var b = doc.body,
            h = doc.documentElement,
            $ = this,
            $$ = win[NS],
            _ = false,
            hooks = {},
            picker = doc.createElement('div'),
            on_down = "touchstart mousedown",
            on_move = "touchmove mousemove",
            on_up = "touchend mouseup",
            on_resize = "orientationchange resize";

        // return a new instance if `CP` was called without the `new` operator
        if (!($ instanceof $$)) {
            return new $$(target, events);
        }

        // store color picker instance to `CP.__instance__`
        $$[instance][target.id || target.name || object_length($$[instance])] = $;

        // trigger color picker panel on click by default
        if (!is_set(events) || events === true) {
            events = on_down;
        }

        // add event
        function on(ev, el, fn) {
            ev = ev.split(/\s+/);
            for (var i = 0, ien = ev.length; i < ien; ++i) {
                el.addEventListener(ev[i], fn, false);
            }
        }

        // remove event
        function off(ev, el, fn) {
            ev = ev.split(/\s+/);
            for (var i = 0, ien = ev.length; i < ien; ++i) {
                el.removeEventListener(ev[i], fn);
            }
        }

        // get mouse/finger coordinate
        function point(el, e) {
            var T = 'touches',
                X = 'clientX',
                Y = 'clientY',
                x = !!e[T] ? e[T][0][X] : e[X],
                y = !!e[T] ? e[T][0][Y] : e[Y],
                o = offset(el);
            return {
                x: x - o.l,
                y: y - o.t
            };
        }

        // get position
        function offset(el) {
            var left, top, rect;
            if (el === win) {
                left = win.pageXOffset || h.scrollLeft;
                top = win.pageYOffset || h.scrollTop;
            } else {
                rect = el.getBoundingClientRect();
                left = rect.left;
                top = rect.top;
            }
            return {
                l: left,
                t: top
            };
        }

        // get closest parent
        function closest(a, b) {
            while ((a = a.parentElement) && a !== b);
            return a;
        }

        // prevent default
        function prevent(e) {
            if (e) e.preventDefault();
        }

        // get dimension
        function size(el) {
            return el === win ? {
                w: win.innerWidth,
                h: win.innerHeight
            } : {
                w: el.offsetWidth,
                h: el.offsetHeight
            };
        }

        // get color data
        function get_data(a) {
            return _ || (is_set(a) ? a : false);
        }

        // set color data
        function set_data(a) {
            _ = a;
        }

        // add hook
        function add(ev, fn, id) {
            if (!is_set(ev)) return hooks;
            if (!is_set(fn)) return hooks[ev];
            if (!is_set(hooks[ev])) hooks[ev] = {};
            if (!is_set(id)) id = object_length(hooks[ev]);
            return hooks[ev][id] = fn, $;
        }

        // remove hook
        function remove(ev, id) {
            if (!is_set(ev)) return hooks = {}, $;
            if (!is_set(id)) return hooks[ev] = {}, $;
            return delete hooks[ev][id], $;
        }

        // trigger hook
        function trigger(ev, a, id) {
            if (!is_set(hooks[ev])) return $;
            if (!is_set(id)) {
                for (var i in hooks[ev]) {
                    hooks[ev][i].apply($, a);
                }
            } else {
                if (is_set(hooks[ev][id])) {
                    hooks[ev][id].apply($, a);
                }
            }
            return $;
        }

        // initialize data ...
        set_data($$.parse(target.getAttribute('data-color') || target.value || [0, 1, 1]));

        // generate color picker pane ...
        picker.className = 'color-picker';
        picker.innerHTML = '<div class="color-picker-control"><span class="color-picker-h"><i></i></span><span class="color-picker-sv"><i></i></span></div>';
        var c = picker[first].children,
            HSV = get_data([0, 1, 1]), // default is red
            H = c[0],
            SV = c[1],
            H_point = H[first],
            SV_point = SV[first],
            start_H = 0,
            start_SV = 0,
            drag_H = 0,
            drag_SV = 0,
            left = 0,
            top = 0,
            P_W = 0,
            P_H = 0,
            v = HSV2HEX(HSV),
            set;

        // on update ...
        function trigger_(k, x) {
            if (!k || k === "h") {
                trigger("change:h", x);
            }
            if (!k || k === "sv") {
                trigger("change:sv", x);
            }
            trigger("change", x);
        }

        // is visible?
        function visible() {
            return picker.parentNode;
        }

        // create
        function create(first, bucket) {
            if (!first) {
                (parent || bucket || b).appendChild(picker), $.visible = true;
            }
            P_W = size(picker).w;
            P_H = size(picker).h;
            var SV_size = size(SV),
                SV_point_size = size(SV_point),
                H_H = size(H).h,
                SV_W = SV_size.w,
                SV_H = SV_size.h,
                H_point_H = size(H_point).h,
                SV_point_W = SV_point_size.w,
                SV_point_H = SV_point_size.h;
            if (first) {
                picker.style.left = picker.style.top = '-9999px';
                function click(e) {
                    var t = e.target,
                        is_target = t === target || closest(t, target) === target;
                    if (is_target) {
                        create();
                    } else {
                        $.exit();
                    }
                    trigger(is_target ? "enter" : "exit", [$]);
                }
                if (events !== false) {
                    on(events, target, click);
                }
                $.create = function() {
                    return create(1), trigger("create", [$]), $;
                };
                $.destroy = function() {
                    if (events !== false) {
                        off(events, target, click);
                    }
                    $.exit(), set_data(false);
                    return trigger("destroy", [$]), $;
                };
            } else {
                fit();
            }
            set = function() {
                HSV = get_data(HSV), color();
                H_point.style.top = (H_H - (H_point_H / 2) - (H_H * +HSV[0])) + 'px';
                SV_point.style.right = (SV_W - (SV_point_W / 2) - (SV_W * +HSV[1])) + 'px';
                SV_point.style.top = (SV_H - (SV_point_H / 2) - (SV_H * +HSV[2])) + 'px';
            };
            $.exit = function(e) {
                if (visible()) {
                    visible().removeChild(picker);
                    $.visible = false;
                }
                off(on_down, H, down_H);
                off(on_down, SV, down_SV);
                off(on_move, doc, move);
                off(on_up, doc, stop);
                off(on_resize, win, fit);
                return $;
            };
            function color(e) {
                var a = HSV2RGB(HSV),
                    b = HSV2RGB([HSV[0], 1, 1]);
                SV.style.backgroundColor = 'rgb(' + b.join(',') + ')';
                set_data(HSV);
                prevent(e);
            };
            set();
            function do_H(e) {
                var y = edge(point(H, e).y, 0, H_H);
                HSV[0] = (H_H - y) / H_H;
                H_point.style.top = (y - (H_point_H / 2)) + 'px';
                color(e);
            }
            function do_SV(e) {
                var o = point(SV, e),
                    x = edge(o.x, 0, SV_W),
                    y = edge(o.y, 0, SV_H);
                HSV[1] = 1 - ((SV_W - x) / SV_W);
                HSV[2] = (SV_H - y) / SV_H;
                SV_point.style.right = (SV_W - x - (SV_point_W / 2)) + 'px';
                SV_point.style.top = (y - (SV_point_H / 2)) + 'px';
                color(e);
            }
            function move(e) {
                if (drag_H) {
                    do_H(e), v = HSV2HEX(HSV);
                    if (!start_H) {
                        trigger("drag:h", [v, $]);
                        trigger("drag", [v, $]);
                        trigger_("h", [v, $]);
                    }
                }
                if (drag_SV) {
                    do_SV(e), v = HSV2HEX(HSV);
                    if (!start_SV) {
                        trigger("drag:sv", [v, $]);
                        trigger("drag", [v, $]);
                        trigger_("sv", [v, $]);
                    }
                }
                start_H = 0,
                start_SV = 0;
            }
            function stop(e) {
                var t = e.target,
                    k = drag_H ? "h" : "sv",
                    a = [HSV2HEX(HSV), $],
                    is_target = t === target || closest(t, target) === target,
                    is_picker = t === picker || closest(t, picker) === picker;
                if (!is_target && !is_picker) {
                    // click outside the target or picker element to exit
                    if (visible() && events !== false) $.exit(), trigger("exit", [$]), trigger_(0, a);
                } else {
                    if (is_picker) {
                        trigger("stop:" + k, a);
                        trigger("stop", a);
                        trigger_(k, a);
                    }
                }
                drag_H = 0,
                drag_SV = 0;
            }
            function down_H(e) {
                start_H = 1,
                drag_H = 1,
                move(e), prevent(e);
                trigger("start:h", [v, $]);
                trigger("start", [v, $]);
                trigger_("h", [v, $]);
            }
            function down_SV(e) {
                start_SV = 1,
                drag_SV = 1,
                move(e), prevent(e);
                trigger("start:sv", [v, $]);
                trigger("start", [v, $]);
                trigger_("sv", [v, $]);
            }
            if (!first) {
                on(on_down, H, down_H);
                on(on_down, SV, down_SV);
                on(on_move, doc, move);
                on(on_up, doc, stop);
                on(on_resize, win, fit);
            }
        } create(1);

        delay(function() {
            var a = [HSV2HEX(HSV), $];
            trigger("create", a);
            trigger_(0, a);
        }, 0);

        // fit to window
        $.fit = function(o) {
            var w = size(win),
                y = size(h),
                screen_w = w.w - y.w, // vertical scroll bar
                screen_h = w.h - h.clientHeight, // horizontal scroll bar
                ww = offset(win),
                to = offset(target);
            left = to.l + ww.l;
            top = to.t + ww.t + size(target).h; // drop!
            if (is_object(o)) {
                is_set(o[0]) && (left = o[0]);
                is_set(o[1]) && (top = o[1]);
            } else {
                var min_x = ww.l,
                    min_y = ww.t,
                    max_x = ww.l + w.w - P_W - screen_w,
                    max_y = ww.t + w.h - P_H - screen_h;
                left = edge(left, min_x, max_x) >> 0;
                top = edge(top, min_y, max_y) >> 0;
            }
            picker.style.left = left + 'px';
            picker.style.top = top + 'px';
            return trigger("fit", [$]), $;
        };

        // for event listener ID
        function fit() {
            return $.fit();
        }

        // set hidden color picker data
        $.set = function(a) {
            if (!is_set(a)) return get_data();
            if (is_string(a)) {
                a = $$.parse(a);
            }
            return set_data(a), set(), $;
        };

        // alias for `$.set()`
        $.get = function(a) {
            return get_data(a);
        };

        // register to global ...
        $.target = target;
        $.picker = picker;
        $.visible = false;
        $.on = add;
        $.off = remove;
        $.fire = trigger;
        $.hooks = hooks;
        $.enter = function(bucket) {
            return create(0, bucket);
        };

        // return the global object
        return $;

    });

})(window, document, 'CP');

/* FileSaver.js
 * A saveAs() FileSaver implementation.
 * 1.3.8
 * 2018-03-22 14:03:47
 *
 * By Eli Grey, https://eligrey.com
 * License: MIT
 *   See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
 */

/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */

/* @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */

//var saveAs = saveAs || (function(view) {
var saveAs = (function(view) {
    "use strict";
    // IE <10 is explicitly unsupported
    if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
        return;
    }
    var doc = view.document
          // only get URL when necessary in case Blob.js hasn't overridden it yet
        , get_URL = function() {
            return view.URL || view.webkitURL || view;
        }
        , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
        , can_use_save_link = "download" in save_link
        , click = function(node) {
            var event = new MouseEvent("click");
            node.dispatchEvent(event);
        }
        , is_safari = /constructor/i.test(view.HTMLElement) || view.safari
        , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent)
        , setImmediate = view.setImmediate || view.setTimeout
        , throw_outside = function(ex) {
            setImmediate(function() {
                throw ex;
            }, 0);
        }
        , force_saveable_type = "application/octet-stream"
        // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
        , arbitrary_revoke_timeout = 1000 * 40 // in ms
        , revoke = function(file) {
            var revoker = function() {
                if (typeof file === "string") { // file is an object URL
                    get_URL().revokeObjectURL(file);
                } else { // file is a File
                    file.remove();
                }
            };
            setTimeout(revoker, arbitrary_revoke_timeout);
        }
        , dispatch = function(filesaver, event_types, event) {
            event_types = [].concat(event_types);
            var i = event_types.length;
            while (i--) {
                var listener = filesaver["on" + event_types[i]];
                if (typeof listener === "function") {
                    try {
                        listener.call(filesaver, event || filesaver);
                    } catch (ex) {
                        throw_outside(ex);
                    }
                }
            }
        }
        , auto_bom = function(blob) {
            // prepend BOM for UTF-8 XML and text/* types (including HTML)
            // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
            //if (blob && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
            if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
                return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type});
            }
            return blob;
        }
        , FileSaver = function(blob, name, no_auto_bom) {
            if (!no_auto_bom) {
                blob = auto_bom(blob);
            }
            // First try a.download, then web filesystem, then object URLs
            var
                  filesaver = this
                , type = (blob) ? blob.type : undefined
                , force = type === force_saveable_type
                , object_url
                , dispatch_all = function() {
                    dispatch(filesaver, "writestart progress write writeend".split(" "));
                }
                // on any filesys errors revert to saving with object URLs
                , fs_error = function() {
                    if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
                        // Safari doesn't allow downloading of blob urls
                        var reader = new FileReader();
                        reader.onloadend = function() {
                            var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
                            var urlTarget = '_blank';
                            var popup = view.open(url, urlTarget);
                            if(!popup) view.location.href = url;
                            url=undefined; // release reference before dispatching
                            filesaver.readyState = filesaver.DONE;
                            dispatch_all();
                        };
                        reader.readAsDataURL(blob);
                        filesaver.readyState = filesaver.INIT;
                        return;
                    }
                    // don't create more object URLs than needed
                    if (!object_url) object_url = get_URL().createObjectURL(blob);
                    if (force) {
                        view.location.href = object_url;
                    } else {
                        var opened = view.open(object_url, "_blank");
                        if (!opened) {
                            // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
                            view.location.href = object_url;
                        }
                    }
                    filesaver.readyState = filesaver.DONE;
                    dispatch_all();
                    revoke(object_url);
                }
            ;
            filesaver.readyState = filesaver.INIT;

            if (can_use_save_link) {
                if (!object_url) object_url = get_URL().createObjectURL(blob);
                setImmediate(function() {
                    save_link.href = object_url;
                    save_link.download = name;
                    click(save_link);
                    dispatch_all();
                    revoke(object_url);
                    filesaver.readyState = filesaver.DONE;
                }, 0);
                return;
            }

            fs_error();
        }
        , FS_proto = FileSaver.prototype
        , saveAs = function(blob, name, no_auto_bom) {
            return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
        }
    ;

    // IE 10+ (native saveAs)
    if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
        return function(blob, name, no_auto_bom) {
            name = name || blob.name || "download";

            if (!no_auto_bom) {
                blob = auto_bom(blob);
            }
            return navigator.msSaveOrOpenBlob(blob, name);
        };
    }

    // todo: detect chrome extensions & packaged apps
    //save_link.target = "_blank";

    FS_proto.abort = function(){};
    FS_proto.readyState = FS_proto.INIT = 0;
    FS_proto.WRITING = 1;
    FS_proto.DONE = 2;

    FS_proto.error =
    FS_proto.onwritestart =
    FS_proto.onprogress =
    FS_proto.onwrite =
    FS_proto.onabort =
    FS_proto.onerror =
    FS_proto.onwriteend =
        null;

    return saveAs;
}(
       typeof self !== "undefined" && self
    || typeof window !== "undefined" && window
    || this
));

/*
 * JavaScript Canvas to Blob
 * https://github.com/blueimp/JavaScript-Canvas-to-Blob
 *
 * Copyright 2012, Sebastian Tschan
 * https://blueimp.net
 *
 * Licensed under the MIT license:
 * https://opensource.org/licenses/MIT
 *
 * Based on stackoverflow user Stoive's code snippet:
 * http://stackoverflow.com/q/4998908
 */

/* global atob, Blob, define */

;(function (window) {
  'use strict';

  var CanvasPrototype =
    window.HTMLCanvasElement && window.HTMLCanvasElement.prototype
  var hasBlobConstructor =
    window.Blob &&
    (function () {
      try {
        return Boolean(new Blob())
      } catch (e) {
        return false
      }
    })()
  var hasArrayBufferViewSupport =
    hasBlobConstructor &&
    window.Uint8Array &&
    (function () {
      try {
        return new Blob([new Uint8Array(100)]).size === 100
      } catch (e) {
        return false
      }
    })()
  var BlobBuilder =
    window.BlobBuilder ||
    window.WebKitBlobBuilder ||
    window.MozBlobBuilder ||
    window.MSBlobBuilder
  var dataURIPattern = /^data:((.*?)(;charset=.*?)?)(;base64)?,/
  var dataURLtoBlob =
    (hasBlobConstructor || BlobBuilder) &&
    window.atob &&
    window.ArrayBuffer &&
    window.Uint8Array &&
    function (dataURI) {
      var matches,
        mediaType,
        isBase64,
        dataString,
        byteString,
        arrayBuffer,
        intArray,
        i,
        bb
      // Parse the dataURI components as per RFC 2397
      matches = dataURI.match(dataURIPattern)
      if (!matches) {
        throw new Error('invalid data URI')
      }
      // Default to text/plain;charset=US-ASCII
      mediaType = matches[2]
        ? matches[1]
        : 'text/plain' + (matches[3] || ';charset=US-ASCII')
      isBase64 = !!matches[4]
      dataString = dataURI.slice(matches[0].length)
      if (isBase64) {
        // Convert base64 to raw binary data held in a string:
        byteString = atob(dataString)
      } else {
        // Convert base64/URLEncoded data component to raw binary:
        byteString = decodeURIComponent(dataString)
      }
      // Write the bytes of the string to an ArrayBuffer:
      arrayBuffer = new ArrayBuffer(byteString.length)
      intArray = new Uint8Array(arrayBuffer)
      for (i = 0; i < byteString.length; i += 1) {
        intArray[i] = byteString.charCodeAt(i)
      }
      // Write the ArrayBuffer (or ArrayBufferView) to a blob:
      if (hasBlobConstructor) {
        return new Blob([hasArrayBufferViewSupport ? intArray : arrayBuffer], {
          type: mediaType
        })
      }
      bb = new BlobBuilder()
      bb.append(arrayBuffer)
      return bb.getBlob(mediaType)
    }
  if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) {
    if (CanvasPrototype.mozGetAsFile) {
      CanvasPrototype.toBlob = function (callback, type, quality) {
        var self = this
        setTimeout(function () {
          if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) {
            callback(dataURLtoBlob(self.toDataURL(type, quality)))
          } else {
            callback(self.mozGetAsFile('blob', type))
          }
        })
      }
    } else if (CanvasPrototype.toDataURL && dataURLtoBlob) {
      CanvasPrototype.toBlob = function (callback, type, quality) {
        var self = this
        setTimeout(function () {
          callback(dataURLtoBlob(self.toDataURL(type, quality)))
        })
      }
    }
  }
  if (typeof define === 'function' && define.amd) {
    define(function () {
      return dataURLtoBlob
    })
  } else if (typeof module === 'object' && module.exports) {
    module.exports = dataURLtoBlob
  } else {
    window.dataURLtoBlob = dataURLtoBlob
  }
})(window)

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

//import * as $ from 'jquery';

class HashUtilsCls {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    //Clone the "fromHash" and return the cloned hash.
    cloneHash(from) { this.icn3dui;
      let to = {};

      if(from === undefined) from = {};

      for(let i in from) {
        to[i] = from[i];
      }

      return to;
    }

    //Get the intersection of two hashes "atoms1" and "atoms2". The returned hash has atom index as key and 1 as value.
    intHash(atoms1, atoms2) { this.icn3dui;
        let results = {};

        if(atoms1 === undefined) atoms1 = {};
        if(atoms2 === undefined) atoms2 = {};

        if(Object.keys(atoms1).length < Object.keys(atoms2).length) {
            for (let i in atoms1) {
                if (atoms2 !== undefined && atoms2[i]) {
                    results[i] = atoms1[i];
                }
            }
        }
        else {
            for (let i in atoms2) {
                if (atoms1 !== undefined && atoms1[i]) {
                    results[i] = atoms2[i];
                }
            }
        }

        return results;
    }

    // get atoms in allAtoms, but not in "atoms"
    //Get atoms in "includeAtoms", but not in "excludeAtoms". The returned hash has atom index as key and 1 as value.
    exclHash(includeAtomsInput, excludeAtoms) { let me = this.icn3dui;
        if(includeAtomsInput === undefined) includeAtomsInput = {};
        if(excludeAtoms === undefined) excludeAtoms = {};

        let includeAtoms = me.hashUtilsCls.cloneHash(includeAtomsInput);

        for (let i in includeAtoms) {
            if (excludeAtoms !== undefined && excludeAtoms[i]) {
                delete includeAtoms[i];
            }
        }

        return includeAtoms;
    }

    //Get the union of two hashes "atoms1" and "atoms2". The returned hash has atom index as key and 1 as value.
    unionHash(atoms1, atoms2) { let me = this.icn3dui;
        // much slower
        //return me.hashUtilsCls.unionHashNotInPlace(atoms1, atoms2);

        // much faster
        return me.hashUtilsCls.unionHashInPlace(atoms1, atoms2);
    }

    unionHashInPlace(atoms1, atoms2) { this.icn3dui;
        if(atoms1 === undefined) atoms1 = {};
        if(atoms2 === undefined) atoms2 = {};

        $.extend(atoms1, atoms2);

        return atoms1;
    }

    unionHashNotInPlace(atoms1, atoms2) { this.icn3dui;
        let results = $.extend({}, atoms1, atoms2);

        return results;
    }

    //Get the intersection of two hashes "atoms1" and "atoms2". The returned hash has atom index as key and atom object as value.
    intHash2Atoms(atoms1, atoms2, allAtoms) { let me = this.icn3dui;
        return me.hashUtilsCls.hash2Atoms(me.hashUtilsCls.intHash(atoms1, atoms2), allAtoms);
    }

    // get atoms in allAtoms, but not in "atoms"
    //Get atoms in "includeAtoms", but not in "excludeAtoms". The returned hash has atom index as key and atom object as value.
    exclHash2Atoms(includeAtoms, excludeAtoms, allAtoms) { let me = this.icn3dui;
        return me.hashUtilsCls.hash2Atoms(me.hashUtilsCls.exclHash(includeAtoms, excludeAtoms), allAtoms);
    }

    //Get the union of two hashes "atoms1" and "atoms2". The returned hash has atom index as key and atom object as value.
    unionHash2Atoms(atoms1, atoms2, allAtoms) { let me = this.icn3dui;
        return me.hashUtilsCls.hash2Atoms(me.hashUtilsCls.unionHash(atoms1, atoms2), allAtoms);
    }

    //The input "hash" has atom index as key and 1 as value. The returned hash has atom index as key and atom object as value.
    hash2Atoms(hash, allAtoms) { this.icn3dui;
        let atoms = {};
        for(let i in hash) {
          atoms[i] = allAtoms[i];
        }

        return atoms;
    }

    hashvalue2array(hash) { this.icn3dui;
        //return $.map(hash, function(v) { return v; });

        let array = [];
        for(let i in hash) {
            array.push(hash[i]);
        }

        return array;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */



// import {ParasCls} from './parasCls.js';

class UtilsCls {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    //Determine whether the current browser is Internet Explorer.
    isIE() { this.icn3dui;
        //http://stackoverflow.com/questions/19999388/check-if-user-is-using-ie-with-jquery
        let ua = window.navigator.userAgent;
        let msie = ua.indexOf("MSIE ");

        if (msie > 0 || !!window.navigator.userAgent.match(/Trident.*rv\:11\./))      // If Internet Explorer
            return true;
        else                 // If another browser, return 0
            return false;
    }

    //Determine whether it is a mobile device.
    isMobile() { this.icn3dui;
        return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
    }

    //Determine whether it is a Mac.
    isMac() { this.icn3dui;
        return /Mac/i.test(window.navigator.userAgent);
    }

    isAndroid() { this.icn3dui;
      return /android/i.test(window.navigator.userAgent.toLowerCase());
    }

    isChrome() { this.icn3dui;
      return navigator.userAgent.includes("Chrome") && navigator.vendor.includes("Google Inc");
    }

    //Determine whether Session Storage is supported in your browser. Session Storage is not supported in Safari.
    isSessionStorageSupported() { this.icn3dui;
        return window.sessionStorage;
    }

    isLocalStorageSupported() { this.icn3dui;
      return window.localStorage;
    }

    // http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
    hexToRgb(hex, a) { this.icn3dui;
         let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
         return result ? {
             r: parseInt(result[1], 16),
             g: parseInt(result[2], 16),
             b: parseInt(result[3], 16),
             a: a
         } : null;
    }

    //isCalphaPhosOnly(atomlist, atomname1, atomname2) {
    isCalphaPhosOnly(atomlist) { this.icn3dui;
          let bCalphaPhosOnly = false;

          let index = 0, testLength = 100; //30
          //var bOtherAtoms = false;
          let nOtherAtoms = 0;
          for(let i in atomlist) {
            if(index < testLength) {
              let atomName = atomlist[i].name.trim();        
              if(atomName !== "CA" && atomName !== "P" && atomName !== "O3'" && atomName !== "O3*") {
                //bOtherAtoms = true;
                //break;
                ++nOtherAtoms;
              }
            }
            else {
              break;
            }

            ++index;
          }

          //if(!bOtherAtoms) {
          if(nOtherAtoms < 0.5 * index) {
            bCalphaPhosOnly = true;
          }

          return bCalphaPhosOnly;
    }

    // from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Determine whether atom1 and atom2 have covalent bond.
    hasCovalentBond(atom0, atom1) { let me = this.icn3dui;
        let r = me.parasCls.covalentRadii[atom0.elem.toUpperCase()] + me.parasCls.covalentRadii[atom1.elem.toUpperCase()];

        //return atom0.coord.distanceToSquared(atom1.coord) < 1.3 * r * r;
        let dx = atom0.coord.x - atom1.coord.x;
        let dy = atom0.coord.y - atom1.coord.y;
        let dz = atom0.coord.z - atom1.coord.z;
        let distSq = dx*dx + dy*dy + dz*dz;

        // r(N) = 0.71, r(H) = 0.31, N-H in residues are about 1.5
        // factor = (1.5 / 1.02) * (1.5 / 1.02) = 2.16
        let factor = ((atom0.elem == 'N' && atom1.elem.substr(0,1) == 'H') || (atom1.elem == 'N' && atom0.elem.substr(0,1) == 'H')) ? 2.2 : 1.3;

        return distSq < factor * r * r;
    }

    //Convert a three-letter residue name to a one-letter residue abbreviation, e.g., 'LYS' to 'K', or ' A' to 'A' for nucleotides.
    residueName2Abbr(residueName) { this.icn3dui;
      let pos = residueName.indexOf(' ');
      if(pos > 0) {
          residueName = residueName.substr(0, pos);
      }

      switch(residueName) {
        case '  A':
          return 'A';
        case '  C':
          return 'C';
        case '  G':
          return 'G';
        case '  T':
          return 'T';
        case '  U':
          return 'U';
        case '  I':
          return 'I';
        case ' DA':
          return 'A';
        case ' DC':
          return 'C';
        case ' DG':
          return 'G';
        case ' DT':
          return 'T';
        case ' DU':
          return 'U';
        case ' DI':
          return 'I';
        case 'DA':
          return 'A';
        case 'DC':
          return 'C';
        case 'DG':
          return 'G';
        case 'DT':
          return 'T';
        case 'DU':
          return 'U';
        case 'DI':
          return 'I';
        case 'ALA':
          return 'A';
        case 'ARG':
          return 'R';
        case 'ASN':
          return 'N';
        case 'ASP':
          return 'D';
        case 'CYS':
          return 'C';
        case 'GLU':
          return 'E';
        case 'GLN':
          return 'Q';
        case 'GLY':
          return 'G';
        case 'HIS':
          return 'H';
        case 'ILE':
          return 'I';
        case 'LEU':
          return 'L';
        case 'LYS':
          return 'K';
        case 'MET':
          return 'M';
        case 'PHE':
          return 'F';
        case 'PRO':
          return 'P';
        case 'SER':
          return 'S';
        case 'THR':
          return 'T';
        case 'TRP':
          return 'W';
        case 'TYR':
          return 'Y';
        case 'VAL':
          return 'V';
        case 'SEC':
          return 'U';
    //        case 'PYL':
    //          return 'O';
    //          break;

        case 'HOH':
          return 'O';
        case 'WAT':
          return 'O';

        default:
          return residueName.trim();
      }
    }

    residueAbbr2Name(residueAbbr) { this.icn3dui;
      residueAbbr = residueAbbr.toUpperCase();

      if(residueAbbr.length > 1) {
          return residueAbbr;
      }

      switch(residueAbbr) {
        case 'A':
          return 'ALA';
        case 'R':
          return 'ARG';
        case 'N':
          return 'ASN';
        case 'D':
          return 'ASP';
        case 'C':
          return 'CYS';
        case 'E':
          return 'GLU';
        case 'Q':
          return 'GLN';
        case 'G':
          return 'GLY';
        case 'H':
          return 'HIS';
        case 'I':
          return 'ILE';
        case 'L':
          return 'LEU';
        case 'K':
          return 'LYS';
        case 'M':
          return 'MET';
        case 'F':
          return 'PHE';
        case 'P':
          return 'PRO';
        case 'S':
          return 'SER';
        case 'T':
          return 'THR';
        case 'W':
          return 'TRP';
        case 'Y':
          return 'TYR';
        case 'V':
          return 'VAL';
        case 'O':
          return 'HOH';

        default:
          return residueAbbr.trim();
      }
    }

    getJSONFromArray(inArray) { this.icn3dui;
        let jsonStr = '';
        for(let i = 0, il= inArray.length; i < il; ++i) {
            jsonStr += JSON.stringify(inArray[i]);
            if(i != il - 1) jsonStr += ', ';
        }
        return jsonStr;
    }

    checkFileAPI() { this.icn3dui;
         if(!window.File || !window.FileReader || !window.FileList || !window.Blob) {
            alert('The File APIs are not fully supported in this browser.');
         }
    }

    getIdArray(resid) { this.icn3dui;
        //var idArray = resid.split('_');
        let idArray = [];

        if(resid) {
            let pos1 = resid.indexOf('_');
            let pos2 = resid.lastIndexOf('_');
            idArray.push(resid.substr(0, pos1));
            idArray.push(resid.substr(pos1 + 1, pos2 - pos1 - 1));
            idArray.push(resid.substr(pos2 + 1));
        }

        return idArray;
    }

    compResid(a, b, type) { let me = this.icn3dui;
      let aArray = a.split(',');
      let bArray = b.split(',');
      let aIdArray, bIdArray;
      if(type == 'save1') {
        aIdArray = me.utilsCls.getIdArray(aArray[0]); //aArray[0].split('_');
        bIdArray = me.utilsCls.getIdArray(bArray[0]); //bArray[0].split('_');
      }
      else if(type == 'save2') {
        aIdArray = me.utilsCls.getIdArray(aArray[1]); //aArray[1].split('_');
        bIdArray = me.utilsCls.getIdArray(bArray[1]); //bArray[1].split('_');
      }
      let aChainid = aIdArray[0] + '_' + aIdArray[1];
      let bChainid = bIdArray[0] + '_' + bIdArray[1];
      let aResi = parseInt(aIdArray[2]);
      let bResi = parseInt(bIdArray[2]);
      if(aChainid > bChainid){
        return 1;
      }
      else if(aChainid < bChainid){
        return -1;
      }
      else if(aChainid == bChainid){
        return (aResi > bResi) ? 1 :(aResi < bResi) ? -1 : 0;
      }
    }

    toggle(id1, id2, id3, id4) { this.icn3dui;
      let itemArray = [id1, id2];
      for(let i in itemArray) {
          let item = itemArray[i];
          $("#" + item).toggleClass('ui-icon-plus');
          $("#" + item).toggleClass('ui-icon-minus');
      }

      itemArray = [id1, id2, id3, id4];
      for(let i in itemArray) {
          let item = itemArray[i];
          $("#" + item).toggleClass('icn3d-shown');
          $("#" + item).toggleClass('icn3d-hidden');
      }
    }

    setViewerWidthHeight(me, bRealSize) { //let me = this.icn3dui;
        if(me.bNode) {
            me.htmlCls.WIDTH = 400;
            me.htmlCls.HEIGHT = 400;
            return;
        }

        me.htmlCls.WIDTH = $( window ).width() - me.htmlCls.LESSWIDTH;
        me.htmlCls.HEIGHT = $( window ).height() - me.htmlCls.EXTRAHEIGHT - me.htmlCls.LESSHEIGHT;

        // width from css
        let viewer_width, viewer_height;

        if(!bRealSize && me.oriWidth !== undefined && me.cfg.width.toString().indexOf('%') === -1) {
            viewer_width = me.oriWidth;
            viewer_height = me.oriHeight;
        }
        else {
            // css width and height with the unit "px"
            viewer_width = $( "#" + me.pre + "viewer" ).css('width');
            viewer_height = $( "#" + me.pre + "viewer" ).css('height');

            viewer_width = (viewer_width) ? viewer_width.replace(/px/g, '') : me.htmlCls.WIDTH;
            viewer_height = (viewer_height) ? viewer_height.replace(/px/g, '') : me.htmlCls.HEIGHT;

            if(!bRealSize) {
                // width and height from input parameter
                if(me.cfg.width.toString().indexOf('%') !== -1) {
                  viewer_width = $( window ).width() * me.cfg.width.substr(0, me.cfg.width.toString().indexOf('%')) / 100.0 - me.htmlCls.LESSWIDTH;
                }
                else if(me.cfg.width) {
                  viewer_width = parseInt(me.cfg.width);
                }

                if(me.cfg.height.toString().indexOf('%') !== -1) {
                  viewer_height = $( window ).height() * me.cfg.height.substr(0, me.cfg.height.toString().indexOf('%')) / 100.0 - me.htmlCls.EXTRAHEIGHT - me.htmlCls.LESSHEIGHT;
                }
                else if(me.cfg.height) {
                  viewer_height = parseInt(me.cfg.height);
                }
            }
        }

        if(viewer_width && me.htmlCls.WIDTH > viewer_width) me.htmlCls.WIDTH = viewer_width;
        if(viewer_height && me.htmlCls.HEIGHT > viewer_height) me.htmlCls.HEIGHT = viewer_height;
    }

    sumArray(numArray) {
      let sum = 0;

      for(let i = 0, il = numArray.length; i < il; ++i) {
        sum += numArray[i];
      }

      return sum;
    }

    getMemDesc() {
      return "<div style='width:150px'><span style='color:red'>Red</span> and <span style='color:blue'>blue</span> membranes indicate <span style='color:red'>extracellular</span> and <span style='color:blue'>intracellular</span> membranes, respectively.<br><br></div>";
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */



class ParasCls {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;

        // https://pubs.acs.org/doi/pdf/10.1021/acs.jproteome.8b00473
        this.glycanHash = {
            'GLC': {'c': '1E90FF', 's': 'sphere'},
            'BGC': {'c': '1E90FF', 's': 'sphere'},

            'NAG': {'c': '1E90FF', 's': 'cube'},
            'NDG': {'c': '1E90FF', 's': 'cube'},
            'GCS': {'c': '1E90FF', 's': 'cube'},
            'PA1': {'c': '1E90FF', 's': 'cube'},

            'GCU': {'c': '1E90FF', 's': 'cone'},
            'BDP': {'c': '1E90FF', 's': 'cone'},
            'G6D': {'c': '1E90FF', 's': 'cone'},

            'DDA': {'c': '1E90FF', 's': 'cylinder'},
            'B6D': {'c': '1E90FF', 's': 'cylinder'},
            'XXM': {'c': '1E90FF', 's': 'cylinder'},


            'MAN': {'c': '00FF00', 's': 'sphere'},
            'BMA': {'c': '00FF00', 's': 'sphere'},

            'BM3': {'c': '00FF00', 's': 'cube'},
            '95Z': {'c': '00FF00', 's': 'cube'},

            'MAV': {'c': '00FF00', 's': 'cone'},
            'BEM': {'c': '00FF00', 's': 'cone'},
            'RAM': {'c': '00FF00', 's': 'cone'},
            'RM4': {'c': '00FF00', 's': 'cone'},

            'TYV': {'c': '00FF00', 's': 'cylinder'},
            'ARA': {'c': '00FF00', 's': 'cylinder'},
            'ARB': {'c': '00FF00', 's': 'cylinder'},
            'KDN': {'c': '00FF00', 's': 'cylinder'},
            'KDM': {'c': '00FF00', 's': 'cylinder'},
            '6PZ': {'c': '00FF00', 's': 'cylinder'},
            'GMH': {'c': '00FF00', 's': 'cylinder'},
            'BDF': {'c': '00FF00', 's': 'cylinder'},


            'GAL': {'c': 'FFFF00', 's': 'sphere'},
            'GLA': {'c': 'FFFF00', 's': 'sphere'},

            'NGA': {'c': 'FFFF00', 's': 'cube'},
            'A2G': {'c': 'FFFF00', 's': 'cube'},
            'X6X': {'c': 'FFFF00', 's': 'cube'},
            '1GN': {'c': 'FFFF00', 's': 'cube'},

            'ADA': {'c': 'FFFF00', 's': 'cone'},
            'GTR': {'c': 'FFFF00', 's': 'cone'},

            'LDY': {'c': 'FFFF00', 's': 'cylinder'},
            'KDO': {'c': 'FFFF00', 's': 'cylinder'},
            'T6T': {'c': 'FFFF00', 's': 'cylinder'},


            'GUP': {'c': 'A52A2A', 's': 'sphere'},
            'GL0': {'c': 'A52A2A', 's': 'sphere'},

            'LGU': {'c': 'A52A2A', 's': 'cone'},

            'ABE': {'c': 'A52A2A', 's': 'cylinder'},
            'XYS': {'c': 'A52A2A', 's': 'cylinder'},
            'XYP': {'c': 'A52A2A', 's': 'cylinder'},
            'SOE': {'c': 'A52A2A', 's': 'cylinder'},


            'PZU': {'c': 'FF69B4', 's': 'cylinder'},
            'RIP': {'c': 'FF69B4', 's': 'cylinder'},
            '0MK': {'c': 'FF69B4', 's': 'cylinder'},


            'ALL': {'c': '8A2BE2', 's': 'sphere'},
            'AFD': {'c': '8A2BE2', 's': 'sphere'},

            'NAA': {'c': '8A2BE2', 's': 'cube'},

            'SIA': {'c': '8A2BE2', 's': 'cylinder'},
            'SIB': {'c': '8A2BE2', 's': 'cylinder'},
            'AMU': {'c': '8A2BE2', 's': 'cylinder'},


            'X0X': {'c': '1E90FF', 's': 'cone'},
            'X1X': {'c': '1E90FF', 's': 'cone'},

            'NGC': {'c': '1E90FF', 's': 'cylinder'},
            'NGE': {'c': '1E90FF', 's': 'cylinder'},


            '4N2': {'c': 'A0522D', 's': 'sphere'},

            'HSQ': {'c': 'A0522D', 's': 'cube'},

            'IDR': {'c': 'A0522D', 's': 'cone'},

            'MUR': {'c': 'A0522D', 's': 'cylinder'},


            'FUC': {'c': 'FF0000', 's': 'cone'},
            'FUL': {'c': 'FF0000', 's': 'cone'}
        };

        // added nucleotides and ions
        this.nucleotidesArray = ['  G', '  A', '  T', '  C', '  U', ' DG', ' DA', ' DT', ' DC', ' DU',
            'G', 'A', 'T', 'C', 'U', 'DG', 'DA', 'DT', 'DC', 'DU'];

        this.ionsArray = ['  K', ' NA', ' MG', ' AL', ' CA', ' TI', ' MN', ' FE', ' NI', ' CU', ' ZN', ' AG', ' BA',
            '  F', ' CL', ' BR', '  I', 'K', 'NA', 'MG', 'AL', 'CA', 'TI', 'MN', 'FE', 'NI', 'CU', 'ZN', 'AG', 'BA',
            'F', 'CL', 'BR', 'I'];

        this.cationsTrimArray = ['K', 'NA', 'MG', 'AL', 'CA', 'TI', 'MN', 'FE', 'NI', 'CU', 'ZN', 'AG', 'BA'];
        this.anionsTrimArray = ['F', 'CL', 'BR', 'I'];

        this.ionCharges = {K: 1, NA: 1, MG: 2, AL: 3, CA: 2, TI: 3, MN: 2, FE: 3, NI: 2, CU: 2, ZN: 2, AG: 1, BA: 2};

        this.vdwRadii = { // Hu, S.Z.; Zhou, Z.H.; Tsai, K.R. Acta Phys.-Chim. Sin., 2003, 19:1073.
             H: 1.08,           HE: 1.34,           LI: 1.75,           BE: 2.05,            B: 1.47,
             C: 1.49,            N: 1.41,            O: 1.40,            F: 1.39,           NE: 1.68,
             NA: 1.84,          MG: 2.05,           AL: 2.11,           SI: 2.07,            P: 1.92,
             S: 1.82,           CL: 1.83,           AR: 1.93,            K: 2.05,           CA: 2.21,
             SC: 2.16,          TI: 1.87,            V: 1.79,           CR: 1.89,           MN: 1.97,
             FE: 1.94,          CO: 1.92,           NI: 1.84,           CU: 1.86,           ZN: 2.10,
             GA: 2.08,          GE: 2.15,           AS: 2.06,           SE: 1.93,           BR: 1.98,
             KR: 2.12,          RB: 2.16,           SR: 2.24,            Y: 2.19,           ZR: 1.86,
             NB: 2.07,          MO: 2.09,           TC: 2.09,           RU: 2.07,           RH: 1.95,
             PD: 2.02,          AG: 2.03,           CD: 2.30,           IN: 2.36,           SN: 2.33,
             SB: 2.25,          TE: 2.23,            I: 2.23,           XE: 2.21,           CS: 2.22,
             BA: 2.51,          LA: 2.40,           CE: 2.35,           PR: 2.39,           ND: 2.29,
             PM: 2.36,          SM: 2.29,           EU: 2.33,           GD: 2.37,           TB: 2.21,
             DY: 2.29,          HO: 2.16,           ER: 2.35,           TM: 2.27,           YB: 2.42,
             LU: 2.21,          HF: 2.12,           TA: 2.17,            W: 2.10,           RE: 2.17,
             OS: 2.16,          IR: 2.02,           PT: 2.09,           AU: 2.17,           HG: 2.09,
             TL: 2.35,          PB: 2.32,           BI: 2.43,           PO: 2.29,           AT: 2.36,
             RN: 2.43,          FR: 2.56,           RA: 2.43,           AC: 2.60,           TH: 2.37,
             PA: 2.43,           U: 2.40,           NP: 2.21,           PU: 2.56,           AM: 2.56,
             CM: 2.56,          BK: 2.56,           CF: 2.56,           ES: 2.56,           FM: 2.56
        };

        this.covalentRadii = { // http://en.wikipedia.org/wiki/Covalent_radius
             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.70,          TI: 1.60,            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.20,           AS: 1.19,           SE: 1.20,           BR: 1.20,
             KR: 1.16,          RB: 2.20,           SR: 1.95,            Y: 1.90,           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.40,           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.90,           YB: 1.87,
             LU: 1.87,          HF: 1.75,           TA: 1.70,            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.40,           AT: 1.50,
             RN: 1.50,          FR: 2.60,           RA: 2.21,           AC: 2.15,           TH: 2.06,
             PA: 2.00,           U: 1.96,           NP: 1.90,           PU: 1.87,           AM: 1.80,
             CM: 1.69
        };

    /*
        this.surfaces = {
            1: undefined,
            2: undefined,
            3: undefined,
            4: undefined
        };
    */

        //'C': this.thr(0xC8C8C8),
        this.atomColors = {
            'H': this.thr(0xFFFFFF),       'He': this.thr(0xFFC0CB),      'HE': this.thr(0xFFC0CB),
            'Li': this.thr(0xB22222),      'LI': this.thr(0xB22222),      'B': this.thr(0x00FF00),
            'C': this.thr(0xAAAAAA),       'N': this.thr(0x0000FF),       'O': this.thr(0xF00000),
            'F': this.thr(0xDAA520),       'Na': this.thr(0x0000FF),      'NA': this.thr(0x0000FF),
            'Mg': this.thr(0x228B22),      'MG': this.thr(0x228B22),      'Al': this.thr(0x808090),
            'AL': this.thr(0x808090),      'Si': this.thr(0xDAA520),      'SI': this.thr(0xDAA520),
            'P': this.thr(0xFFA500),       'S': this.thr(0xFFC832),       'Cl': this.thr(0x00FF00),
            'CL': this.thr(0x00FF00),      'Ca': this.thr(0x808090),      'CA': this.thr(0x808090),
            'Ti': this.thr(0x808090),      'TI': this.thr(0x808090),      'Cr': this.thr(0x808090),
            'CR': this.thr(0x808090),      'Mn': this.thr(0x808090),      'MN': this.thr(0x808090),
            'Fe': this.thr(0xFFA500),      'FE': this.thr(0xFFA500),      'Ni': this.thr(0xA52A2A),
            'NI': this.thr(0xA52A2A),      'Cu': this.thr(0xA52A2A),      'CU': this.thr(0xA52A2A),
            'Zn': this.thr(0xA52A2A),      'ZN': this.thr(0xA52A2A),      'Br': this.thr(0xA52A2A),
            'BR': this.thr(0xA52A2A),      'Ag': this.thr(0x808090),      'AG': this.thr(0x808090),
            'I': this.thr(0xA020F0),       'Ba': this.thr(0xFFA500),      'BA': this.thr(0xFFA500),
            'Au': this.thr(0xDAA520),      'AU': this.thr(0xDAA520)
        };

        this.atomnames = {
            'H': 'Hydrogen',        'HE': 'Helium',         'LI': 'Lithium',        'B': 'Boron',           
            'C': 'Carbon',          'N': 'Nitrogen',        'O': 'Oxygen',          'F': 'Fluorine',       
            'NA': 'Sodium',         'MG': 'Magnesium',      'AL': 'Aluminum',       'SI': 'Silicon',      
            'P': 'Phosphorus',      'S': 'Sulfur',          'CL': 'Chlorine',       'CA': 'Calcium',      
            'TI': 'Titanium',       'CR': 'Chromium',       'MN': 'Manganese',      'FE': 'Iron',      
            'NI': 'Nickel',         'CU': 'Copper',         'ZN': 'Zinc',           'BR': 'Bromine',
            'AG': 'Silver',         'I': 'Iodine',          'BA': 'Barium',         'AU': 'Gold'
        };

        this.defaultAtomColor = this.thr(0xCCCCCC);

        this.stdChainColors = [
            // first 6 colors from MMDB
            this.thr(0xFF00FF),            this.thr(0x0000FF),            this.thr(0x996633),
            this.thr(0x00FF99),            this.thr(0xFF9900),            this.thr(0xFF6666),
            this.thr(0x32CD32),            this.thr(0x1E90FF),            this.thr(0xFA8072),
            this.thr(0xFFA500),            this.thr(0x00CED1),            this.thr(0xFF69B4),
            this.thr(0x00FF00),            this.thr(0x0000FF),            this.thr(0xFF0000),
            this.thr(0xFFFF00),            this.thr(0x00FFFF),            this.thr(0xFF00FF),
            this.thr(0x3CB371),            this.thr(0x4682B4),            this.thr(0xCD5C5C),
            this.thr(0xFFE4B5),            this.thr(0xAFEEEE),            this.thr(0xEE82EE),
            this.thr(0x006400),            this.thr(0x00008B),            this.thr(0x8B0000),
            this.thr(0xCD853F),            this.thr(0x008B8B),            this.thr(0x9400D3)
        ];

        this.backgroundColors = {
            black: this.thr(0x000000),
             grey: this.thr(0xCCCCCC),
            white: this.thr(0xFFFFFF),
            transparent: this.thr(0xFFFFFF) //this.thr(0x000000)
        };

        this.residueColors = {
            ALA: this.thr(0xC8C8C8),       ARG: this.thr(0x145AFF),       ASN: this.thr(0x00DCDC),
            ASP: this.thr(0xE60A0A),       CYS: this.thr(0xE6E600),       GLN: this.thr(0x00DCDC),
            GLU: this.thr(0xE60A0A),       GLY: this.thr(0xEBEBEB),       HIS: this.thr(0x8282D2),
            ILE: this.thr(0x0F820F),       LEU: this.thr(0x0F820F),       LYS: this.thr(0x145AFF),
            MET: this.thr(0xE6E600),       PHE: this.thr(0x3232AA),       PRO: this.thr(0xDC9682),
            SER: this.thr(0xFA9600),       THR: this.thr(0xFA9600),       TRP: this.thr(0xB45AB4),
            TYR: this.thr(0x3232AA),       VAL: this.thr(0x0F820F),       ASX: this.thr(0xFF69B4),
            GLX: this.thr(0xFF69B4),         'G': this.thr(0x008000),       'A': this.thr(0x6080FF),
            'T': this.thr(0xFF8000),         'C': this.thr(0xFF0000),       'U': this.thr(0xFF8000),
            'DG': this.thr(0x008000),       'DA': this.thr(0x6080FF),      'DT': this.thr(0xFF8000),
            'DC': this.thr(0xFF0000),       'DU': this.thr(0xFF8000)
        };

        // calculated in iCn3D, the value could fluctuate 10-20 in different proteins
        this.residueArea = {
            ALA: 247,       ARG: 366,       ASN: 290,       ASP: 285,       CYS: 271,
            GLN: 336,       GLU: 325,       GLY: 217,       HIS: 340,       ILE: 324,
            LEU: 328,       LYS: 373,       MET: 346,       PHE: 366,       PRO: 285,
            SER: 265,       THR: 288,       TRP: 414,       TYR: 387,       VAL: 293,
            ASX: 290,       GLX: 336,         'G': 520,       'A': 507,       'T': 515,
            'C': 467,         'U': 482,      'DG': 520,      'DA': 507,      'DT': 515,
            'DC': 467,       'DU': 482
        };

        this.defaultResidueColor = this.thr(0xBEA06E);

        this.chargeColors = {
            // charged residues
            '  G': this.thr(0xFF0000),     '  A': this.thr(0xFF0000),     '  T': this.thr(0xFF0000),
            '  C': this.thr(0xFF0000),     '  U': this.thr(0xFF0000),     ' DG': this.thr(0xFF0000),
            ' DA': this.thr(0xFF0000),     ' DT': this.thr(0xFF0000),     ' DC': this.thr(0xFF0000),
            ' DU': this.thr(0xFF0000),       'G': this.thr(0xFF0000),       'A': this.thr(0xFF0000),
            'T': this.thr(0xFF0000),         'C': this.thr(0xFF0000),       'U': this.thr(0xFF0000),
            'DG': this.thr(0xFF0000),       'DA': this.thr(0xFF0000),      'DT': this.thr(0xFF0000),
            'DC': this.thr(0xFF0000),       'DU': this.thr(0xFF0000),     'ARG': this.thr(0x0000FF),
            'LYS': this.thr(0x0000FF),     'ASP': this.thr(0xFF0000),     'GLU': this.thr(0xFF0000),
            'HIS': this.thr(0x8080FF),     'GLY': this.thr(0x888888),     'PRO': this.thr(0x888888),
            'ALA': this.thr(0x888888),     'VAL': this.thr(0x888888),     'LEU': this.thr(0x888888),
            'ILE': this.thr(0x888888),     'PHE': this.thr(0x888888),     'SER': this.thr(0x888888),
            'THR': this.thr(0x888888),     'ASN': this.thr(0x888888),     'GLN': this.thr(0x888888),
            'TYR': this.thr(0x888888),     'MET': this.thr(0x888888),     'CYS': this.thr(0x888888),
            'TRP': this.thr(0x888888)
        };

        this.hydrophobicColors = {
            // charged residues
            '  G': this.thr(0xFF0000),     '  A': this.thr(0xFF0000),     '  T': this.thr(0xFF0000),
            '  C': this.thr(0xFF0000),     '  U': this.thr(0xFF0000),     ' DG': this.thr(0xFF0000),
            ' DA': this.thr(0xFF0000),     ' DT': this.thr(0xFF0000),     ' DC': this.thr(0xFF0000),
            ' DU': this.thr(0xFF0000),       'G': this.thr(0xFF0000),       'A': this.thr(0xFF0000),
            'T': this.thr(0xFF0000),         'C': this.thr(0xFF0000),       'U': this.thr(0xFF0000),
            'DG': this.thr(0xFF0000),       'DA': this.thr(0xFF0000),      'DT': this.thr(0xFF0000),
            'DC': this.thr(0xFF0000),       'DU': this.thr(0xFF0000),     'ARG': this.thr(0x0000FF),
            'LYS': this.thr(0x0000FF),     'ASP': this.thr(0xFF0000),     'GLU': this.thr(0xFF0000),
            'HIS': this.thr(0x8080FF),

            //this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * ( + 0.81)/(1.14 + 0.81)),
            // hydrophobic
            // https://en.m.wikipedia.org/wiki/Hydrophobicity_scales#Wimley%E2%80%93White_whole_residue_hydrophobicity_scales
            'TRP': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-1.85 + 1.85)/(0 + 1.85)),
            'PHE': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-1.13 + 1.85)/(0 + 1.85)),
            'TYR': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.94 + 1.85)/(0 + 1.85)),
            'LEU': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.56 + 1.85)/(0 + 1.85)),
            'ILE': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.31 + 1.85)/(0 + 1.85)),
            'CYS': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.24 + 1.85)/(0 + 1.85)),
            'MET': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.23 + 1.85)/(0 + 1.85)),

            // polar
            'GLY': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.01 + 0.58)/(0 + 0.58)),
            'VAL': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.07 + 0.58)/(0 + 0.58)),
            'SER': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.13 + 0.58)/(0 + 0.58)),
            'THR': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.14 + 0.58)/(0 + 0.58)),
            'ALA': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.17 + 0.58)/(0 + 0.58)),
            'ASN': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.42 + 0.58)/(0 + 0.58)),
            'PRO': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.45 + 0.58)/(0 + 0.58)),
            'GLN': this.thr().setHSL(1/6.0, 1, 0.5 + 0.5 * (-0.58 + 0.58)/(0 + 0.58))
        };

        this.normalizedHPColors = {
            // charged residues
            '  G': this.thr(0xFFFFFF),     '  A': this.thr(0xFFFFFF),     '  T': this.thr(0xFFFFFF),
            '  C': this.thr(0xFFFFFF),     '  U': this.thr(0xFFFFFF),     ' DG': this.thr(0xFFFFFF),
            ' DA': this.thr(0xFFFFFF),     ' DT': this.thr(0xFFFFFF),     ' DC': this.thr(0xFFFFFF),
            ' DU': this.thr(0xFFFFFF),       'G': this.thr(0xFFFFFF),       'A': this.thr(0xFFFFFF),
            'T': this.thr(0xFFFFFF),         'C': this.thr(0xFFFFFF),       'U': this.thr(0xFFFFFF),
            'DG': this.thr(0xFFFFFF),       'DA': this.thr(0xFFFFFF),      'DT': this.thr(0xFFFFFF),
            'DC': this.thr(0xFFFFFF),       'DU': this.thr(0xFFFFFF),     'ARG': this.thr(0xFFFFFF),
            'LYS': this.thr(0xFFFFFF),     'ASP': this.thr(0xFFFFFF),     'GLU': this.thr(0xFFFFFF),
            'HIS': this.thr(0xFFFFFF),

            //this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * ( + 0.81)/(1.14 + 0.81)),
            // hydrophobic
            // https://en.m.wikipedia.org/wiki/Hydrophobicity_scales#Wimley%E2%80%93White_whole_residue_hydrophobicity_scales
            // 0.65 ~ -1.85: white ~ green
            'TRP': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-1.85 + 1.85)/2.5),
            'PHE': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-1.13 + 1.85)/2.5),
            'TYR': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.94 + 1.85)/2.5),
            'LEU': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.56 + 1.85)/2.5),
            'ILE': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.31 + 1.85)/2.5),
            'CYS': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.24 + 1.85)/2.5),
            'MET': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (-0.23 + 1.85)/2.5),

            // polar
            'GLY': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.01 + 1.85)/2.5),
            'VAL': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.07 + 1.85)/2.5),
            'SER': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.13 + 1.85)/2.5),
            'THR': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.14 + 1.85)/2.5),
            'ALA': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.17 + 1.85)/2.5),
            'ASN': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.42 + 1.85)/2.5),
            'PRO': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.45 + 1.85)/2.5),
            'GLN': this.thr().setHSL(1/3.0, 1, 0.5 + 0.5 * (0.58 + 1.85)/2.5)
        };

        this.hydrophobicValues = {
            // charged residues
            '  G': 3,     '  A': 3,     '  T': 3,
            '  C': 3,     '  U': 3,     ' DG': 3,
            ' DA': 3,     ' DT': 3,     ' DC': 3,
            ' DU': 3,       'G': 3,       'A': 3,
            'T': 3,         'C': 3,       'U': 3,
            'DG': 3,       'DA': 3,      'DT': 3,
            'DC': 3,       'DU': 3,     'ARG': 1,
            'LYS': 1,     'ASP': 3,     'GLU': 3,
            'HIS': 2,

            // + 0.81)/(1.14 + 0.81)),
            // hydrophobic
            // https://en.m.wikipedia.org/wiki/Hydrophobicity_scales#Wimley%E2%80%93White_whole_residue_hydrophobicity_scales
            // 0.65 ~ -1.85: white ~ green
            'TRP': -1.85,
            'PHE': -1.13,
            'TYR': -0.94,
            'LEU': -0.56,
            'ILE': -0.31,
            'CYS': -0.24,
            'MET': -0.23,

            // polar
            'GLY': 0.01,
            'VAL': 0.07,
            'SER': 0.13,
            'THR': 0.14,
            'ALA': 0.17,
            'ASN': 0.42,
            'PRO': 0.45,
            'GLN': 0.58
        };

        this.residueAbbrev = {
            ALA: "A (Ala)",       ARG: "R (Arg)",       ASN: "N (Asn)",
            ASP: "D (Asp)",       CYS: "C (Cys)",       GLN: "Q (Gln)",
            GLU: "E (Glu)",       GLY: "G (Gly)",       HIS: "H (His)",
            ILE: "I (Ile)",       LEU: "L (Leu)",       LYS: "K (Lys)",
            MET: "M (Met)",       PHE: "F (Phe)",       PRO: "P (Pro)",
            SER: "S (Ser)",       THR: "T (Thr)",       TRP: "W (Trp)",
            TYR: "Y (Tyr)",       VAL: "V (Val)",       
            //ASX: "B (Asx)",       GLX: "Z (Glx)",   
            ASX: "X (Asx)",       GLX: "X (Glx)",       
            'G': "Guanine",       'A': "Adenine",
            'T': "Thymine",         'C': "Cytosine",       'U': "Uracil",
            'DG': "deoxy-Guanine",       'DA': "deoxy-Adenine",      'DT': "deoxy-Thymine",
            'DC': "deoxy-Cytosine",       'DU': 'deoxy-Uracil'
        };

        this.ssColors = {
            helix: this.thr(0xFF0000),
            sheet: this.thr(0x008000),
             coil: this.thr(0x6080FF) //this.thr(0xEEEEEE) //this.thr(0x6080FF)
        };

        this.ssColors2 = {
            helix: this.thr(0xFF0000),
            sheet: this.thr(0xFFC800),
             coil: this.thr(0x6080FF) //this.thr(0xEEEEEE) //this.thr(0x6080FF)
        };

        this.resn2restype = {
            "ALA": 1, "ARG": 4, "ASN": 7, "ASP": 10, "CYS": 13, "GLN": 16, "GLU": 19, "GLY": 22, "HIS": 25, "ILE": 28, "LEU": 31, "LYS": 34, "MET": 37, "PHE": 40, "PRO": 43, "SER": 46, "THR": 49, "TRP": 52, "TYR": 55, "VAL": 58
        };

        this.nuclMainArray = ["C1'", "C1*", "C2'", "C2*", "C3'", "C3*", "C4'", "C4*", "C5'", "C5*", "O3'", "O3*", "O4'", "O4*", "O5'", "O5*", "P", "OP1", "O1P", "OP2", "O2P"];

        // https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt, range from -4 to 11
        this.b62ResArray = ['A', 'R', 'N', 'D', 'C', 'Q', 'E', 'G', 'H', 'I', 'L', 'K', 'M', 'F',
            'P', 'S', 'T', 'W', 'Y', 'V', 'B', 'Z', 'X', '*']; // length: 24
        this.b62Matrix = [
            [4, -1, -2, -2, 0, -1, -1, 0, -2, -1, -1, -1, -1, -2, -1, 1, 0, -3, -2, 0, -2, -1, 0, -4],
            [-1, 5, 0, -2, -3, 1, 0, -2, 0, -3, -2, 2, -1, -3, -2, -1, -1, -3, -2, -3, -1, 0, -1, -4],
            [-2, 0, 6, 1, -3, 0, 0, 0, 1, -3, -3, 0, -2, -3, -2, 1, 0, -4, -2, -3, 3, 0, -1, -4],
            [-2, -2, 1, 6, -3, 0, 2, -1, -1, -3, -4, -1, -3, -3, -1, 0, -1, -4, -3, -3, 4, 1, -1, -4],
            [0, -3, -3, -3, 9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1, -2, -2, -1, -3, -3, -2, -4],
            [-1, 1, 0, 0, -3, 5, 2, -2, 0, -3, -2, 1, 0, -3, -1, 0, -1, -2, -1, -2, 0, 3, -1, -4],
            [-1, 0, 0, 2, -4, 2, 5, -2, 0, -3, -3, 1, -2, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1, -4],
            [0, -2, 0, -1, -3, -2, -2, 6, -2, -4, -4, -2, -3, -3, -2, 0, -2, -2, -3, -3, -1, -2, -1, -4],
            [-2, 0, 1, -1, -3, 0, 0, -2, 8, -3, -3, -1, -2, -1, -2, -1, -2, -2, 2, -3, 0, 0, -1, -4],
            [-1, -3, -3, -3, -1, -3, -3, -4, -3, 4, 2, -3, 1, 0, -3, -2, -1, -3, -1, 3, -3, -3, -1, -4],
            [-1, -2, -3, -4, -1, -2, -3, -4, -3, 2, 4, -2, 2, 0, -3, -2, -1, -2, -1, 1, -4, -3, -1, -4],
            [-1, 2, 0, -1, -3, 1, 1, -2, -1, -3, -2, 5, -1, -3, -1, 0, -1, -3, -2, -2, 0, 1, -1, -4],
            [-1, -1, -2, -3, -1, 0, -2, -3, -2, 1, 2, -1, 5, 0, -2, -1, -1, -1, -1, 1, -3, -1, -1, -4],
            [-2, -3, -3, -3, -2, -3, -3, -3, -1, 0, 0, -3, 0, 6, -4, -2, -2, 1, 3, -1, -3, -3, -1, -4],
            [-1, -2, -2, -1, -3, -1, -1, -2, -2, -3, -3, -1, -2, -4, 7, -1, -1, -4, -3, -2, -2, -1, -2, -4],
            [1, -1, 1, 0, -1, 0, 0, 0, -1, -2, -2, 0, -1, -2, -1, 4, 1, -3, -2, -2, 0, 0, 0, -4],
            [0, -1, 0, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -2, -1, 1, 5, -2, -2, 0, -1, -1, 0, -4],
            [-3, -3, -4, -4, -2, -2, -3, -2, -2, -3, -2, -3, -1, 1, -4, -3, -2, 11, 2, -3, -4, -3, -2, -4],
            [-2, -2, -2, -3, -2, -1, -2, -3, 2, -1, -1, -2, -1, 3, -3, -2, -2, 2, 7, -1, -3, -2, -1, -4],
            [0, -3, -3, -3, -1, -2, -2, -3, -3, 3, 1, -2, 1, -1, -2, -2, 0, -3, -1, 4, -3, -2, -1, -4],
            [-2, -1, 3, 4, -3, 0, 1, -1, 0, -3, -4, 0, -3, -3, -2, 0, -1, -4, -3, -3, 4, 1, -1, -4],
            [-1, 0, 0, 1, -3, 3, 4, -2, 0, -3, -3, 1, -1, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1, -4],
            [0, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, 0, 0, -2, -1, -1, -1, -1, -1, -4],
            [-4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, 1],
        ];
    }

    thr(color) { this.icn3dui;
        if(color == '#0') color = '#000';
        return new THREE.Color(color);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class MyEventCls {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    onId(id, eventName, myFunction) { this.icn3dui;
        if(Object.keys(window).length < 2) return;

        if(id.substr(0, 1) == '#') id = id.substr(1);
        if(document.getElementById(id)) {
            let eventArray = eventName.split(' ');
            eventArray.forEach(event => {
                document.getElementById(id).addEventListener(event, myFunction);
            });
        }
    }

    onIds(idArray, eventName, myFunction) { let me = this.icn3dui;
        let bArray = Array.isArray(idArray);
        if(bArray) {
            idArray.forEach(id => {
                me.myEventCls.onId(id, eventName, myFunction);
            });
        }
        else {
            me.myEventCls.onId(idArray, eventName, myFunction);
        }
    }

    // CSS selector such as class
/*
    onSel(selector, eventName, myFunction) { let me = this.icn3dui;
        let elemArray = document.querySelectorAll(selector); // non-live
        elemArray.forEach(elem => {
            let eventArray = eventName.split(' ');
            eventArray.forEach(event => {
                elem.addEventListener(event, myFunction);
            });
        });
    }

    onSelClass(selector, eventName, myFunction) { let me = this.icn3dui;
        selector = selector.replace(/\./gi, '');
        let classArray = selector.split(',');
        classArray.forEach(item => {
            let elemArray = document.getElementsByClassName(item.trim()); // live
            if(Array.isArray(elemArray)) {
                elemArray.forEach(elem => {
                    let eventArray = eventName.split(' ');
                    eventArray.forEach(event => {
                        elem.addEventListener(event, myFunction);
                    });
                });
            }
        });
    }
*/
}

// from Thomas Madej at NCBI
/* A routine to return the superposition rmsd for 'n' pairs of corresponding
 * points.  It also returns the translation vectors and rotation matrix.
 *
 * Based on the appendix in the paper:
 *
 *  A.D. McLachlan, "Gene Duplications in the Structural Evolution of
 *  Chymotrypsin", J. Mol. Biol. 128 (1979) 49-79.
 */



class RmsdSuprCls {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    getRmsdSuprCls(co1, co2, n) { let me = this.icn3dui;
    //    let TINY0 = 1.0e-10;
        let supr;
        let rot = new Array(9);

        let i, k, flag;
        //double cp[3], cq[3];
        let cp = new THREE.Vector3(), cq = new THREE.Vector3();

        let da, ra, rb, d1, d2, d3, e, s, v;
        //double ap[MAX_RES][3], bp[MAX_RES][3], mat[9];
        let ap = [], bp = [];
    //    let mat = new Array(9);

        //double h1[3], h2[3], h3[3], k1[3], k2[3], k3[3];
        let h1 = new Array(3), h2 = new Array(3), h3 = new Array(3), k1 = new Array(3), k2 = new Array(3), k3 = new Array(3);

        supr = 0.0;

        if (n <= 1) return {'rot': undefined, 'trans1': undefined, 'trans2': undefined, 'rmsd': 999};

        // read in and reformat the coordinates
        // calculate the centroids
        let finalCnt = n;
        for (i = 0; i < n; i++) {
            if(co1[i] === undefined || co2[i] === undefined) {
                --finalCnt;
                continue;
            }
            ap.push(co1[i].clone());
            bp.push(co2[i].clone());

            cp.add(co1[i]);
            cq.add(co2[i]);
        }

        n = finalCnt;
        if (n <= 1) return {'rot': undefined, 'trans1': undefined, 'trans2': undefined, 'rmsd': 999};

        cp.multiplyScalar(1.0 / n);
        cq.multiplyScalar(1.0 / n);

        // save the translation vectors
        let xc1 = cp;
        let xc2 = cq;

        // translate coordinates
        for (i = 0; i < n; i++) {
            ap[i].sub(cp);
            bp[i].sub(cq);
        }

        // radii of gyration
        for (i = 0, ra = rb = 0.0; i < n; i++) {
            ra += ap[i].x*ap[i].x + ap[i].y*ap[i].y + ap[i].z*ap[i].z;
            rb += bp[i].x*bp[i].x + bp[i].y*bp[i].y + bp[i].z*bp[i].z;
        }

        ra /= n;
        rb /= n;

        let u = new Array(9); //var u00, u01, u02, u10, u11, u12, u20, u21, u22;

        // correlation matrix U
        for (i = 0; i < 9; ++i) {
            u[i] = 0;
        }

        for (i = 0; i < n; i++) {
            u[0] += ap[i].x*bp[i].x;
            u[1] += ap[i].x*bp[i].y;
            u[2] += ap[i].x*bp[i].z;
            u[3] += ap[i].y*bp[i].x;
            u[4] += ap[i].y*bp[i].y;
            u[5] += ap[i].y*bp[i].z;
            u[6] += ap[i].z*bp[i].x;
            u[7] += ap[i].z*bp[i].y;
            u[8] += ap[i].z*bp[i].z;
        }

        for (i = 0; i < 9; ++i) {
            u[i] /= n;
        }

        let eigenRet = me.rmsdSuprCls.getEigenVectors(u);
        k = eigenRet.k;
        h1 = eigenRet.h1;
        h2 = eigenRet.h2;
        h3 = eigenRet.h3;

        k1 = eigenRet.k1;
        k2 = eigenRet.k2;
        k3 = eigenRet.k3;

        d1 = eigenRet.d1;
        d2 = eigenRet.d2;
        d3 = eigenRet.d3;

        flag = eigenRet.flag;

        s = eigenRet.s;

        if (k != 1) {
            supr = 100.0;
            rot[0] = 1.0; rot[1] = 0.0; rot[2] = 0.0;
            rot[3] = 0.0; rot[4] = 1.0; rot[5] = 0.0;
            rot[6] = 0.0; rot[7] = 0.0; rot[8] = 1.0;
            return {'rot': rot, 'trans1': xc1, 'trans2': xc2, 'rmsd': supr};
        }

        if (flag == 1) {
            // compute the k-vectors via the h-vectors
            k1[0] = u[0]*h1[0] + u[3]*h1[1] + u[6]*h1[2];
            k1[1] = u[1]*h1[0] + u[4]*h1[1] + u[7]*h1[2];
            k1[2] = u[2]*h1[0] + u[5]*h1[1] + u[8]*h1[2];
            da = Math.sqrt(d1);
            k1[0] /= da;
            k1[1] /= da;
            k1[2] /= da;
            k2[0] = u[0]*h2[0] + u[3]*h2[1] + u[6]*h2[2];
            k2[1] = u[1]*h2[0] + u[4]*h2[1] + u[7]*h2[2];
            k2[2] = u[2]*h2[0] + u[5]*h2[1] + u[8]*h2[2];
            da = Math.sqrt(d2);
            k2[0] /= da;
            k2[1] /= da;
            k2[2] /= da;
            k3[0] = u[0]*h3[0] + u[3]*h3[1] + u[6]*h3[2];
            k3[1] = u[1]*h3[0] + u[4]*h3[1] + u[7]*h3[2];
            k3[2] = u[2]*h3[0] + u[5]*h3[1] + u[8]*h3[2];
            da = Math.sqrt(d3);
            k3[0] /= da;
            k3[1] /= da;
            k3[2] /= da;
        }
        else if (flag == 2) {
            // compute the h-vectors via the k-vectors
            h1[0] = u[0]*k1[0] + u[1]*k1[1] + u[2]*k1[2];
            h1[1] = u[3]*k1[0] + u[4]*k1[1] + u[5]*k1[2];
            h1[2] = u[6]*k1[0] + u[7]*k1[1] + u[8]*k1[2];
            da = Math.sqrt(d1);
            h1[0] /= da;
            h1[1] /= da;
            h1[2] /= da;
            h2[0] = u[0]*k2[0] + u[1]*k2[1] + u[2]*k2[2];
            h2[1] = u[3]*k2[0] + u[4]*k2[1] + u[5]*k2[2];
            h2[2] = u[6]*k2[0] + u[7]*k2[1] + u[8]*k2[2];
            da = Math.sqrt(d2);
            h2[0] /= da;
            h2[1] /= da;
            h2[2] /= da;
            h3[0] = u[0]*k3[0] + u[1]*k3[1] + u[2]*k3[2];
            h3[1] = u[3]*k3[0] + u[4]*k3[1] + u[5]*k3[2];
            h3[2] = u[6]*k3[0] + u[7]*k3[1] + u[8]*k3[2];
            da = Math.sqrt(d3);
            h3[0] /= da;
            h3[1] /= da;
            h3[2] /= da;
        }

        if (s > 0.0) {
            rot[0] = (k1[0]*h1[0] + k2[0]*h2[0] + k3[0]*h3[0]);
            rot[1] = (k1[0]*h1[1] + k2[0]*h2[1] + k3[0]*h3[1]);
            rot[2] = (k1[0]*h1[2] + k2[0]*h2[2] + k3[0]*h3[2]);
            rot[3] = (k1[1]*h1[0] + k2[1]*h2[0] + k3[1]*h3[0]);
            rot[4] = (k1[1]*h1[1] + k2[1]*h2[1] + k3[1]*h3[1]);
            rot[5] = (k1[1]*h1[2] + k2[1]*h2[2] + k3[1]*h3[2]);
            rot[6] = (k1[2]*h1[0] + k2[2]*h2[0] + k3[2]*h3[0]);
            rot[7] = (k1[2]*h1[1] + k2[2]*h2[1] + k3[2]*h3[1]);
            rot[8] = (k1[2]*h1[2] + k2[2]*h2[2] + k3[2]*h3[2]);
        }
        else {
            rot[0] = (k1[0]*h1[0] + k2[0]*h2[0] - k3[0]*h3[0]);
            rot[1] = (k1[0]*h1[1] + k2[0]*h2[1] - k3[0]*h3[1]);
            rot[2] = (k1[0]*h1[2] + k2[0]*h2[2] - k3[0]*h3[2]);
            rot[3] = (k1[1]*h1[0] + k2[1]*h2[0] - k3[1]*h3[0]);
            rot[4] = (k1[1]*h1[1] + k2[1]*h2[1] - k3[1]*h3[1]);
            rot[5] = (k1[1]*h1[2] + k2[1]*h2[2] - k3[1]*h3[2]);
            rot[6] = (k1[2]*h1[0] + k2[2]*h2[0] - k3[2]*h3[0]);
            rot[7] = (k1[2]*h1[1] + k2[2]*h2[1] - k3[2]*h3[1]);
            rot[8] = (k1[2]*h1[2] + k2[2]*h2[2] - k3[2]*h3[2]);
        }

        // optimal rotation correction via eigenvalues
        d1 = Math.sqrt(d1);
        d2 = Math.sqrt(d2);
        d3 = Math.sqrt(d3);
        v = d1 + d2 + s*d3;
        e = ra + rb - 2.0*v;

        if (e > 0.0) {
            supr = Math.sqrt(e);
        }
        else {
            supr = undefined;
        }

        return {'rot': rot, 'trans1': xc1, 'trans2': xc2, 'rmsd': supr};

    }; // end rmsd_supr


    eigen_values(a0) { this.icn3dui;
        let v00, v01, v02, v10, v11, v12, v20, v21, v22;
        let a, b, c, p, q, t, u, v, d1, d2, d3;

        // initialization
        v00 = a0[0]; v01 = a0[1]; v02 = a0[2];
        v10 = a0[3]; v11 = a0[4]; v12 = a0[5];
        v20 = a0[6]; v21 = a0[7]; v22 = a0[8];

        // coefficients of the characteristic polynomial for V
        // det(xI - V) = x^3 + a*x^2 + b*x + c
        a = -(v00 + v11 + v22);
        b = v00*v11 + (v00 + v11)*v22 - v12*v21 - v01*v10 - v02*v20;
        c = -v00*v11*v22 + v00*v12*v21 + v01*v10*v22 - v01*v12*v20 - v02*v10*v21
            + v02*v11*v20;

        // transformed polynomial: x = y - a/3, poly(y) = y^3 + p*y + q
        p = -a*a/3.0 + b;
        q = a*a*a/13.5 - a*b/3.0 + c;

        // solutions y = u + v
        t = 0.25*q*q + p*p*p/27.0;

        if (t < 0.0) {
            let r, theta;

            // things are a bit more complicated
            r = Math.sqrt(0.25*q*q - t);
            theta = Math.acos(-0.5*q/r);
            d1 = 2.0*Math.cbrt(r)*Math.cos(theta/3.0);
        }
        else {
            u = Math.cbrt(-0.5*q + Math.sqrt(t));
            v = Math.cbrt(-0.5*q - Math.sqrt(t));
            d1 = u + v;
        }

        // return to the original characteristic polynomial
        d1 -= a/3.0;
        a += d1;
        c /= -d1;

        // solve the quadratic x^2 + a*x + c = 0
        d2 = 0.5*(-a + Math.sqrt(a*a - 4.0*c));
        d3 = 0.5*(-a - Math.sqrt(a*a - 4.0*c));

        // order the eigenvalues: d1 >= d2 >= d3
        if (d2 < d3) {
            t = d3;
            d3 = d2;
            d2 = d3;
        }

        if (d1 < d2) {
            t = d2;
            d2 = d1;
            d1 = t;
        }

        if (d2 < d3) {
            t = d3;
            d3 = d2;
            d2 = d3;
        }

        return {'d1': d1, 'd2': d2, 'd3': d3};
    }; // end eigen_values

    // Return the basis for the null space of the input matrix.
    null_basis(a0, v1, v2, v3, epsi) { this.icn3dui;
        let k, k0, spec;
        let a11, a12, a13, a21, a22, a23, a31, a32, a33;
        let b22, b23, b32, b33;
        let t, mx0;

        // initialization
        a11 = a0[0]; a12 = a0[1]; a13 = a0[2];
        a21 = a0[3]; a22 = a0[4]; a23 = a0[5];
        a31 = a0[6]; a32 = a0[7]; a33 = a0[8];

        // scale the matrix, so find the max entry
        mx0 = Math.abs(a11);
        if (Math.abs(a12) > mx0) mx0 = Math.abs(a12);
        if (Math.abs(a13) > mx0) mx0 = Math.abs(a13);
        if (Math.abs(a21) > mx0) mx0 = Math.abs(a21);
        if (Math.abs(a22) > mx0) mx0 = Math.abs(a22);
        if (Math.abs(a23) > mx0) mx0 = Math.abs(a23);
        if (Math.abs(a31) > mx0) mx0 = Math.abs(a31);
        if (Math.abs(a32) > mx0) mx0 = Math.abs(a32);
        if (Math.abs(a33) > mx0) mx0 = Math.abs(a33);

        if (mx0 < 1.0e-10) {
            // interpret this as the matrix of all 0's
            k0 = 3;
            return {'k': k0, 'v1': v1, 'v2': v2, 'v3': v3};
        }

        spec = 0;
        a11 /= mx0; a12 /= mx0; a13 /= mx0;
        a21 /= mx0; a22 /= mx0; a23 /= mx0;
        a31 /= mx0; a32 /= mx0; a33 /= mx0;

        if ((Math.abs(a11) < epsi) && (Math.abs(a21) < epsi) && (Math.abs(a31) < epsi)) {
            // let x1 is independent
            k = 1;
            v1[0] = 1.0; v1[1] = 0.0; v1[2] = 0.0;

            if ((Math.abs(a12) < epsi) && (Math.abs(a22) < epsi) && (Math.abs(a32) < epsi)) {
                // let x2 is independent
                k = 2;
                v2[0] = 0.0; v2[1] = 1.0; v2[2] = 0.0;

                if ((Math.abs(a13) < epsi) && (Math.abs(a23) < epsi) && (Math.abs(a33) < epsi)) {
                    // let x3 is independent
                    k = 3;
                    v3[0] = 0.0; v3[1] = 0.0; v3[2] = 1.0;
                }

                // else, we must have x3 = 0.0, so we're done
            }
            else {
                // reorder so that a12 is maximized
                mx0 = Math.abs(a12);

                if (Math.abs(a22) > mx0) {
                    // swap rows 1 and 2
                    t = a11; a11 = a21; a21 = t;
                    t = a12; a12 = a22; a22 = t;
                    t = a13; a13 = a23; a23 = t;
                    mx0 = Math.abs(a12);
                }

                if (Math.abs(a32) > mx0) {
                    // swap rows 1 and 3
                    t = a11; a11 = a31; a31 = t;
                    t = a12; a12 = a32; a32 = t;
                    t = a13; a13 = a33; a33 = t;
                }

                // let x2 is dependent, x2 = -a13/a12*x3
                b32 = a23 - a22*a13/a12;
                b33 = a33 - a32*a13/a12;

                if ((Math.abs(b32) < epsi) && (Math.abs(b33) < epsi)) {
                    //* let x3 is independent
                    k = 2;
                    v2[0] = 0.0; v2[1] = -a13/a12; v2[2] = 1.0;
                    spec = 1;
                }

                // else, we must have x3 = x2 = 0.0, so we're done
            }
        }
        else {
            // reorder so that a11 is maximized
            mx0 = Math.abs(a11);

            if (Math.abs(a12) > mx0) {
                // swap rows 1 and 2
                t = a11; a11 = a21; a21 = t;
                t = a12; a12 = a22; a22 = t;
                t = a13; a13 = a23; a23 = t;
                mx0 = Math.abs(a11);
            }

            if (Math.abs(a13) > mx0) {
                // swap rows 1 and 3
                t = a11; a11 = a31; a31 = t;
                t = a12; a12 = a32; a32 = t;
                t = a13; a13 = a33; a33 = t;
            }

            // let x1 is dependent, x1 = -a12/a11*x2 - a13/a11*x3
            b22 = a22 - a21*a12/a11;
            b23 = a23 - a21*a13/a11;
            b32 = a32 - a31*a12/a11;
            b33 = a33 - a31*a13/a11;

            if ((Math.abs(b22) < epsi) && (Math.abs(b32) < epsi)) {
                // let x2 is independent
                k = 1;
                v1[0] = -a12/a11; v1[1] = 1.0; v1[2] = 0.0;

                if ((Math.abs(b23) < epsi) && (Math.abs(b33) < epsi)) {
                    // let x3 is independent
                    k = 2;
                    v2[0] = -a13/a11; v2[1] = 0.0; v2[2] = 1.0;
                    spec = 2;
                }

                // else, we must have x3 = 0.0, so we're done
            }
            else {
                // reorder so that b22 is maximized
                if (Math.abs(b22) < Math.abs(b32)) {
                    t = b22; b22 = b32; b32 = t;
                    t = b23; b23 = b33; b33 = t;
                }

                // let x2 is dependent, x2 = -b23/b22*x3
                if (Math.abs(b33 - b23*b32/b22) < epsi) {
                    // let x3 is independent
                    k = 1;
                    v1[0] = (a12/a11)*(b23/b22) - a13/a11;
                    v1[1] = -b23/b22; v1[2] = 1.0;
                    spec = 3;
                }
                else {
                    // the null space contains only the zero vector
                    k0 = 0;
                    v1[0] = 0.0; v1[1] = 0.0; v1[2] = 0.0;
                    //return;
                    return {'k': k0, 'v1': v1, 'v2': v2, 'v3': v3};
                }
            }
        }

        k0 = k;

        if (spec > 0) {
            // special cases, basis should be orthogonalized
            if (spec == 1) {
                // 2nd vector must be normalized
                a11 = v2[0]; a12 = v2[1]; a13 = v2[2];
                t = Math.sqrt(a11*a11 + a12*a12 + a13*a13);
                v2[0] = a11/t; v2[1] = a12/t; v2[2] = a13/t;
            }
            else if (spec == 2) {
                // 1st, 2nd vectors must be orthogonalized
                a11 = v1[0]; a12 = v1[1]; a13 = v1[2];
                a21 = v2[0]; a22 = v2[1]; a23 = v2[2];
                t = a11*a21 + a12*a22 + a13*a23;

                if (Math.abs(t) >= epsi) {
                    v2[0] = a11 + t*a21;
                    v2[1] = a12 + t*a22;
                    v2[2] = a13 + t*a23;
                    a21 = v2[0]; a22 = v2[1]; a23 = v2[2];
                }

                // normalize the vectors
                t = Math.sqrt(a11*a11 + a12*a12 + a13*a13);
                v1[0] = a11/t; v1[1] = a12/t; v1[2] = a13/t;
                t = Math.sqrt(a21*a21 + a22*a22 + a23*a23);
                v2[0] = a21/t; v2[1] = a22/t; v2[2] = a23/t;
            }
            else {
                // 1st vector must be normalized
                a11 = v1[0]; a12 = v1[1]; a13 = v1[2];
                t = Math.sqrt(a11*a11 + a12*a12 + a13*a13);
                v1[0] = a11/t; v1[1] = a12/t; v1[2] = a13/t;
            }
        }

        return {'k': k0, 'v1': v1, 'v2': v2, 'v3': v3};
    }; // end null_basis


    getEigenForSelection(coord, n) { let me = this.icn3dui;
        let i;
        let cp = new THREE.Vector3();
        let ap = [];

        // read in and reformat the coordinates
        // calculate the centroids
        for (i = 0; i < n; i++) {
            ap.push(coord[i]);

            cp.add(coord[i]);
        }

        cp.multiplyScalar(1.0 / n);

        // translate coordinates
        for (i = 0; i < n; i++) {
            ap[i].sub(cp);
        }

        let u = new Array(9); //var u00, u01, u02, u10, u11, u12, u20, u21, u22;

        for (i = 0; i < 9; ++i) {
            u[i] = 0;
        }

        // http://individual.utoronto.ca/rav/Web/FR/cov.htm
        // https://builtin.com/data-science/step-step-explanation-principal-component-analysis
        for (i = 0; i < n; i++) {
            u[0] += ap[i].x*ap[i].x;
            u[1] += ap[i].x*ap[i].y;
            u[2] += ap[i].x*ap[i].z;
            u[3] += ap[i].y*ap[i].x;
            u[4] += ap[i].y*ap[i].y;
            u[5] += ap[i].y*ap[i].z;
            u[6] += ap[i].z*ap[i].x;
            u[7] += ap[i].z*ap[i].y;
            u[8] += ap[i].z*ap[i].z;
        }

        for (i = 0; i < 9; ++i) {
            u[i] /= n;
        }

        return me.rmsdSuprCls.getEigenVectors(u);
    };

    getEigenVectors(u, bJustPc1) { let me = this.icn3dui;
    //    let TINY0 = 1.0e-10;
        let TINY0 = 1.0e-8;
        let k, flag;
        let mat = new Array(9);

        let h1 = new Array(3), h2 = new Array(3), h3 = new Array(3), k1 = new Array(3), k2 = new Array(3), k3 = new Array(3);

        let dU, d1, d2, d3, s;

        // determinant of U
        dU = u[0]*(u[4]*u[8] - u[5]*u[7]);
        dU -= u[1]*(u[3]*u[8] - u[5]*u[6]);
        dU += u[2]*(u[3]*u[7] - u[4]*u[6]);
        s = (dU < 0.0) ? -1.0 : 1.0;

        let v1 = new Array(3), v2 = new Array(3);
        for(let i = 0; i < 3; ++i) {
            v1[i] = new THREE.Vector3();
            v2[i] = new THREE.Vector3();
        }

        // compute V = UU' (it is symmetric)
        v1[0].x = u[0]*u[0] + u[1]*u[1] + u[2]*u[2];
        v1[0].y = u[0]*u[3] + u[1]*u[4] + u[2]*u[5];
        v1[0].z = u[0]*u[6] + u[1]*u[7] + u[2]*u[8];
        v1[1].x = v1[0].y;
        v1[1].y = u[3]*u[3] + u[4]*u[4] + u[5]*u[5];
        v1[1].z = u[3]*u[6] + u[4]*u[7] + u[5]*u[8];
        v1[2].x = v1[0].z;
        v1[2].y = v1[1].z;
        v1[2].z = u[6]*u[6] + u[7]*u[7] + u[8]*u[8];

        // also compute V = U'U, as it may be needed
        v2[0].x = u[0]*u[0] + u[3]*u[3] + u[6]*u[6];
        v2[0].y = u[0]*u[1] + u[3]*u[4] + u[6]*u[7];
        v2[0].z = u[0]*u[2] + u[3]*u[5] + u[6]*u[8];
        v2[1].x = v2[0].y;
        v2[1].y = u[1]*u[1] + u[4]*u[4] + u[7]*u[7];
        v2[1].z = u[1]*u[2] + u[4]*u[5] + u[7]*u[8];
        v2[2].x = v2[0].z;
        v2[2].y = v2[1].z;
        v2[2].z = u[2]*u[2] + u[5]*u[5] + u[8]*u[8];

        // compute the eigenvalues
        mat[0] = v1[0].x; mat[1] = v1[0].y; mat[2] = v1[0].z;
        mat[3] = v1[1].x; mat[4] = v1[1].y; mat[5] = v1[1].z;
        mat[6] = v1[2].x; mat[7] = v1[2].y; mat[8] = v1[2].z;

        let eigen = me.rmsdSuprCls.eigen_values(mat);

        d1 = eigen.d1;
        d2 = eigen.d2;
        d3 = eigen.d3;

        // now we need the eigenvectors
        flag = 1;
        mat[0] -= d1;
        mat[4] -= d1;
        mat[8] -= d1;
        let basis = me.rmsdSuprCls.null_basis(mat, h1, h2, h3, TINY0);
        k = basis.k;
        h1 = basis.v1;
        h2 = basis.v2;
        h3 = basis.v3;

        if(bJustPc1) return {"k": k, "h1": h1, "h2": h2, "h3": h3, "k1": k1, "k2": k2, "k3": k3, "d1": d1, "d2": d2, "d3": d3, "flag": flag, "s": s};

        if (k == 1) {
            mat[0] += d1 - d2;
            mat[4] += d1 - d2;
            mat[8] += d1 - d2;
            basis = me.rmsdSuprCls.null_basis(mat, h2, h3, h1, TINY0);
            k = basis.k;
            h2 = basis.v1;
            h3 = basis.v2;
            h1 = basis.v3;

            if (k == 1) {
                mat[0] += d2 - d3;
                mat[4] += d2 - d3;
                mat[8] += d2 - d3;
                basis = me.rmsdSuprCls.null_basis(mat, h3, h1, h2, TINY0);
                k = basis.k;
                h3 = basis.v1;
                h1 = basis.v2;
                h2 = basis.v3;
            }
        }

        if (k != 1) {
            // retry the computation, but using V = U'U
            mat[0] = v2[0].x; mat[1] = v2[0].y; mat[2] = v2[0].z;
            mat[3] = v2[1].x; mat[4] = v2[1].y; mat[5] = v2[1].z;
            mat[6] = v2[2].x; mat[7] = v2[2].y; mat[8] = v2[2].z;

            // now we need the eigenvectors
            flag = 2;
            mat[0] -= d1;
            mat[4] -= d1;
            mat[8] -= d1;
            basis = me.rmsdSuprCls.null_basis(mat, k1, k2, k3, TINY0);
            k = basis.k;
            k1 = basis.v1;
            k2 = basis.v2;
            k3 = basis.v3;

            if (k == 1) {
                mat[0] += d1 - d2;
                mat[4] += d1 - d2;
                mat[8] += d1 - d2;
                basis = me.rmsdSuprCls.null_basis(mat, k2, k3, k1, TINY0);
                k = basis.k;
                k2 = basis.v1;
                k3 = basis.v2;
                k1 = basis.v3;

                if (k == 1) {
                    mat[0] += d2 - d3;
                    mat[4] += d2 - d3;
                    mat[8] += d2 - d3;
                    basis = me.rmsdSuprCls.null_basis(mat, k3, k1, k2, TINY0);
                    k = basis.k;
                    k3 = basis.v1;
                    k1 = basis.v2;
                    k2 = basis.v3;
                }
            }
        }

        return {"k": k, "h1": h1, "h2": h2, "h3": h3, "k1": k1, "k2": k2, "k3": k3, "d1": d1, "d2": d2, "d3": d3, "flag": flag, "s": s};
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */



class SubdivideCls {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    // cubic splines for four points: http://thalestriangles.blogspot.com/2014/02/a-bit-of-ex-spline-ation.html
    // https://math.stackexchange.com/questions/577641/how-to-calculate-interpolating-splines-in-3d-space
    subdivide(_pnts, _clrs, DIV, bShowArray, bHighlight, prevone, nexttwo, bExtendLastRes) { let me = this.icn3dui;

        let ret = [];
        let pos = [];
        let color = [];

        let pnts = new Array(); // Smoothing test

        let prevoneLen = (prevone !== undefined) ? prevone.length : 0;
        let nexttwoLenOri = (nexttwo !== undefined) ? nexttwo.length : 0;

        let maxDist = 6.0;

        if(prevoneLen > 0
            && Math.abs(prevone[0].x - _pnts[0].x) <= maxDist
            && Math.abs(prevone[0].y - _pnts[0].y) <= maxDist
            && Math.abs(prevone[0].z - _pnts[0].z) <= maxDist
            ) {
          pnts.push(prevone[0]);
          prevoneLen = 1;
        }
        else {
          prevoneLen = 0;
        }

        pnts.push(_pnts[0]);
        for (let i = 1, lim = _pnts.length - 1; i < lim; ++i) {
            let p0 = _pnts[i], p1 = _pnts[i + 1];
            pnts.push(p0.smoothen ? p0.clone().add(p1).multiplyScalar(0.5) : p0);
        }
        pnts.push(_pnts[_pnts.length - 1]);

        let nexttwoLen = 0;
        if(nexttwoLenOri > 0
            && Math.abs(nexttwo[0].x - _pnts[_pnts.length - 1].x) <= maxDist
            && Math.abs(nexttwo[0].y - _pnts[_pnts.length - 1].y) <= maxDist
            && Math.abs(nexttwo[0].z - _pnts[_pnts.length - 1].z) <= maxDist
            ) {
          pnts.push(nexttwo[0]);
          ++nexttwoLen;
        }

        if(nexttwoLenOri > 1
            && Math.abs(nexttwo[0].x - nexttwo[1].x) <= maxDist
            && Math.abs(nexttwo[0].y - nexttwo[1].y) <= maxDist
            && Math.abs(nexttwo[0].z - nexttwo[1].z) <= maxDist
            ) {
          pnts.push(nexttwo[1]);
          ++nexttwoLen;
        }

        let savedPoints = [];
        let savedPos = [];
        let savedColor = [];

        //var nexttwoLen = nexttwoLenOri;
        if(bExtendLastRes) {
            nexttwoLen = (nexttwoLenOri > 0) ? nexttwoLenOri - 1 : 0;
        }

        let alpha = 1, newI;

        for (let i = -1, size = pnts.length, DIVINV = 1 / DIV; i <= size - 3; ++i) {
            newI = i - prevoneLen;
            let p0 = pnts[i === -1 ? 0 : i];
            let p1 = pnts[i + 1];
            let p2 = pnts[i + 2];
            let p3 = pnts[i === size - 3 ? size - 1 : i + 3];

            let t0 = 0;
            let t1 = me.subdivideCls.getKnot(alpha, t0, p0, p1);
            let t2 = me.subdivideCls.getKnot(alpha, t1, p1, p2);
            let t3 = me.subdivideCls.getKnot(alpha, t2, p2, p3);

            if(t1 - t0 < 1e-4) t1 = t0 + 1;
            if(t2 - t1 < 1e-4) t2 = t1 + 1;
            if(t3 - t2 < 1e-4) t3 = t2 + 1;

            //if(i > -1 && bHighlight && bShowArray !== undefined && bShowArray[i + 1]) {
            if(i > -1 && (bShowArray === undefined || bShowArray[newI + 1]) ) {
                // get from previous i for the first half of residue
                if(i >= -1 + prevoneLen && i <= size - 3 - nexttwoLen + 1) {
                    ret = ret.concat(savedPoints);
                    pos = pos.concat(savedPos);
                    color = color.concat(savedColor);
                }
            }

            savedPoints = [];
            savedPos = [];
            savedColor = [];

            let step = (t2 - t1) * DIVINV;
            for (let j = 0; j < DIV; ++j) {
                let t = t1 + step * j;
                let x = me.subdivideCls.getValueFromKnot(t, t0, t1, t2, t3, p0.x, p1.x, p2.x, p3.x);
                let y = me.subdivideCls.getValueFromKnot(t, t0, t1, t2, t3, p0.y, p1.y, p2.y, p3.y);
                let z = me.subdivideCls.getValueFromKnot(t, t0, t1, t2, t3, p0.z, p1.z, p2.z, p3.z);

                if(!bShowArray) {
                    if(i >= -1 + prevoneLen && i <= size - 3 - nexttwoLen) {
                        ret.push(new THREE.Vector3(x, y, z));
                        pos.push(newI + 1);
                        color.push(_clrs[newI+1]);
                    }
                }
                else {
                    if(i >= -1 + prevoneLen && i <= size - 3 - nexttwoLen) {
                        if(bShowArray[newI + 1]) {
                            if(j <= parseInt((DIV) / 2) ) {
                                ret.push(new THREE.Vector3(x, y, z));
                                pos.push(bShowArray[newI + 1]);
                                color.push(_clrs[newI+1]);
                            }
                        }
                    }

                    if(i >= -1 + prevoneLen && i <= size - 3 - nexttwoLen + 1) {
                        if(bShowArray[newI + 2]) {
                            if(j > parseInt((DIV) / 2) ) {
                                savedPoints.push(new THREE.Vector3(x, y, z));
                                savedPos.push(bShowArray[newI + 2]);
                                savedColor.push(_clrs[newI+2]);
                            }
                        }
                    }
                } // end else

            } // end for (let j = 0;
        } // end for (let i = -1;

        if(!bShowArray || bShowArray[newI + 1]) {
            //if(bHighlight) {
            ret = ret.concat(savedPoints);
            pos = pos.concat(savedPos);
            color = color.concat(savedColor);
            //}

            ret.push(pnts[pnts.length - 1 - nexttwoLen]);
            pos.push(pnts.length - 1 - nexttwoLen);
            color.push(_clrs[pnts.length - 1 - nexttwoLen]);
        }

        savedPoints = [];
        savedPos = [];
        savedColor = [];
        pnts = [];

        let pnts_positions = [];

        pnts_positions.push(ret);
        pnts_positions.push(pos);
        pnts_positions.push(color);

        return pnts_positions;
    };


    getKnot(alpha, ti, Pi, Pj) { this.icn3dui;
        //var alpha = 1;

        //return Math.pow(Pi.distanceTo(Pj), alpha) + ti;
        return Pi.distanceTo(Pj) + ti;
    }

    getValueFromKnot(t, t0, t1, t2, t3, y0, y1, y2, y3) { this.icn3dui;
        let inf = 9999;

        // m(i) = ( t(i+1) - t(i) == 0 ) ? 0 : ( y(i+1) - y(i) ) / ( t(i+1) - t(i) )
        let m0 = (y1 - y0) / (t1 - t0);
        let m1 = (y2 - y1) / (t2 - t1);
        let m2 = (y3 - y2) / (t3 - t2);

        // L(i) = m(i) * (t - t(i)) + y(i)
        //var L0 = m0 * (t - t0) + y0;
        let L1 = m1 * (t - t1) + y1;
        //var L2 = m2 * (t - t2) + y2;

        let denom = (t1 + t2) * (t1 + t2) - 4*(t0*t1 + t2*t3 - t0*t3);
        let d1, d2;

        if(denom == 0) {
            d1 = inf;
            d2 = inf;
        }
        else {
            d1 = 6 * (3*m1*t1 + 2*m0*t3 + m2*t1 - 2*m0*t1 - 2*m1*t3 - m1*t2 - m2*t1) / denom;
            d2 = 6 * (3*m1*t2 + 2*m2*t0 + m0*t1 - 2*m1*t0 - 2*m2*t2 - m0*t2 - m1*t1) / denom;
        }

        // a(i) = ( 2*d(i) + d(i+1) ) / 6 / (t(i) - t(i+1))
        // b(i) = ( 2*d(i+1) + d(i) ) / 6 / (t(i+1) - t(i))
        //var a0 = ( 2*d0 + d1 ) / 6 / (t0 - t1);
        let a1 = ( 2*d1 + d2 ) / 6 / (t1 - t2);
        //var a2 = ( 2*d2 + d3 ) / 6 / (t2 - t3);

        //var b0 = ( 2*d1 + d0 ) / 6 / (t1 - t0);
        let b1 = ( 2*d2 + d1 ) / 6 / (t2 - t1);
        //var b2 = ( 2*d3 + d2 ) / 6 / (t3 - t2);

        // C(i) = a(i)*(t - t(i))*(t - t(i+1))*(t - t(i+1)) + b(i)*(t - t(i))*(t - t(i))*(t - t(i+1))
        //var C0 = a0*(t - t0)*(t - t1)*(t - t1) + b0*(t - t0)*(t - t0)*(t - t1);
        let C1 = a1*(t - t1)*(t - t2)*(t - t2) + b1*(t - t1)*(t - t1)*(t - t2);
        //var C2 = a2*(t - t2)*(t - t3)*(t - t3) + b2*(t - t2)*(t - t2)*(t - t3);

        let F1 = L1 + C1;

        return F1;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ConvertTypeCls {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    passFloat32( array, output ){ let me = this.icn3dui;
        let n = array.length;
        if( !output ) output = new Uint8Array( 4 * n );
        let dv = me.convertTypeCls.getDataView( output );
        for( let i = 0; i < n; ++i ){
            dv.setFloat32( 4 * i, array[ i ], true); // litteEndian = true
        }        return me.convertTypeCls.getUint8View( output );
    }

    passInt8( array, output ){ let me = this.icn3dui;
        let n = array.length;
        if( !output ) output = new Uint8Array( 1 * n );
        let dv = me.convertTypeCls.getDataView( output );
        for( let i = 0; i < n; ++i ){
            dv.setInt8( 1 * i, array[ i ], true); // litteEndian = true
        }        return me.convertTypeCls.getUint8View( output );
    }

    passInt16( array, output ){ let me = this.icn3dui;
        let n = array.length;
        if( !output ) output = new Uint8Array( 2 * n );
        let dv = me.convertTypeCls.getDataView( output );
        for( let i = 0; i < n; ++i ){
            dv.setInt16( 2 * i, array[ i ], true); // litteEndian = true
        }        return me.convertTypeCls.getUint8View( output );
    }

    passInt32( array, output ){ let me = this.icn3dui;
        let n = array.length;
        if( !output ) output = new Uint8Array( 4 * n );
        let dv = me.convertTypeCls.getDataView( output );
        for( let i = 0; i < n; ++i ){
            dv.setInt32( 4 * i, array[ i ], true); // litteEndian = true
        }        return me.convertTypeCls.getUint8View( output );
    }

    getUint8View( typedArray ){ let me = this.icn3dui;
        return me.convertTypeCls.getView( Uint8Array, typedArray );
    }

    getDataView( typedArray ){ let me = this.icn3dui;
        return me.convertTypeCls.getView( DataView, typedArray );
    }

    getView( ctor, typedArray, elemSize ){ this.icn3dui;
        return typedArray ? new ctor(
            typedArray.buffer,
            typedArray.byteOffset,
            typedArray.byteLength / ( elemSize || 1 )
        ) : undefined;
    }

    getBlobFromBufferAndText(arrayBuffer, text) { let me = this.icn3dui;
        let strArray = new Uint8Array(arrayBuffer);

        let strArray2 = new Uint8Array(text.length);
        for(let i = 0; i < text.length; ++i) {
           strArray2[i] = me.convertTypeCls.passInt8([text.charCodeAt(i)])[0];
        }

        let blobArray = []; // hold blobs

        //blobArray.push(new Blob([strArray0],{ type: "application/octet-stream"}));
        blobArray.push(new Blob([strArray],{ type: "application/octet-stream"}));
        blobArray.push(new Blob([strArray2],{ type: "application/octet-stream"}));

        //var blob = new Blob(blobArray,{ type: "application/octet-stream"});
        let blob = new Blob(blobArray,{ type: "image/png"});

        return blob;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ClickMenu {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    setAlphaFoldLegend() { let me = this.icn3dui; me.icn3d;
        let legendHtml;
        legendHtml = '<div>';
        legendHtml += '<span class="icn3d-square" style="background-color: rgb(0, 83, 204);">&nbsp;</span> <span>Very high (pLDDT &gt; 90)</span><br>';
        legendHtml += '<span class="icn3d-square" style="background-color: rgb(101, 203, 243);">&nbsp;</span> <span>Confident (90 &gt; pLDDT &gt; 70)</span><br>';
        legendHtml += '<span class="icn3d-square" style="background-color: rgb(255, 209, 19);">&nbsp;</span> <span>Low (70 &gt; pLDDT &gt; 50)</span><br>';
        legendHtml += '<span class="icn3d-square" style="background-color: rgb(255, 125, 69);">&nbsp;</span> <span>Very low (pLDDT &lt; 50)</span><br>';
        legendHtml += '</div>';

        return legendHtml;
    }

    setLegendHtml(bAf) { let me = this.icn3dui, ic = me.icn3d;
        let legendHtml = "<br>";
        if(bAf) {
            legendHtml += this.setAlphaFoldLegend();
        }
        else {
            let startColorStr = (ic.startColor == 'red') ? '#F00' : (ic.startColor == 'green') ? '#0F0' : '#00F';
            let midColorStr = (ic.midColor == 'white') ? '#FFF' : '#000';
            let endColorStr = (ic.endColor == 'red') ? '#F00' : (ic.endColor == 'green') ? '#0F0' : '#00F';
            let rangeStr = startColorStr + ' 0%, ' + midColorStr + ' 50%, ' + endColorStr + ' 100%';

            legendHtml += "<div style='height: 20px; background: linear-gradient(to right, " + rangeStr + ");'></div><table width='100%' border='0' cellspacing='0' cellpadding='0'><tr><td width='33%'>" + ic.startValue + "</td><td width='33%' align='center'>" + ic.midValue + "</td><td width='33%' align='right'>" + ic.endValue + "</td></tr></table>";
        }

        return legendHtml;
    }

    SetChainsAdvancedMenu() { let me = this.icn3dui, ic = me.icn3d;
        if(ic.bSetChainsAdvancedMenu === undefined || !ic.bSetChainsAdvancedMenu) {
            let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
            ic.definedSetsCls.setPredefinedInMenu();
            ic.bSetChainsAdvancedMenu = true;
            ic.hAtoms = me.hashUtilsCls.cloneHash(prevHAtoms);
        }
    }

    setSetsMenus(id, bOneset) { let me = this.icn3dui, ic = me.icn3d;
        this.SetChainsAdvancedMenu();

        let id1 = id;
        let id2 = id + '2';

        let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
        if($("#" + me.pre + id1).length) {
            $("#" + me.pre + id1).html("  <option value='selected'>selected</option>" + definedAtomsHtml);
        }
        if(!bOneset && $("#" + me.pre + id2).length) {
            $("#" + me.pre + id2).html("  <option value='selected' selected>selected</option>" + definedAtomsHtml);
        }

        $("#" + me.pre + id1).resizable();
        if(!bOneset) $("#" + me.pre + id2).resizable();
    }

    applyShownMenus() { let me = this.icn3dui; me.icn3d;
        let idArray = [];
        for(let id in me.htmlCls.allMenus) {
            if(me.htmlCls.shownMenus.hasOwnProperty(id)) {
                $("#" + me.pre + id).parent().show();
            }
            else {            
                $("#" + me.pre + id).parent().hide();     
                idArray.push(id);         
            }
        }   

        if(Object.keys(me.htmlCls.shownMenus).length == Object.keys(me.htmlCls.allMenus).length) {
            $(".icn3d-menusep").show();
        }
        else {
            $(".icn3d-menusep").hide();
        }

        // save to localStorage
        if(localStorage) localStorage.setItem('hiddenmenus', JSON.stringify(idArray));
    }

    getHiddenMenusFromCache() { let me = this.icn3dui; me.icn3d;
        me.htmlCls.shownMenus = {};

        let idArrayStr = (localStorage) ? localStorage.getItem('hiddenmenus') : '';
        
        if(idArrayStr && idArrayStr != '[]') {
            let idArray = JSON.parse(idArrayStr);

            // for(let i = 0, il = idArray.length; i < il; ++i) {
            //     me.htmlCls.shownMenus[idArray[i]] = 1;
            // }
            for(let menu in me.htmlCls.allMenus) {
                if(idArray.indexOf(menu) == -1) {
                    me.htmlCls.shownMenus[menu] = 1;
                }
            }
        }
        else {
            me.htmlCls.shownMenus = me.hashUtilsCls.cloneHash(me.htmlCls.allMenus);
        }
    }
    
    displayShownMenus() { let me = this.icn3dui; me.icn3d;
        let html = "<form name='" + me.pre + "selmenu'>";
        html += "<table><tr><th>File</th><th>Select</th><th>View</th><th>Style</th><th>Color</th><th>Analysis</th><th>Help</th></tr>";
        html += "<tr>";
        for(let id in me.htmlCls.allMenusSel) {
            // skip all unicolor: too many
            if(id.substr(0, 6) == 'uniclr' 
                || id.substr(0, 11) == 'mn5_opacity'
                || id.substr(0, 14) == 'mn6_labelscale'
                || id.substr(0, 4) == 'faq_'
                || id.substr(0, 4) == 'dev_') {
                    continue;
            }

            if(id == 'mn1_searchstru') {
                html += "<td valign='top'>";
            }
            else if(id == 'mn2_definedsets') {
                html += "</td><td valign='top'>";
            }
            else if(id == 'mn2_show_selected') {
                html += "</td><td valign='top'>";
            }
            else if(id == 'mn3_proteinwrap' || (me.cfg.cid && id == 'mn3_ligwrap')) {
                html += "</td><td valign='top'>";
            }
            else if(id == 'mn4_clrwrap') {
                html += "</td><td valign='top'>";
            }
            else if(id == 'mn6_selectannotations') {
                html += "</td><td valign='top'>";
            }
            else if(id == 'abouticn3d') {
                html += "</td><td valign='top'>";
            }

            let checkStr = (me.htmlCls.shownMenus.hasOwnProperty(id)) ? "checked" : "";

            let selType = me.htmlCls.allMenusSel[id];
            let styleStr = (selType == 3) ? " style='margin-left:30px'" : ((selType == 2) ? " style='margin-left:15px'" : "");

            html += "<span style='white-space:nowrap'><input type='checkbox' name='" + id + "' value='" + id + "'" + checkStr + styleStr + ">" + me.htmlCls.allMenus[id] + "</span><br>";
        }  
        html += "</td></tr></table></form>";

        $("#" + me.pre + "menulist").html(html);
    }


    clickMenu1() { let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        let thisClass = this;
    //mn 1
    //    clkMn1_mmtfid: function() {
        me.myEventCls.onIds("#" + me.pre + "mn1_vastplus", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_vastplus', 'Please input PDB ID for VAST+');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_vast", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_vast', 'Please input chain or PDB file for VAST');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_foldseek", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_foldseek', 'Submit your selection to Foldseek');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_mmtfid", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_mmtfid', 'Please input MMTF ID');
        });

    //    clkMn1_pdbid: function() {
        me.myEventCls.onIds("#" + me.pre + "mn1_pdbid", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_pdbid', 'Please input PDB ID');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_afid", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_afid', 'Please input AlphaFold UniProt ID');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_refseqid", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_refseqid', 'Please input NCBI Protein Accession');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_opmid", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_opmid', 'Please input OPM PDB ID');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_align", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_align', 'Align two PDB structures');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_alignaf", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_alignaf', 'Align two AlphaFold structures');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_chainalign", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_chainalign', 'Align multiple chains by structure alignment');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_chainalign2", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_chainalign2', 'Align multiple chains by sequence alignment');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_chainalign3", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_chainalign3', 'Align multiple chains residue by residue');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_mutation", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_mutation', 'Show the mutations in 3D');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_pdbfile", "click", function(e) { me.icn3d; //e.preventDefault();
           //me = me.setIcn3dui($(this).attr('id'));
           me.htmlCls.dialogCls.openDlg('dl_pdbfile', 'Please input PDB File');
        });
        me.myEventCls.onIds(["#" + me.pre + "mn1_pdbfile_app", "#" + me.pre + "tool_pdbfile"], "click", function(e) { me.icn3d; //e.preventDefault();
           //me = me.setIcn3dui($(this).attr('id'));
           me.htmlCls.dialogCls.openDlg('dl_pdbfile_app', 'Please append PDB Files');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_mol2file", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_mol2file', 'Please input Mol2 File');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_sdffile", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_sdffile', 'Please input SDF File');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_xyzfile", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_xyzfile', 'Please input XYZ File');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_afmapfile", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_afmapfile', 'Please input AlphaFold PAE File');
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_urlfile", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_urlfile', 'Load data by URL');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_fixedversion", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_fixedversion', 'Open Share Link URL in the archived version of iCn3D');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_fixedversion", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let url = $("#" + me.pre + "sharelinkurl").val();
           thisClass.setLogCmd("open " + url, false);
           localStorage.setItem('fixedversion', '1');
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(url, urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_mmciffile", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_mmciffile', 'Please input mmCIF File');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_mmcifid", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_mmcifid', 'Please input mmCIF ID');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_mmdbid", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_mmdbid', 'Please input MMDB or PDB ID');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn1_mmdbafid", , "#" + me.pre + "tool_mmdbafid"], "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_mmdbafid', 'Please input PDB/MMDB/AlphaFold UniProt IDs');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_blast_rep_id", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_blast_rep_id', 'Align sequence to structure');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_gi", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_gi', 'Please input protein gi');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_cid", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_cid', 'Please input PubChem CID');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_pngimage", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_pngimage', 'Please input the PNG image');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_state", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_state', 'Please input the state file');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_selection", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_selection', 'Please input the selection file');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_dsn6", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_dsn6', 'Please input the DSN6 file to display electron density map');
        });


        me.myEventCls.onIds(["#" + me.pre + "mn1_delphi", "#" + me.pre + "mn1_delphi2", "#" + me.pre + "tool_delphi"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.loadPhiFrom = 'delphi';
           $("#" + me.pre + "dl_delphi_tabs").tabs();
           me.htmlCls.dialogCls.openDlg('dl_delphi', 'Please set parameters to display DelPhi potential map');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_phi", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.loadPhiFrom = 'phi';
           $("#" + me.pre + "dl_phi_tabs").tabs();
           $("#" + me.pre + "phitab1_tabs").tabs();
           $("#" + me.pre + "phitab2_tabs").tabs();
           me.htmlCls.dialogCls.openDlg('dl_phi', 'Please input local phi or cube file to display DelPhi potential map');
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_phiurl", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.loadPhiFrom = 'phiurl';
           $("#" + me.pre + "dl_phiurl_tabs").tabs();
           $("#" + me.pre + "phiurltab1_tabs").tabs();
           $("#" + me.pre + "phiurltab2_tabs").tabs();
           me.htmlCls.dialogCls.openDlg('dl_phiurl', 'Please input URL phi or cube file to display DelPhi potential map');
        });


        me.myEventCls.onIds("#" + me.pre + "mn1_dsn6url", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_dsn6url', 'Please input the DSN6 file to display electron density map');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportState", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export state file", false);
           let file_pref =(ic.inputid) ? ic.inputid : "custom";

           ic.saveFileCls.saveFile(file_pref + '_statefile.txt', 'command');
        });


        me.myEventCls.onIds("#" + me.pre + "mn1_exportPdbRes", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.setHtmlCls.exportPdb();

           thisClass.setLogCmd("export pdb", true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportSecondary", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.setHtmlCls.exportSecondary();

           thisClass.setLogCmd("export secondary structure", true);
        });

        me.myEventCls.onIds(["#" + me.pre + "delphipdb", "#" + me.pre + "phipdb"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let pdbStr = ic.saveFileCls.getSelectedResiduePDB();

           thisClass.setLogCmd("export PDB of selected residues", false);
           let file_pref =(ic.inputid) ? ic.inputid : "custom";
           ic.saveFileCls.saveFile(file_pref + '_icn3d_residues.pdb', 'text', [pdbStr]);
        });

        me.myEventCls.onIds(["#" + me.pre + "delphipqr", "#" + me.pre + "phipqr", "#" + me.pre + "phiurlpqr"], "click", async function(e) { me.icn3d; //e.preventDefault();
           await me.htmlCls.setHtmlCls.exportPqr();
           thisClass.setLogCmd("export pqr", true);
        });

        me.myEventCls.onIds(["#" + me.pre + "delphipdbh", "#" + me.pre + "phipqr", "#" + me.pre + "phiurlpqr"], "click", async function(e) { me.icn3d; //e.preventDefault();
            let bPdb = true;
            await me.htmlCls.setHtmlCls.exportPqr(bPdb);
            thisClass.setLogCmd("export pdbh", false);
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportStl", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export stl file", false);
           //ic.threeDPrintCls.hideStabilizer();
           ic.export3DCls.exportStlFile('');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportVrml", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export vrml file", false);
           //ic.threeDPrintCls.hideStabilizer();
           ic.export3DCls.exportVrmlFile('');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportStlStab", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export stl stabilizer file", false);
           //ic.bRender = false;
           ic.threeDPrintCls.hideStabilizer();
           ic.threeDPrintCls.resetAfter3Dprint();
           ic.threeDPrintCls.addStabilizer();
           ic.export3DCls.exportStlFile('_stab');
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportVrmlStab", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export vrml stabilizer file", false);
           //ic.bRender = false;
           ic.threeDPrintCls.hideStabilizer();
           ic.threeDPrintCls.resetAfter3Dprint();
           ic.threeDPrintCls.addStabilizer();
           ic.export3DCls.exportVrmlFile('_stab');
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_exportInteraction", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export interactions", false);
           if(me.cfg.mmdbid !== undefined) await ic.viewInterPairsCls.retrieveInteractionData();
           ic.viewInterPairsCls.exportInteractions();
        });

        me.myEventCls.onIds(["#" + me.pre + "mn1_exportCanvas", "#" + me.pre + "saveimage"], "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           // do not record the export command
           //thisClass.setLogCmd("export canvas", true);
           thisClass.setLogCmd("export canvas", false);
           //var file_pref =(ic.inputid) ? ic.inputid : "custom";
           //ic.saveFileCls.saveFile(file_pref + '_image_icn3d_loadable.png', 'png');
           let bPngHtml = true;
           await ic.shareLinkCls.shareLink(bPngHtml);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_exportCanvas1", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export canvas 1", true);
           ic.scaleFactor = 1;
           await ic.shareLinkCls.shareLink(true, true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_exportCanvas2", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export canvas 2", true);
           ic.scaleFactor = 2;
           await ic.shareLinkCls.shareLink(true, true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_exportCanvas4", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export canvas 4", true);
           ic.scaleFactor = 4;
           await ic.shareLinkCls.shareLink(true, true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_exportCanvas8", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export canvas 8", true);
           ic.scaleFactor = 8;
           await ic.shareLinkCls.shareLink(true, true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportCounts", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export counts", false);
           let text = '<html><body><div style="text-align:center"><br><b>Total Count for atoms with coordinates</b>:<br/><table align=center border=1 cellpadding=10 cellspacing=0><tr><th>Structure Count</th><th>Chain Count</th><th>Residue Count</th><th>Atom Count</th></tr>';
           text += '<tr><td>' + Object.keys(ic.structures).length + '</td><td>' + Object.keys(ic.chains).length + '</td><td>' + Object.keys(ic.residues).length + '</td><td>' + Object.keys(ic.atoms).length + '</td></tr>';
           text += '</table><br/>';
           text += '<b>Counts by Chain for atoms with coordinates</b>:<br/><table align=center border=1 cellpadding=10 cellspacing=0><tr><th>Structure</th><th>Chain</th><th>Residue Count</th><th>Atom Count</th></tr>';
           let chainArray = Object.keys(ic.chains);

           for(let i = 0, il = chainArray.length; i < il; ++i) {
               let chainid = chainArray[i];
               //if(!chainid) continue;

               let pos = chainid.indexOf('_');
               let structure = chainid.substr(0, pos);
               let chain = chainid.substr(pos + 1);
               let residueHash = {};
               let atoms = ic.chains[chainid];
               for(let j in atoms) {
                   residueHash[ic.atoms[j].resi] = 1;
               }
               text += '<tr><td>' + structure + '</td><td>' + chain + '</td><td>' + Object.keys(residueHash).length + '</td><td>' + Object.keys(ic.chains[chainid]).length + '</td></tr>';
           }
           text += '</table><br/></div></body></html>';
           let file_pref =(ic.inputid) ? ic.inputid : "custom";
           ic.saveFileCls.saveFile(file_pref + '_counts.html', 'html', text);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportSelections", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export all selections", false);
          
           thisClass.SetChainsAdvancedMenu();

           let text = ic.saveFileCls.exportCustomAtoms();
           let file_pref =(ic.inputid) ? ic.inputid : "custom";
           ic.saveFileCls.saveFile(file_pref + '_selections.txt', 'text', [text]);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_exportSelDetails", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("export all selections with details", false);
          
           thisClass.SetChainsAdvancedMenu();

           let bDetails = true;
           let text = ic.saveFileCls.exportCustomAtoms(bDetails);
           let file_pref =(ic.inputid) ? ic.inputid : "custom";
           ic.saveFileCls.saveFile(file_pref + '_sel_details.txt', 'text', [text]);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn1_sharelink", "#" + me.pre + "tool_sharelink"], "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
            await ic.shareLinkCls.shareLink();
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_replayon", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
          await ic.resizeCanvasCls.replayon();
          thisClass.setLogCmd("replay on", true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_replayoff", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
            await ic.resizeCanvasCls.replayoff();
            thisClass.setLogCmd("replay off", true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_menuall", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.shownMenus = me.hashUtilsCls.cloneHash(me.htmlCls.allMenus);

            thisClass.applyShownMenus();    
          });

        me.myEventCls.onIds("#" + me.pre + "mn1_menusimple", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.shownMenus = me.hashUtilsCls.cloneHash(me.htmlCls.simpleMenus);

            thisClass.applyShownMenus();
          });

        me.myEventCls.onIds("#" + me.pre + "mn1_menupref", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_menupref', 'Select Menus');

            thisClass.getHiddenMenusFromCache();

            thisClass.displayShownMenus();
         });

         me.myEventCls.onIds("#" + me.pre + "apply_menupref", "click", function(e) { me.icn3d; //e.preventDefault();
            var checkboxes = document.querySelectorAll('form[name="' + me.pre + 'selmenu"] input:checked');
            me.htmlCls.shownMenus = {};
            for (var checkbox of checkboxes) {
                me.htmlCls.shownMenus[checkbox.value] = 1;
            }

            thisClass.applyShownMenus();
         });

         me.myEventCls.onIds("#" + me.pre + "reset_menupref", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.shownMenus = me.hashUtilsCls.cloneHash(me.htmlCls.simpleMenus);

            thisClass.applyShownMenus();
            thisClass.displayShownMenus();
         });

         me.myEventCls.onIds("#" + me.pre + "reset_menupref_all", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.shownMenus = me.hashUtilsCls.cloneHash(me.htmlCls.allMenus);

            thisClass.applyShownMenus();
            thisClass.displayShownMenus();
         });

         me.myEventCls.onIds("#" + me.pre + "savepref", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            let menuStr = '[';

            //var checkboxes = document.querySelectorAll('form[name="' + me.pre + 'selmenu"] input:checked');
            var checkboxes = document.querySelectorAll('form[name="' + me.pre + 'selmenu"] input:not(:checked)');
            let cnt = 0;
            for (var checkbox of checkboxes) {
                if(cnt > 0) menuStr += ', ';
                menuStr += '"' + checkbox.value + '"';
                ++cnt;
            }
            
            menuStr += ']';
    
            ic.saveFileCls.saveFile('icn3d_menus_pref.txt', 'text', [menuStr]);
         });

         me.myEventCls.onIds("#" + me.pre + "reload_menupreffile", "click", function(e) { me.icn3d; //e.preventDefault();
 
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let file = $("#" + me.pre + "menupreffile")[0].files[0];
            if(!file) {
              alert("Please select a file before clicking 'Load'");
            }
            else {
              me.htmlCls.setHtmlCls.fileSupport();
              let reader = new FileReader();
              reader.onload = function(e) {
                let dataStr = e.target.result; // or = reader.result;
                let idArray = JSON.parse(dataStr);

                me.htmlCls.shownMenus = {};
                // for(let i = 0, il = idArray.length; i < il; ++i) {
                //     me.htmlCls.shownMenus[idArray[i]] = 1;
                // }
                for(let menu in me.htmlCls.allMenus) {
                    if(idArray.indexOf(menu) == -1) {
                        me.htmlCls.shownMenus[menu] = 1;
                    }
                }

                thisClass.applyShownMenus();
                thisClass.displayShownMenus();
              };
              reader.readAsText(file);
            }
         });

        me.myEventCls.onIds("#" + me.pre + "mn1_menuloadpref", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_menuloadpref', 'Please input the menu preference file');
        });
         

        me.myEventCls.onIds("#" + me.pre + "mn1_link_structure", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let url = ic.saveFileCls.getLinkToStructureSummary(true);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(url, urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_link_bind", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           url = "https://www.ncbi.nlm.nih.gov/pccompound?LinkName=pccompound_structure&from_uid=" + ic.inputid;
           thisClass.setLogCmd("link to 3D protein structures bound to CID " + ic.inputid + ": " + url, false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(url, urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_link_vast", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           if(ic.inputid === undefined) {
                   url = "https://www.ncbi.nlm.nih.gov/pccompound?term=" + ic.molTitle;
                   thisClass.setLogCmd("link to compounds " + ic.molTitle + ": " + url, false);
           }
           else {
               let url;
               if(me.cfg.cid !== undefined) {
                       url = "https://www.ncbi.nlm.nih.gov/pccompound?LinkName=pccompound_pccompound_3d&from_uid=" + ic.inputid;
                       thisClass.setLogCmd("link to compounds with structure similar to CID " + ic.inputid + ": " + url, false);
               }
               else {
                   let idArray = ic.inputid.split('_');
                   
                   if(idArray.length === 1) {
                       url = me.htmlCls.baseUrl + "vastplus/vastplus.cgi?uid=" + ic.inputid;
                       thisClass.setLogCmd("link to structures similar to " + ic.inputid + ": " + url, false);
                   }
                   else if(idArray.length === 2) {
                       url = me.htmlCls.baseUrl + "vastplus/vastplus.cgi?uid=" + idArray[0];
                       thisClass.setLogCmd("link to structures similar to " + idArray[0] + ": " + url, false);
                   }
               }
           }

           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(url, urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_link_pubmed", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           if(ic.inputid === undefined) {
               let url;
               url = "https://www.ncbi.nlm.nih.gov/pubmed/?term=" + ic.molTitle;
               thisClass.setLogCmd("link to literature about " + ic.molTitle + ": " + url, false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(url, urlTarget);
           }
           else if(ic.pmid) {
               let idArray = ic.pmid.toString().split('_');
               let url;
               if(idArray.length === 1) {
                   url = "https://www.ncbi.nlm.nih.gov/pubmed/" + ic.pmid;
                   thisClass.setLogCmd("link to PubMed ID " + ic.pmid + ": " + url, false);
               }
               else if(idArray.length === 2) {
                   url = "https://www.ncbi.nlm.nih.gov/pubmed/?term=" + idArray[0] + " OR " + idArray[1];
                   thisClass.setLogCmd("link to PubMed IDs " + idArray[0] + ", " + idArray[1] + ": " + url, false);
               }
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(url, urlTarget);
           }
           else if(isNaN(ic.inputid)) {
               let idArray = ic.inputid.toString().split('_');
               let url;
               if(idArray.length === 1) {
                   url = "https://www.ncbi.nlm.nih.gov/pubmed/?term=" + ic.inputid;
                   thisClass.setLogCmd("link to literature about PDB " + ic.inputid + ": " + url, false);
               }
               else if(idArray.length === 2) {
                   url = "https://www.ncbi.nlm.nih.gov/pubmed/?term=" + idArray[0] + " OR " + idArray[1];
                   thisClass.setLogCmd("link to literature about PDB " + idArray[0] + " OR " + idArray[1] + ": " + url, false);
               }
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(url, urlTarget);
           }
           else {
               if(me.cfg.cid !== undefined) {
                   alert("No literature information is available for this compound in the SDF file.");
               }
               else {
                   alert("No literature information is available for this structure.");
               }
           }
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_link_protein", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
          //ic.saveFileCls.setEntrezLinks('protein');
          let structArray = Object.keys(ic.structures);
          let chainArray = Object.keys(ic.chains);
          let text = '';
          for(let i = 0, il = chainArray.length; i < il; ++i) {
              let firstAtom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chainArray[i]]);
              if(ic.proteins.hasOwnProperty(firstAtom.serial) && chainArray[i].length == 6) {
                  text += chainArray[i] + '[accession] OR ';
              }
          }
          if(text.length > 0) text = text.substr(0, text.length - 4);
          let url = "https://www.ncbi.nlm.nih.gov/protein/?term=" + text;
          thisClass.setLogCmd("link to Entrez protein about PDB " + structArray + ": " + url, false);
          let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
          window.open(url, urlTarget);
        });

    }

    clickMenu2() { let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        let thisClass = this;

        me.myEventCls.onIds(["#" + me.pre + "mn6_selectannotations", "#" + me.pre + "tool_selectannotations"], "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           await ic.showAnnoCls.showAnnotations();
           thisClass.setLogCmd("view annotations", true);
           //thisClass.setLogCmd("window annotations", true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_selectall", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select all", true);
           ic.selectionCls.selectAll();
           ic.hlUpdateCls.removeHlAll();
           ic.drawCls.draw();
        });
        me.myEventCls.onIds("#" + me.pre + "clearall", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("clear all", true);
           ic.bSelectResidue = false;
           ic.selectionCls.selectAll();
           ic.hlUpdateCls.removeHlAll();
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_selectdisplayed", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select displayed set", true);
           //ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);
           ic.hAtoms = me.hashUtilsCls.cloneHash(ic.viewSelectionAtoms);
           ic.hlUpdateCls.updateHlAll();
           //ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_fullstru", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("show all", true);
           ic.selectionCls.showAll();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_selectcomplement", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) {
               thisClass.setLogCmd("select complement", true);
               ic.resid2specCls.selectComplement();
           }
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_selectmainchains", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select main chains", true);
           ic.selectionCls.selectMainChains();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_selectsidechains", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select side chains", true);
           ic.selectionCls.selectSideChains();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_selectmainsidechains", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select main side chains", true);
           ic.selectionCls.selectMainSideChains();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_propPos", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select prop positive", true);
           ic.resid2specCls.selectProperty('positive');
        });
        me.myEventCls.onIds("#" + me.pre + "mn2_propNeg", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select prop negative", true);
           ic.resid2specCls.selectProperty('negative');
        });
        me.myEventCls.onIds("#" + me.pre + "mn2_propHydro", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select prop hydrophobic", true);
           ic.resid2specCls.selectProperty('hydrophobic');
        });
        me.myEventCls.onIds("#" + me.pre + "mn2_propPolar", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           thisClass.setLogCmd("select prop polar", true);
           ic.resid2specCls.selectProperty('polar');
        });
        me.myEventCls.onIds("#" + me.pre + "mn2_propBfactor", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_propbybfactor', 'Select residue based on B-factor');
        });
        me.myEventCls.onIds("#" + me.pre + "mn2_propSolAcc", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_propbypercentout', 'Select residue based on the percentage of solvent accessilbe surface area');
        });
        me.myEventCls.onIds("#" + me.pre + "applypropbybfactor", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let from = $("#" + me.pre + "minbfactor").val();
           let to = $("#" + me.pre + "maxbfactor").val();
           thisClass.setLogCmd("select prop b factor | " + from + '_' + to, true);
           ic.resid2specCls.selectProperty('b factor', from, to);
        });
        me.myEventCls.onIds("#" + me.pre + "applypropbypercentout", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let from = $("#" + me.pre + "minpercentout").val();
           let to = $("#" + me.pre + "maxpercentout").val();
           thisClass.setLogCmd("select prop percent out | " + from + '_' + to, true);
           ic.resid2specCls.selectProperty('percent out', from, to);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_alignment", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');
           thisClass.setLogCmd("window aligned sequences", true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_window_table", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_allinteraction', 'Show interactions');
           thisClass.setLogCmd("window interaction table", true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_window_linegraph", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_linegraph', 'Show interactions between two lines of residue nodes');
           thisClass.setLogCmd("window interaction graph", true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_window_scatterplot", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_scatterplot', 'Show interactions as map');
           thisClass.setLogCmd("window interaction scatterplot", true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn1_window_graph", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_graph', 'Force-directed graph');
           thisClass.setLogCmd("window force-directed graph", true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_yournote", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_yournote', 'Your note about the current display');
        });

        me.myEventCls.onIds("#" + me.pre + "applyyournote", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.yournote = $("#" + me.pre + "yournote").val();
           if(me.cfg.shownote) document.title = ic.yournote;
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd('your note | ' + ic.yournote, true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_command", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_advanced2', 'Select by specification');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn2_definedsets", "#" + me.pre + "definedsets", "#" + me.pre + "definedsets2", "#" + me.pre + "tool_definedsets"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.definedSetsCls.showSets();
           thisClass.setLogCmd('defined sets', true);
           //thisClass.setLogCmd('window defined sets', true);
        });
        $(document).on("click", "#" + me.pre + "setOr", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            ic.setOperation = 'or';
        });
        $(document).on("click", "#" + me.pre + "setAnd", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            ic.setOperation = 'and';
        });
        $(document).on("click", "#" + me.pre + "setNot", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            ic.setOperation = 'not';
        });


        me.myEventCls.onIds("#" + me.pre + "mn2_pkNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pk = 0;
           ic.opts['pk'] = 'no';
           thisClass.setLogCmd('set pk off', true);
           ic.drawCls.draw();
           ic.hlObjectsCls.removeHlObjects();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_pkYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pk = 1;
           ic.opts['pk'] = 'atom';
           thisClass.setLogCmd('set pk atom', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_pkResidue", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pk = 2;
           ic.opts['pk'] = 'residue';
           thisClass.setLogCmd('set pk residue', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_pkStrand", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pk = 3;
           ic.opts['pk'] = 'strand';
           thisClass.setLogCmd('set pk strand', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_pkDomain", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pk = 4;
           ic.opts['pk'] = 'domain';
           thisClass.setLogCmd('set pk domain', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_pkChain", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pk = 5;
           ic.opts['pk'] = 'chain';
           thisClass.setLogCmd('set pk chain', true);
        });

        me.myEventCls.onIds("#" + me.pre + "adjustmem", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_adjustmem', 'Adjust the Z-axis positions of the membrane');
        });

        me.myEventCls.onIds("#" + me.pre + "togglemem", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.selectionCls.toggleMembrane();
           thisClass.setLogCmd('toggle membrane', true);
        });

        me.myEventCls.onIds("#" + me.pre + "selectplane", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_selectplane', 'Select a region between two planes');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn2_aroundsphere", "#" + me.pre + "tool_aroundsphere"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            thisClass.SetChainsAdvancedMenu();

            let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
            if($("#" + me.pre + "atomsCustomSphere").length) {
                $("#" + me.pre + "atomsCustomSphere").html("  <option value='non-selected' selected>non-selected</option><option value='selected'>selected</option>" + definedAtomsHtml);
            }
            if($("#" + me.pre + "atomsCustomSphere2").length) {
                $("#" + me.pre + "atomsCustomSphere2").html("  <option value='selected' selected>selected</option>" + definedAtomsHtml);
            }
            me.htmlCls.dialogCls.openDlg('dl_aroundsphere', 'Select a sphere around a set of residues');
            ic.bSphereCalc = false;
            //thisClass.setLogCmd('set calculate sphere false', true);
            $("#" + me.pre + "atomsCustomSphere").resizable();
            $("#" + me.pre + "atomsCustomSphere2").resizable();
        });

        me.myEventCls.onIds(["#" + me.pre + "mn2_select_chain", "#" + me.pre + "definedSets"], "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_select_chain', 'Select Structure/Chain/Custom Selection');
        });

    }

    clickMenu3() { let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        let thisClass = this;
    // mn 3
        me.myEventCls.onIds(["#" + me.pre + "mn3_proteinsRibbon","#" + me.pre + "tool_proteinsRibbon"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'ribbon');
           thisClass.setLogCmd('style proteins ribbon', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsStrand", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'strand');
           thisClass.setLogCmd('style proteins strand', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsCylinder", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'cylinder and plate');
           thisClass.setLogCmd('style proteins cylinder and plate', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsSchematic", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'schematic');
           thisClass.setLogCmd('style proteins schematic', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsCalpha", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'c alpha trace');
           thisClass.setLogCmd('style proteins c alpha trace', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsBackbone", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'backbone');
           thisClass.setLogCmd('style proteins backbone', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsBfactor", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'b factor tube');
           thisClass.setLogCmd('style proteins b factor tube', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsLines", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'lines');
           thisClass.setLogCmd('style proteins lines', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsStick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'stick');
           thisClass.setLogCmd('style proteins stick', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn3_proteinsBallstick", "#" + me.pre + "tool_proteinsBallstick"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'ball and stick');
           thisClass.setLogCmd('style proteins ball and stick', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn3_proteinsSphere", "#" + me.pre + "tool_proteinsSphere"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'sphere');
           thisClass.setLogCmd('style proteins sphere', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_proteinsNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('proteins', 'nothing');
           thisClass.setLogCmd('style proteins nothing', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_sidecLines", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('sidec', 'lines2');
           thisClass.setLogCmd('style sidec lines2', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_sidecStick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('sidec', 'stick2');
           thisClass.setLogCmd('style sidec stick2', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_sidecBallstick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('sidec', 'ball and stick2');
           thisClass.setLogCmd('style sidec ball and stick2', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_sidecSphere", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('sidec', 'sphere2');
           thisClass.setLogCmd('style sidec sphere2', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_sidecNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('sidec', 'nothing');
           thisClass.setLogCmd('style sidec nothing', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ntbaseLines", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.setOptionCls.setStyle('ntbase', 'lines2');
            thisClass.setLogCmd('style ntbase lines2', true);
         });
 
         me.myEventCls.onIds("#" + me.pre + "mn3_ntbaseStick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.setOptionCls.setStyle('ntbase', 'stick2');
            thisClass.setLogCmd('style ntbase stick2', true);
         });
 
         me.myEventCls.onIds("#" + me.pre + "mn3_ntbaseBallstick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.setOptionCls.setStyle('ntbase', 'ball and stick2');
            thisClass.setLogCmd('style ntbase ball and stick2', true);
         });
 
         me.myEventCls.onIds("#" + me.pre + "mn3_ntbaseSphere", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.setOptionCls.setStyle('ntbase', 'sphere2');
            thisClass.setLogCmd('style ntbase sphere2', true);
         });
 
         me.myEventCls.onIds("#" + me.pre + "mn3_ntbaseNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.setOptionCls.setStyle('ntbase', 'nothing');
            thisClass.setLogCmd('style ntbase nothing', true);
         });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclCartoon", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'nucleotide cartoon');
           thisClass.setLogCmd('style nucleotides nucleotide cartoon', true);
       });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclBackbone", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'backbone');
           thisClass.setLogCmd('style nucleotides backbone', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclSchematic", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'schematic');
           thisClass.setLogCmd('style nucleotides schematic', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclPhos", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'o3 trace');
           thisClass.setLogCmd('style nucleotides o3 trace', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclLines", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'lines');
           thisClass.setLogCmd('style nucleotides lines', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclStick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'stick');
           thisClass.setLogCmd('style nucleotides stick', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclBallstick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'ball and stick');
           thisClass.setLogCmd('style nucleotides ball and stick', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclSphere", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'sphere');
           thisClass.setLogCmd('style nucleotides sphere', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_nuclNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('nucleotides', 'nothing');
           thisClass.setLogCmd('style nucleotides nothing', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ligLines", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('chemicals', 'lines');
           thisClass.setLogCmd('style chemicals lines', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ligStick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('chemicals', 'stick');
           thisClass.setLogCmd('style chemicals stick', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ligBallstick", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('chemicals', 'ball and stick');
           thisClass.setLogCmd('style chemicals ball and stick', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ligSchematic", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('chemicals', 'schematic');
           thisClass.setLogCmd('style chemicals schematic', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ligSphere", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('chemicals', 'sphere');
           thisClass.setLogCmd('style chemicals sphere', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ligNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('chemicals', 'nothing');
           thisClass.setLogCmd('style chemicals nothing', true);
        });


        me.myEventCls.onIds("#" + me.pre + "mn3_glycansCartYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bGlycansCartoon = true;
           ic.drawCls.draw();
           thisClass.setLogCmd('glycans cartoon yes', true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn3_glycansCartNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bGlycansCartoon = false;
           ic.drawCls.draw();
           thisClass.setLogCmd('glycans cartoon no', true);
        });


        me.myEventCls.onIds("#" + me.pre + "mn3_hydrogensYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.showInterCls.showHydrogens();
           ic.drawCls.draw();
           thisClass.setLogCmd('hydrogens', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_hydrogensNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.showInterCls.hideHydrogens();
           ic.drawCls.draw();
           thisClass.setLogCmd('set hydrogens off', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ionsSphere", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('ions', 'sphere');
           thisClass.setLogCmd('style ions sphere', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ionsDot", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('ions', 'dot');
           thisClass.setLogCmd('style ions dot', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_ionsNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('ions', 'nothing');
           thisClass.setLogCmd('style ions nothing', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_waterSphere", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('water', 'sphere');
           thisClass.setLogCmd('style water sphere', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_waterDot", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('water', 'dot');
           thisClass.setLogCmd('style water dot', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_waterNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setStyle('water', 'nothing');
           thisClass.setLogCmd('style water nothing', true);
        });

    }

    clickMenu4() { let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        let thisClass = this;
    // mn 4
        me.myEventCls.onIds("#" + me.pre + "mn4_clrSpectrum", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'spectrum');
           thisClass.setLogCmd('color spectrum', true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn4_clrSpectrumChain", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'spectrum for chains');
           thisClass.setLogCmd('color spectrum for chains', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrSpectrumAcrossSets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
             thisClass.SetChainsAdvancedMenu();
             let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
             if($("#" + me.pre + "atomsCustomColorSpectrumAcross").length) {
                 $("#" + me.pre + "atomsCustomColorSpectrumAcross").html(definedAtomsHtml);
             }

             if(ic.bRender) me.htmlCls.dialogCls.openDlg('dl_colorspectrumacrosssets', 'Please select sets to apply spectrum color for sets');
             $("#" + me.pre + "atomsCustomColorSpectrumAcross").resizable();
         });

         me.myEventCls.onIds("#" + me.pre + "mn4_clrSpectrumSets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
             thisClass.SetChainsAdvancedMenu();
             let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
             if($("#" + me.pre + "atomsCustomColorSpectrum").length) {
                 $("#" + me.pre + "atomsCustomColorSpectrum").html(definedAtomsHtml);
             }

             if(ic.bRender) me.htmlCls.dialogCls.openDlg('dl_colorspectrumbysets', 'Please select sets to apply spectrum color for residues');
             $("#" + me.pre + "atomsCustomColorSpectrum").resizable();
         });

         me.myEventCls.onIds("#" + me.pre + "mn4_clrRainbowAcrossSets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
             thisClass.SetChainsAdvancedMenu();
             let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
             if($("#" + me.pre + "atomsCustomColorRainbowAcross").length) {
                 $("#" + me.pre + "atomsCustomColorRainbowAcross").html(definedAtomsHtml);
             }

             if(ic.bRender) me.htmlCls.dialogCls.openDlg('dl_colorrainbowacrosssets', 'Please select sets to apply rainbow color for sets');
             $("#" + me.pre + "atomsCustomColorRainbowAcross").resizable();
         });

         me.myEventCls.onIds("#" + me.pre + "mn4_clrRainbowSets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
             thisClass.SetChainsAdvancedMenu();
             let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
             if($("#" + me.pre + "atomsCustomColorRainbow").length) {
                 $("#" + me.pre + "atomsCustomColorRainbow").html(definedAtomsHtml);
             }

             if(ic.bRender) me.htmlCls.dialogCls.openDlg('dl_colorrainbowbysets', 'Please select sets to apply rainbow color for residues');
             $("#" + me.pre + "atomsCustomColorRainbow").resizable();
         });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrRainbow", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'rainbow');
           thisClass.setLogCmd('color rainbow', true);
        });
        me.myEventCls.onIds(["#" + me.pre + "mn4_clrRainbowChain", "#" + me.pre + "tool_clrRainbowChain"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'rainbow for chains');
           thisClass.setLogCmd('color rainbow for chains', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn4_clrChain", "#" + me.pre + "tool_clrChain"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'chain');
           thisClass.setLogCmd('color chain', true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn4_clrStructure", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.setOptionCls.setOption('color', 'structure');
            thisClass.setLogCmd('color structure', true);
         });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrdomain", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'domain');
           thisClass.setLogCmd('color domain', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrsets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'defined sets');
           thisClass.setLogCmd('color defined sets', true);
        });


        me.myEventCls.onIds(["#" + me.pre + "mn4_clrSSGreen", "#" + me.pre + "tool_clrSSGreen"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.sheetcolor = 'green';
           ic.setOptionCls.setOption('color', 'secondary structure green');
           thisClass.setLogCmd('color secondary structure green', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrSSYellow", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.sheetcolor = 'yellow';
           ic.setOptionCls.setOption('color', 'secondary structure yellow');
           thisClass.setLogCmd('color secondary structure yellow', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrSSSpectrum", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'secondary structure spectrum');
           thisClass.setLogCmd('color secondary structure spectrum', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrResidue", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            //ic.legendClick = 2;
            ic.setOptionCls.setOption('color', 'residue');
            thisClass.setLogCmd('color residue', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrResidueCustom", "click", function(e) { me.icn3d; //e.preventDefault();
            //ic.legendClick = 2;
            me.htmlCls.dialogCls.openDlg('dl_rescolorfile', 'Please input the file on residue colors');
        });

        me.myEventCls.onIds("#" + me.pre + "reload_rescolorfile", "click", function(e) { let ic = me.icn3d; //e.preventDefault();

           if(!me.cfg.notebook) dialog.dialog( "close" );
           let file = $("#" + me.pre + "rescolorfile")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             me.htmlCls.setHtmlCls.fileSupport();
             let reader = new FileReader();
             reader.onload = function(e) {
               let dataStrTmp = e.target.result; // or = reader.result;
               let dataStr = dataStrTmp.replace(/#/g, "");
               ic.customResidueColors = JSON.parse(dataStr);
               for(let res in ic.customResidueColors) {
                   ic.customResidueColors[res.toUpperCase()] = me.parasCls.thr("#" + ic.customResidueColors[res]);
               }
               ic.setOptionCls.setOption('color', 'residue custom');
               thisClass.setLogCmd('color residue custom | ' + dataStr, true);
             };
             reader.readAsText(file);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_customcolorfile", "click", function(e) { let ic = me.icn3d; //e.preventDefault();

           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.startColor = $("#" + me.pre + "startColor").val();
           ic.midColor = $("#" + me.pre + "midColor").val();
           ic.endColor = $("#" + me.pre + "endColor").val();

           let legendHtml = thisClass.setLegendHtml();
           //$("#" + me.pre + "legend").html(legendHtml).show();
           $("#" + me.pre + "dl_legend").html(legendHtml);
           me.htmlCls.dialogCls.openDlg('dl_legend', 'Color range');

           ic.addTrackCls.setCustomFile('color', ic.startColor, ic.midColor, ic.endColor);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_customref", "click", function(e) { me.icn3d; //e.preventDefault();
 
            me.htmlCls.dialogCls.openDlg('dl_customref', 'Set custom reference numbers');
         });

        me.myEventCls.onIds("#" + me.pre + "reload_customreffile", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
 
            if(!me.cfg.notebook) dialog.dialog( "close" );
            
            let file = $("#" + ic.pre + "cstreffile")[0].files[0];
            if(!file) {
                alert("Please select a file before clicking 'Apply'");
            }
            else {
                me.utilsCls.checkFileAPI();
                let reader = new FileReader();
                reader.onload = async function(e) {
                    let dataStr = e.target.result; // or = reader.result;
                    await ic.refnumCls.parseCustomRefFile(dataStr);

                    dataStr = dataStr.replace(/\r/g, '').replace(/\n/g, '\\n');

                    thisClass.setLogCmd('custom refnum | ' + dataStr, true);
                };
                reader.readAsText(file);
            }
        }); 

        me.myEventCls.onIds("#" + me.pre + "remove_legend", "click", function(e) { me.icn3d; //e.preventDefault();

           $("#" + me.pre + "legend").hide();

           thisClass.setLogCmd('remove legend', true);
        });
        me.myEventCls.onIds("#" + me.pre + "reload_customtubefile", "click", function(e) { let ic = me.icn3d; //e.preventDefault();

           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.addTrackCls.setCustomFile('tube');
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrCharge", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            //ic.legendClick = 3;
            ic.setOptionCls.setOption('color', 'charge');
            thisClass.setLogCmd('color charge', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrHydrophobic", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            //ic.legendClick = 4; 
            ic.setOptionCls.setOption('color', 'hydrophobic');
            thisClass.setLogCmd('color hydrophobic', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrNormalizedHP", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            //ic.legendClick = 4;
            ic.setOptionCls.setOption('color', 'normalized hydrophobic');
            thisClass.setLogCmd('color normalized hydrophobic', true);
        });


        me.myEventCls.onIds(["#" + me.pre + "mn4_clrAtom", "#" + me.pre + "tool_clrAtom"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            //ic.legendClick = 1;
            ic.setOptionCls.setOption('color', 'atom');
            thisClass.setLogCmd('color atom', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrBfactor", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            //ic.legendClick = 5;
            ic.setOptionCls.setOption('color', 'b factor');
            thisClass.setLogCmd('color b factor', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrConfidence", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            //ic.legendClick = 6;
            ic.setOptionCls.setOption('color', 'confidence');
            thisClass.setLogCmd('color confidence', true);
        });


        me.myEventCls.onIds("#" + me.pre + "mn4_clrArea", "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_colorbyarea', "Color based on residue's solvent accessibility");
        });
        me.myEventCls.onIds("#" + me.pre + "applycolorbyarea", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.midpercent = $("#" + me.pre + 'midpercent').val();
            ic.setOptionCls.setOption('color', 'area');
            thisClass.setLogCmd('color area | ' + ic.midpercent, true);

        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrBfactorNorm", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'b factor percentile');
           thisClass.setLogCmd('color b factor percentile', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrIdentity", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'identity');
           thisClass.setLogCmd('color identity', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrConserved", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('color', 'conservation');
           thisClass.setLogCmd('color conservation', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrCustom", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_clr', 'Color picker');
        });

        $(document).on("click", ".icn3d-color-rad-text", function(e) { let ic = me.icn3d; //e.preventDefault();
          e.stopImmediatePropagation();
          e.preventDefault();
          let color = $(this).attr('color');
          ic.setOptionCls.setOption("color", color);
          thisClass.setLogCmd("color " + color, true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrSave", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.saveColor();
           thisClass.setLogCmd('save color', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn4_clrApplySave", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.applySavedColor();
           thisClass.setLogCmd('apply saved color', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_styleSave", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.saveStyle();
           thisClass.setLogCmd('save style', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_styleApplySave", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.applySavedStyle();
           thisClass.setLogCmd('apply saved style', true);
        });

    }

    clickMenu5() { let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        let thisClass = this;
    // mn 5
        me.myEventCls.onIds("#" + me.pre + "mn5_neighborsYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = true;
           ic.applyMapCls.removeLastSurface();
           ic.applyMapCls.applySurfaceOptions();
           if(ic.bRender) ic.drawCls.render();
           thisClass.setLogCmd('set surface neighbors on', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_neighborsNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = false;
           ic.applyMapCls.removeLastSurface();
           ic.applyMapCls.applySurfaceOptions();
           if(ic.bRender) ic.drawCls.render();
           thisClass.setLogCmd('set surface neighbors off', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn5_surfaceVDW", "#" + me.pre + "tool_surfaceVDW"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = false;
           ic.setOptionCls.setOption('surface', 'Van der Waals surface');
           thisClass.setLogCmd('set surface Van der Waals surface', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_surfaceSAS", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = false;
           ic.setOptionCls.setOption('surface', 'solvent accessible surface');
           thisClass.setLogCmd('set surface solvent accessible surface', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_surfaceMolecular", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = false;
           ic.setOptionCls.setOption('surface', 'molecular surface');
           thisClass.setLogCmd('set surface molecular surface', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_surfaceVDWContext", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = true;
           ic.setOptionCls.setOption('surface', 'Van der Waals surface with context');
           thisClass.setLogCmd('set surface Van der Waals surface with context', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_surfaceSASContext", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = true;
           ic.setOptionCls.setOption('surface', 'solvent accessible surface with context');
           thisClass.setLogCmd('set surface solvent accessible surface with context', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_surfaceMolecularContext", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bConsiderNeighbors = true;
           ic.setOptionCls.setOption('surface', 'molecular surface with context');
           thisClass.setLogCmd('set surface molecular surface with context', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_surfaceNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('surface', 'nothing');
           thisClass.setLogCmd('set surface nothing', true);
        });

        $(document).on("click", "." + me.pre + "mn5_opacity", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.transparentRenderOrder = false;

            let value = $(this).attr('v');
           ic.setOptionCls.setOption('opacity', value);
           thisClass.setLogCmd('set surface opacity ' + value, true);
        });

        $(document).on("click", "." + me.pre + "mn5_opacityslow", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.transparentRenderOrder = true;

            let value = $(this).attr('v');
            ic.setOptionCls.setOption('opacity', value);
            thisClass.setLogCmd('set surface2 opacity ' + value, true);
         });

        me.myEventCls.onIds("#" + me.pre + "mn5_wireframeYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('wireframe', 'yes');
           thisClass.setLogCmd('set surface wireframe on', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_wireframeNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('wireframe', 'no');
           thisClass.setLogCmd('set surface wireframe off', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_elecmap2fofc", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_elecmap2fofc', '2Fo-Fc Electron Density Map');
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_elecmapfofc", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_elecmapfofc', 'Fo-Fc Electron Density Map');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn5_elecmapNo", "#" + me.pre + "elecmapNo2", "#" + me.pre + "elecmapNo3", "#" + me.pre + "elecmapNo4", "#" + me.pre + "elecmapNo5"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('map', 'nothing');
           thisClass.setLogCmd('setoption map nothing', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "delphimapNo", "#" + me.pre + "phimapNo", "#" + me.pre + "phiurlmapNo", "#" + me.pre + "mn1_phimapNo"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('phimap', 'nothing');
           thisClass.setLogCmd('setoption phimap nothing', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "delphimapNo2", "#" + me.pre + "phimapNo2", "#" + me.pre + "phiurlmapNo2"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //ic.setOptionCls.setOption('surface', 'nothing');
           //thisClass.setLogCmd('set surface nothing', true);
           ic.setOptionCls.setOption('phisurface', 'nothing');
           thisClass.setLogCmd('setoption phisurface nothing', true);
        });

        me.myEventCls.onIds("#" + me.pre + "applymap2fofc", "click", function(e) { let ic = me.icn3d; //e.preventDefault();

           //if(!me.cfg.notebook) dialog.dialog( "close" );
           let sigma2fofc = parseFloat($("#" + me.pre + "sigma2fofc" ).val());
           let type = '2fofc';
           ic.dsn6ParserCls.dsn6Parser(ic.inputid, type, sigma2fofc);
           //ic.setOptionCls.setOption('map', '2fofc');
           thisClass.setLogCmd('set map 2fofc sigma ' + sigma2fofc, true);
        });

        me.myEventCls.onIds("#" + me.pre + "applymapfofc", "click", function(e) { let ic = me.icn3d; //e.preventDefault();

           //if(!me.cfg.notebook) dialog.dialog( "close" );
           let sigmafofc = parseFloat($("#" + me.pre + "sigmafofc" ).val());
           let type = 'fofc';
           ic.dsn6ParserCls.dsn6Parser(ic.inputid, type, sigmafofc);
           //ic.setOptionCls.setOption('map', 'fofc');
           thisClass.setLogCmd('set map fofc sigma ' + sigmafofc, true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_mapwireframeYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //ic.dsn6ParserCls.dsn6Parser(ic.inputid);
           ic.setOptionCls.setOption('mapwireframe', 'yes');
           thisClass.setLogCmd('set map wireframe on', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_mapwireframeNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('mapwireframe', 'no');
           thisClass.setLogCmd('set map wireframe off', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_emmap", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_emmap', 'EM Density Map');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn5_emmapNo", "#" + me.pre + "emmapNo2"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('emmap', 'nothing');
           thisClass.setLogCmd('setoption emmap nothing', true);
        });

        me.myEventCls.onIds("#" + me.pre + "applyemmap", "click", function(e) { let ic = me.icn3d; //e.preventDefault();

           //if(!me.cfg.notebook) dialog.dialog( "close" );
           let empercentage = parseFloat($("#" + me.pre + "empercentage" ).val());
           let type = 'em';
           //ic.emd = 'emd-3906';

           ic.densityCifParserCls.densityCifParser(ic.inputid, type, empercentage, ic.emd);
           thisClass.setLogCmd('set emmap percentage ' + empercentage, true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_emmapwireframeYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //ic.dsn6ParserCls.dsn6Parser(ic.inputid);
           ic.setOptionCls.setOption('emmapwireframe', 'yes');
           thisClass.setLogCmd('set emmap wireframe on', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_emmapwireframeNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('emmapwireframe', 'no');
           thisClass.setLogCmd('set emmap wireframe off', true);
        });

    }

    clickMenu6() { let me = this.icn3dui, ic = me.icn3d;
        if(me.bNode) return;

        let thisClass = this;
    // mn 6
        me.myEventCls.onIds("#" + me.pre + "mn6_assemblyYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bAssembly = true;
           thisClass.setLogCmd('set assembly on', true);
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_assemblyNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bAssembly = false;
           thisClass.setLogCmd('set assembly off', true);
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_igrefYes", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
            thisClass.setLogCmd('ig refnum on', true);
            await ic.refnumCls.showIgRefNum();
         });

         me.myEventCls.onIds("#" + me.pre + "mn6_igrefNo", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
            thisClass.setLogCmd('ig refnum off', true);
            await ic.refnumCls.hideIgRefNum();
         });


        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelAtoms", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.residueLabelsCls.addAtomLabels(ic.hAtoms);
           ic.selectionCls.saveSelectionIfSelected();
           thisClass.setLogCmd('add atom labels', true);
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelElements", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.residueLabelsCls.addAtomLabels(ic.hAtoms, true);
           ic.selectionCls.saveSelectionIfSelected();
           thisClass.setLogCmd('add element labels', true);
           ic.drawCls.draw();
        });


        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelResidues", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.residueLabelsCls.addResidueLabels(ic.hAtoms);
           ic.selectionCls.saveSelectionIfSelected();
           thisClass.setLogCmd('add residue labels', true);
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelResnum", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.residueLabelsCls.addResidueLabels(ic.hAtoms, undefined, undefined, true);
           ic.selectionCls.saveSelectionIfSelected();
           thisClass.setLogCmd('add residue number labels', true);
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelChains", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.analysisCls.addChainLabels(ic.hAtoms);
           ic.selectionCls.saveSelectionIfSelected();
           thisClass.setLogCmd('add chain labels', true);
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelTermini", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.analysisCls.addTerminiLabels(ic.hAtoms);
           ic.selectionCls.saveSelectionIfSelected();
           thisClass.setLogCmd('add terminal labels', true);
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_addlabel', 'Add custom labels by selection');
           ic.pk = 1;
           ic.opts['pk'] = 'atom';
           ic.pickpair = true;
           ic.pAtomNum = 0;
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_addlabelSelection", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_addlabelselection', 'Add custom labels by the selected');
        });

         me.myEventCls.onIds("#" + me.pre + "mn6_labelColor", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_labelColor', 'Change color for all labels');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn2_saveselection","#" + me.pre + "tool_saveselection"], "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_saveselection', 'Save the selected');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn6_addlabelNo", "#" + me.pre + "removeLabels"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.labelcolor = undefined;
            ic.pickpair = false;
           //ic.labels['residue'] = [];
           //ic.labels['custom'] = [];
           let select = "set labels off";
           thisClass.setLogCmd(select, true);
           for(let name in ic.labels) {
               //if(name === 'residue' || name === 'custom') {
                   ic.labels[name] = [];
               //}
           }
           ic.drawCls.draw();
        });

        $(document).on("click", "." + me.pre + "mn6_labelscale", function(e) { let ic = me.icn3d; //e.preventDefault();
           let value = $(this).attr('v');
           ic.labelScale = value;
           ic.drawCls.draw();
           thisClass.setLogCmd('set label scale ' + value, true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_distanceYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_distance', 'Measure the distance of atoms');
           ic.pk = 1;
           ic.opts['pk'] = 'atom';
           ic.pickpair = true;
           ic.pAtomNum = 0;
           ic.bMeasureDistance = true;
        });


        me.myEventCls.onIds("#" + me.pre + "mn6_distTwoSets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_disttwosets', 'Measure the distance between two sets');

            thisClass.setSetsMenus('atomsCustomDist');

           ic.bMeasureDistance = true;
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_distManySets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_distmanysets', 'Measure the pairwise distance among many sets');

            thisClass.setSetsMenus('atomsCustomDistTable');

           ic.bMeasureDistance = true;
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_distanceNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pickpair = false;
           let select = "set lines off";
           thisClass.setLogCmd(select, true);
           ic.labels['distance'] = [];
           ic.lines['distance'] = [];
           ic.distPnts = [];
           ic.pk = 2;
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_cartoonshape", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_cartoonshape', 'Draw cartoon for a set');

            let bOneset = true;
            thisClass.setSetsMenus('cartoonshape', bOneset);

           ic.bCartoonshape = true;
        });

        me.myEventCls.onIds("#" + me.pre + "mn5_linebtwsets", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_linebtwsets', 'Draw a line between two sets');

            thisClass.setSetsMenus('linebtwsets');

           ic.bLinebtwsets = true;
        });


        me.myEventCls.onIds(["#" + me.pre + "mn2_selectedcenter", "#" + me.pre + "zoomin_selection", "#" + me.pre + "tool_selectedcenter"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //thisClass.setLogCmd('zoom selection', true);
           ic.transformCls.zoominSelection();
           ic.drawCls.draw();
           thisClass.setLogCmd('zoom selection', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_center", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //thisClass.setLogCmd('center selection', true);
           ic.applyCenterCls.centerSelection();
           ic.drawCls.draw();
           thisClass.setLogCmd('center selection', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn6_resetOrientation", "#" + me.pre + "resetOrientation", "#" + me.pre + "tool_resetOrientation"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //thisClass.setLogCmd('reset orientation', true);
           ic.transformCls.resetOrientation();
           //ic.setColorCls.applyOriginalColor();
           ic.drawCls.draw();
           thisClass.setLogCmd('reset orientation', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn6_chemicalbindingshow", "#" + me.pre + "chemicalbindingshow"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('chemicalbinding', 'show');
           thisClass.setLogCmd('set chemicalbinding show', true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn6_chemicalbindinghide", "#" + me.pre + "chemicalbindinghide"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('chemicalbinding', 'hide');
           thisClass.setLogCmd('set chemicalbinding hide', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_sidebyside", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           if(ic.bInputfile) {
                alert("Side-by-Side does NOT work when the input is from a local file.");
                return;
           }
           let url = ic.shareLinkCls.shareLinkUrl(undefined);
           //if(url.indexOf('http') !== 0) {
           //    alert("The url is more than 4000 characters and may not work.");
           //}
           //else {
               url = url.replace("full.html", "full2.html");
               url += '&closepopup=1';
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(url, urlTarget);
               thisClass.setLogCmd('side by side | ' + url, true);
           //}
        });


        $(document).on("click", "." + me.pre + "mn6_rotate", function(e) { let ic = me.icn3d; //e.preventDefault();
           let value = $(this).attr('v').toLowerCase();
           let direction = value.split(' ')[1];

           thisClass.setLogCmd(value, true);
           ic.bStopRotate = false;
           ic.transformCls.rotateCount = 0;
           ic.transformCls.rotateCountMax = 6000;
           ic.ROT_DIR = direction;
           ic.resizeCanvasCls.rotStruc(direction);
        });

        $(document).on("click", "." + me.pre + "mn6_rotate90", function(e) { let ic = me.icn3d; //e.preventDefault();
          let value = $(this).attr('v').toLowerCase();
          let direction = value.split('-')[0];

          thisClass.setLogCmd(value, true);
          let axis;
          if(direction == 'x') {
              axis = new THREE.Vector3(1,0,0);
          }
          else if(direction == 'y') {
              axis = new THREE.Vector3(0,1,0);
          }
          else if(direction == 'z') {
              axis = new THREE.Vector3(0,0,1);
          }
          let angle = 0.5 * Math.PI;
          ic.transformCls.setRotation(axis, angle);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_cameraPers", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('camera', 'perspective');
           thisClass.setLogCmd('set camera perspective', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_cameraOrth", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('camera', 'orthographic');
           thisClass.setLogCmd('set camera orthographic', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_bkgdBlack", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setStyleCls.setBackground('black');
        });

        me.myEventCls.onIds("#" + me.pre + "tool_bkgd", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            if(ic.opts['background'] == 'black') {
                ic.setStyleCls.setBackground('white');
            }
            else {
                ic.setStyleCls.setBackground('black');
            }
         });

        me.myEventCls.onIds("#" + me.pre + "mn6_bkgdGrey", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setStyleCls.setBackground('grey');
        });

        me.myEventCls.onIds(["#" + me.pre + "mn6_bkgdWhite", "#" + me.pre + "tool_bkgdWhite"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setStyleCls.setBackground('white');
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_bkgdTransparent", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setStyleCls.setBackground('transparent');
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_showfogYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //ic.setOptionCls.setOption('fog', 'yes');
           ic.opts['fog'] = 'yes';
           ic.fogCls.setFog(true);
           ic.drawCls.draw();
           thisClass.setLogCmd('set fog on', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_showfogNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           //ic.setOptionCls.setOption('fog', 'no');
           ic.opts['fog'] = 'no';
           ic.fogCls.setFog(true);
           ic.drawCls.draw();
           thisClass.setLogCmd('set fog off', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_showslabYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('slab', 'yes');
           thisClass.setLogCmd('set slab on', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_showslabNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('slab', 'no');
           thisClass.setLogCmd('set slab off', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_showaxisYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.setOptionCls.setOption('axis', 'yes');
           thisClass.setLogCmd('set axis on', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_showaxisSel", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pc1 = true;

           ic.axesCls.setPc1Axes();
           thisClass.setLogCmd('set pc1 axis', true);
        });


        me.myEventCls.onIds("#" + me.pre + "mn6_showaxisNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.pc1 = false;
           ic.axes = [];

           ic.setOptionCls.setOption('axis', 'no');

           thisClass.setLogCmd('set axis off', true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_symmetry", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bAxisOnly = false;
           await ic.symdCls.retrieveSymmetry(Object.keys(ic.structures)[0]);
           //me.htmlCls.dialogCls.openDlg('dl_symmetry', 'Symmetry');
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_symd", "click", async function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bAxisOnly = false;
           await ic.symdCls.retrieveSymd();
           ic.bSymd = true;

           thisClass.setLogCmd('symd symmetry', true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn6_clear_sym", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.symdArray = [];
           ic.drawCls.draw();
           thisClass.setLogCmd('clear symd symmetry', true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn6_axes_only", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bAxisOnly = true;
           ic.drawCls.draw();
           thisClass.setLogCmd('show axis', true);
        });


        me.myEventCls.onIds("#" + me.pre + "mn6_area", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            ic.analysisCls.calculateArea();
            thisClass.setLogCmd('area', true);
        });

        me.myEventCls.onIds("#" + me.pre + "applysymmetry", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.bAxisOnly = false;

           let title = $("#" + me.pre + "selectSymmetry" ).val();

           ic.symmetrytitle =(title === 'none') ? undefined : title;
           //if(title !== 'none') ic.applySymmetry(title);
           ic.drawCls.draw();
           thisClass.setLogCmd('symmetry ' + title, true);
        });
        me.myEventCls.onIds("#" + me.pre + "clearsymmetry", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let title = 'none';
           ic.symmetrytitle = undefined;
           ic.drawCls.draw();
           thisClass.setLogCmd('symmetry ' + title, true);
        });

        me.myEventCls.onIds(["#" + me.pre + "mn6_hbondsYes", "#" + me.pre + "hbondsYes"], "click", function(e) { let ic = me.icn3d; //e.preventDefault();
            thisClass.SetChainsAdvancedMenu();

            let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
            if($("#" + me.pre + "atomsCustomHbond").length) {
                $("#" + me.pre + "atomsCustomHbond").html("  <option value='non-selected' selected>non-selected</option><option value='selected'>selected</option>" + definedAtomsHtml);
            }
            if($("#" + me.pre + "atomsCustomHbond2").length) {
                $("#" + me.pre + "atomsCustomHbond2").html("  <option value='selected' selected>selected</option>" + definedAtomsHtml);
            }
           me.htmlCls.dialogCls.openDlg('dl_hbonds', 'Hydrogen bonds/interactions between two sets of atoms');
           ic.bHbondCalc = false;
           //thisClass.setLogCmd('set calculate hbond false', true);
           $("#" + me.pre + "atomsCustomHbond").resizable();
           $("#" + me.pre + "atomsCustomHbond2").resizable();
        });

        me.myEventCls.onIds(["#" + me.pre + "mn6_contactmap"], "click", function(e) { me.icn3d; //e.preventDefault();
            me.htmlCls.dialogCls.openDlg('dl_contact', 'Set contact map');
        });


        me.myEventCls.onIds("#" + me.pre + "mn6_hbondsNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.showInterCls.hideHbondsContacts();
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_stabilizerYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let select = "stabilizer";
           ic.threeDPrintCls.addStabilizer();
           ic.threeDPrintCls.prepareFor3Dprint();
           //ic.drawCls.draw();
           thisClass.setLogCmd(select, true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_stabilizerNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let select = "set stabilizer off";
           thisClass.setLogCmd(select, true);
           ic.threeDPrintCls.hideStabilizer();
           ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_stabilizerOne", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_stabilizer', 'Add One Stabilizer');
           ic.pk = 1;
           ic.opts['pk'] = 'atom';
           ic.pickpair = true;
           ic.pAtomNum = 0;
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_stabilizerRmOne", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_stabilizer_rm', 'Remove One Stabilizer');
           ic.pk = 1;
           ic.opts['pk'] = 'atom';
           ic.pickpair = true;
           ic.pAtomNum = 0;
        });

        me.myEventCls.onIds("#" + me.pre + "mn1_thicknessSet", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_thickness', 'Set Thickness for 3D Printing');
        });

        me.myEventCls.onIds("#" + me.pre + "mn3_setThickness", "click", function(e) { me.icn3d; //e.preventDefault();
           me.htmlCls.dialogCls.openDlg('dl_thickness2', 'Style Preferences');
        });


        me.myEventCls.onIds("#" + me.pre + "mn6_ssbondsYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let select = "disulfide bonds";
           thisClass.setLogCmd(select, true);
           ic.showInterCls.showSsbonds();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_ssbondsExport", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.viewInterPairsCls.exportSsbondPairs();
           thisClass.setLogCmd("export disulfide bond pairs", false);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_ssbondsNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.opts["ssbonds"] = "no";
           let select = "set disulfide bonds off";
           thisClass.setLogCmd(select, true);
           ic.lines['ssbond'] = [];
           ic.setOptionCls.setStyle('sidec', 'nothing');
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_clbondsYes", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           let select = "cross linkage";
           thisClass.setLogCmd(select, true);
           //ic.bShowCrossResidueBond = true;
           //ic.setOptionCls.setStyle('proteins', 'lines')
           ic.showInterCls.showClbonds();
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_clbondsExport", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.viewInterPairsCls.exportClbondPairs();
           thisClass.setLogCmd("export cross linkage pairs", false);
        });

        me.myEventCls.onIds("#" + me.pre + "mn6_clbondsNo", "click", function(e) { let ic = me.icn3d; //e.preventDefault();
           ic.opts["clbonds"] = "no";
           let select = "set cross linkage off";
           thisClass.setLogCmd(select, true);
           //ic.bShowCrossResidueBond = false;
           //ic.setOptionCls.setStyle('proteins', 'ribbon')
           ic.lines['clbond'] = [];
           ic.setOptionCls.setStyle('sidec', 'nothing');
        });


        $("#" + me.pre + "newvs2").on('submit', function() {
            // fill the pdbstr
            let pdbstr = ic.saveFileCls.getAtomPDB(ic.hAtoms);
            $("#" + me.pre + "pdbstr").val(pdbstr);

            return true;
        });

        $("#" + me.pre + "fssubmit").on('click', function() {
            let pdbstr = ic.saveFileCls.getAtomPDB(ic.hAtoms);
            let url = 'https://search.foldseek.com/api/ticket';


            let template = "<!doctype html>\n<head>\n<title>Loading Foldseek</title>\n<style>\n  body {\n    background-color: #121212;\n    color: #fff;\n    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n    height: 100vh;\n    display: flex;\n    flex-direction: column;\n    flex-wrap: wrap;\n    justify-content: center;\n    align-items: center;\n  }\n  .loader {\n    display: block;\n    width: 80px;\n    height: 80px;\n  }\n  .loader:after {\n    content: \" \";\n    display: block;\n    width: 64px;\n    height: 64px;\n    margin: 8px;\n    border-radius: 50%;\n    border: 6px solid #fff;\n    border-color: #fff transparent #fff transparent;\n    animation: loader 1.2s linear infinite;\n  }\n  @keyframes loader {\n    0% {\n      transform: rotate(0deg);\n    }\n    100% {\n      transform: rotate(360deg);\n    }\n  }\n</style>\n</head>\n<body>\n<div>Foldseek is loading...</div><div class=\"loader\"></div>\n</body>";

            let urlTarget = '_blank';
            let w = window.open('', urlTarget);
            w.document.body.innerHTML = template;

            $.ajax({
                url: url,
                type: 'POST',
                data: { 
                    q : pdbstr,
                    database: ["afdb50", "afdb-swissprot", "gmgcl_id", "pdb100"],
                    mode: "3diaa"
                },
                dataType: 'text',
                success: function(data) {
                    w.location = 'https://search.foldseek.com/queue/' + JSON.parse(data).id;
                },
                error : function(xhr, textStatus, errorThrown ) {
                  console.log("Error in submitting data to Foldseek...");
                }
            });
        });
    } 

    //Show the input command in log. If "bSetCommand" is true, the command will be saved in the state file as well.
    setLogCmd(str, bSetCommand, bAddLogs) {var me = this.icn3dui, ic = me.icn3d;
      if(str.trim() === '') return false;
      let pos = str.indexOf('|||');
      if(pos !== -1) str = str.substr(0, pos);
      let transformation = {};
      transformation.factor = ic._zoomFactor;
      transformation.mouseChange = ic.mouseChange;
      transformation.quaternion = {};
      transformation.quaternion._x = parseFloat(ic.quaternion._x).toPrecision(5);
      transformation.quaternion._y = parseFloat(ic.quaternion._y).toPrecision(5);
      transformation.quaternion._z = parseFloat(ic.quaternion._z).toPrecision(5);
      transformation.quaternion._w = parseFloat(ic.quaternion._w).toPrecision(5);
      if(bSetCommand) {
          // save the command only when it's not a history command, i.e., not in the process of going back and forth
          if(ic.bAddCommands) {
              // If a new command was called, remove the forward commands and push to the command array
              if(ic.STATENUMBER < ic.commands.length) {
                  let oldCommand = ic.commands[ic.STATENUMBER - 1];
                  let pos = oldCommand.indexOf('|||');
                  if(pos != -1 && str !== oldCommand.substr(0, pos)) {
                    ic.commands = ic.commands.slice(0, ic.STATENUMBER);
                    ic.commands.push(str + '|||' + ic.transformCls.getTransformationStr(transformation));
                    ic.optsHistory.push(me.hashUtilsCls.cloneHash(ic.opts));
                    ic.optsHistory[ic.optsHistory.length - 1].hlatomcount = Object.keys(ic.hAtoms).length;
                    if(me.utilsCls.isSessionStorageSupported()) ic.setStyleCls.saveCommandsToSession();
                    ic.STATENUMBER = ic.commands.length;
                  }
              }
              else {
                ic.commands.push(str + '|||' + ic.transformCls.getTransformationStr(transformation));
                ic.optsHistory.push(me.hashUtilsCls.cloneHash(ic.opts));
                if(ic.hAtoms !== undefined) ic.optsHistory[ic.optsHistory.length - 1].hlatomcount = Object.keys(ic.hAtoms).length;
                if(me.utilsCls.isSessionStorageSupported()) ic.setStyleCls.saveCommandsToSession();
                ic.STATENUMBER = ic.commands.length;
              }
          }
      }
      if(ic.bAddLogs && me.cfg.showcommand) {
          let finalStr = (bSetCommand) ? str : '[comment] ' + str;
          ic.logs.push(finalStr);
          // move cursor to the end, and scroll to the end
          $("#" + me.pre + "logtext").val("> " + ic.logs.join("\n> ") + "\n> ");
          if($("#" + me.pre + "logtext")[0]) {
            $("#" + me.pre + "logtext").scrollTop($("#" + me.pre + "logtext")[0].scrollHeight);
          }
      }
      ic.setStyleCls.adjustIcon();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SetMenu {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
        //this.sh = this.icn3dui.htmlCls.setHtmlCls;
    }

    // simpify the calls of the following functions from setHtmlCls
    getLink(id, text, bSimpleMenu, selType) { let me = this.icn3dui;
        return me.htmlCls.setHtmlCls.getLink(id, text, bSimpleMenu, selType);
    }

    getMenuText(id, text, classname, bSimpleMenu, selType) { let me = this.icn3dui;
        return me.htmlCls.setHtmlCls.getMenuText(id, text, classname, bSimpleMenu, selType);
    }

    getMenuUrl(id, url, text, bSimpleMenu, selType) { let me = this.icn3dui;
        return me.htmlCls.setHtmlCls.getMenuUrl(id, url, text, bSimpleMenu, selType);
    }

    getMenuSep() { let me = this.icn3dui;
        return me.htmlCls.setHtmlCls.getMenuSep();
    }

    getLinkWrapper(id, text, wrapper, bSimpleMenu, selType, bHide) { let me = this.icn3dui; me.icn3d;
        return me.htmlCls.setHtmlCls.getLinkWrapper(id, text, wrapper, bSimpleMenu, selType, bHide);
    }

    getLinkWrapper2(id, text, wrapper, bSimpleMenu, selType) { let me = this.icn3dui; me.icn3d;
        return me.htmlCls.setHtmlCls.getLinkWrapper2(id, text, wrapper, bSimpleMenu, selType);
    }

    getRadio(radioid, id, text, bChecked, bSimpleMenu, selType) { let me = this.icn3dui;
        return me.htmlCls.setHtmlCls.getRadio(radioid, id, text, bChecked, bSimpleMenu, selType);
    }

    getRadClr(radioid, id, text, color, bChecked, bSimpleMenu, selType) { let me = this.icn3dui;
        return me.htmlCls.setHtmlCls.getRadioColor(radioid, id, text, color, bChecked, bSimpleMenu, selType);
    }

    //Set the HTML code for the menus shown at the top of the viewer.
    setTopMenusHtml(id, str1, str2) { let me = this.icn3dui;
        if(me.bNode) return '';

        let titleColor =(me.htmlCls.opts['background'] == 'black') ? me.htmlCls.GREYD : 'black';

        let html = "";

        html += "<div style='position:relative;'>";

        html += me.htmlCls.divStr + "popup' class='icn3d-text icn3d-popup'></div>";

        html += this.setReplayHtml();

        html += "<!--https://forum.jquery.com/topic/looking-for-a-jquery-horizontal-menu-bar-->";
        html += me.htmlCls.divStr + "mnlist' style='position:absolute; z-index:999; float:left; display:table-row; margin-top: -2px;'>";
        html += "<table border='0' cellpadding='0' cellspacing='0' width='100'><tr>";

        let tdStr = '<td valign="top">';
        html += tdStr + this.setMenu1() + '</td>';

        html += tdStr + this.setMenu2() + '</td>';

        html += tdStr + this.setMenu2b() + '</td>';
        html += tdStr + this.setMenu3() + '</td>';
        html += tdStr + this.setMenu4() + '</td>';

        html += tdStr + this.setMenu5() + '</td>';
        //html += tdStr + this.setMenu5b() + '</td>';
        html += tdStr + this.setMenu6() + '</td>';

        me.htmlCls.shownMenus = me.hashUtilsCls.cloneHash(me.htmlCls.simpleMenus); 

        html += tdStr + "<div style='position:relative; margin-left:6px;'>" + str1;
        html += "<div class='icn3d-commandTitle' style='min-width:40px; margin-top: 3px; white-space: nowrap;'>" + str2;

        html += tdStr + '<div class="icn3d-commandTitle" style="white-space:nowrap; margin-top:10px; border-left:solid 1px #888888"><span id="' + me.pre +  'selection_expand" class="icn3d-expand icn3d-link" style="display:block;" title="Expand">' + me.htmlCls.space2 + 'Toolbar <span class="ui-icon ui-icon-plus" style="width:15px"></span>' + me.htmlCls.space2 + '</span><span id="' + me.pre +  'selection_shrink" class="icn3d-shrink icn3d-link" style="display:none;" title="Shrink">' + me.htmlCls.space2 + 'Toolbar <span class="ui-icon ui-icon-minus" style="width:15px"></span>' + me.htmlCls.space2 + '</span></div></td>';

        html += tdStr + '<div class="icn3d-commandTitle" style="white-space:nowrap; margin-top:8px; border-left:solid 1px #888888">' + me.htmlCls.space2 + '<input type="text" id="' + me.pre + 'search_seq" size="10" placeholder="one-letter seq."> <button style="white-space:nowrap;" id="' + me.pre + 'search_seq_button">Search</button> <a style="text-decoration: none;" href="' + me.htmlCls.baseUrl + 'icn3d/icn3d.html#selectb" target="_blank" title="Specification tips">?</a></div></td>';

        html += "</tr>";
        html += "</table>";
        html += "</div>";

        html += this.setTools();

        // show title at the top left corner
        html += me.htmlCls.divStr + "title' class='icn3d-commandTitle' style='font-size:1.2em; font-weight:normal; position:absolute; z-index:1; float:left; display:table-row; margin: 85px 0px 0px 5px; color:" + titleColor + "; width:" + me.htmlCls.WIDTH + "px'></div>";

        html += me.htmlCls.divStr + "viewer' style='position:relative; width:100%; height:100%; background-color: " + me.htmlCls.GREYD + ";'>";

        // deprecated, use the dialog dl_legend instead
        //html += me.htmlCls.divStr + "legend' class='icn3d-text icn3d-legend'></div>";

        html += me.htmlCls.divStr + "mnLogSection'>";
        html += "<div style='height: " + me.htmlCls.MENU_HEIGHT + "px;'></div>";
    //        html += "<div style='height: " + me.htmlCls.MENU_HEIGHT + "px;'></div>";

        html += " </div>";

        if(me.cfg.mmtfid === undefined) {
            //var tmpStr =(ic.realHeight < 300) ? 'top:100px; font-size: 1.2em;' : 'top:180px; font-size: 1.8em;';
            let tmpStr = 'top:180px; font-size: 1.8em;';
            html += me.htmlCls.divStr + "wait' style='position:absolute; left:50px; " + tmpStr + " color: #444444;'>Loading data...</div>";
        }
        html += "<canvas id='" + me.pre + "canvas' style='width:100%; height: 100%; background-color: #FFF;'>Your browser does not support WebGL.</canvas>";

        // separate for the log box
        if(me.cfg.showcommand === undefined || me.cfg.showcommand) {
            html += this.setLogWindow();
        }

        html += "</div>";

        html += "</div>";

        html += me.htmlCls.setDialogCls.setDialogs();

        html += me.htmlCls.setDialogCls.setCustomDialogs();

        $( "#" + id).html(html);

        // mn display
        $("accordion").accordion({ collapsible: true, active: false, heightStyle: "content"});
        $("accordion div").removeClass("ui-accordion-content ui-corner-all ui-corner-bottom ui-widget-content");

        $(".icn3d-mn-item").menu({position: { my: "left top", at: "right top" }});
        $(".icn3d-mn-item").hover(function(){},function(){$("accordion").accordion( "option", "active", "none");});

        $("#" + me.pre + "accordion1").hover( function(){ $("#" + me.pre + "accordion1 div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion1 div").css("display", "none"); } );
        $("#" + me.pre + "accordion2").hover( function(){ $("#" + me.pre + "accordion2 div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion2 div").css("display", "none"); } );
        $("#" + me.pre + "accordion2b").hover( function(){ $("#" + me.pre + "accordion2b div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion2b div").css("display", "none"); } );
        $("#" + me.pre + "accordion3").hover( function(){ $("#" + me.pre + "accordion3 div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion3 div").css("display", "none"); } );
        $("#" + me.pre + "accordion4").hover( function(){ $("#" + me.pre + "accordion4 div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion4 div").css("display", "none"); } );
        $("#" + me.pre + "accordion5").hover( function(){ $("#" + me.pre + "accordion5 div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion5 div").css("display", "none"); } );
        $("#" + me.pre + "accordion6").hover( function(){ $("#" + me.pre + "accordion6 div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion6 div").css("display", "none"); } );
    }

    setTopMenusHtmlMobile(id, str1, str2) { let me = this.icn3dui;
        if(me.bNode) return '';

        let titleColor =(me.htmlCls.opts['background'] == 'black') ? me.htmlCls.GREYD : 'black';

        let html = "";

        html += "<div style='position:relative;'>";

        html += me.htmlCls.divStr + "popup' class='icn3d-text icn3d-popup'></div>";

        html += this.setReplayHtml();

        if(!me.utilsCls.isMobile()) {
            let marginLeft = me.htmlCls.WIDTH - 40 + 5;

            html += me.htmlCls.buttonStr + "fullscreen' style='position:absolute; z-index:1999; display:block; padding:0px; margin: 12px 0px 0px " + marginLeft + "px; width:30px; height:34px; border-radius:4px; border:none; background-color:#f6f6f6;' title='Full screen'>";
            html += "<svg fill='#1c94c4' viewBox='0 0 24 24' width='24' height='24'>";
            html += "<path d='M0 0h24v24H0z' fill='none'></path>";
            html += "<path d='M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z'></path>";
            html += "</svg>";
            html += "</button>";
        }

        html += "<!--https://forum.jquery.com/topic/looking-for-a-jquery-horizontal-menu-bar-->";
        html += me.htmlCls.divStr + "mnlist' style='position:absolute; z-index:999; float:left; display:block; margin: 5px 0px 0px 5px;'>";

        //html += "<div class='icn3d-menu'>";
        html += "<div>";
        html += "<accordion id='" + me.pre + "accordion0' class='icn3d-accordion'>";
        if(me.cfg.notebook) {
            html += "<h3 style='width:20px; height:24px; position:relative; padding: 0'><span style='position:absolute; left:3px; top:4px;'>&#9776;</span></h3>";
        }
        else {
            html += "<h3 style='width:30px; height:34px; position:relative; padding: 0; margin-top:7px!important; background-color:#f6f6f6;'><span style='position:absolute; left:7px; top:8px;'>&#9776;</span></h3>";
        }
        html += "<div>";

        let liStr = "<li><span class='icn3d-menu-color'";

        html += "<ul class='icn3d-mn-item'>";
        html += liStr + ">File</span>";
        html += this.setMenu1_base();
        html += liStr + ">Select</span>";
        html += this.setMenu2_base();
        html += liStr + ">View</span>";
        html += this.setMenu2b_base();
        html += liStr + " id='" + me.pre + "style'>Style</span>";
        html += this.setMenu3_base();
        html += liStr + " id='" + me.pre + "color'>Color</span>";
        html += this.setMenu4_base();
        html += liStr + ">Analysis</span>";
        html += this.setMenu5_base();
        html += liStr + ">Help</span>";
        html += this.setMenu6_base();

        me.htmlCls.shownMenus = me.hashUtilsCls.cloneHash(me.htmlCls.simpleMenus); 

        html += "<li><div style='position:relative; margin-top:-6px;'>" + str1;
        html += "<div class='icn3d-commandTitle' style='margin-top: 3px; white-space: nowrap;'>" + str2;

        //if(me.cfg.align !== undefined) {
            html += "<li><span id='" + me.pre + "alternate2' class='icn3d-menu-color' title='Alternate the structures'>Alternate</span>";
        //}

        html += "</ul>";

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        html += "</div>";

        //html += me.htmlCls.setMenuCls.setTools();

        // show title at the top left corner
        html += me.htmlCls.divStr + "title' class='icn3d-commandTitle' style='font-size:1.2em; font-weight:normal; position:absolute; z-index:1; float:left; display:block; margin: 12px 0px 0px 40px; color:" + titleColor + "; width:" +(me.htmlCls.WIDTH - 40).toString() + "px'></div>";
        html += me.htmlCls.divStr + "viewer' style='position:relative; width:100%; height:100%; background-color: " + me.htmlCls.GREYD + ";'>";
        // don't show legend in mobile
        //html += me.htmlCls.divStr + "legend' class='icn3d-text icn3d-legend'></div>";
        html += me.htmlCls.divStr + "mnLogSection'>";
        html += "<div style='height: " + me.htmlCls.MENU_HEIGHT + "px;'></div>";
        html += "</div>";

        if(me.cfg.mmtfid === undefined) {
            //var tmpStr =(ic.realHeight < 300) ? 'top:100px; font-size: 1.2em;' : 'top:180px; font-size: 1.8em;';
            let tmpStr = 'top:180px; font-size: 1.8em;';
            html += me.htmlCls.divStr + "wait' style='position:absolute; left:50px; " + tmpStr + " color: #444444;'>Loading data...</div>";
        }
        html += "<canvas id='" + me.pre + "canvas' style='width:100%; height: 100%; background-color: #FFF;'>Your browser does not support WebGL.</canvas>";

        // separate for the log box
        if(me.cfg.showcommand === undefined || me.cfg.showcommand) {
            html += this.setLogWindow();
        }

        html += "</div>";

        html += "</div>";

        html += me.htmlCls.setDialogCls.setDialogs();

        html += me.htmlCls.setDialogCls.setCustomDialogs();

        $( "#" + id).html(html);

        // mn display
        $("accordion").accordion({ collapsible: true, active: false, heightStyle: "content"});
        $("accordion div").removeClass("ui-accordion-content ui-corner-all ui-corner-bottom ui-widget-content");

        $(".icn3d-mn-item").menu({position: { my: "left top", at: "right top" }});
        $(".icn3d-mn-item").hover(function(){},function(){$("accordion").accordion( "option", "active", "none");});

        $("#" + me.pre + "accordion0").hover( function(){ $("#" + me.pre + "accordion0 div").css("display", "block"); }, function(){ $("#" + me.pre + "accordion0 div").css("display", "none"); } );
    }

    setReplayHtml(id) { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = '';

        html += me.htmlCls.divStr + "replay' style='display:none; position:absolute; z-index:9999; top:" + parseInt(me.htmlCls.HEIGHT - 100).toString() + "px; left:20px;'>";
        html += "<div title='Click to replay one step'><svg style='cursor:pointer;' fill='#f8b84e' viewBox='0 0 60 60' width='40' height='40'>";
        html += '<circle style="fill:#f8b84e;" cx="29" cy="29" r="29"/>';
        html += '<g>';
        html += '<polygon style="fill:#FFFFFF;" points="44,29 22,44 22,29.273 22,14"/>';
        html += '<path style="fill:#FFFFFF;" d="M22,45c-0.16,0-0.321-0.038-0.467-0.116C21.205,44.711,21,44.371,21,44V14c0-0.371,0.205-0.711,0.533-0.884c0.328-0.174,0.724-0.15,1.031,0.058l22,15C44.836,28.36,45,28.669,45,29s-0.164,0.64-0.437,0.826l-22,15C22.394,44.941,22.197,45,22,45z M23,15.893v26.215L42.225,29L23,15.893z"/>';
        html += '</g>';
        html += "</svg></div>";
        html += me.htmlCls.divStr + "replay_menu' style='background-color:#DDDDDD; padding:3px; font-weight:bold;'></div>";
        html += me.htmlCls.divStr + "replay_cmd' style='background-color:#DDDDDD; padding:3px; max-width:250px'></div>";
        html += "</div>";

        return html;
    }

    //Set the HTML code for the tools section. It includes several buttons, and is the second line at the top of the viewer.
    setTools() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += me.htmlCls.divStr + "selection' style='display:none;'><div style='position:absolute; z-index:555; float:left; display:table-row; margin: 32px 0px 0px 0px;'>";
        //html += "<table style='margin-top: 3px; width:100px;'>";
        html += "<table style='margin-top: 3px; width:770px; background-color:#EEE;'>";

        html += this.setTools_base();

        // add custom buttons here
        // ...

        html += "</table>";
        html += "</div></div>";

        return html;
    }

    setButton(buttonStyle, id, title, text, color) { let me = this.icn3dui;
        if(me.bNode) return '';

        color =(color !== undefined) ? 'color:' + color : '';
        let bkgdColor = me.utilsCls.isMobile() ? ' background-color:#DDDDDD;' : '';
        return "<div style='margin:3px 0px 0px 10px;'><button style='-webkit-appearance:" + buttonStyle + "; height:36px;" + bkgdColor + "' id='" + me.pre + id + "'><span style='white-space:nowrap;" + color + "' class='icn3d-commandTitle' title='" + title + "'>" + text + "</span></button></div>";
    }

    setIcon(iconType, id, title, iconStyle, url, bText, bHighlight) { let me = this.icn3dui;
        if(me.bNode) return '';

        let color = (bHighlight) ? 'color:#f8b84e; ' : 'color:#1c94c4; ';
        let bkgdColor = ' background-color:#EEE; ';
        let cssCursor = (iconType == 'text') ? '' : 'cursor:pointer;';

        //let iconHtml = '<i id="' + me.pre + id + '" class="fa fa-' + iconStyle + '" title="' + title + '" style="font-size:20px; ' + color + bkgdColor + cssCursor + cssBorder + '"></i>';
        let iconHtml;
        if(bText) {
            iconHtml = '<div id="' + me.pre + id + '" title="' + title + '" style="font-family: Arial, Helvetica, sans-serif; font-size:16px; width:16px; height:16px;' + color + bkgdColor + cssCursor + '">' + iconStyle + '</div>';
        }
        else {
            iconHtml = '<i id="' + me.pre + id + '" class="las la-' + iconStyle + '" title="' + title + '" style="width:16px; height:16px;' + color + bkgdColor + cssCursor + '"></i>';
        }

        if(iconType == 'link') {
            return '<a href="' + url + '" target="_blank">' + iconHtml + '</a>';
        }
        else {
            return iconHtml;
        }
    }

    setTools_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        // second row
        let html = "<tr valign='center'>";

        let iconType = 'regular';
        let tdStr = "<td valign='top' align='center'>";
        let tdStrBorder = "<td valign='top' align='center' style='border-left: solid 1px #888888'>";

        // line-awesome: https://icons8.com/line-awesome
        // File menu
        html += tdStr + this.setIcon(iconType, 'tool_mmdbafid', 'Input PDB/MMDB/AlphaFold IDs', 'id', undefined, true) + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_pdbfile', 'Input PDB Files (appendable)', 'file-alt') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_sharelink', 'Get Share Link', 'link') + "</td>";
        html += tdStr + this.setIcon(iconType, 'saveimage', 'Save iCn3D PNG Image', 'camera') + "</td>";

        // Select menu
        html += tdStrBorder + this.setIcon(iconType, 'tool_definedsets', 'Defined Sets', 'object-group') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_aroundsphere', 'Select by Distance', 'dot-circle') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_saveselection', 'Save Selection as a Set', 'save') + "</td>";
        html += tdStr + this.setIcon(iconType, 'toggleHighlight', 'Toggle Highlight', 'highlighter') + "</td>";

        // View menu
        html += tdStrBorder + this.setIcon(iconType, 'show_selected', 'View Selection', 'eye') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_selectedcenter', 'Zoom in Selection', 'search-plus') + "</td>";
        html += tdStr + this.setIcon(iconType, 'alternate', "Alternate the Structures by keying the letter 'a'", 'a', undefined, true, true) + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_resetOrientation', 'Reset Orientation', 'undo-alt') + "</td>";

        // Style menu
        html += tdStrBorder + this.setIcon(iconType, 'tool_proteinsRibbon', 'Style Ribbon for proteins', 'dna') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_proteinsSphere', 'Style Sphere for proteins', 'volleyball-ball') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_surfaceVDW', 'Show Van der Waals Surface', 'cloud') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_bkgd', 'Toggle Background Color', 'adjust') + "</td>";

        // Color menu
        html += tdStrBorder + this.setIcon(iconType, 'tool_clrRainbowChain', 'Color Rainbow for Chains', 'rainbow') + "</td>"; 
        html += tdStr + this.setIcon(iconType, 'tool_clrSSGreen', 'Color by Secondary Structures', 'ring') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_clrChain', 'Color by Chains', 'layer-group') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_clrAtom', 'Color by Atoms', 'atom') + "</td>";

        // Analysis menu
        html += tdStrBorder + this.setIcon(iconType, 'tool_selectannotations', 'Sequences & Annotations', 'grip-lines') + "</td>";
        html += tdStr + this.setIcon(iconType, 'hbondsYes', 'Interactions', 'users') + "</td>";
        html += tdStr + this.setIcon(iconType, 'tool_delphi', 'Delphi Potentials', 'cloud-meatball') + "</td>";
        html += tdStr + this.setIcon(iconType, 'removeLabels', 'Remove Labels', 'remove-format') + "</td>";

        // Help menu
        html += tdStrBorder + this.setIcon('link', 'tool-gallery', 'Gallery', 'image', 'https://www.ncbi.nlm.nih.gov/Structure/icn3d/icn3d.html#gallery') + "</td>";
        html += tdStr + this.setIcon('link', 'tool-video', 'Videos', 'file-video', 'https://www.ncbi.nlm.nih.gov/Structure/icn3d/icn3d.html#videos') + "</td>";
        html += tdStr + this.setIcon('link', 'tool-github', 'iCn3D GitHub', 'code', 'https://github.com/ncbi/icn3d') + "</td>";
        html += tdStr + this.setIcon('link', 'tool-hints', 'Transform Hints', 'info-circle', 'https://www.ncbi.nlm.nih.gov/Structure/icn3d/icn3d.html#useicn3d') + "</td>";

        html += "</tr>";

        return html;
    }

    setTheme(color) { let me = this.icn3dui;
        if(me.bNode) return '';

        let borderColor, bkgdColor, bkgdImg, iconImg, activeTabColor;

        me.htmlCls.themecolor = color;

        if(color == 'orange') {
            borderColor = '#e78f08';
            bkgdColor = '#f6a828';
            bkgdImg = 'ui-bg_gloss-wave_35_f6a828_500x100.png';
            iconImg = 'ui-icons_ef8c08_256x240.png';
            activeTabColor = '#eb8f00';
        }
        else if(color == 'black') {
            borderColor = '#333333';
            bkgdColor = '#333333';
            bkgdImg = 'ui-bg_gloss-wave_25_333333_500x100.png';
            iconImg = 'ui-icons_222222_256x240.png';
            activeTabColor = '#222222';
        }
        else if(color == 'blue') {
            borderColor = '#4297d7';
            bkgdColor = '#5c9ccc';
            bkgdImg = 'ui-bg_gloss-wave_55_5c9ccc_500x100.png';
            iconImg = 'ui-icons_228ef1_256x240.png';
            activeTabColor = '#444';
        }

        $('.ui-widget-header').css({
            'border': '1px solid ' + borderColor,
            'background': bkgdColor + ' url("https://www.ncbi.nlm.nih.gov/Structure/icn3d/lib/images/' + bkgdImg + '") 50% 50% repeat-x',
            'color':'#fff',
            'font-weight':'bold'
        });

        $('.ui-button .ui-icon').css({
            'background-image': 'url(https://www.ncbi.nlm.nih.gov/Structure/icn3d/lib/images/' + iconImg + ')'
        });

        $('.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited').css({
            'color': activeTabColor,
            'text-decoration': 'none'
        });
    }

    //Set the textarea for the log output.
    setLogWindow(bUpdate, bCmdWindowInput) { let me = this.icn3dui;
        if(me.bNode) return '';

        let bCmdWindow, html = "";

        // check comand window 
        let value = me.htmlCls.setHtmlCls.getCookie('cmdwindow');
        if(value != '') {
            bCmdWindow = (bCmdWindowInput !== undefined) ? bCmdWindowInput : parseInt(value);
            if(bCmdWindow == 1) { // default 0
                me.htmlCls.LOG_HEIGHT = 180; //65;
                me.htmlCls.CMD_HEIGHT = 0.8*me.htmlCls.LOG_HEIGHT;

                if(!bUpdate) html += me.htmlCls.divStr + "cmdlog' style='float:left; margin-top: 5px; width: 100%;'>";
                html += "<textarea id='" + me.pre + "logtext' rows='2' style='width: 100%; height: " + me.htmlCls.CMD_HEIGHT + "px;  margin: auto; padding: 5px; box-sizing: border-box; border: 4px inset orange; background-color: " + me.htmlCls.GREYD + ";'></textarea>";
            }
            else {
                me.htmlCls.LOG_HEIGHT = 65;
                me.htmlCls.CMD_HEIGHT = 0.8*me.htmlCls.LOG_HEIGHT;

                if(!bUpdate) html += me.htmlCls.divStr + "cmdlog' style='float:left; margin-top: 5px; width: 100%;'>";
                html += "<textarea id='" + me.pre + "logtext' rows='2' style='width: 100%; height: " + me.htmlCls.CMD_HEIGHT + "px; padding: 0px; border: 0px; background-color: " + me.htmlCls.GREYD + ";'></textarea>";                 
            }
        }
        else {
            bCmdWindow = 0;

            me.htmlCls.LOG_HEIGHT = 65;
            me.htmlCls.CMD_HEIGHT = 0.8*me.htmlCls.LOG_HEIGHT;

            if(!bUpdate) html += me.htmlCls.divStr + "cmdlog' style='float:left; margin-top: 5px; width: 100%;'>";
            html += "<textarea id='" + me.pre + "logtext' rows='2' style='width: 100%; height: " + me.htmlCls.CMD_HEIGHT + "px; padding: 0px; border: 0px; background-color: " + me.htmlCls.GREYD + ";'></textarea>";
        }
        
        if(!bUpdate) html += "</div>";

        if(bUpdate) {
            me.htmlCls.clickMenuCls.setLogCmd('set cmdwindow ' + bCmdWindow, true);
            $("#" + me.pre + "cmdlog").html(html);
        }

        return html;
    }

    //Set the menu "File" at the top of the viewer.
    setMenu1() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<div class='icn3d-menu'>";

        html += "<accordion id='" + me.pre + "accordion1' class='icn3d-accordion'>";
        html += "<h3>File</h3>";
        html += "<div>";

        html += this.setMenu1_base();

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        return html;
    }

    setMenu1_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<ul class='icn3d-mn-item'>";
        //html += "<li><a href='https://www.ncbi.nlm.nih.gov/structure' target='_blank'>Search Structure " + me.htmlCls.wifiStr + "</a></li>";
        html += this.getMenuUrl('mn1_searchstru', "https://www.ncbi.nlm.nih.gov/structure", "Search Structure " + me.htmlCls.wifiStr, 1, 1);

        html += this.getMenuText('mn1_searchsimilar', 'Search Similar', undefined, undefined, 1);
        html += "<ul>";
        html += this.getLink('mn1_vastplus', 'NCBI VAST+ (PDB Complex)' + me.htmlCls.wifiStr, undefined, 2);
        html += this.getLink('mn1_vast', 'NCBI VAST (PDB Chain)' + me.htmlCls.wifiStr, undefined, 2);
        html += this.getLink('mn1_foldseek', 'Foldseek (PDB & AlphaFold)' + me.htmlCls.wifiStr, undefined, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn1_retrievebyid', 'Retrieve by ID', undefined, 1, 1); 
        html += "<ul>";
        
        html += this.getLink('mn1_mmdbafid', 'PDB/MMDB/AlphaFold IDs' + me.htmlCls.wifiStr, 1, 2);
        html += this.getLink('mn1_mmdbid', 'NCBI MMDB ID (annotation) ' + me.htmlCls.wifiStr, undefined, 2);
        html += this.getLink('mn1_mmtfid', 'RCSB MMTF ID (fast) ' + me.htmlCls.wifiStr, undefined, 2);
        html += this.getLink('mn1_pdbid', 'RCSB PDB ID ' + me.htmlCls.wifiStr, undefined, 2);

        html += this.getMenuText('mn1_afwrap', 'AlphaFold Structures', undefined, undefined, 2);
        html += "<ul>";
        
        html += this.getLink('mn1_afid', 'UniProt ID ' + me.htmlCls.wifiStr, undefined, 3);
        html += this.getLink('mn1_refseqid', 'NCBI Protein Accession ' + me.htmlCls.wifiStr, undefined, 3);
        // html += this.getLink('mn1_proteinname', 'Protein Name ' + me.htmlCls.wifiStr, undefined, 3);
        html += "</ul>";

        
        html += this.getLink('mn1_opmid', 'OPM PDB ID ' + me.htmlCls.wifiStr, undefined, 2);
        html += this.getLink('mn1_mmcifid', 'RCSB mmCIF ID ' + me.htmlCls.wifiStr, undefined, 2);
        //html += this.getLink('mn1_gi', 'NCBI gi ' + me.htmlCls.wifiStr, undefined, 2);

        html += this.getLink('mn1_cid', 'PubChem CID ' + me.htmlCls.wifiStr, 1, 2);
        
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn1_openfile', 'Open File', undefined, 1, 1);
        html += "<ul>";
//        html += this.getLink('mn1_pdbfile', 'PDB File');
//        html += this.getLink('mn1_pdbfile_app', 'PDB File (append)');
        html += this.getLink('mn1_pdbfile_app', 'PDB Files (appendable)', 1, 2);
        html += this.getLink('mn1_mmciffile', 'mmCIF File', undefined, 2);
        html += this.getLink('mn1_mol2file', 'Mol2 File', undefined, 2);
        html += this.getLink('mn1_sdffile', 'SDF File', undefined, 2);
        html += this.getLink('mn1_xyzfile', 'XYZ File', undefined, 2);
        html += this.getLink('mn1_afmapfile', 'AlphaFold PAE File', undefined, 2);

        html += this.getLink('mn1_urlfile', 'URL(Same Host) ' + me.htmlCls.wifiStr, undefined, 2);
        html += this.getMenuSep();
        html += this.getLink('mn1_pngimage', 'iCn3D PNG Image', 1, 2);
        html += this.getLink('mn1_state', 'State/Script File', undefined, 2);
        html += this.getLink('mn1_fixedversion', 'Share Link in Archived Ver. ' + me.htmlCls.wifiStr, undefined, 2);
        html += this.getLink('mn1_selection', 'Selection File', undefined, 2);

        html += this.getMenuSep();

        html += this.getMenuText('mn1_dsn6wrap', 'Electron Density(DSN6)', undefined, undefined, 2);
        html += "<ul>";
        html += this.getLink('mn1_dsn6', 'Local File', undefined, 3);
        html += this.getLink('mn1_dsn6url', 'URL(Same Host) ' + me.htmlCls.wifiStr, undefined, 3);
        html += "</ul>";

        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn1_alignwrap', 'Align', undefined, 1, 1);
        html += "<ul>";
        
        html += this.getMenuText('mn1_chainalignwrap', 'Multiple Chains', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadio('mn1_chainalignRad', 'mn1_chainalign', 'by Structure Alignment ' + me.htmlCls.wifiStr, undefined, 1, 3);
        html += this.getRadio('mn1_chainalignRad', 'mn1_chainalign2', 'by Sequence Alignment ' + me.htmlCls.wifiStr, undefined, 1, 3);
        html += this.getRadio('mn1_chainalignRad', 'mn1_chainalign3', 'Residue by Residue', undefined, undefined, 3);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn1_aligntwostru', 'Protein Complexes', undefined, undefined, 2);
        html += "<ul>";
        html += this.getLink('mn1_align', 'Two PDB Structures ' + me.htmlCls.wifiStr, undefined, 3);
        html += this.getLink('mn1_alignaf', 'Two AlphaFold Structures ' + me.htmlCls.wifiStr, undefined, 3);
        html += "</ul>";

        html += this.getLink('mn1_blast_rep_id', 'Sequence to Structure', undefined, 2);

        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn2_realignWrap', 'Realign Selection', undefined, undefined, 1);
        html += "<ul>";

        html += this.getMenuText('mn2_chainrealignwrap', 'Multiple Chains', undefined, undefined, 2);
        html += "<ul>";
        html += this.getRadio('mn2_realign', 'mn2_realignonstruct', 'by Structure Alignment ' + me.htmlCls.wifiStr, undefined, undefined, 3);
        html += this.getRadio('mn2_realign', 'mn2_realignonseqalign', 'by Sequence Alignment ' + me.htmlCls.wifiStr, undefined, undefined, 3);
        html += this.getRadio('mn2_realign', 'mn2_realignresbyres', 'Residue by Residue', undefined, undefined, 3);
        html += "</ul>";

        html += this.getLink('mn2_realigntwostru', 'Protein Complexes', undefined, 2);

        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn1_3dpprint', '3D Printing', undefined, 1, 1);
        html += "<ul>";
        if(me.cfg.cid === undefined) {
            html += this.getLink('mn1_exportVrmlStab', 'WRL/VRML(Color, W/ Stab.)', 1, 2);
            html += this.getLink('mn1_exportStlStab', 'STL(W/ Stabilizers)', 1, 2);
            html += this.getMenuSep();
            html += this.getLink('mn1_exportVrml', 'WRL/VRML(Color)', undefined, 2);
            html += this.getLink('mn1_exportStl', 'STL', undefined, 2);

            html += this.getMenuSep();
            html += this.getLink('mn1_stabilizerYes', 'Add All Stabilizers', undefined, 2);
            html += this.getLink('mn1_stabilizerNo', 'Remove All Stabilizers', undefined, 2);
            html += this.getMenuSep();
            html += this.getLink('mn1_stabilizerOne', 'Add One Stabilizer', undefined, 2);
            html += this.getLink('mn1_stabilizerRmOne', 'Remove One Stabilizer', undefined, 2);
            html += this.getMenuSep();
            html += this.getLink('mn1_thicknessSet', 'Set Thickness', undefined, 2);
        }
        else {
            html += this.getLink('mn1_exportVrml', 'VRML(Color)', 1, 2);
            html += this.getLink('mn1_exportStl', 'STL', 1, 2);
        }

        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn1_savefile', 'Save File', undefined, 1, 1);
        html += "<ul>";
        html += this.getMenuText('mn1_savepngimage', 'iCn3D PNG Image', undefined, 1, 2);
        html += "<ul>";
        html += this.getLink('mn1_exportCanvas', 'Original Size & HTML', 1, 3);
        html += this.getLink('mn1_exportCanvas1', 'Original Size', undefined, 3);

        html += this.getLink('mn1_exportCanvas2', '2X Large', undefined, 3);
        html += this.getLink('mn1_exportCanvas4', '4X Large', undefined, 3);
        html += this.getLink('mn1_exportCanvas8', '8X Large', undefined, 3);

        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn1_exportState', 'State File', undefined, 2);
        html += this.getLink('mn1_exportSelections', 'Selection File', undefined, 2);
        html += this.getLink('mn1_exportSelDetails', 'Selection Details', undefined, 2);
        html += this.getLink('mn1_exportCounts', 'Residue Counts', undefined, 2);

        html += this.getLink('mn1_exportPdbRes', 'PDB', 1, 2);
        html += this.getLink('delphipdbh', 'PDB with Hydrogens', undefined, 2);

        if(me.cfg.cid === undefined) {
            html += this.getLink('mn1_exportSecondary', 'Secondary Structure', undefined, 2);
        }

        html += "<li><br/></li>";

        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn1_sharelink', 'Share Link ' + me.htmlCls.wifiStr, 1, 1);

        html += this.getLink('mn1_replayon', 'Replay Each Step', undefined, 1);

        html += this.getMenuSep();

        html += this.getMenuText('mn1_menuwrap', 'Customize Menus', undefined, 1, 1);
        html += "<ul>";
        html += this.getLink('mn1_menuall', 'All Menus', 1, 2);
        html += this.getLink('mn1_menusimple', 'Simple Menus', 1, 2);
        html += this.getMenuSep();
        html += this.getLink('mn1_menupref', 'Preferences', 1, 2);
        html += this.getLink('mn1_menuloadpref', 'Load Preferences', 1, 2);
        html += "</ul>";
        html += "</li>";

        html += "<li><br/></li>";

        html += "</ul>";

        return html;
    }

    //Set the menu "Select" at the top of the viewer.
    setMenu2() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<div class='icn3d-menu'>";
        html += "<accordion id='" + me.pre + "accordion2' class='icn3d-accordion'>";
        html += "<h3>Select</h3>";
        html += "<div>";

        html += this.setMenu2_base();

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        return html;
    }

    setMenu2_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<ul class='icn3d-mn-item'>";

        html += this.getLink('mn2_definedsets', 'Defined Sets', 1, 1);
        html += this.getLink('mn2_selectall', 'All', undefined, 1);
        html += this.getLink('mn2_selectdisplayed', 'Displayed Set', undefined, 1);
        html += this.getLink('mn2_aroundsphere', 'by Distance', 1, 1);

        html += this.getMenuText('mn2_selbyprop', 'by Property', undefined, undefined, 1);
        html += "<ul>";
        html += this.getLink('mn2_propPos', 'Positive', undefined, 2);
        html += this.getLink('mn2_propNeg', 'Negative', undefined, 2);
        html += this.getLink('mn2_propHydro', 'Hydrophobic', undefined, 2);
        html += this.getLink('mn2_propPolar', 'Polar', undefined, 2);
        html += this.getLink('mn2_propBfactor', 'B-factor/pLDDT', undefined, 2);
        html += this.getLink('mn2_propSolAcc', 'Solvent Accessibility', undefined, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn2_selectcomplement', 'Inverse', undefined, 1);
        html += this.getLink('mn2_selectmainchains', 'Main Chains', undefined, 1);
        html += this.getLink('mn2_selectsidechains', 'Side Chains', undefined, 1);
        html += this.getLink('mn2_selectmainsidechains', 'Main & Side Chains', undefined, 1);
        html += this.getLink('mn2_command', 'Advanced', undefined, 1);

        if(me.cfg.cid === undefined) {
            html += this.getMenuText('mn2_selon3d', 'Select on 3D', undefined, 1, 1);
            html += "<ul>";

            html += "<li>\"Alt\"+Click: start selection</li>";
            html += "<li>\"Ctrl\"+Click: union selection</li>";
            html += "<li>\"Shift\"+Click: range Selection</li>";
            html += this.getMenuSep();
            html += this.getRadio('mn2_pk', 'mn2_pkChain', 'Chain', undefined, 1, 2);
            if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined) {
                html += this.getRadio('mn2_pk', 'mn2_pkDomain', '3D Domain', undefined, undefined, 2);
            }
            html += this.getRadio('mn2_pk', 'mn2_pkStrand', 'Strand/Helix', undefined, undefined, 2);
            html += this.getRadio('mn2_pk', 'mn2_pkResidue', 'Residue', true, 1, 2);
            html += this.getRadio('mn2_pk', 'mn2_pkYes', 'Atom', undefined, 1, 2);
            html += this.getRadio('mn2_pk', 'mn2_pkNo', 'None', undefined, undefined, 2);
            html += "</ul>";
            html += "</li>";
        }
        else {
            if(me.utilsCls.isMobile()) {
                html += "<li><span>Touch to pick</span></li>";
            }
            else {
                html += "<li><span>Picking with<br>\"Alt\" + Click</span></li>";
            }
        }

        html += this.getMenuSep();

        html += this.getLink('mn2_saveselection', 'Save Selection', 1, 1);
        html += this.getLink('clearall', 'Clear Selection', undefined, 1);
        html += this.getLink('mn2_saveresidue', 'Save Res. in Sel.', 1, 1);

        html += this.getMenuSep();

        html += this.getMenuText('mn2_hlcolor', 'Highlight Color', undefined, undefined, 1);
        html += "<ul>";
        html += this.getRadio('mn2_hl_clr', 'mn2_hl_clrYellow', 'Yellow', true, undefined, 2);
        html += this.getRadio('mn2_hl_clr', 'mn2_hl_clrGreen', 'Green', undefined, undefined, 2);
        html += this.getRadio('mn2_hl_clr', 'mn2_hl_clrRed', 'Red', undefined, undefined, 2);
        html += "</ul>";
        html += "</li>";
        html += this.getMenuText('mn2_hlstyle', 'Highlight Style', undefined, undefined, 1);
        html += "<ul>";

        html += this.getRadio('mn2_hl_style', 'mn2_hl_styleOutline', 'Outline', true, undefined, 2);
        html += this.getRadio('mn2_hl_style', 'mn2_hl_styleObject', '3D Objects', undefined, undefined, 2);

        html += "</ul>";
        html += "</li>";

        html += this.getLink('toggleHighlight2', 'Toggle Highlight', 1, 1);

        html += "<li><br/></li>";

        html += "</ul>";

        return html;
    }

    setMenu2b() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<div class='icn3d-menu'>";
        html += "<accordion id='" + me.pre + "accordion2b' class='icn3d-accordion'>";
        html += "<h3>View</h3>";
        html += "<div>";

        html += this.setMenu2b_base();

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        return html;
    }

    setMenu2b_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";
        html += "<ul class='icn3d-mn-item'>";

        html += this.getLink('mn2_show_selected', 'View Selection', 1, 1);
        html += this.getLink('mn2_hide_selected', 'Hide Selection', 1, 1);
        html += this.getLink('mn2_selectedcenter', 'Zoom in Selection', 1, 1);
        html += this.getLink('mn6_center', 'Center Selection', undefined, 1);
        html += this.getLink('mn2_fullstru', 'View Full Structure');
        html += this.getLinkWrapper('mn2_alternate', 'Alternate(Key "a")', 'mn2_alternateWrap', undefined, 1);

        if(me.cfg.opmid !== undefined) {
            html += this.getLinkWrapper('togglemem', 'Toggle Membrane', 'togglememli', undefined, 1);
        }
        else if(me.cfg.mmdbafid !== undefined || me.cfg.afid !== undefined) {
            // hide by default
            html += this.getLinkWrapper('togglemem', 'Toggle Membrane', 'togglememli', undefined, 1, true);
        }

        if(me.cfg.opmid !== undefined) {
            html += this.getLinkWrapper('adjustmem', 'Adjust Membrane', 'adjustmemli', undefined, 1);
            html += this.getLinkWrapper('selectplane', 'Select between<br>Two X-Y Planes</span>', 'selectplaneli', undefined, 1);
        }

        html += this.getMenuSep();

        html += this.getMenuText('mn2_vrarhints', 'VR & AR Hints', undefined, 1, 1);
        html += "<ul>";
        html += this.getMenuUrl("vrhint", me.htmlCls.baseUrl + "icn3d/icn3d.html#vr", "VR: VR Headsets", 1, 2);
        html += this.getMenuUrl("arhint", me.htmlCls.baseUrl + "icn3d/icn3d.html#ar", "AR: Chrome in Android", 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn6_sidebyside', 'Side by Side', 1, 1);

        html += this.getMenuText('mn2_rotate', 'Rotate', undefined, 1, 1);
        html += "<ul>";

        html += this.getMenuText('mn2_rotate90', 'Rotate 90&deg;', undefined, undefined, 2);
        html += "<ul>";
        html += this.getRadio('mn6_rotate90', 'mn6_rotatex', 'X-axis(Shift + Key M)', undefined, undefined, 2);
        html += this.getRadio('mn6_rotate90', 'mn6_rotatey', 'Y-axis(Shift + Key J)', undefined, undefined, 2);
        html += this.getRadio('mn6_rotate90', 'mn6_rotatez', 'Z-axis', undefined, undefined, 2);
        html += "</ul>";
        html += "</li>";
        html += this.getMenuText('mn2_rotateauto', 'Auto Rotation', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadio('mn6_rotate', 'mn6_rotateleft', 'Rotate Left', undefined, 1, 3);
        html += this.getRadio('mn6_rotate', 'mn6_rotateright', 'Rotate Right', undefined, 1, 3);
        html += this.getRadio('mn6_rotate', 'mn6_rotateup', 'Rotate Up', undefined, 1, 3);
        html += this.getRadio('mn6_rotate', 'mn6_rotatedown', 'Rotate Down', undefined, 1, 3);
        html += "</ul>";
        html += "</li>";

        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn2_camera', 'Camera', undefined, undefined, 1);
        html += "<ul>";
        html += this.getRadio('mn6_camera', 'mn6_cameraPers', 'Perspective', true, undefined, 2);
        html += this.getRadio('mn6_camera', 'mn6_cameraOrth', 'Orthographic', undefined, undefined, 2);
        html += "</ul>";
        html += "</li>";
        html += this.getMenuText('mn2_fog', 'Fog for Selection', undefined, undefined, 1);
        html += "<ul>";
        html += this.getRadio('mn6_showfog', 'mn6_showfogYes', 'On', undefined, undefined, 2);
        html += this.getRadio('mn6_showfog', 'mn6_showfogNo', 'Off', true, undefined, 2);
        html += "</ul>";
        html += "</li>";
        html += this.getMenuText('mn2_slab', 'Slab for Selection', undefined, undefined, 1);
        html += "<ul>";
        html += this.getRadio('mn6_showslab', 'mn6_showslabYes', 'On', undefined, undefined, 2);
        html += this.getRadio('mn6_showslab', 'mn6_showslabNo', 'Off', true, undefined, 2);
        html += "</ul>";
        html += "</li>";
        html += this.getMenuText('mn2_axes', 'XYZ-axes', undefined, 1);
        html += "<ul>";
        html += this.getRadio('mn6_showaxis', 'mn6_showaxisYes', 'Original', undefined, undefined, 2);
        html += this.getRadio('mn6_showaxis', 'mn6_showaxisSel', 'Prin. Axes on Sel.', undefined, undefined, 2);
        html += this.getRadio('mn6_showaxis', 'mn6_showaxisNo', 'Hide', true, undefined, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuSep();

        html += this.getMenuText('mn2_resetwrap', 'Reset', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn6_reset', 'reset', 'All', undefined, 1, 2);
        html += this.getRadio('mn6_reset', 'mn6_resetOrientation', 'Orientation', undefined, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn6_back', 'Undo', undefined, 1);
        html += this.getLink('mn6_forward', 'Redo', undefined, 1);

        html += this.getLink('mn6_fullscreen', 'Full Screen', undefined, 1);
    //    html += this.getLink('mn6_exitfullscreen', 'Exit Full Screen');

        html += "<li><br/></li>";

        html += "</ul>";

        return html;
    }

    //Set the menu "Style" at the top of the viewer.
    setMenu3() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<div class='icn3d-menu'>";
        html += "<accordion id='" + me.pre + "accordion3' class='icn3d-accordion'>";
        html += "<h3 id='" + me.pre + "style'>Style</h3>";
        html += "<div>";

        html += this.setMenu3_base();

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        return html;
    }

    setMenu3_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<ul class='icn3d-mn-item'>";

        if(me.cfg.cid === undefined) {
            html += this.getMenuText('mn3_proteinwrap', 'Proteins', undefined, 1, 1);
            html += "<ul>";
            if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined) {
                html += this.getRadio('mn3_proteins', 'mn3_proteinsRibbon', 'Ribbon', undefined, 1, 2);
            }
            else {
                html += this.getRadio('mn3_proteins', 'mn3_proteinsRibbon', 'Ribbon', true, 1, 2);
            }

            html += this.getRadio('mn3_proteins', 'mn3_proteinsStrand', 'Strand', undefined, undefined, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsCylinder', 'Cylinder and Plate', undefined, undefined, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsSchematic', 'Schematic', undefined, 1, 2);

            if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined) {
                html += this.getRadio('mn3_proteins', 'mn3_proteinsCalpha', 'C Alpha Trace', true, 1, 2);
            }
            else {
                html += this.getRadio('mn3_proteins', 'mn3_proteinsCalpha', 'C Alpha Trace', undefined, 1, 2);
            }

            html += this.getRadio('mn3_proteins', 'mn3_proteinsBackbone', 'Backbone', undefined, undefined, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsBfactor', 'B-factor Tube', undefined, 1, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsLines', 'Lines', undefined, 1, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsStick', 'Stick', undefined, 1, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsBallstick', 'Ball and Stick', undefined, 1, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsSphere', 'Sphere', undefined, 1, 2);
            html += this.getRadio('mn3_proteins', 'mn3_proteinsNo', 'Hide', undefined, 1, 2);
            html += "</ul>";
            html += "</li>";

            html += this.getMenuText('mn3_sidecwrap', 'Side Chains', undefined, 1, 1);
            html += "<ul>";

            html += this.getRadio('mn3_sidec', 'mn3_sidecLines', 'Lines', undefined, 1, 2);
            html += this.getRadio('mn3_sidec', 'mn3_sidecStick', 'Stick', undefined, 1, 2);
            html += this.getRadio('mn3_sidec', 'mn3_sidecBallstick', 'Ball and Stick', undefined, 1, 2);
            html += this.getRadio('mn3_sidec', 'mn3_sidecSphere', 'Sphere', undefined, 1, 2);
            html += this.getRadio('mn3_sidec', 'mn3_sidecNo', 'Hide', true, 1, 2);
            html += "</ul>";
            html += "</li>";

            html += this.getMenuText('mn3_nuclwrap', 'Nucleotides', undefined, 1, 1);
            html += "<ul>";
            html += this.getRadio('mn3_nucl', 'mn3_nuclCartoon', 'Cartoon', true, 1, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclPhos', "O3' Trace", undefined, 1, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclBackbone', 'Backbone', undefined, undefined, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclSchematic', 'Schematic', undefined, 1, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclLines', 'Lines', undefined, 1, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclStick', 'Stick', undefined, 1, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclBallstick', 'Ball and Stick', undefined, 1, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclSphere', 'Sphere', undefined, 1, 2);
            html += this.getRadio('mn3_nucl', 'mn3_nuclNo', 'Hide', undefined, 1, 2);
            html += "</ul>";
            html += "</li>";

            html += this.getMenuText('mn3_ntbasewrap', 'Nucl. Bases', undefined, 1, 1);
            html += "<ul>";

            html += this.getRadio('mn3_ntbase', 'mn3_ntbaseLines', 'Lines', undefined, 1, 2);
            html += this.getRadio('mn3_ntbase', 'mn3_ntbaseStick', 'Stick', undefined, 1, 2);
            html += this.getRadio('mn3_ntbase', 'mn3_ntbaseBallstick', 'Ball and Stick', undefined, 1, 2);
            html += this.getRadio('mn3_ntbase', 'mn3_ntbaseSphere', 'Sphere', undefined, 1, 2);
            html += this.getRadio('mn3_ntbase', 'mn3_ntbaseNo', 'Hide', true, 1, 2);
            html += "</ul>";
            html += "</li>";
        }

        html += this.getMenuText('mn3_ligwrap', 'Chemicals', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn3_lig', 'mn3_ligLines', 'Lines', undefined, 1, 2);
        if(me.cfg.cid === undefined) {
            html += this.getRadio('mn3_lig', 'mn3_ligStick', 'Stick', true, 1, 2);
            html += this.getRadio('mn3_lig', 'mn3_ligBallstick', 'Ball and Stick', undefined, 1, 2);
        }
        else {
            html += this.getRadio('mn3_lig', 'mn3_ligStick', 'Stick', undefined, 1, 2);
            html += this.getRadio('mn3_lig', 'mn3_ligBallstick', 'Ball and Stick', true, 1, 2);
        }
        html += this.getRadio('mn3_lig', 'mn3_ligSchematic', 'Schematic', undefined, 1, 2);
        html += this.getRadio('mn3_lig', 'mn3_ligSphere', 'Sphere', undefined, 1, 2);
        html += this.getRadio('mn3_lig', 'mn3_ligNo', 'Hide', undefined, 1, 2);
        html += "</ul>";
        html += "</li>";

        //if(me.cfg.cid !== undefined) {
            html += this.getMenuText('mn3_hydrogenswrap', 'Hydrogens', undefined, 1, 1);
            html += "<ul>";
            html += this.getRadio('mn3_hydrogens', 'mn3_hydrogensYes', 'Show', true, 1, 2);
            html += this.getRadio('mn3_hydrogens', 'mn3_hydrogensNo', 'Hide', undefined, 1, 2);
            html += "</ul>";
            html += "</li>";
        //}

        if(me.cfg.cid === undefined) {
            html += this.getMenuText('mn3_glycanwrap', 'Glycans', undefined, undefined, 1);
            html += "<ul>";
            html += this.getRadio('mn3_glycansCart', 'mn3_glycansCartYes', 'Show Cartoon', undefined, undefined, 2);
            html += this.getRadio('mn3_glycansCart', 'mn3_glycansCartNo', 'Hide Cartoon', true, undefined, 2);
            html += "</ul>";
            html += "</li>";
        }

        html += this.getMenuText('mn3_ionswrap', 'Ions', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn3_ions', 'mn3_ionsSphere', 'Sphere', true, 1, 2);
        html += this.getRadio('mn3_ions', 'mn3_ionsDot', 'Dot', undefined, 1, 2);
        html += this.getRadio('mn3_ions', 'mn3_ionsNo', 'Hide', undefined, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn3_waterwrap', 'Water', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn3_water', 'mn3_waterSphere', 'Sphere', undefined, 1, 2);
        html += this.getRadio('mn3_water', 'mn3_waterDot', 'Dot', undefined, 1, 2);
        html += this.getRadio('mn3_water', 'mn3_waterNo', 'Hide', true, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn3_setThickness', 'Preferences', undefined, 1);

        html += this.getMenuSep();
        html += this.getLink('mn3_styleSave', 'Save Style', undefined, 2);
        html += this.getLink('mn3_styleApplySave', 'Apply Saved Style', undefined, 2);

        html += this.getMenuSep();

        html += this.getMenuText('mn5_surfacewrap', 'Surface Type', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn5_surface', 'mn5_surfaceVDW', 'Van der Waals', undefined, 1, 2);
        html += this.getRadio('mn5_surface', 'mn5_surfaceVDWContext', 'VDW with Context', undefined, undefined, 2);
        html += this.getRadio('mn5_surface', 'mn5_surfaceMolecular', 'Molecular Surface', undefined, 1, 2);
        html += this.getRadio('mn5_surface', 'mn5_surfaceMolecularContext', 'MS with Context', undefined, undefined, 2);
        html += this.getRadio('mn5_surface', 'mn5_surfaceSAS', 'Solvent Accessible', undefined, 1, 2);
        html += this.getRadio('mn5_surface', 'mn5_surfaceSASContext', 'SA with Context', undefined, undefined, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn5_surfaceNo', 'Remove Surface', 1, 1);

        html += this.getMenuText('mn5_surfaceop', 'Surface Opacity', undefined, 1, 1);
        html += "<ul>";

        html += this.getMenuText('mn5_surfaceopfast', 'Fast Transparency', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadio('mn5_opacity', 'mn5_opacity10', '1.0', true, 1, 3);

        for(let i = 9; i > 0; --i) {
            html += this.getRadio('mn5_opacity', 'mn5_opacity0' + i, '0.' + i, 1, 3);
        }
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn5_surfaceopslow', 'Slow Transparency', undefined, undefined, 2);
        html += "<ul>";
        html += this.getRadio('mn5_opacityslow', 'mn5_opacityslow10', '1.0', true, undefined, 3);

        for(let i = 9; i > 0; --i) {
            html += this.getRadio('mn5_opacityslow', 'mn5_opacityslow0' + i, '0.' + i, undefined, undefined, 3);
        }
        html += "</ul>";
        html += "</li>";

        html += "</ul>"; // end of Surface Opacity

        html += this.getMenuText('mn5_wireframewrap', 'Surface Wireframe', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn5_wireframe', 'mn5_wireframeYes', 'Yes', undefined, 1, 2);
        html += this.getRadio('mn5_wireframe', 'mn5_wireframeNo', 'No', true, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuSep();

        html += this.getLink('mn5_cartoonshape', 'Cartoon for a Set', undefined, 1);
        html += this.getLink('mn5_linebtwsets', 'Line btw. Two Sets', undefined, 1);

        if(me.cfg.cid === undefined && me.cfg.align === undefined && me.cfg.chainalign === undefined && me.cfg.mmdbaf === undefined) {
            html += this.getMenuSep();

            html += this.getLinkWrapper2('mn5_map', 'Electron Density', 'mapWrapper1', undefined, 1);

            html += "<ul>";
            html += this.getLink('mn5_elecmap2fofc', '2Fo-Fc Map', undefined, 2);
            html += this.getLink('mn5_elecmapfofc', 'Fo-Fc Map', undefined, 2);
            html += this.getLinkWrapper('mn5_elecmapNo', 'Remove Map', 'mapWrapper2', undefined, 2);

            html += "</ul>";
            html += "</li>";

            html += this.getLinkWrapper2('mn5_map3', 'Map Wireframe', 'mapWrapper3', undefined, 1);
            
            html += "<ul>";
            html += this.getRadio('mn5_mapwireframe', 'mn5_mapwireframeYes', 'Yes', true, undefined, 2);
            html += this.getRadio('mn5_mapwireframe', 'mn5_mapwireframeNo', 'No', undefined, undefined, 2);
            html += "</ul>";
            html += "</li>";

            if(me.cfg.mmtfid === undefined) {
                html += this.getLinkWrapper('mn5_emmap', 'EM Density Map', 'emmapWrapper1', undefined, 1);
                html += this.getLinkWrapper('mn5_emmapNo', 'Remove EM Map', 'emmapWrapper2', undefined, 1);

                html += this.getLinkWrapper2('mn5_emmap3', 'EM Map Wireframe', 'emmapWrapper3', undefined, 1);
                html += "<ul>";
                html += this.getRadio('mn5_emmapwireframe', 'mn5_emmapwireframeYes', 'Yes', true, undefined, 2);
                html += this.getRadio('mn5_emmapwireframe', 'mn5_emmapwireframeNo', 'No', undefined, undefined, 2);
                html += "</ul>";
                html += "</li>";
            }
        }

        html += this.getMenuSep();

        html += this.getMenuText('mn6_bkgdwrap', 'Background', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn6_bkgd', 'mn6_bkgdTransparent', 'Transparent', undefined, 1, 2);
        html += this.getRadio('mn6_bkgd', 'mn6_bkgdBlack', 'Black', true, 1, 2);
        html += this.getRadio('mn6_bkgd', 'mn6_bkgdGrey', 'Gray', undefined, 1, 2);
        html += this.getRadio('mn6_bkgd', 'mn6_bkgdWhite', 'White', undefined, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn6_themewrap', 'Dialog Color', undefined, undefined, 1);
        html += "<ul>";
        html += this.getRadio('mn6_theme', 'mn6_themeBlue', 'Blue', true, undefined, 2);
        html += this.getRadio('mn6_theme', 'mn6_themeOrange', 'Orange', undefined, undefined, 2);
        html += this.getRadio('mn6_theme', 'mn6_themeBlack', 'Black', undefined, undefined, 2);
        html += "</ul>";
        html += "</li>";

        html += "<li><br/></li>";

        html += "</ul>";

        return html;
    }

    //Set the menu "Color" at the top of the viewer.
    setMenu4() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<div class='icn3d-menu'>";
        html += "<accordion id='" + me.pre + "accordion4' class='icn3d-accordion'>";
        html += "<h3 id='" + me.pre + "color'>Color</h3>";
        html += "<div>";

        html += this.setMenu4_base();

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        return html;
    }

    setMenu4_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<ul class='icn3d-mn-item'>";

        html += this.getMenuText('mn4_clrwrap', 'Unicolor', 'icn3d-menupd', 1, 1);
        html += "<ul>";

        html += this.getMenuText('uniclrRedwrap', 'Red', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrRed1', 'Red', 'F00', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed2', 'Indian Red', 'CD5C5C', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed3', 'Light Coral', 'F08080', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed4', 'Salmon', 'FA8072', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed5', 'Dark Salmon', 'E9967A', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed6', 'Light Salmon', 'FFA07A', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed7', 'Crimson', 'DC143C', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed8', 'Fire Brick', 'B22222', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrRed9', 'Dark Red', '8B0000', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrPinkwrap', 'Pink', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrPink1', 'Pink', 'FFC0CB', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrPink2', 'Light Pink', 'FFB6C1', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrPink3', 'Hot Pink', 'FF69B4', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrPink4', 'Deep Pink', 'FF1493', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrPink5', 'Medium Violet Red', 'C71585', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrPink6', 'Pale Violet Red', 'DB7093', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrOrangewrap', 'Orange', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrOran1', 'Orange', 'FFA500', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrOran2', 'Dark Orange', 'FF8C00', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrOran3', 'Orange Red', 'FF4500', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrOran4', 'Tomato', 'FF6347', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrOran5', 'Coral', 'FF7F50', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrOran6', 'Light Salmon', 'FFA07A', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrYellowwrap', 'Yellow', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrYllw1', 'Yellow', 'FF0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw2', 'Gold', 'FFD700', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw3', 'Light Yellow', 'FFFFE0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw4', 'Lemon Chiffon', 'FFFACD', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw5', 'Light Golden Rod', 'FAFAD2', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw6', 'Papaya Whip', 'FFEFD5', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw7', 'Moccasin', 'FFE4B5', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw8', 'Peach Puff', 'FFDAB9', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw9', 'Pale Golden Rod', 'EEE8AA', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw10', 'Khaki', 'F0E68C', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrYllw11', 'Dark Khaki', 'BDB76B', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrMagentawrap', 'Magenta', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrMgnt1', 'Magenta', 'F0F', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt2', 'Orchid', 'DA70D6', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt3', 'Violet', 'EE82EE', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt4', 'Plum', 'DDA0DD', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt5', 'Thistle', 'D8BFD8', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt6', 'Lavender', 'E6E6FA', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt7', 'Medium Orchid', 'BA55D3', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt8', 'Medium Purple', '9370DB', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt9', 'Rebecca Purple', '663399', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt10', 'Blue Violet', '8A2BE2', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt11', 'Dark Violet', '9400D3', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt12', 'Dark Orchid', '9932CC', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt13', 'Dark Magenta', '8B008B', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt14', 'Purple', '800080', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt15', 'Indigo', '4B0082', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt16', 'Slat Blue', '6A5ACD', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt17', 'Dark Slate Blue', '483D8B', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrMgnt18', 'Medium Slat Blue', '6A5ACD', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrGreenwrap', 'Green', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrGrn1', 'Green', '0F0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn2', 'Dark Green', '006400', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn3', 'Yellow Green', '9ACD32', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn4', 'Olive Drab', '6B8E23', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn5', 'Olive', '808000', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn6', 'Dark Olive Green', '556B2F', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn7', 'Medium Aquamarine', '66CDAA', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn8', 'Dark Sea Green', '8FBC8B', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn9', 'Lignt Sea Green', '20B2AA', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn10', 'Dark Cyan', '008B8B', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn11', 'Teal', '008080', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn12', 'Forest Green', '228B22', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn13', 'Sea Green', '2E8B57', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn14', 'Medium Sea Green', '3CB371', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn15', 'Spring Green', '00FF7F', undefined, 1, 3);
        //html += this.getRadClr('mn4_clr', 'uniclrGrn16', 'Medium Spring Green', '00FA9A', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn16', 'Medium Spring', '00FA9A', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn17', 'Light Green', '90EE90', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn18', 'Pale Green', '98FB98', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn19', 'Lime Green', '32CD32', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn20', 'Lawn Green', '7CFC00', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn21', 'Chartreuse', '7FFF00', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGrn22', 'Green Yellow', 'ADFF2F', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrCyanwrap', 'Cyan', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrCyan1', 'Cyan', '0FF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrCyan2', 'Light Cyan', 'E0FFFF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrCyan3', 'Pale Turquoise', 'AFEEEE', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrCyan4', 'Aquamarine', '7FFFD4', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrCyan5', 'Turquoise', '40E0D0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrCyan6', 'Medium Turquoise', '48D1CC', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrCyan7', 'Dark Turquoise', '00CED1', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrBluewrap', 'Blue', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrBlue1', 'Blue', '00F', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue2', 'Medium Blue', '0000CD', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue3', 'Dark Blue', '00008B', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue4', 'Navy', '000080', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue5', 'Midnight Blue', '191970', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue6', 'Royal Blue', '4169E1', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue7', 'Medium Slate Blue', '7B68EE', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue8', 'Corn Flower Blue', '6495ED', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue9', 'Dodger Blue', '1E90FF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue10', 'Deep Sky Blue', '00BFFF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue11', 'Light Sky Blue', '87CEFA', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue12', 'Sky Blue', '87CEEB', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue13', 'Light Blue', 'ADD8E6', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue14', 'Powder Blue', 'B0E0E6', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue15', 'Light Steel Blue', 'B0C4DE', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue16', 'Steel Blue', '4682B4', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBlue17', 'Cadet Blue', '5F9EA0', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrBrownwrap', 'Brown', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrBrown1', 'Brown', 'A52A2A', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown2', 'Maroon', '800000', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown3', 'Sienna', 'A0522D', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown4', 'Saddle Brown', '8B4513', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown5', 'Chocolate', 'D2691E', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown6', 'Peru', 'CD853F', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown7', 'Dark Golden Rod', 'B8860B', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown8', 'Golden Rod', 'DAA520', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown9', 'Sandy Brown', 'F4A460', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown10', 'Rosy Brown', 'BC8F8F', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown11', 'Tan', 'D2B48C', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown12', 'Burlywood', 'DEB887', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown13', 'Wheat', 'F5DEB3', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown14', 'Navajo White', 'FFDEAD', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown15', 'Bisque', 'FFE4C4', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown16', 'Blanched Almond', 'FFEBCD', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrBrown17', 'Corn Silk', 'FFF8DC', undefined, 1, 3);
        html += "</ul>";

        html += "<li><span>White</span>";
        html += this.getMenuText('uniclrWhitewrap', 'White', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrWhite1', 'White', 'FFF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite2', 'Snow', 'FFFAFA', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite3', 'Honey Dew', 'F0FFF0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite4', 'Mint Cream', 'F5FFFA', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite5', 'Azure', 'F0FFFF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite6', 'Alice Blue', 'F0F8FF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite7', 'Ghost White', 'F8F8FF', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite8', 'White Smoke', 'F5F5F5', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite9', 'Sea Shell', 'FFF5EE', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite10', 'Beige', 'F5F5DC', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite11', 'Old Lace', 'FDF5E6', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite12', 'Floral White', 'FFFAF0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite13', 'Ivory', 'FFFFF0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite14', 'Antique White', 'FAEBD7', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite15', 'Linen', 'FAF0E6', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite16', 'Lavenderblush', 'FFF0F5', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrWhite17', 'Misty Rose', 'FFE4E1', undefined, 1, 3);
        html += "</ul>";

        html += this.getMenuText('uniclrGraywrap', 'Gray', undefined, 1, 2);
        html += "<ul>";
        html += this.getRadClr('mn4_clr', 'uniclrGray1', 'Gray', '808080', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray2', 'Dim Gray', '696969', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray3', 'Light Slate Gray', '778899', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray4', 'Slate Gray', '708090', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray5', 'Dark Slate Gray', '2F4F4F', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray6', 'Black', '000000', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray7', 'Dark Gray', 'A9A9A9', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray8', 'Silver', 'C0C0C0', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray9', 'Light Gray', 'D3D3D3', undefined, 1, 3);
        html += this.getRadClr('mn4_clr', 'uniclrGray10', 'Gainsboro', 'DCDCDC', undefined, 1, 3);
        html += "</ul>";

        html += "</ul>";

        html += this.getRadio('mn4_clr', 'mn4_clrCustom', 'Color Picker', undefined, undefined, 1);
        html += this.getMenuSep();

        if(me.cfg.cid === undefined) {
            html += this.getMenuText('mn4_clrRainbowwrap', 'Rainbow (R-V)', 'icn3d-menupd', 1, 1);
            html += "<ul>";
            html += this.getRadio('mn4_clr', 'mn4_clrRainbow', 'for Selection', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrRainbowChain', 'for Chains', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrRainbowSets', 'for Sets', undefined, undefined, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrRainbowAcrossSets', 'across Sets', undefined, undefined, 2);
            html += "</ul>";

            html += this.getMenuText('mn4_clrSpectrumwrap', 'Spectrum (V-R)', 'icn3d-menupd', 1, 1);
            html += "<ul>";
            html += this.getRadio('mn4_clr', 'mn4_clrSpectrum', 'for Selection', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrSpectrumChain', 'for Chains', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrSpectrumSets', 'for Sets', undefined, undefined, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrSpectrumAcrossSets', 'across Sets', undefined, undefined, 2);
            html += "</ul>";

            html += this.getMenuText('mn4_clrSSwrap', 'Secondary', 'icn3d-menupd', 1, 1);
            html += "<ul>";
            html += this.getRadio('mn4_clr', 'mn4_clrSSGreen', 'Sheet in Green', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrSSYellow', 'Sheet in Yellow', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrSSSpectrum', 'Spectrum', undefined, undefined, 2);
            html += "</ul>";

            html += this.getRadio('mn4_clr', 'mn4_clrCharge', 'Charge', undefined, 1, 1);

            html += this.getMenuText('mn4_hydrophobicwrap', 'Hydrophobicity', 'icn3d-menupd', 1, 1);
            html += "<ul>";
            html += this.getRadio('mn4_clr', 'mn4_clrNormalizedHP', 'Normalized', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrHydrophobic', 'Wimley-White', undefined, undefined, 2);
            html += "</ul>";

            html += this.getMenuText('mn4_clrBfactorwrap', 'B-factor', 'icn3d-menupd', 1, 1);
            html += "<ul>";
            html += this.getRadio('mn4_clr', 'mn4_clrBfactor', 'Original', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrBfactorNorm', 'Percentile', undefined, 1, 2);
            html += "</ul>";

            html += this.getRadio('mn4_clr', 'mn4_clrArea', 'Solvent<br><span style="padding-left:1.5em;">Accessibility</span>', undefined, undefined, 1);

            html += this.getRadio('mn4_clr', 'mn4_clrStructure', 'Structure', undefined, 1, 1);

            if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined || me.cfg.blast_rep_id !== undefined) {
                html += this.getRadio('mn4_clr', 'mn4_clrChain', 'Chain', undefined, 1, 1);
            }
            else {
                html += this.getRadio('mn4_clr', 'mn4_clrChain', 'Chain', true, 1, 1);
            }

            //if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined) {
              html += this.getRadio('mn4_clr', 'mn4_clrdomain', '3D Domain', undefined, undefined, 1);
            //}

            if(me.cfg.cid === undefined) {
                html += this.getMenuText('mn4_clrsetswrap', 'Defined Sets', 'icn3d-menupd', undefined, 1);
                html += "<ul>";
                html += this.getRadio('mn4_clr', 'mn4_clrsets', 'Rainbow for Selected Sets<br><span style="padding-left:1.5em;">in "Analysis > Defined Sets"</span>', undefined, undefined, 2);
                html += "</ul>";
                html += "</li>";
            }

            html += this.getMenuText('mn4_clrResiduewrap', 'Residue', 'icn3d-menupd', 1, 1);
            html += "<ul>";
            html += this.getRadio('mn4_clr', 'mn4_clrResidue', 'Default', undefined, 1, 2);
            html += this.getRadio('mn4_clr', 'mn4_clrResidueCustom', 'Custom', undefined, undefined, 2);
            html += "</ul>";

            html += this.getRadio('mn4_clr', 'mn4_clrAtom', 'Atom', undefined, 1, 1);

            if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined) {
              html += this.getRadio('mn4_clr', 'mn4_clrIdentity', 'Identity', true, undefined, 2);
              html += this.getRadio('mn4_clr', 'mn4_clrConserved', 'Conservation', undefined, undefined, 2);
            }
            else if(me.cfg.blast_rep_id !== undefined) {
              html += this.getRadio('mn4_clr', 'mn4_clrIdentity', 'Identity', undefined, undefined, 2);
              html += this.getRadio('mn4_clr', 'mn4_clrConserved', 'Conservation', true, undefined, 2);
            }

            //if(me.cfg.afid) html += this.getRadio('mn4_clr', 'mn4_clrConfidence', 'AF Confidence');
            //if(!me.cfg.mmtfid && !me.cfg.pdbid && !me.cfg.opmid && !me.cfg.mmdbid && !me.cfg.gi && !me.cfg.uniprotid && !me.cfg.blast_rep_id && !me.cfg.cid && !me.cfg.mmcifid && !me.cfg.align && !me.cfg.chainalign) {
                html += this.getRadio('mn4_clr', 'mn4_clrConfidence', 'AlphaFold<br><span style="padding-left:1.5em;">Confidence</span>', undefined, 1, 1);
            //}
        }
        else {
            //if(!me.cfg.hidelicense) html += this.getRadio('mn4_clr', 'mn1_delphi2', 'DelPhi<br><span style="padding-left:1.5em;">Potential ' + me.htmlCls.licenseStr + '</span>');
            html += this.getRadio('mn4_clr', 'mn4_clrAtom', 'Atom', true, 1, 1);
        }

        html += this.getMenuSep();

        html += this.getLink('mn4_clrSave', 'Save Color', undefined, 1);
        html += this.getLink('mn4_clrApplySave', 'Apply Saved Color', undefined, 1);

        html += "<li><br/></li>";
        html += "</ul>";

        return html;
    }

    //Set the menu "Surface" at the top of the viewer.
    setMenu5() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<div class='icn3d-menu'>";
        html += "<accordion id='" + me.pre + "accordion5' class='icn3d-accordion'>";
        html += "<h3 id='" + me.pre + "analysis' style='font-size:1.2em'>&nbsp;Analysis</h3>";
        html += "<div>";

        html += this.setMenu5_base();

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        return html;
    }

    setMenu5_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<ul class='icn3d-mn-item'>";

        if(me.cfg.cid === undefined) {
            html += this.getLink('mn6_selectannotations', 'Seq. & Annotations ' + me.htmlCls.wifiStr, 1, 1);

            //if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined) { // || ic.bRealign || ic.bSymd || ic.bInputfile) {
                html += this.getLink('mn2_alignment', 'Aligned Seq. ' + me.htmlCls.wifiStr, undefined, 1);
            //}

            if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined || me.cfg.blast_rep_id !== undefined || me.cfg.align !== undefined || me.cfg.chainalign !== undefined) {
              html += this.getLink('mn2_2ddgm', '2D Diagram ' + me.htmlCls.wifiStr, 1, 1);
            }

            html += this.getMenuText('2dctnwrap', '2D Cartoon', undefined, undefined, 1);
            html += "<ul>";
            html += this.getLink('2dctn_chain', 'Chain Level', undefined, 2);
            html += this.getLink('2dctn_domain', 'Domain Level', undefined, 2);
            html += this.getLink('2dctn_secondary', 'Helix/Sheet Level', undefined, 2);
            html += "</ul>";
            html += "</li>";

            html += this.getLink('definedsets2', 'Defined Sets', 1, 1);

            html += this.getMenuSep();

            html += this.getLink('mn6_hbondsYes', 'Interactions', 1, 1);

            html += this.getLink('mn6_contactmap', 'Contact Map', undefined, 1);

            if(!me.cfg.notebook) {
                html += this.getLink('mn1_mutation', 'Mutation ' + me.htmlCls.wifiStr, 1, 1);
            }

            //html += this.getMenuSep();
        }

        if(!me.cfg.notebook && !me.cfg.hidelicense) {
            html += this.getMenuText('mn1_delphiwrap', 'DelPhi Potential', undefined, 1, 1);

            html += "<ul>";       
                html += this.getLink('mn1_delphi', 'DelPhi Potential ' + me.htmlCls.licenseStr, 1, 2);    

                html += this.getMenuText('mn1_phiwrap', 'Load PQR/Phi', undefined, undefined, 2);
                html += "<ul>";
                html += this.getLink('mn1_phi', 'Local PQR/Phi/Cube File', undefined, 3);
                html += this.getLink('mn1_phiurl', 'URL PQR/Phi/Cube File', undefined, 3);
                html += "</ul>";
                html += "</li>";
                html += this.getLink('delphipqr', 'Download PQR', undefined, 2);
            html += "</ul>";
            html += "</li>";

            //html += this.getMenuSep();
        }

        html += this.getMenuSep();

        html += this.getMenuText('mn6_distancewrap', 'Distance', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn6_distance', 'mn6_distanceYes', 'between Two Atoms', undefined, 1, 2);
        html += this.getRadio('mn6_distance', 'mn6_distTwoSets', 'between Two Sets', undefined, undefined, 2);
        html += this.getRadio('mn6_distance', 'mn6_distManySets', 'Among Many Sets', undefined, undefined, 2);
        html += this.getRadio('mn6_distance', 'mn6_distanceNo', 'Hide', true, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getLink('mn6_area', 'Surface Area', 1, 1);

        html += this.getMenuText('mn6_addlabelwrap', 'Label', undefined, 1, 1);
        html += "<ul>";
        html += this.getRadio('mn6_addlabel', 'mn6_addlabelYes', 'by Picking Atoms', undefined, undefined, 2);
        html += this.getRadio('mn6_addlabel', 'mn6_addlabelSelection', 'per Selection', undefined, undefined, 2);
        html += this.getRadio('mn6_addlabel', 'mn6_addlabelAtoms', 'per Atom', undefined, undefined, 2);
        html += this.getRadio('mn6_addlabel', 'mn6_addlabelElements', 'per Atom Element', undefined, 1, 2);
        if(me.cfg.cid === undefined) {
            html += this.getRadio('mn6_addlabel', 'mn6_addlabelResidues', 'per Residue', undefined, 1, 2);
            html += this.getRadio('mn6_addlabel', 'mn6_addlabelResnum', 'per Residue & Number', undefined, 1, 2);
            html += this.getRadio('mn6_addlabel', 'mn6_addlabelChains', 'per Chain', undefined, undefined, 2);
            html += this.getRadio('mn6_addlabel', 'mn6_addlabelTermini', 'N- & C-Termini', undefined, 1, 2);
        }

        html += this.getMenuSep();
        html += this.getRadio('mn6_addlabel', 'mn6_labelColor', 'Change Label Color', undefined, 1, 2);
        html += this.getRadio('mn6_addlabel', 'mn6_addlabelNo', 'Remove', true, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('labelscalewrap', 'Label Scale', undefined, 1, 1);
        html += "<ul>";

        for(let i = 1; i <= 4; ++i) {
            let twoi = 2 * i;
            html += this.getRadio('mn6_labelscale', 'mn6_labelscale0' + twoi, '0.' + twoi, undefined, 1, 2);
        }

        for(let i = 1; i <= 5; ++i) {
            if(i == 1) {
                html += this.getRadio('mn6_labelscale', 'mn6_labelscale' + i + '0', i + '.0', true, 1, 2);
            }
            else {
                html += this.getRadio('mn6_labelscale', 'mn6_labelscale' + i + '0', i + '.0', undefined, 1, 2);
            }
        }

        html += "</ul>";
        html += "</li>";

        html += this.getMenuSep();

        if(me.cfg.cid === undefined) {
            html += this.getMenuText('mn6_chemicalbindingwrap', 'Chem. Binding', undefined, undefined, 1);
            html += "<ul>";
            html += this.getRadio('mn6_chemicalbinding', 'mn6_chemicalbindingshow', 'Show', undefined, undefined, 2);
            html += this.getRadio('mn6_chemicalbinding', 'mn6_chemicalbindinghide', 'Hide', true, undefined, 2);
            html += "</ul>";
            html += "</li>";

            html += this.getMenuText('mn6_ssbondswrap', 'Disulfide Bonds', undefined, 1, 1);
            html += "<ul>";
            html += this.getRadio('mn6_ssbonds', 'mn6_ssbondsYes', 'Show', true, 1, 2);
            html += this.getRadio('mn6_ssbonds', 'mn6_ssbondsExport', 'Export Pairs', undefined, undefined, 2);
            html += this.getRadio('mn6_ssbonds', 'mn6_ssbondsNo', 'Hide', undefined, 1, 2);
            html += "</ul>";
            html += "</li>";

            html += this.getMenuText('mn6_clbondswrap', 'Cross-Linkages', undefined, undefined, 1);
            html += "<ul>";
            html += this.getRadio('mn6_clbonds', 'mn6_clbondsYes', 'Show', true, undefined, 2);
            html += this.getRadio('mn6_clbonds', 'mn6_clbondsExport', 'Export Pairs', undefined, undefined, 2);
            html += this.getRadio('mn6_clbonds', 'mn6_clbondsNo', 'Hide', undefined, undefined, 2);
            html += "</ul>";
            html += "</li>";

            let bOnePdb = me.cfg.mmtfid !== undefined || me.cfg.pdbid !== undefined || me.cfg.opmid !== undefined || me.cfg.mmcifid !== undefined || me.cfg.mmdbid !== undefined || me.cfg.mmdbafid !== undefined || me.cfg.gi !== undefined || me.cfg.blast_rep_id !== undefined;

            if(bOnePdb) {
              html += this.getMenuText('assemblyWrapper', 'Assembly', undefined, 1, 1);
              html += "<ul>";

              if(!me.cfg.bu) {
                html += this.getRadio('mn6_assembly', 'mn6_assemblyYes', 'Biological Assembly', undefined, 1, 2);
                html += this.getRadio('mn6_assembly', 'mn6_assemblyNo', 'Asymmetric Unit', true, 1, 2);
              }
              else {
                html += this.getRadio('mn6_assembly', 'mn6_assemblyYes', 'Biological Assembly', true, 1, 2);
                html += this.getRadio('mn6_assembly', 'mn6_assemblyNo', 'Asymmetric Unit', undefined, 1, 2);
              }

              html += "</ul>";
              html += "</li>";
            }

            html += this.getMenuText('mn6_symmetrywrap', 'Symmetry', undefined, undefined, 1);

            html += "<ul>";
            if(bOnePdb) html += this.getLink('mn6_symmetry', 'from PDB(precalculated) ' + me.htmlCls.wifiStr, undefined, 2);

            html += this.getLink('mn6_symd', 'from SymD(Dynamic) ' + me.htmlCls.wifiStr, undefined, 2);
            html += this.getLink('mn6_clear_sym', 'Clear SymD Symmetry', undefined, 2);
            html += this.getLink('mn6_axes_only', 'Show Axes Only', undefined, 2);

            html += "</ul>";
            html += "</li>";

            html += this.getMenuText('mn6_igrefwrap', 'Ref. Number', undefined, undefined, 1);

            html += "<ul>";
/*
            html += this.getLink('mn6_igrefYes', 'Show Ig Ref. Number', undefined, 2);
            html += this.getLink('mn6_igrefNo', 'Hide Ig Ref. Number', undefined, 2);

            html += this.getMenuSep();
*/
            html += this.getLink('mn6_customref', 'Custom Ref. Number', undefined, 2);
            html += "</ul>";
            html += "</li>";

            html += this.getMenuSep();
        }

        html += this.getLink('mn6_yournote', 'Window Title', undefined, 1);

        if(me.cfg.cid !== undefined) {
            html += this.getMenuText('mn1_linkwrap', 'Links', undefined, undefined, 1);
            
            html += "<ul>";
            html += this.getLink('mn1_link_structure', 'Compound Summary ' + me.htmlCls.wifiStr, undefined, 2);
            html += this.getLink('mn1_link_vast', 'Similar Compounds ' + me.htmlCls.wifiStr, undefined, 2);
            html += this.getLink('mn1_link_bind', 'Structures Bound ' + me.htmlCls.wifiStr, undefined, 2);
            html += "</ul>";
            html += "</li>";
        }
        else {
            html += this.getMenuText('mn1_linkwrap', 'Links', undefined, undefined, 1);
            html += "<ul>";
            html += this.getLink('mn1_link_structure', 'Structure Summary ' + me.htmlCls.wifiStr, undefined, 2);
            html += this.getLink('mn1_link_vast', 'Similar Structures ' + me.htmlCls.wifiStr, undefined, 2);
            html += this.getLink('mn1_link_pubmed', 'Literature ' + me.htmlCls.wifiStr, undefined, 2);
            html += this.getLink('mn1_link_protein', 'Protein ' + me.htmlCls.wifiStr, undefined, 2);
            //html += this.getLink('mn1_link_gene', 'Gene');
            //html += this.getLink('mn1_link_chemicals', 'Chemicals');
            html += "</ul>";
            html += "</li>";
        }

        html += "<li><br/></li>";

        html += "</ul>";

        return html;
    }

    //Set the menu "Other" at the top of the viewer.
    setMenu6() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<div class='icn3d-menu'>";
        html += "<accordion id='" + me.pre + "accordion6' class='icn3d-accordion'>";
        html += "<h3>Help</h3>";
        html += "<div>";

        html += this.setMenu6_base();

        html += "</div>";
        html += "</accordion>";
        html += "</div>";

        return html;
    }

    setMenu6_base() { let me = this.icn3dui;
        if(me.bNode) return '';

        let html = "";

        html += "<ul class='icn3d-mn-item'>";

        html += this.getMenuUrl('abouticn3d', me.htmlCls.baseUrl + "icn3d/icn3d.html#about", "About iCn3D<span style='font-size:0.9em'> " + me.REVISION + "</span>", 1, 1);

        html += this.getMenuUrl('gallery', me.htmlCls.baseUrl + "icn3d/icn3d.html#gallery", "Live Gallery " + me.htmlCls.wifiStr, 1, 1);
        html += this.getMenuUrl('video', me.htmlCls.baseUrl + "icn3d/icn3d.html#videos", "iCn3D Videos", 1, 1);

        html += this.getMenuText('mn6_faq', 'FAQ', undefined, 1, 1);

        html += "<ul>";
        html += this.getMenuUrl('faq_viewstru', me.htmlCls.baseUrl + "icn3d/icn3d.html#viewstru", "View structure", 1, 2);
        html += this.getMenuUrl('faq_tfstru', me.htmlCls.baseUrl + "icn3d/icn3d.html#tfstru", "Transform Structure", 1, 2);
        html += this.getMenuUrl('faq_selsubset', me.htmlCls.baseUrl + "icn3d/icn3d.html#selsubset", "Select Subsets", 1, 2);
        html += this.getMenuUrl('faq_stylecolor', me.htmlCls.baseUrl + "icn3d/icn3d.html#changestylecolor", "Change Style/Color", 1, 2);
        html += this.getMenuUrl('faq_savework', me.htmlCls.baseUrl + "icn3d/icn3d.html#saveview", "Save Work", 1, 2);
        html += this.getMenuUrl('faq_showanno', me.htmlCls.baseUrl + "icn3d/icn3d.html#showanno", "Show Annotations", 1, 2);
        html += this.getMenuUrl('faq_exportanno', me.htmlCls.baseUrl + "icn3d/icn3d.html#exportanno", "Export Annotations", 1, 2);
        html += this.getMenuUrl('faq_interanal', me.htmlCls.baseUrl + "icn3d/icn3d.html#interanalysis", "Interaction Analysis", 1, 2);
        html += this.getMenuUrl('faq_mutanal', me.htmlCls.baseUrl + "icn3d/icn3d.html#mutationanalysis", "Mutation Analysis", 1, 2);
        html += this.getMenuUrl('faq_elecpot', me.htmlCls.baseUrl + "icn3d/icn3d.html#elecpot", "Electrostatic Pot.", 1, 2);
        html += this.getMenuUrl('faq_simipdb', me.htmlCls.baseUrl + "icn3d/icn3d.html#simivast", "Similar PDB", 1, 2);
        html += this.getMenuUrl('faq_simialphapdb', me.htmlCls.baseUrl + "icn3d/icn3d.html#simifoldseek", "Similar AlphaFold/PDB", 1, 2);
        html += this.getMenuUrl('faq_alnstru', me.htmlCls.baseUrl + "icn3d/icn3d.html#alignmul", "Align Multiple Structures", 1, 2);
        html += this.getMenuUrl('faq_batchanal', me.htmlCls.baseUrl + "icn3d/icn3d.html#batchanalysis", "Batch Analysis", 1, 2);
        html += this.getMenuUrl('faq_embedicn3d', me.htmlCls.baseUrl + "icn3d/icn3d.html#embedicn3d", "Embed iCn3D", 1, 2);
        html += "</ul>";
        html += "</li>";

        //html += liStr + "https://www.ncbi.nlm.nih.gov/structure' target='_blank'>Search Structure " + me.htmlCls.wifiStr + "</a></li>";
        //html += liStr + me.htmlCls.baseUrl + "icn3d/icn3d.html#citing' target='_blank'>Citing iCn3D</a></li>";
        html += this.getMenuUrl('citing', me.htmlCls.baseUrl + "icn3d/icn3d.html#citing", "Citing iCn3D", undefined, 1);

        html += this.getMenuText('mn6_source', 'Source Code', undefined, 1, 1);
        html += "<ul>";
        html += this.getMenuUrl('github', "https://github.com/ncbi/icn3d", "GitHub (browser) " + me.htmlCls.wifiStr, 1, 2);
        html += this.getMenuUrl('npm', "https://www.npmjs.com/package/icn3d", "npm (Node.js) " + me.htmlCls.wifiStr, 1, 2);
        html += this.getMenuUrl('notebook', "https://pypi.org/project/icn3dpy", "Jupyter Notebook " + me.htmlCls.wifiStr, 1, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuText('mn6_develop', 'Develop', undefined, undefined, 1);
        html += "<ul>";
        html += this.getMenuUrl('dev_embedicn3d2', me.htmlCls.baseUrl + "icn3d/icn3d.html#HowToUse", "Embed iCn3D", undefined, 2);
        html += this.getMenuUrl('dev_urlpara', me.htmlCls.baseUrl + "icn3d/icn3d.html#parameters", "URL Parameters", undefined, 2);
        html += this.getMenuUrl('dev_command', me.htmlCls.baseUrl + "icn3d/icn3d.html#commands", "Commands", undefined, 2);

        html += this.getMenuUrl('dev_datastru', me.htmlCls.baseUrl + "icn3d/icn3d.html#datastructure", "Data Structure", undefined, 2);
        html += this.getMenuUrl('dev_classstru', me.htmlCls.baseUrl + "icn3d/icn3d.html#classstructure", "Class Structure", undefined, 2);
        html += this.getMenuUrl('dev_addclass', me.htmlCls.baseUrl + "icn3d/icn3d.html#addclass", "Add New Classes", undefined, 2);
        html += this.getMenuUrl('dev_modfunc', me.htmlCls.baseUrl + "icn3d/icn3d.html#modifyfunction", "Modify Functions", undefined, 2);
        html += this.getMenuUrl('dev_restful', me.htmlCls.baseUrl + "icn3d/icn3d.html#restfulapi", "RESTful APIs", undefined, 2);
        html += this.getMenuUrl('dev_contributor', me.htmlCls.baseUrl + "icn3d/icn3d.html#contributors", "Codeathon Contributors", undefined, 2);
        html += "</ul>";
        html += "</li>";

        html += this.getMenuUrl('helpdoc', me.htmlCls.baseUrl + "icn3d/docs/icn3d_help.html", "Help Doc " + me.htmlCls.wifiStr, 1, 1);

        html += this.getMenuSep();

        html += this.getMenuText('mn6_tfhint', 'Transform Hints', undefined, 1, 1);
        html += "<ul>";
        html += this.getMenuText('mn6_rotate', 'Rotate', undefined, 1, 2);
        html += "<ul>";
        html += "<li>Left Mouse (Click & Drag)</li>";
        html += "<li>Key l: Left</li>";
        html += "<li>Key j: Right</li>";
        html += "<li>Key i: Up</li>";
        html += "<li>Key m: Down</li>";
        html += "<li>Shift + Key l: Left 90&deg;</li>";
        html += "<li>Shift + Key j: Right 90&deg;</li>";
        html += "<li>Shift + Key i: Up 90&deg;</li>";
        html += "<li>Shift + Key m: Down 90&deg;</li>";
        html += "</ul>";
        html += "</li>";
        html += this.getMenuText('mn6_zoom', 'Zoom', undefined, 1, 2);
        html += "<ul>";
        html += "<li>Middle Mouse <br>(Pinch & Spread)</li>";
        html += "<li>Key z: Zoom in</li>";
        html += "<li>Key x: Zoom out</li>";
        html += "</ul>";
        html += "</li>";
        html += this.getMenuText('mn6_translate', 'Translate', undefined, 1, 2);
        html += "<ul>";
        html += "<li>Right Mouse <br>(Two Finger Click & Drag)</li>";
        html += "</ul>";
        html += "</li>";
        html += "</ul>";
        html += "</li>";

        html += this.getMenuUrl('selhints', me.htmlCls.baseUrl + "icn3d/icn3d.html#selsubset", "Selection Hints", undefined, 1);
        html += this.getMenuUrl('helpdesk', "https://support.nlm.nih.gov/support/create-case/", "Write to Help Desk", 1, 1);

        html += "<li><br/></li>";
        html += "</ul>";

        return html;
    }

    //Hide the menu at the top and just show the canvas. "width" and "height" are the width and height of the canvas.
    hideMenu() { let me = this.icn3dui;
      if(me.bNode) return;

      if($("#" + me.pre + "mnlist")[0] !== undefined) $("#" + me.pre + "mnlist")[0].style.display = "none";
      if($("#" + me.pre + "mnLogSection")[0] !== undefined) $("#" + me.pre + "mnLogSection")[0].style.display = "none";
      if($("#" + me.pre + "cmdlog")[0] !== undefined) $("#" + me.pre + "cmdlog")[0].style.display = "none";
      $("#" + me.pre + "title")[0].style.margin = "10px 0 0 10px";
    }

    //Show the menu at the top and the canvas. "width" and "height" are the width and height of the canvas.
    showMenu() { let me = this.icn3dui;
      if(me.bNode) return;

      if($("#" + me.pre + "mnlist")[0] !== undefined) $("#" + me.pre + "mnlist")[0].style.display = "block";
      if($("#" + me.pre + "mnLogSection")[0] !== undefined) $("#" + me.pre + "mnLogSection")[0].style.display = "block";
      if($("#" + me.pre + "cmdlog")[0] !== undefined) $("#" + me.pre + "cmdlog")[0].style.display = "block";
      //if($("#" + me.pre + "title")[0] !== undefined) $("#" + me.pre + "title")[0].style.display = "block";
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Dialog {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    //Open a dialog to input parameters. "id" is the id of the div section holding the html content.
    //"title" is the title of the dialog. The dialog can be out of the viewing area.
    openDlg(id, title) {  let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        id = me.pre + id;

        if(!me.cfg.notebook) {
            this.openDlgRegular(id, title);
        }
        else {
            this.openDlgNotebook(id, title);
        }

        if(!me.htmlCls.themecolor) me.htmlCls.themecolor = 'blue';

        me.htmlCls.setMenuCls.setTheme(me.htmlCls.themecolor);
    }

    addSaveButton(id) {  let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        // adda save button
        if(this.dialogHashSave === undefined || !this.dialogHashSave.hasOwnProperty(id)) {
            $("#" + id).parent().children('.ui-dialog-titlebar')
            .append("<div pid='" + id + "' class='icn3d-saveicon ui-icon ui-icon-disk' title='Save as an HTML file' style='background-color:white; background-image: url(&quot;https://www.ncbi.nlm.nih.gov/Structure/icn3d/lib/images/ui-icons_228ef1_256x240.png&quot;);'></div>");

            if(this.dialogHashSave === undefined) this.dialogHashSave = {};
            this.dialogHashSave[id] = 1;
        }
    }

    addHideButton(id) {  let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        // adda save button
        if(this.dialogHashHide === undefined || !this.dialogHashHide.hasOwnProperty(id)) {
            $("#" + id).parent().children('.ui-dialog-titlebar')
            .append("<div pid='" + id + "' class='icn3d-hideicon ui-icon ui-icon-arrowthick-2-ne-sw' title='Resize the window' style='background-color:white; background-image: url(&quot;https://www.ncbi.nlm.nih.gov/Structure/icn3d/lib/images/ui-icons_228ef1_256x240.png&quot;);'></div>");

            if(this.dialogHashHide === undefined) this.dialogHashHide = {};
            this.dialogHashHide[id] = 1;
        }
    }

    getDialogStatus() {  let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        let status = {};
        let id2flag = {};

        // determine whether dialogs initilaized
        let bSelectannotationsInit = $('#' + me.pre + 'dl_selectannotations').hasClass('ui-dialog-content'); // initialized
        let bGraph = $('#' + me.pre + 'dl_graph').hasClass('ui-dialog-content'); // initialized
        let bLineGraph = $('#' + me.pre + 'dl_linegraph').hasClass('ui-dialog-content'); // initialized
        let bScatterplot = $('#' + me.pre + 'dl_scatterplot').hasClass('ui-dialog-content'); // initialized
        let bContactmap = $('#' + me.pre + 'dl_contactmap').hasClass('ui-dialog-content'); // initialized
        let bAlignerrormap = $('#' + me.pre + 'dl_alignerrormap').hasClass('ui-dialog-content'); // initialized
        let bTable = $('#' + me.pre + 'dl_interactionsorted').hasClass('ui-dialog-content'); // initialized
        let bAlignmentInit = $('#' + me.pre + 'dl_alignment').hasClass('ui-dialog-content'); // initialized
        let bTwoddgmInit = $('#' + me.pre + 'dl_2ddgm').hasClass('ui-dialog-content'); // initialized
        let bTwodctnInit = $('#' + me.pre + 'dl_2dctn').hasClass('ui-dialog-content'); // initialized
        let bSetsInit = $('#' + me.pre + 'dl_definedsets').hasClass('ui-dialog-content'); // initialized

        status.bSelectannotationsInit2 = false, status.bGraph2 = false, status.bLineGraph2 = false;
        status.bScatterplot2 = false, status.bTable2 = false, status.bAlignmentInit2 = false;
        status.bTwoddgmInit2 = false, status.bTwodctnInit2 = false, status.bSetsInit2 = false;

        id2flag.dl_selectannotations = 'bSelectannotationsInit2';
        id2flag.dl_graph = 'bGraph2';
        id2flag.dl_linegraph = 'bLineGraph2';
        id2flag.dl_scatterplot = 'bScatterplot2';	
        id2flag.dl_contactmap = 'bContactmap2';
        id2flag.dl_alignerrormap = 'bAlignerrormap2';
        id2flag.dl_interactionsorted = 'bTable2';
        id2flag.dl_alignment = 'bAlignmentInit2';
        id2flag.dl_2ddgm = 'bTwoddgmInit2';
        id2flag.dl_2dctn = 'bTwodctnInit2';
        id2flag.dl_definedsets = 'bSetsInit2';

        if(bSelectannotationsInit) status.bSelectannotationsInit2 = $('#' + me.pre + 'dl_selectannotations').dialog( 'isOpen' );
        if(bGraph) status.bGraph2 = $('#' + me.pre + 'dl_graph').dialog( 'isOpen' );
        if(bLineGraph) status.bLineGraph2 = $('#' + me.pre + 'dl_linegraph').dialog( 'isOpen' );
        if(bScatterplot) status.bScatterplot2 = $('#' + me.pre + 'dl_scatterplot').dialog( 'isOpen' );
        if(bContactmap) status.bContactmap2 = $('#' + me.pre + 'dl_contactmap').dialog( 'isOpen' );
        if(bAlignerrormap) status.bAlignerror2 = $('#' + me.pre + 'dl_alignerrormap').dialog( 'isOpen' );
        if(bTable) status.bTable2 = $('#' + me.pre + 'dl_interactionsorted').dialog( 'isOpen' );
        if(bAlignmentInit) status.bAlignmentInit2 = $('#' + me.pre + 'dl_alignment').dialog( 'isOpen' );
        if(bTwoddgmInit) status.bTwoddgmInit2 = $('#' + me.pre + 'dl_2ddgm').dialog( 'isOpen' );
        if(bTwodctnInit) status.bTwodctnInit2 = $('#' + me.pre + 'dl_2dctn').dialog( 'isOpen' );
        if(bSetsInit) status.bSetsInit2 = $('#' + me.pre + 'dl_definedsets').dialog( 'isOpen' );

        return {status: status, id2flag: id2flag};
    }

    openDlgHalfWindow(id, title, dialogWidth, bForceResize) {  let me = this.icn3dui, ic = me.icn3d;
        if(me.bNode) return;

        let thisClass = this;

        let twoddgmWidth = me.htmlCls.width2d + 20;

        //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - dialogWidth - me.htmlCls.LESSWIDTH, me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT, bForceResize);
        ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - dialogWidth, me.htmlCls.HEIGHT, bForceResize);

        //height = me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT;
        let height = me.htmlCls.HEIGHT;
        let width = dialogWidth;

        let position;
        if(me.cfg.showmenu && !me.utilsCls.isMobile() && !me.cfg.mobilemenu) {
            position ={ my: "left top", at: "right top+40", of: "#" + me.pre + "viewer", collision: "none" };
        }
        else {
            position ={ my: "left top", at: "right top", of: "#" + me.pre + "viewer", collision: "none" };
        }

        // disable resize
        me.cfg.resize = false;

        window.dialog = $( "#" + id ).dialog({
          autoOpen: true,
          title: title,
          height: height,
          width: width,
          modal: false,
          position: position,
          close: function(e) {
              let result = thisClass.getDialogStatus();
              let status = result.status;
              let id2flag = result.id2flag;

              // check the condition when all the rest dialogs are closed
              let bCheckAll = false;
              for(let idname in id2flag) {
                let bCheckRest = (id === me.pre + idname);
                for(let idstatus in status) {
                    // just check the rest, not itself
                    if(status.hasOwnProperty(idstatus)) continue;
                    bCheckRest = bCheckRest && !status[idstatus];
                }
                bCheckAll = bCheckAll || bCheckRest;
              }

              if(bCheckAll) {
                  if(status.bTwoddgmInit2 || status.bTwodctnInit2 || status.bSetsInit2) {
                      //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH - twoddgmWidth, me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT, true);
                      let canvasWidth = me.utilsCls.isMobile() ? me.htmlCls.WIDTH : me.htmlCls.WIDTH - twoddgmWidth;
                      ic.resizeCanvasCls.resizeCanvas(canvasWidth, me.htmlCls.HEIGHT, true);

                      if(status.bTwoddgmInit2) thisClass.openDlg2Ddgm(me.pre + 'dl_2ddgm', undefined, status.bSetsInit2);
                      if(status.bTwodctnInit2) thisClass.openDlg2Ddgm(me.pre + 'dl_2dctn', undefined, status.bSetsInit2);
                      if(status.bSetsInit2) thisClass.openDlg2Ddgm(me.pre + 'dl_definedsets');
                  }
                  else {
                      //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH, me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT, true);
                      ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH, me.htmlCls.HEIGHT, true);
                  }
              }
          },
          resize: function(e) {
              if(id == me.pre + 'dl_selectannotations') {
                  ic.annotationCls.hideFixedTitle();
              }
              else if(id == me.pre + 'dl_graph') {
                  let width = $("#" + id).width();
                  let height = $("#" + id).height();

                  d3.select("#" + me.svgid).attr("width", width).attr("height", height);
              }
              else if(id == me.pre + 'dl_linegraph' || id == me.pre + 'dl_scatterplot' || id == me.pre + 'dl_contactmap' || id == me.pre + 'dl_alignerrormap') {
                  let oriWidth =(status.bTwoddgmInit2 || status.bSetsInit2) ?(me.htmlCls.WIDTH - twoddgmWidth)/2 : me.htmlCls.WIDTH / 2;
                  let ratio = $("#" + id).width() / oriWidth;

                  if(id == me.pre + 'dl_linegraph') {
                      let width = ic.linegraphWidth * ratio;
                      $("#" + me.linegraphid).attr("width", width);
                  }
                  else if(id == me.pre + 'dl_scatterplot') {
                      let width = ic.scatterplotWidth * ratio;
                      $("#" + me.scatterplotid).attr("width", width);
                  }
                  else if(id == me.pre + 'dl_contactmap') {
                      let width = ic.contactmapWidth * ratio;
                      $("#" + me.contactmapid).attr("width", width);
                  }
                  else if(id == me.pre + 'dl_alignerrormap') {
                    let width = ic.alignerrormapWidth * ratio;
                    $("#" + me.alignerrormapid).attr("width", width);
                }
              }
          }
        });

        this.addSaveButton(id);
        this.addHideButton(id);
    }

    openDlg2Ddgm(id, inHeight, bDefinedSets) {  let me = this.icn3dui, ic = me.icn3d;
        if(me.bNode) return;

        let thisClass = this;

        let twoddgmWidth = me.htmlCls.width2d + 20;
        let at, title;
        if(id === me.pre + 'dl_definedsets') {
            at = "right top";
            title = 'Select sets';
        }
        else if(id === me.pre + 'dl_2ddgm' || id === me.pre + 'dl_2dctn') {
            if(bDefinedSets) {
                at = "right top+240";
            }
            else {
                at = "right top";
            }

            title = (id === me.pre + 'dl_2ddgm') ? '2D Diagram' : '2D Cartoon';
        }

        //var position ={ my: "left top", at: at, of: "#" + me.pre + "canvas", collision: "none" }
        let position ={ my: "left top+" + me.htmlCls.MENU_HEIGHT, at: at, of: "#" + me.pre + "viewer", collision: "none" };

        let height = 'auto';

        window.dialog = $( '#' + id ).dialog({
          autoOpen: true,
          title: title,
          height: height,
          width: twoddgmWidth,
          modal: false,
          position: position,
          close: function(e) {
              let status = thisClass.getDialogStatus().status;

              if((!status.bSelectannotationsInit2) &&(!status.bGraph2) &&(!status.bLineGraph2) &&(!status.bScatterplot2) &&(!status.bTable2) &&(!status.bAlignmentInit2) ) {
                    //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH, me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT, true);
                    ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH, me.htmlCls.HEIGHT, true);
              }
          },
          resize: function(e, ui) {
              if(id == me.pre + 'dl_2dctn') {
                ic.resizeRatioX = ui.size.width / me.htmlCls.width2d; //ui.originalSize.width;
                ic.resizeRatioY = ui.size.height / (me.htmlCls.width2d + 70); //ui.originalSize.height;
              }
          },
          resizeStop: function(e, ui) {
            ic.resizeRatioX = ui.size.width / me.htmlCls.width2d; //ui.originalSize.width;
            ic.resizeRatioY = ui.size.height / (me.htmlCls.width2d + 70); //ui.originalSize.height;
          }
        });

        this.addSaveButton(id);
        this.addHideButton(id);
    }

    openDlgRegular(id, title) {  let me = this.icn3dui, ic = me.icn3d;
        if(me.bNode) return;

        let width = 400, height = 150;
        let twoddgmWidth = me.htmlCls.width2d + 20;

        let status = this.getDialogStatus().status;

        if(id === me.pre + 'dl_selectannotations' || id === me.pre + 'dl_graph' || id === me.pre + 'dl_linegraph' || id === me.pre + 'dl_scatterplot' || id === me.pre + 'dl_contactmap'  || id === me.pre + 'dl_alignerrormap' || id === me.pre + 'dl_interactionsorted' || id === me.pre + 'dl_alignment') {
            //var dialogWidth = 0.5 *(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH) - twoddgmWidth * 0.5;
            let dialogWidth = 0.5 *(me.htmlCls.WIDTH) - twoddgmWidth * 0.5;

            //if(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH >= me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT) {
            if(me.htmlCls.WIDTH >= me.htmlCls.HEIGHT) {
                this.openDlgHalfWindow(id, title, dialogWidth, true);

                if(status.bTwoddgmInit2 || status.bTwodctnInit2 || status.bSetsInit2) {
                    ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - dialogWidth - twoddgmWidth, me.htmlCls.HEIGHT, true);

                    if(status.bTwoddgmInit2) this.openDlg2Ddgm(me.pre + 'dl_2ddgm', undefined, status.bSetsInit2);
                    if(status.bTwodctnInit2) this.openDlg2Ddgm(me.pre + 'dl_2dctn', undefined, status.bSetsInit2);
                    if(status.bSetsInit2) this.openDlg2Ddgm(me.pre + 'dl_definedsets');
                }
            }
            else {
                //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH,(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT) * 0.5, true);
                ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH,(me.htmlCls.HEIGHT) * 0.5, true);

                //height =(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT) * 0.5;
                height =(me.htmlCls.HEIGHT) * 0.5;

                //width = me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH;
                width = me.htmlCls.WIDTH;

                let position ={ my: "left top", at: "left bottom+32", of: "#" + me.pre + "canvas", collision: "none" };

                window.dialog = $( "#" + id ).dialog({
                  autoOpen: true,
                  title: title,
                  height: height,
                  width: width,
                  modal: false,
                  position: position,
                  close: function(e) {
                      if((id === me.pre + 'dl_selectannotations' &&(!status.bAlignmentInit2) &&(!status.bGraph2) &&(!status.bTable2) &&(!status.bLineGraph2) &&(!status.bScatterplot2) &&(!status.bContactmap2) &&(!status.bAlignerrormap2))
                        ||(id === me.pre + 'dl_graph' &&(!status.bSelectannotationsInit2) &&(!status.bAlignmentInit2) &&(!status.bTable2) &&(!status.bLineGraph2) &&(!status.bScatterplot2) &&(!status.bContactmap2) &&(!status.bAlignerrormap2))
                        ||(id === me.pre + 'dl_alignment' &&(!status.bSelectannotationsInit2) &&(!status.bGraph2) &&(!status.bTable2) &&(!status.bLineGraph2) &&(!status.bScatterplot2) &&(!status.bContactmap2) &&(!status.bAlignerrormap2))
                        ||(id === me.pre + 'dl_interactionsorted' &&(!status.bSelectannotationsInit2) &&(!status.bGraph2) &&(!status.bAlignmentInit2) &&(!status.bLineGraph2) &&(!status.bScatterplot2) &&(!status.bContactmap2) &&(!status.bAlignerrormap2))
                        ||(id === me.pre + 'dl_linegraph' &&(!status.bSelectannotationsInit2) &&(!status.bGraph2) &&(!status.bAlignmentInit2) &&(!status.bTable2) &&(!status.bScatterplot2) &&(!status.bContactmap2) &&(!status.bAlignerrormap2))
                        ||(id === me.pre + 'dl_scatterplot' &&(!status.bSelectannotationsInit2) &&(!status.bGraph2) &&(!status.bAlignmentInit2) &&(!status.bTable2) &&(!status.bLineGraph2) &&(!status.bContactmap2) &&(!status.bAlignerrormap2))
                        ||(id === me.pre + 'dl_contactmap' &&(!status.bSelectannotationsInit2) &&(!status.bGraph2) &&(!status.bAlignmentInit2) &&(!status.bTable2) &&(!status.bLineGraph2) &&(!status.bScatterplot2) &&(!status.bAlignerrormap2))
                        ||(id === me.pre + 'dl_alignerrormap' &&(!status.bSelectannotationsInit2) &&(!status.bGraph2) &&(!status.bAlignmentInit2) &&(!status.bTable2) &&(!status.bLineGraph2) &&(!status.bScatterplot2) &&(!status.bContactmap2))
                        ) {
                          if(status.bTwoddgmInit2 || status.bTwodctnInit2 || status.bSetsInit2) {
                              let canvasWidth = me.utilsCls.isMobile() ? me.htmlCls.WIDTH : me.htmlCls.WIDTH - twoddgmWidth;
                              ic.resizeCanvasCls.resizeCanvas(canvasWidth, me.htmlCls.HEIGHT, true);

                              if(status.bTwoddgmInit2) thisClass.openDlg2Ddgm(me.pre + 'dl_2ddgm', undefined, status.bSetsInit2);
                              if(status.bTwodctnInit2) thisClass.openDlg2Ddgm(me.pre + 'dl_2dctn', undefined, status.bSetsInit2);
                              if(status.bSetsInit2) thisClass.openDlg2Ddgm(me.pre + 'dl_definedsets');
                          }
                          else {
                              //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH, me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT, true);
                              ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH, me.htmlCls.HEIGHT, true);
                          }
                      }
                  },
                  resize: function(e) {
                      if(id == me.pre + 'dl_selectannotations') {
                          ic.annotationCls.hideFixedTitle();
                      }
                      else if(id == me.pre + 'dl_graph') {
                          let width = $("#" + id).width();
                          let height = $("#" + id).height();

                          d3.select("#" + me.svgid).attr("width", width).attr("height", height);
                      }
                      else if(id == me.pre + 'dl_linegraph' || id == me.pre + 'dl_scatterplot' || id == me.pre + 'dl_contactmap' || id == me.pre + 'dl_alignerrormap') {
                          let oriWidth =(status.bTwoddgmInit2 || status.bSetsInit2) ?(me.htmlCls.WIDTH - twoddgmWidth)/2 : me.htmlCls.WIDTH / 2;
                          let ratio = $("#" + id).width() / oriWidth;

                          if(id == me.pre + 'dl_linegraph') {
                              let width = ic.linegraphWidth * ratio;
                              $("#" + me.linegraphid).attr("width", width);
                          }
                          else if(id == me.pre + 'dl_scatterplot') {
                              let width = ic.scatterplotWidth * ratio;
                              $("#" + me.scatterplotid).attr("width", width);
                          }
                          else if(id == me.pre + 'dl_contactmap') {
                              let width = ic.contactmapWidth * ratio;
                              $("#" + me.contactmapid).attr("width", width);
                          }
                          else if(id == me.pre + 'dl_alignerrormap') {
                            let width = ic.alignerrormapWidth * ratio;
                            $("#" + me.alignerrormapid).attr("width", width);
                        }
                      }
                  }
                });

                this.addSaveButton(id);
                this.addHideButton(id);
            }
        }
        else if(id === me.pre + 'dl_2ddgm' || id === me.pre + 'dl_2dctn') {
            let tmpWidth = 0;

            //if(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH >= me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT) {
            if(me.htmlCls.WIDTH >= me.htmlCls.HEIGHT) {
                if(status.bSelectannotationsInit2 || status.bGraph2 || status.bLineGraph2 || status.bScatterplot2 || status.bTable2 || status.bAlignmentInit2) {
                    //tmpWidth = 0.5 *(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH) - twoddgmWidth * 0.5;
                    tmpWidth = 0.5 *(me.htmlCls.WIDTH) - twoddgmWidth * 0.5;
                }
                //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH - tmpWidth - twoddgmWidth, me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT, true);
                ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - tmpWidth - twoddgmWidth, me.htmlCls.HEIGHT, true);

                this.openDlg2Ddgm(id, undefined, status.bSetsInit2);
            }
            else {
                //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH - tmpWidth - twoddgmWidth,(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT)*0.5, true);
                let canvasWidth = me.utilsCls.isMobile() ? me.htmlCls.WIDTH : me.htmlCls.WIDTH - twoddgmWidth;
                ic.resizeCanvasCls.resizeCanvas(canvasWidth,(me.htmlCls.HEIGHT)*0.5, true);
                //this.openDlg2Ddgm(id,(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT)*0.5);
                this.openDlg2Ddgm(id,(me.htmlCls.HEIGHT)*0.5);

                //this.openDlg2Ddgm(id,(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT)*0.5, bSetsInit2);
                this.openDlg2Ddgm(id,(me.htmlCls.HEIGHT)*0.5, status.bSetsInit2);
            }
        }
        else {
            height = 'auto';
            width = 'auto';

            if(id === me.pre + 'dl_addtrack') {
                width='50%';
            }

            let position;

            if(id === me.pre + 'dl_definedsets') {
                let tmpWidth = 0;

                //if(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH >= me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT) {
                if(me.htmlCls.WIDTH >= me.htmlCls.HEIGHT) {
                    if(status.bSelectannotationsInit2 || status.bGraph2 || status.bLineGraph2 || status.bScatterplot2 || status.bTable2 || status.bAlignmentInit2) {
                        //tmpWidth = 0.5 *(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH) - twoddgmWidth * 0.5;
                        tmpWidth = 0.5 *(me.htmlCls.WIDTH) - twoddgmWidth * 0.5;
                    }
                    //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH - tmpWidth - twoddgmWidth, me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT, true);
                    ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - tmpWidth - twoddgmWidth, me.htmlCls.HEIGHT, true);
                    this.openDlg2Ddgm(id);

                    if(status.bTwoddgmInit2) this.openDlg2Ddgm(me.pre + 'dl_2ddgm', undefined, true);
                    if(status.bTwodctnInit2) this.openDlg2Ddgm(me.pre + 'dl_2dctn', undefined, true);
                }
                else {
                    //ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH - me.htmlCls.LESSWIDTH - tmpWidth - twoddgmWidth,(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT)*0.5, true);
                    let canvasWidth = me.utilsCls.isMobile() ? me.htmlCls.WIDTH : me.htmlCls.WIDTH - twoddgmWidth;
                    ic.resizeCanvasCls.resizeCanvas(canvasWidth,(me.htmlCls.HEIGHT)*0.5, true);
                    //this.openDlg2Ddgm(id,(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT)*0.5);
                    this.openDlg2Ddgm(id,(me.htmlCls.HEIGHT)*0.5);

                    //if(bTwoddgmInit2) this.openDlg2Ddgm(me.pre + 'dl_2ddgm',(me.htmlCls.HEIGHT - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT)*0.5, true);
                    if(status.bTwoddgmInit2) this.openDlg2Ddgm(me.pre + 'dl_2ddgm',(me.htmlCls.HEIGHT)*0.5, true);
                    if(status.bTwodctnInit2) this.openDlg2Ddgm(me.pre + 'dl_2dctn',(me.htmlCls.HEIGHT)*0.5, true);
                }
            }
            else {
                if(me.utilsCls.isMobile()) {
                    position ={ my: "left top", at: "left bottom-50", of: "#" + me.pre + "canvas", collision: "none" };
                }
                else if(id === me.pre + 'dl_allinteraction' || id === me.pre + 'dl_buriedarea') {
                    //position ={ my: "right top", at: "right top+50", of: "#" + me.pre + "dl_selectannotations", collision: "none" }
                    position ={ my: "right top", at: "right top+50", of: "#" + ic.divid, collision: "none" };

                    width = 700;
                    height = 500;
                }
                else if(id === me.pre + 'dl_rmsd') {
                    position ={ my: "left bottom", at: "left+20 bottom-20", of: "#" + me.pre + "canvas", collision: "none" };
                }
                else if(id === me.pre + 'dl_legend') {
                    position ={ my: "left bottom", at: "left+20 bottom-20", of: "#" + me.pre + "canvas", collision: "none" };
                }
                else if(id === me.pre + 'dl_symd') {
                    position ={ my: "left top", at: "right-200 bottom-200", of: "#" + me.pre + "canvas", collision: "none" };
                }
                else {
                    if(me.cfg.align) {
                        position ={ my: "left top", at: "left top+90", of: "#" + me.pre + "canvas", collision: "none" };
                    }
                    else {
                        position ={ my: "left top", at: "left top+50", of: "#" + me.pre + "canvas", collision: "none" };
                    }
                }

                window.dialog = $( "#" + id ).dialog({
                  autoOpen: true,
                  title: title,
                  height: height,
                  width: width,
                  modal: false,
                  position: position
                });

                this.addSaveButton(id);
                this.addHideButton(id);
            }
        }

        $(".ui-dialog .ui-button span")
          .removeClass("ui-icon-closethick")
          .addClass("ui-icon-close");
    }

    openDlgNotebook(id, title) {  let me = this.icn3dui, ic = me.icn3d;
        if(me.bNode) return;

        let width = 400, height = 150;
        let twoddgmWidth = me.htmlCls.width2d + 20;

        if(id === me.pre + 'dl_selectannotations' || id === me.pre + 'dl_graph' || id === me.pre + 'dl_linegraph' || id === me.pre + 'dl_scatterplot' || id === me.pre + 'dl_contactmap'  || id === me.pre + 'dl_alignerrormap' || id === me.pre + 'dl_interactionsorted' || id === me.pre + 'dl_alignment') {
            $( "#" + id ).show();

            height =(me.htmlCls.HEIGHT) * 0.5;

            width = me.htmlCls.WIDTH;

            $( "#" + id ).width(width);
            $( "#" + id ).height(height);

            $( "#" + id ).resize(function(e) {
                  let oriWidth = me.htmlCls.WIDTH / 2;
                  let ratio = $("#" + id).width() / oriWidth;

                  if(id == me.pre + 'dl_selectannotations') {
                      ic.annotationCls.hideFixedTitle();
                  }
                  else if(id == me.pre + 'dl_graph') {
                      let width = $("#" + id).width();
                      let height = $("#" + id).height();

                      d3.select("#" + me.svgid).attr("width", width).attr("height", height);
                  }
                  else if(id == me.pre + 'dl_linegraph') {
                      let width = ic.linegraphWidth * ratio;

                      $("#" + me.linegraphid).attr("width", width);
                  }
                  else if(id == me.pre + 'dl_scatterplot') {
                      let width = ic.scatterplotWidth * ratio;

                      $("#" + me.scatterplotid).attr("width", width);
                  }
                  else if(id == me.pre + 'dl_contactmap') {
                      let width = ic.contactmapWidth * ratio;

                      $("#" + me.contactmapid).attr("width", width);
                  }
                  else if(id == me.pre + 'dl_alignerrormap') {
                    let width = ic.alignerrormapWidth * ratio;

                    $("#" + me.alignerrormapid).attr("width", width);
                }
            });
        }
        else {
            if(ic.bRender) $( "#" + id ).show();

            height = 'auto';
            width = 'auto';

            if(id === me.pre + 'dl_addtrack') {
                width='50%';
            }
            else if(id === me.pre + 'dl_2ddgm' || id === me.pre + 'dl_2dctn' || id === me.pre + 'dl_definedsets') {
                width=twoddgmWidth;
            }
            else if(id === me.pre + 'dl_allinteraction' || id === me.pre + 'dl_buriedarea') {
                width = 700;
                height = 500;
            }

            $( "#" + id ).width(width);
            $( "#" + id ).height(height);
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SetDialog {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    //A placeholder for all custom dialogs.
    setCustomDialogs() { let me = this.icn3dui; me.icn3d;
        if(me.bNode) return '';

        let html = "";
        return html;
    }

    getHtmlAlignResidueByResidue(chainids, predefinedid, buttonid) { let me = this.icn3dui; me.icn3d;
        let html = '';

        html += "All chains will be aligned to the first chain in the comma-separated chain IDs. Each chain ID has the form of PDBID_chain (e.g., 1HHO_A, case sensitive) or UniprotID (e.g., P69905 for AlphaFold structures).<br/><br/>";
        html += "<b>Chain IDs</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + chainids + "' value='P69905,P01942,1HHO_A' size=50><br/><br/>";
        
        html += "Each alignment is defined as \" | \"-separated residue lists in one line. \"10-50\" means a range of residues from 10 to 50.<br><textarea id='" + me.pre + predefinedid + "' rows='5' style='width: 100%; height: " +(me.htmlCls.LOG_HEIGHT) + "px; padding: 0px; border: 0px;'>1,5,10-50 | 1,5,10-50\n2,6,11-51 | 1,5,10-50</textarea><br/>";
        html += me.htmlCls.buttonStr + buttonid + "'><b>Align Residue by Residue</b></button><br/>";
        return html;
    }

    //Set the html for all popup dialogs.
    setDialogs() { let me = this.icn3dui, ic = me.icn3d;
        if(me.bNode) return '';

        let html = "";

        let defaultColor = "#ffff00"; //ic.colorBlackbkgd; 
 
        me.htmlCls.optionStr = "<option value=";

        html += "<!-- dialog will not be part of the form -->";

        let divClass =(me.cfg.notebook) ? '' : 'icn3d-hidden';
        let dialogClass =(me.cfg.notebook) ? 'icn3d-hidden' : '';
        html += me.htmlCls.divStr + "alldialogs' class='" + divClass + " icn3d-dialog'>";

        html += me.htmlCls.divStr + "dl_2ddgm' class='" + dialogClass + " icn3d-dl_2ddgm' style='background-color:white'>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_2dctn' class='" + dialogClass + " icn3d-dl_2dctn' style='background-color:white'>";

        me.svgid_ct = me.pre + "icn3d_cartoon";

        let buttonStrTmp = '<button class="icn3d-commandTitle" style="-webkit-appearance:button; height:24px;background-color:#DDD;" id="';
        let tmpStr = 'icn3d-node-text';
        html += me.htmlCls.divNowrapStr + "Dynamically generated for selected residues. <br>Nodes can be dragged or clicked.</div>";
        html += me.htmlCls.divNowrapStr + buttonStrTmp + me.svgid_ct + '_svg">SVG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.svgid_ct + '_png">PNG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.svgid_ct + '_json">JSON</button><br>';
        html += "<b>Label</b>: <select id='" + me.svgid_ct + "_label'>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "0'>No</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "4'>4px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "8' selected>8px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "12'>12px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "16'>16px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "24'>24px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "32'>32px</option>";
        html += "</select>";
        html += "</div>";

        html += "<svg id='" + me.svgid_ct + "' viewBox='" + "0,0," + me.htmlCls.width2d + "," + me.htmlCls.width2d + "'>";
        html += "</svg>";

        html += "</div>";

    //    if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined || ic.bRealign || ic.bSymd) {
          html += me.htmlCls.divStr + "dl_alignment' class='" + dialogClass + "' style='background-color:white;'>";
          html += me.htmlCls.divStr + "symd_info'></div>";
          html += me.htmlCls.divStr + "alignseqguide_wrapper'><br>" + me.htmlCls.setHtmlCls.setAlignSequenceGuide() + "</div>";
          html += me.htmlCls.divStr + "dl_sequence2' class='icn3d-dl_sequence'>";
          html += "</div>";
          html += "</div>";
    //    }

        html += me.htmlCls.divStr + "dl_definedsets' class='" + dialogClass + "'>";
        html += me.htmlCls.divStr + "dl_setsmenu'>";
        html += "<b>Defined Sets:</b> <br/>";
        html += "<select id='" + me.pre + "atomsCustom' multiple size='6' style='min-width:130px;'>";
        html += "</select>";
        html += "<div style='margin: 6px 0 6px 0;'>" + me.htmlCls.buttonStr + "deletesets'><b>Delete Selected Sets</b></button></div>";
        html += '        <b>Set Operations</b>: <div style="width:20px; margin-top:6px; display:inline-block;"><span id="' + me.pre + 'dl_command_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="' + me.pre + 'dl_command_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div><br>';
        html += "</div>";

        html += me.htmlCls.divStr + "dl_command' style='display:none;'>";
        html += me.htmlCls.divStr + "dl_setoperations'>";
        html += "<label for='" + me.pre + "setOr'>" + me.htmlCls.inputRadioStr + "name='" + me.pre + "setOperation' id='" + me.pre + "setOr' checked> Union(or) </label><br/>";
        html += "<label for='" + me.pre + "setAnd'>" + me.htmlCls.inputRadioStr + "name='" + me.pre + "setOperation' id='" + me.pre + "setAnd'> Intersection(and) </label><br/>";
        html += "<label for='" + me.pre + "setNot'>" + me.htmlCls.inputRadioStr + "name='" + me.pre + "setOperation' id='" + me.pre + "setNot'> Exclusion(not) </label>";
        html += "</div><br>";

        html += me.htmlCls.setHtmlCls.setAdvanced();

        html += "</div>";
        html += "</div>";

        html += me.htmlCls.setHtmlCls.setAdvanced(2);

        html += me.htmlCls.divStr + "dl_vastplus' class='" + dialogClass + "' style='max-width:500px'>";
        html += "Note: <b>VAST+</b> finds other macromolecular structures that have a similar biological unit. To do this, VAST+ takes into consideration the complete set of 3D domains that VAST identified within a query structure, throughout all of its component protein molecules, and finds other macromolecular structures that have a similar set of proteins/3D domains.<br><br>"; 
        html += "PDB ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "vastpluspdbid' value='1HHO' size=8><br>";
        html += me.htmlCls.buttonStr + "reload_vastplus'>VAST+</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_vast' class='" + dialogClass + "' style='max-width:500px'>";
        html += 'Note: <b>VAST</b> identifies 3D domains (substructures) within each protein structure in the Molecular Modeling Database (MMDB), and then finds other protein structures that have one or more similar 3D domains, using purely geometric criteria. You have two ways to do a VAST search.<br><br>'; 

        html += '<b>Optione 1</b>, search with your selection (all residues are selected by default) in the loaded structures:<br>'; 
        html += '<form method=post enctype=multipart/form-data action="https://www.ncbi.nlm.nih.gov/Structure/vast/VSMmdb.cgi" id="' + me.pre + 'newvs2" name="newvs2" target="_blank">';
        html += '<input type=hidden id="' + me.pre + 'pdbstr" name="pdbstr">';
        html += "Searching against: <input type='radio' name='dataset' value='Non-redundant subset' checked> Medium-redundancy Subset of PDB <a href='https://www.ncbi.nlm.nih.gov/Structure/VAST/vasthelp.html#VASTNR' title='Medium-redundancy Subset' target='_blank'>?</a> <input type='radio' name='dataset' value='All'>All of PDB <br>";
        // the submit value has to be "Submit" in order to make the backend cgi works
        //html += '<input type="submit" name="' + me.pre + 'cmdVSMmdb" value="VAST Search"></input>';
        html += '<input type="submit" id="' + me.pre + 'cmdVSMmdb2" name="cmdVSMmdb" value="Submit"></input>';
        html += "</form><br>";

        html += '<b>Optione 2</b>, search with PDB ID and chain name:<br>'; 
        html += "PDB ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "vastpdbid' value='1HHO' size=8> &nbsp;&nbsp;";
        html += "Chain Name: " + me.htmlCls.inputTextStr + "id='" + me.pre + "vastchainid' value='A' size=8> <br>";
        html += me.htmlCls.buttonStr + "reload_vast'>VAST</button><br><br>";

        html += '<b>Optione 3</b>, search with a PDB file:<br>'; 
        html += '<form method=post enctype=multipart/form-data action="https://www.ncbi.nlm.nih.gov/Structure/vast/VSMmdb.cgi" id="' + me.pre + 'newvs" name="newvs" target="_blank">';
        html += "PDB File: " + me.htmlCls.inputFileStr + " name='pdbfile' size=8><br>";
        html += "Searching against: <input type='radio' name='dataset' value='Non-redundant subset' checked> Medium-redundancy Subset of PDB <a href='https://www.ncbi.nlm.nih.gov/Structure/VAST/vasthelp.html#VASTNR' title='Medium-redundancy Subset' target='_blank'>?</a> <input type='radio' name='dataset' value='All'>All of PDB <br>";
        // the submit value has to be "Submit" in order to make the backend cgi works
        //html += '<input type="submit" name="' + me.pre + 'cmdVSMmdb" value="VAST Search"></input>';
        html += '<input type="submit" name="cmdVSMmdb" value="Submit"></input>';
        html += "</form><br>";

        html += "</div>";

        html += me.htmlCls.divStr + "dl_foldseek' class='" + dialogClass + "' style='max-width:500px'>";
        html += '1. <input type="submit" id="' + me.pre + 'fssubmit" name="fssubmit" value="Submit"></input> your selection (all residues are selected by default) in the loaded structures to <a href="https://search.foldseek.com/search" target="_blank">Foldseek</a> web server.<br><br>';
        html += '2 (Optional). Once you see the structure neighbors, you can view the alignment in iCn3D by inputing a list of PDB chain IDs or AlphaFold UniProt IDs below. <br><br>The PDB chain IDs are the same as the record names such as "1hho_A". The UniProt ID is the text between "AF-" and "-F1". For example, the UniProt ID for the record name "AF-P69905-F1-model_v4" is "P69905".<br><br>'; 

        html += "Chain ID List: " + me.htmlCls.inputTextStr + "id='" + me.pre + "foldseekchainids' value='P69905,P01942,1hho_A' size=30> ";
        html += me.htmlCls.buttonStr + "reload_foldseek'>Align</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_mmtfid' class='" + dialogClass + "'>";
        html += "MMTF ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "mmtfid' value='1TUP' size=8> ";
        html += me.htmlCls.buttonStr + "reload_mmtf'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_pdbid' class='" + dialogClass + "'>";
        html += "PDB ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "pdbid' value='1TUP' size=8> ";
        html += me.htmlCls.buttonStr + "reload_pdb'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_afid' class='" + dialogClass + "'>";
        html += "Note: AlphaFold produces a per-residue confidence score (pLDDT) between 0 and 100:<br>";
        html += me.htmlCls.clickMenuCls.setAlphaFoldLegend() + "<br>";

        let afid = (me.cfg.afid) ? me.cfg.afid : 'A4D1S0';

        html += "<a href='https://alphafold.ebi.ac.uk/' target='_blank'>AlphaFold Uniprot</a> ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "afid' value='" + afid + "' size=10><br><br>";
        html += me.htmlCls.buttonStr + "reload_af'>Load Structure</button><br><br>"; 
        html += "PAE Map: " + me.htmlCls.buttonStr + "reload_afmap'>Load Half</button>"
            + me.htmlCls.buttonStr + "reload_afmapfull' style='margin-left:30px'>Load Full (slow)</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_refseqid' class='" + dialogClass + "'>";
        html += "NCBI Protein Accession: " + me.htmlCls.inputTextStr + "id='" + me.pre + "refseqid' value='NP_001743.1' size=8> ";
        html += me.htmlCls.buttonStr + "reload_refseq'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_opmid' class='" + dialogClass + "'>";
        html += "<a href='https://opm.phar.umich.edu' target='_blank'>Orientations of Proteins in Membranes(OPM)</a> PDB ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "opmid' value='6JXR' size=8> ";
        html += me.htmlCls.buttonStr + "reload_opm'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_pdbfile' class='" + dialogClass + "'>";
        html += "Note: Several PDB files could be concatenated into a single PDB file. Use the line \"ENDMDL\" to separate PDB files.<br><br>";
        html += "PDB File: " + me.htmlCls.inputFileStr + " id='" + me.pre + "pdbfile' size=8> ";
        html += me.htmlCls.buttonStr + "reload_pdbfile'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_pdbfile_app' class='" + dialogClass + "'>";
        html += "Multiple PDB Files: <input type='file' multiple id='" + me.pre + "pdbfile_app' size=8> ";
        html += me.htmlCls.buttonStr + "reload_pdbfile_app'>Append</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_rescolorfile' class='" + dialogClass + "'>";
        html += '<div style="width:450px;">The custom JSON file on residue colors has the following format for proteins("ALA" and "ARG") and nucleotides("G" and "A"):<br>';
        html += '{"ALA":"#C8C8C8", "ARG":"#145AFF", ..., "G":"#008000", "A":"#6080FF", ...}</div><br>';
        html += "Residue Color File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "rescolorfile' size=8> ";
        html += me.htmlCls.buttonStr + "reload_rescolorfile'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_customcolor' class='" + dialogClass + "'>";
        html += " <input type='hidden' id='" + me.pre + "customcolor_chainid' value=''>";
        html += '<div style="width:450px;">The custom file for the structure has two columns separated by space or tab: ';
        html += 'residue number, and score in the range of 0-100. If you click "Apply Custom Color" button, ';
        html += 'the scores 0, 50 and 100 correspond to the three colors specified below. If you click "Apply Custom Tube", ';
        html += 'the selected residues will be displayed in a style similar to "B-factor Tube".</div><br>';
        html += "Custom File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "cstcolorfile' size=8> <br><br>";
        html += "1. " + me.htmlCls.buttonStr + "reload_customcolorfile'>Apply Custom Color</button>" + me.htmlCls.buttonStr + "remove_legend' style='margin-left:30px;'>Remove Legend</button><br>";
        html += "<span style='margin-left:15px'>Score to Color: 0:</span> <select id='" + me.pre + "startColor'>";
        html += me.htmlCls.optionStr + "'red'>Red</option>";
        html += me.htmlCls.optionStr + "'green'>Green</option>";
        html += me.htmlCls.optionStr + "'blue' selected>Blue</option>";
        html += "</select>";
        html += "<span style='margin-left:30px'>50</span>: <select id='" + me.pre + "midColor'>";
        html += me.htmlCls.optionStr + "'white' selected>White</option>";
        html += me.htmlCls.optionStr + "'black'>Black</option>";
        html += "</select>";
        html += "<span style='margin-left:30px'>100</span>: <select id='" + me.pre + "endColor'>";
        html += me.htmlCls.optionStr + "'red' selected>Red</option>";
        html += me.htmlCls.optionStr + "'green'>Green</option>";
        html += me.htmlCls.optionStr + "'blue'>Blue</option>";
        html += "</select><br>";
        html += "or<br><br>";
        html += "2. " + me.htmlCls.buttonStr + "reload_customtubefile'>Apply Custom Tube</button>";

        html += "</div>";

        html += me.htmlCls.divStr + "dl_customref' class='" + dialogClass + "'>";
        html += '<div style="width:550px;">You can define your own reference numbers in a custom file using Excel, and then export it as a CSV file. An example file is shown below with cells separated by commas.<br>';
        html += '<pre>refnum,11,12,,21,22<br>';
        html += '1TUP_A,100,101,,,132<br>';
        html += '1TUP_B,110,111,,141,142</pre>';
        html += 'The first row defines the reference residue numbers. The 1st cell could be anything. The rest cells are reference residue numbers (e.g., 11, 12, 21, 22) or empty cells. Each chain has a separate row. The first cell of the second row is the chain ID "1TUP_A". The rest cells are the corresponding real residue numbers for reference residue numbers in the first row. For example, the reference numbers for residues 100, 101, and 132 in the chain 1TUP_A are 11, 12, and 22, respectively. The thrid row shows the real residue numbers for the chain "1TUP_B".<br><br>';
        html += 'To select all residues corresponding to the reference numbers, you can simplay replace ":" with "%" in the <a href="https://www.ncbi.nlm.nih.gov/Structure/icn3d/icn3d.html#selectb" target="_blank">Specification</a>. For example, "%12"  selects the residue 101 in 1TUP_A and the residue 111 in 1TUP_B. ".A%12" has the chain "A" filter and selects the residue 101 in 1TUP_A.<br>';
        html += '</div><br>';
        html += "Custom File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "cstreffile' size=8> <br><br>";
        html += me.htmlCls.buttonStr + "reload_customreffile'>Apply Custom Reference Numbers</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_align' class='" + dialogClass + "'>";
        html += "Enter the PDB IDs or MMDB IDs of the structures: <br/><br/>ID1: " + me.htmlCls.inputTextStr + "id='" + me.pre + "alignid1' value='1HHO' size=8>" + me.htmlCls.space3 + me.htmlCls.space3 + "ID2: " + me.htmlCls.inputTextStr + "id='" + me.pre + "alignid2' value='4N7N' size=8><br/><br/>";
        html += "<b>VAST+ based on VAST</b>: " + me.htmlCls.buttonStr + "reload_align_ori'>All Matching Molecules Superposed</button>" + me.htmlCls.space3 + me.htmlCls.buttonStr + "reload_align_refined'>Invariant Substructure Superposed</button><br><br>";
        html += "<b>VAST+ based on TM-align</b>: " + me.htmlCls.buttonStr + "reload_align_tmalign'>All Matching Molecules Superposed</button><br><br>";
        html += "</div>";
        
        html += me.htmlCls.divStr + "dl_alignaf' class='" + dialogClass + "'>";
        html += "Enter two <a href='https://alphafold.ebi.ac.uk/' target='_blank'>AlphaFold Uniprot</a> IDs: <br/><br/>ID1: " + me.htmlCls.inputTextStr + "id='" + me.pre + "alignafid1' value='P41327' size=8>" + me.htmlCls.space3 + me.htmlCls.space3 + "ID2: " + me.htmlCls.inputTextStr + "id='" + me.pre + "alignafid2' value='P41331' size=8><br/><br/>";
        html += me.htmlCls.buttonStr + "reload_alignaf_tmalign'>Align with TM-align</button>" + me.htmlCls.buttonStr + "reload_alignaf' style='margin-left:30px'>Align with VAST</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_chainalign' class='" + dialogClass + "'>";
        html += "<div style='width:550px'>";
        html += "All chains will be aligned to the first chain in the comma-separated chain IDs. Each chain ID has the form of PDBID_chain (e.g., 1HHO_A, case sensitive) or UniprotID (e.g., P69905 for AlphaFold structures).<br/><br/>";
        html += "<b>Chain IDs</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "chainalignids' value='P69905,P01942,1HHO_A' size=50><br/><br/>";
        html += me.htmlCls.buttonStr + "reload_chainalign_tmalign'><b>Align with TM-align</b></button>" + me.htmlCls.buttonStr + "reload_chainalign_asym' style='margin-left:30px'><b>Align with VAST</b></button><br/><br/>";

        html += "(Note: To align chains in custom PDB files, you could load them in \"File > Open File > PDB Files (appendable)\" and click \"Analysis > Defined Sets\". Finally select multiple chains in Defined Sets and click \"File > Realign Selection\".)<br><br>";
        html += "</div></div>";

        html += me.htmlCls.divStr + "dl_chainalign2' class='" + dialogClass + "'>";
        html += "<div style='width:550px'>";
        html += "All chains will be aligned to the first chain in the comma-separated chain IDs. Each chain ID has the form of PDBID_chain (e.g., 1HHO_A, case sensitive) or UniprotID (e.g., P69905 for AlphaFold structures).<br/><br/>";
        html += "<b>Chain IDs</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "chainalignids2' value='P69905,P01942,1HHO_A' size=50><br/><br/>";

        html += "The sequence alignment (followed by structure alignemnt) is based on residue numbers in the First/Master chain: <br>" + me.htmlCls.inputTextStr + "id='" + me.pre + "resalignids' value='1,5,10-50' size=50><br/>";
        html += me.htmlCls.buttonStr + "reload_chainalign_asym2' style='margin-top:3px;'><b>Align by Sequence Alignment</b></button><br/><br/>";

        html += "(Note: To align chains in custom PDB files, you could load them in \"File > Open File > PDB Files (appendable)\" and click \"Analysis > Defined Sets\". Finally select multiple chains in Defined Sets and click \"File > Realign Selection\".)<br><br>";
        html += "</div></div>";

        html += me.htmlCls.divStr + "dl_chainalign3' class='" + dialogClass + "'>";
        html += "<div style='width:550px'>";
        html += this.getHtmlAlignResidueByResidue('chainalignids3', 'predefinedres', 'reload_chainalign_asym3');
        html += "</div></div>";

        html += me.htmlCls.divStr + "dl_realignresbyres' class='" + dialogClass + "'>";
        html += "<div style='width:550px'>";
        html += "<b>Option 1</b>: " + me.htmlCls.buttonStr + "realignSelection'><b>Realign Current Selection Residue by Residue</b></button><br/><br/>";
        html += "<b>Option 2</b>: <br>";
        html += "<div class='icn3d-box'>" + this.getHtmlAlignResidueByResidue('chainalignids4', 'predefinedres2', 'reload_chainalign_asym4') + "</div>";
        html += "</div></div>";

        html += me.htmlCls.divStr + "dl_mutation' class='" + dialogClass + "'>";
        html += "<div style='width:500px'>";
        html += 'Please specify the mutations with a comma separated mutation list. Each mutation can be specified as "[<b>uppercase</b> PDB ID or AlphaFold UniProt ID]_[Chain ID]_[Residue Number]_[One Letter Mutant Residue]". E.g., the mutation of N501Y in the E chain of PDB 6M0J can be specified as "6M0J_E_501_Y". For AlphaFold structures, the "Chain ID" is "A".<br/><br/>';
        html += "<div style='display:inline-block; width:110px'>Mutations: </div>" + me.htmlCls.inputTextStr + "id='" + me.pre + "mutationids' value='6M0J_E_484_K,6M0J_E_501_Y,6M0J_E_417_N' size=50><br/><br/>";
 
        html += '<b>ID Type</b>: ';
        html += '<input type="radio" name="' + me.pre + 'idsource" id="' + me.pre + 'type_mmdbid" value="mmdbid" checked>PDB ID';
        html += '<input type="radio" name="' + me.pre + 'idsource" id="' + me.pre + 'type_afid" value="afid" style="margin-left:20px">AlphaFold UniProt ID<br><br>';

        html += '<b>Show Mutation in</b>: ';
        html += '<input type="radio" name="' + me.pre + 'pdbsource" id="' + me.pre + 'showin_currentpage" value="currentpage">Current Page';
        html += '<input type="radio" name="' + me.pre + 'pdbsource" id="' + me.pre + 'showin_newpage" value="newpage" style="margin-left:20px" checked>New Page<br><br>';

        html += me.htmlCls.buttonStr + "reload_mutation_3d' title='Show the mutations in 3D using the scap program'>3D with scap</button>";
        html += me.htmlCls.buttonStr + "reload_mutation_inter' style='margin-left:20px' title='Show the mutations in 3D and the change of interactions'>Interactions</button>";
        html += me.htmlCls.buttonStr + "reload_mutation_pdb' style='margin-left:20px' title='Show the mutations in 3D and export the PDB of the mutant within 10 angstrom'>PDB</button>";
        html += "<br/><br/></div></div>";

        html += me.htmlCls.divStr + "dl_mol2file' class='" + dialogClass + "'>";
        html += "Mol2 File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "mol2file' size=8> ";
        html += me.htmlCls.buttonStr + "reload_mol2file'>Load</button>";
        html += "</div>";
        html += me.htmlCls.divStr + "dl_sdffile' class='" + dialogClass + "'>";
        html += "SDF File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "sdffile' size=8> ";
        html += me.htmlCls.buttonStr + "reload_sdffile'>Load</button>";
        html += "</div>";
        html += me.htmlCls.divStr + "dl_xyzfile' class='" + dialogClass + "'>";
        html += "XYZ File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "xyzfile' size=8> ";
        html += me.htmlCls.buttonStr + "reload_xyzfile'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_afmapfile' class='" + dialogClass + "'>";
        html += "AlphaFold PAE File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "afmapfile' size=8> <br><br>";
        html += me.htmlCls.buttonStr + "reload_afmapfile'>Load Half PAE Map</button>" 
          + me.htmlCls.buttonStr + "reload_afmapfilefull' style='margin-left:30px'>Load Full PAE Map (slow)</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_urlfile' class='" + dialogClass + "'>";
        html += "File type: ";
        html += "<select id='" + me.pre + "filetype'>";
        html += me.htmlCls.optionStr + "'pdb' selected>PDB</option>";
        html += me.htmlCls.optionStr + "'mmcif'>mmCIF</option>";
        html += me.htmlCls.optionStr + "'mol2'>Mol2</option>";
        html += me.htmlCls.optionStr + "'sdf'>SDF</option>";
        html += me.htmlCls.optionStr + "'xyz'>XYZ</option>";
        html += me.htmlCls.optionStr + "'icn3dpng'>iCn3D PNG</option>";
        html += me.htmlCls.optionStr + "'pae'>AlphaFold PAE</option>";
        html += "</select><br/>";
        html += "URL in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + "urlfile' size=20><br/> ";
        html += me.htmlCls.buttonStr + "reload_urlfile'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_mmciffile' class='" + dialogClass + "'>";
        html += "mmCIF File: " + me.htmlCls.inputFileStr + "id='" + me.pre + "mmciffile' value='1TUP' size=8> ";
        html += me.htmlCls.buttonStr + "reload_mmciffile'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_mmcifid' class='" + dialogClass + "'>";
        html += "mmCIF ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "mmcifid' value='1TUP' size=8> ";
        html += me.htmlCls.buttonStr + "reload_mmcif'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_mmdbid' class='" + dialogClass + "' style='max-width:500px'>";
        html += "MMDB or PDB ID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "mmdbid' value='1TUP' size=8> <br><br>";
        html += me.htmlCls.buttonStr + "reload_mmdb_asym'>Load Asymmetric Unit (All Chains)</button>" + me.htmlCls.buttonStr + "reload_mmdb' style='margin-left:30px'>Load Biological Unit</button><br/><br/><br>";
        html += '<b>Note</b>: The "<b>biological unit</b>" is the <b>biochemically active form of a biomolecule</b>, <div style="width:20px; margin:6px 0 0 20px; display:inline-block;"><span id="'
          + me.pre + 'asu_bu_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="'
          + me.pre + 'asu_bu_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div>';

        html += me.htmlCls.divStr + "asu_bu' style='display:none;'>";
        html += 'which can range from a monomer (single protein molecule) to an oligomer of 100+ protein molecules.<br><br>The "<b>asymmetric unit</b>" is the raw 3D structure data resolved by X-ray crystallography, NMR, or Cryo-electron microscopy. The asymmetric unit is equivalent to the biological unit in approximately 60% of structure records. In the remaining 40% of the records, the asymmetric unit represents a portion of the biological unit that can be reconstructed using crystallographic symmetry, or it represents multiple copies of the biological unit.</div>';

        html += "</div>";

        html += me.htmlCls.divStr + "dl_mmdbafid' class='" + dialogClass + "' style='max-width:500px'>";
        html += "List of PDB, MMDB, or AlphaFold UniProt IDs: " + me.htmlCls.inputTextStr + "id='" + me.pre + "mmdbafid' placeholder='e.g., 1HHO,4N7N,P69905,P01942' size=30> <br><br>";
        html += me.htmlCls.buttonStr + "reload_mmdbaf'>Load Biological Unit</button>" + me.htmlCls.buttonStr + "reload_mmdbaf_asym' style='margin-left:30px'>Load Asymmetric Unit (All Chains)</button>" + "<br/><br/><br>";
        html += '<b>Note</b>: The "<b>biological unit</b>" is the <b>biochemically active form of a biomolecule</b>, <div style="width:20px; margin:6px 0 0 20px; display:inline-block;"><span id="'
        + me.pre + 'asu_bu2_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="'
        + me.pre + 'asu_bu2_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div>';

        html += me.htmlCls.divStr + "asu_bu2' style='display:none;'>";
        html += 'which can range from a monomer (single protein molecule) to an oligomer of 100+ protein molecules.<br><br>The "<b>asymmetric unit</b>" is the raw 3D structure data resolved by X-ray crystallography, NMR, or Cryo-electron microscopy. The asymmetric unit is equivalent to the biological unit in approximately 60% of structure records. In the remaining 40% of the records, the asymmetric unit represents a portion of the biological unit that can be reconstructed using crystallographic symmetry, or it represents multiple copies of the biological unit.</div>';

        html += "</div>";

        html += me.htmlCls.divStr + "dl_blast_rep_id' style='max-width:600px;' class='" + dialogClass + "'>";
        html += "Enter a Sequence ID (or FASTA sequence) and the aligned Structure ID, which can be found using the <a href='https://blast.ncbi.nlm.nih.gov/Blast.cgi?PROGRAM=blastp&PAGE_TYPE=BlastSearch&DATABASE=pdb' target='_blank'>BLAST</a> search against the pdb database with the Sequence ID or FASTA sequence as input.<br><br> ";
        html += "<b>Sequence ID</b>(NCBI protein accession of a sequence): " + me.htmlCls.inputTextStr + "id='" + me.pre + "query_id' value='NP_001108451.1' size=8><br> ";
        html += "or FASTA sequence: <br><textarea id='" + me.pre + "query_fasta' rows='5' style='width: 100%; height: " +(me.htmlCls.LOG_HEIGHT) + "px; padding: 0px; border: 0px;'></textarea><br><br>";
        html += "<b>Structure ID</b>(NCBI protein accession of a chain of a 3D structure): " + me.htmlCls.inputTextStr + "id='" + me.pre + "blast_rep_id' value='1TSR_A' size=8><br> ";
        //html += me.htmlCls.buttonStr + "reload_blast_rep_id'>Load</button>";
        html += me.htmlCls.buttonStr + "reload_blast_rep_id'>Align with BLAST</button> " + me.htmlCls.wifiStr
            + me.htmlCls.buttonStr + "reload_alignsw' style='margin-left:30px'>Align with Global Smith-Waterman</button>"
            + me.htmlCls.buttonStr + "reload_alignswlocal' style='margin-left:30px'>Align with Local Smith-Waterman</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_yournote' class='" + dialogClass + "'>";
        html += "Your note will be saved in the HTML file when you click \"File > Save File > iCn3D PNG Image\".<br><br>";
        html += "<textarea id='" + me.pre + "yournote' rows='5' style='width: 100%; height: " +(me.htmlCls.LOG_HEIGHT) + "px; padding: 0px; border: 0px;' placeholder='Enter your note here'></textarea><br>";
        html += me.htmlCls.buttonStr + "applyyournote'>Save</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_gi' class='" + dialogClass + "'>";
        html += "Protein gi: " + me.htmlCls.inputTextStr + "id='" + me.pre + "gi' value='1310960' size=8> ";
        html += me.htmlCls.buttonStr + "reload_gi'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_cid' class='" + dialogClass + "'>";
        html += "PubChem CID: " + me.htmlCls.inputTextStr + "id='" + me.pre + "cid' value='2244' size=8> ";
        html += me.htmlCls.buttonStr + "reload_cid'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_pngimage' class='" + dialogClass + "'>";
        html += "iCn3D PNG image: " + me.htmlCls.inputFileStr + "id='" + me.pre + "pngimage'><br/>";
        html += me.htmlCls.buttonStr + "reload_pngimage' style='margin-top: 6px;'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_state' class='" + dialogClass + "'>";
        html += "State file: " + me.htmlCls.inputFileStr + "id='" + me.pre + "state'><br/>";
        html += me.htmlCls.buttonStr + "reload_state' style='margin-top: 6px;'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_fixedversion' style='max-width:500px' class='" + dialogClass + "'>";
        html += "Since January 6, 2021, you can show the original view with the archived version of iCn3D by pasting your URL below and click \"Show Originial View\". Note the version in the parameter \"v\" was used to replace \"full.html\" with \"full_[v].html\" in the URL.<br><br>";
        html += "Share Link URL: " + me.htmlCls.inputTextStr + "id='" + me.pre + "sharelinkurl' size=60><br>";
        html += me.htmlCls.buttonStr + "reload_fixedversion'>Show Original View</button><br><br>";
        html += "</div>";


        html += me.htmlCls.divStr + "dl_selection' class='" + dialogClass + "'>";
        html += "Selection file: " + me.htmlCls.inputFileStr + "id='" + me.pre + "selectionfile'><br/>";
        html += me.htmlCls.buttonStr + "reload_selectionfile' style='margin-top: 6px;'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_menuloadpref' class='" + dialogClass + "'>";
        html += "Preference file: " + me.htmlCls.inputFileStr + "id='" + me.pre + "menupreffile'><br/>";
        html += me.htmlCls.buttonStr + "reload_menupreffile' style='margin-top: 6px;'>Load</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_dsn6' class='" + dialogClass + "'>";
        html += "<b>Note</b>: Always load a PDB file before loading DSN6 files. <br/><br/><br/>";

        html += "<span style='white-space:nowrap;font-weight:bold;'>2fofc contour at: <select id='" + me.pre + "dsn6sigma2fofc'>";

        let optArray1 = ['0', '0.5', '1', '1.5', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray1, 3);

        html += "</select> &sigma;</span><br/>";
        html += me.htmlCls.inputFileStr + "id='" + me.pre + "dsn6file2fofc'> " + me.htmlCls.buttonStr + "reload_dsn6file2fofc' style='margin-top: 6px;'>Load</button><br><br><br/>";

        html += "<span style='white-space:nowrap;font-weight:bold;'>fofc contour at: <select id='" + me.pre + "dsn6sigmafofc'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray1, 5);

        html += "</select> &sigma;</span><br/>";
        html += me.htmlCls.inputFileStr + "id='" + me.pre + "dsn6filefofc'> " + me.htmlCls.buttonStr + "reload_dsn6filefofc' style='margin-top: 6px;'>Load</button><br><br><br>";

        html += me.htmlCls.buttonStr + "elecmapNo4'>Remove Map</button><br>";

        html += "</div>";

        html += me.htmlCls.divStr + "dl_dsn6url' class='" + dialogClass + "'>";
        html += "<b>Note</b>: Always load a PDB file before loading DSN6 files. <br/><br/><br/>";

        html += "<span style='white-space:nowrap;font-weight:bold;'>2fofc contour at: <select id='" + me.pre + "dsn6sigmaurl2fofc'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray1, 3);

        html += "</select> &sigma;</span><br/>";
        html += "URL in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + "dsn6fileurl2fofc' size=20>" + me.htmlCls.space3 + me.htmlCls.buttonStr + "reload_dsn6fileurl2fofc' style='margin-top: 6px;'>Load</button><br><br><br/>";

        html += "<span style='white-space:nowrap;font-weight:bold;'>fofc contour at: <select id='" + me.pre + "dsn6sigmaurlfofc'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray1, 5);

        html += "</select> &sigma;</span><br/>";
        html += "URL in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + "dsn6fileurlfofc' size=20>" + me.htmlCls.space3 + me.htmlCls.buttonStr + "reload_dsn6fileurlfofc' style='margin-top: 6px;'>Load</button><br><br><br/>";

        html += me.htmlCls.buttonStr + "elecmapNo5'>Remove Map</button><br>";

        html += "</div>";

        html += me.htmlCls.divStr + "dl_clr' class='" + dialogClass + "'>";
        html += "Click in the input box to use the color picker:<br><br> ";
        html += "Custom Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "colorcustom' value='FF0000' size=8> ";
        html += me.htmlCls.buttonStr + "applycustomcolor'>Apply</button>";
        html += "</div>";

        html += me.htmlCls.setHtmlCls.getPotentialHtml('delphi', dialogClass);

        html += me.htmlCls.setHtmlCls.getPotentialHtml('local', dialogClass);
        html += me.htmlCls.setHtmlCls.getPotentialHtml('url', dialogClass);

        html += me.htmlCls.divStr + "dl_symmetry' class='" + dialogClass + "'><br>";
        html += me.htmlCls.divNowrapStr + "Symmetry: <select id='" + me.pre + "selectSymmetry'>";
        html += "</select>" + me.htmlCls.space3;
        html += me.htmlCls.buttonStr + "applysymmetry'>Apply</button>" + me.htmlCls.space3;
        html += me.htmlCls.buttonStr + "clearsymmetry'>Clear</button></div>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_symd' style='max-width:400px' class='" + dialogClass + "'><br>";

        html += "</div>";

        html += me.htmlCls.divStr + "dl_contact' class='" + dialogClass + "'>";
        html += "<span style='white-space:nowrap;font-weight:bold;'>Distance: <select id='" + me.pre + "contactdist'>";
        html += me.htmlCls.setHtmlCls.getOptionHtml(['4', '5', '6', '7', '8', '9', '10'], 4);
        html += "</select></span>";
        html += "<span style='margin-left:30px; white-space:nowrap;font-weight:bold;'>Contact Type: <select id='" + me.pre + "contacttype'>";
        html += me.htmlCls.optionStr + "'calpha' >between C-alpha Atoms</option>";
        html += me.htmlCls.optionStr + "'cbeta' selected>between C-beta Atoms</option>";
        html += me.htmlCls.optionStr + "'heavyatoms' >between Heavy Atoms</option>";
        html += "</select></span><br><br>";
        html += "<span style='white-space:nowrap;'>" + me.htmlCls.buttonStr + "applycontactmap'>Display</button></span><br>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_hbonds' class='" + dialogClass + "'>";
        html += "1. Choose interaction types and their thresholds:<br>";
        html += "<div class='icn3d-box'><table border=0 width=450><tr>";
        html += "<td style='white-space:nowrap'>" + me.htmlCls.inputCheckStr + "id='" + me.pre + "analysis_hbond' checked>Hydrogen Bonds <span style='background-color:#" + me.htmlCls.hbondColor + "'>" + me.htmlCls.space3 + "</span></td>";
        html += "<td>";
        html += me.htmlCls.divNowrapStr + " <select id='" + me.pre + "hbondthreshold'>";

        let optArray2 = ['3.2', '3.3', '3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4.0', '4.1', '4.2'];
        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray2, 6);

        html += "</select> &#197;" + me.htmlCls.space3 + "</div></td>";
        html += "<td style='white-space:nowrap'>" + me.htmlCls.inputCheckStr + "id='" + me.pre + "analysis_saltbridge' checked>Salt Bridge/Ionic <span style='background-color:#" + me.htmlCls.ionicColor + "'>" + me.htmlCls.space3 + "</span></td>";
        html += "<td>";
        html += me.htmlCls.divNowrapStr + " <select id='" + me.pre + "saltbridgethreshold'>";

        let optArray3 = ['3', '4', '5', '6', '7', '8'];
        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray3, 3);

        html += "</select> &#197;" + me.htmlCls.space3 + "</div></td>";
        html += "<td style='white-space:nowrap'>" + me.htmlCls.inputCheckStr + "id='" + me.pre + "analysis_contact' checked>Contacts/Interactions <span style='background-color:#" + me.htmlCls.contactColor + "'>" + me.htmlCls.space3 + "</span></td>";
        html += "<td>";
        html += me.htmlCls.divNowrapStr + " <select id='" + me.pre + "contactthreshold'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray3, 1);

        html += "</select> &#197;" + me.htmlCls.space3 + "</div></td>";
        html += "</tr>";

        html += "<tr>";
        html += "<td style='white-space:nowrap'>" + me.htmlCls.inputCheckStr + "id='" + me.pre + "analysis_halogen' checked>Halogen Bonds <span style='background-color:#" + me.htmlCls.halogenColor + "'>" + me.htmlCls.space3 + "</span></td>";
        html += "<td>";
        html += me.htmlCls.divNowrapStr + " <select id='" + me.pre + "halogenthreshold'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray2, 6);

        html += "</select> &#197;" + me.htmlCls.space3 + "</div></td>";
        html += "<td style='white-space:nowrap'>" + me.htmlCls.inputCheckStr + "id='" + me.pre + "analysis_pication' checked>&pi;-Cation <span style='background-color:#" + me.htmlCls.picationColor + "'>" + me.htmlCls.space3 + "</span></td>";
        html += "<td>";
        html += me.htmlCls.divNowrapStr + " <select id='" + me.pre + "picationthreshold'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray3, 3);

        html += "</select> &#197;" + me.htmlCls.space3 + "</div></td>";
        html += "<td style='white-space:nowrap'>" + me.htmlCls.inputCheckStr + "id='" + me.pre + "analysis_pistacking' checked>&pi;-Stacking <span style='background-color:#" + me.htmlCls.pistackingColor + "'>" + me.htmlCls.space3 + "</span></td>";
        html += "<td>";
        html += me.htmlCls.divNowrapStr + " <select id='" + me.pre + "pistackingthreshold'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(['3', '4', '5'], 99);

        html += me.htmlCls.optionStr + "'5.5' selected>5.5</option>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(['6', '7', '8'], 99);

        html += "</select> &#197;" + me.htmlCls.space3 + "</div></td>";
        html += "</tr></table></div>";

        html += "<table border=0 width=400 cellspacing=10><tr><td>";

        html += me.htmlCls.divNowrapStr + "2. Select the first set:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "atomsCustomHbond2' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td><td>";

        html += me.htmlCls.divNowrapStr + "3. Select the second set:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "atomsCustomHbond' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td></tr></table>";
        
        html += "<div>4. " + me.htmlCls.buttonStr + "applyhbonds'>3D Display Interactions</button></div><br>";

        html += "<div style='text-indent:1.1em'>" + me.htmlCls.buttonStr + "hbondWindow'>Highlight Interactions in Table</button><span style='margin-left:30px; font-wieght:bold'>Sort Interactions on</span>: " + me.htmlCls.buttonStr + "sortSet1'> Set 1</button>" + me.htmlCls.buttonStr + "sortSet2' style='margin-left:20px'>Set 2</button></div><br>";

        html += "<div style='text-indent:1.1em'>" + me.htmlCls.buttonStr + "hbondLineGraph'>2D Interaction Network</button> to show interactions between two lines of residue nodes</div><br>";

        html += "<div style='text-indent:1.1em'>" + me.htmlCls.buttonStr + "hbondScatterplot'>2D Interaction Map</button> to show interactions as map</div><br>";

        tmpStr = ': </td><td><input style="margin-left:-12px" type="text" id="';

        html += "<div style='text-indent:1.1em'>" + me.htmlCls.buttonStr + "hbondGraph'>2D Graph(Force-Directed)</button> to show interactions with strength parameters in 0-200:</div>";
        html += '<div style="text-indent:1.1em"><table><tr><td>Helix or Sheet' + tmpStr + me.pre + 'dist_ss" size="4" value="100"></td>';
        html += '<td>Coil or Nucleotide' + tmpStr + me.pre + 'dist_coil" size="4" value="50"></td>';
        html += '<td>Disulfide Bonds' + tmpStr + me.pre + 'dist_ssbond" size="4" value="50"></td></tr>';
        html += '<tr><td>Hydrogen Bonds' + tmpStr + me.pre + 'dist_hbond" size="4" value="50"></td>';
        html += '<td>Salt Bridge/Ionic' + tmpStr + me.pre + 'dist_ionic" size="4" value="50"></td>';
        html += '<td>Contacts' + tmpStr + me.pre + 'dist_inter" size="4" value="25"></td></tr>';
        html += '<tr><td>Halogen Bonds' + tmpStr + me.pre + 'dist_halogen" size="4" value="50"></td>';
        html += '<td>&pi;-Cation' + tmpStr + me.pre + 'dist_pication" size="4" value="50"></td>';
        html += '<td>&pi;-Stacking' + tmpStr + me.pre + 'dist_pistacking" size="4" value="50"></td></tr></table></div>';
        html += '<div style="text-indent:1.1em">(Note: you can also adjust thresholds at #1 to add/remove interactions.)</div><br>';

    //    html += "<div style='text-indent:1.1em'>" + me.htmlCls.buttonStr + "hbondExport'>Save</button> H-bond/contact pairs in a file</div><br>";
        html += "<div style='text-indent:1.1em'>" + me.htmlCls.buttonStr + "areaWindow'>Buried Surface Area</button></div><br>";

        html += "<div>5. " + me.htmlCls.buttonStr + "hbondReset'>Reset</button> and select new sets</div>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_realign' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "1. Select sets below <br>or use your current selection:</div><br>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomRealign' multiple size='5' style='min-width:130px;'>";
        html += "</select></div><br>";

        html += "<div>2. " + me.htmlCls.buttonStr + "applyRealign'>Realign by Sequence</button></div><br>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_realignbystruct' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "1. Select sets in two chains below <br>or use your current selection:</div><br>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomRealignByStruct' multiple size='5' style='min-width:130px;'>";
        html += "</select></div><br>";

        html += "<div>2. " + me.htmlCls.buttonStr + "applyRealignByStruct_tmalign'>Realign with TM-align</button>" + me.htmlCls.buttonStr + "applyRealignByStruct' style='margin-left:30px'>Realign with VAST</button></div><br>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_realigntwostru' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "1. Select sets in two structures below <br>or use your current selection:</div><br>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomRealignByStruct2' multiple size='5' style='min-width:130px;'>";
        html += "</select></div><br>";

        html += "2. Overall maximum RMSD: " + me.htmlCls.inputTextStr + "id='" + me.pre + "maxrmsd' value='30' size='2'> &#197; <br><br>";

        html += "<div>3. " + me.htmlCls.buttonStr + "applyRealignByStruct_vastplus'>VAST+ Alignment based on TM-align</button></div><br>";
        html += "</div>";


        html += me.htmlCls.divStr + "dl_colorspectrumacrosssets' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "1. Select sets below:</div><br>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomColorSpectrumAcross' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "<div>2. " + me.htmlCls.buttonStr + "applyColorSpectrumAcrossSets'>Spectrum Color for Sets</button></div><br>";
        html += "</div>";

        
        html += me.htmlCls.divStr + "dl_colorspectrumbysets' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "1. Select sets below:</div><br>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomColorSpectrum' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "<div>2. " + me.htmlCls.buttonStr + "applyColorSpectrumBySets'>Spectrum Color for Residues in Sets</button></div><br>";
        html += "</div>";

        
        html += me.htmlCls.divStr + "dl_colorrainbowacrosssets' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "1. Select sets below:</div><br>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomColorRainbowAcross' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "<div>2. " + me.htmlCls.buttonStr + "applyColorRainbowAcrossSets'>Rainbow Color for Sets</button></div><br>";
        html += "</div>";


        html += me.htmlCls.divStr + "dl_colorrainbowbysets' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "1. Select sets below:</div><br>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomColorRainbow' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "<div>2. " + me.htmlCls.buttonStr + "applyColorRainbowBySets'>Rainbow Color for Residues in Sets</button></div><br>";
        html += "</div>";


        html += me.htmlCls.divStr + "dl_allinteraction' style='background-color:white' class='" + dialogClass + "'>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_interactionsorted' style='background-color:white' class='" + dialogClass + "'>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_linegraph' style='background-color:white' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + '<div style="width:20px; margin-top:6px; display:inline-block;"><span id="'
          + me.pre + 'dl_linegraphcolor_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="display:none; width:15px;" title="Expand"></span><span id="'
          + me.pre + 'dl_linegraphcolor_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="width:15px;" title="Shrink"></span></div>';

        html += me.htmlCls.space2 + "Hold Ctrl key to select multiple nodes/lines.</div>";

        html += me.htmlCls.divStr + "dl_linegraphcolor' style='display:block;'>";

        html += me.htmlCls.setHtmlCls.setColorHints();

        html += "</div><br>";

        //let buttonStrTmp = '<button class="icn3d-commandTitle" style="-webkit-appearance:button; height:24px;background-color:#DDD;" id="';

        me.linegraphid = me.pre + 'linegraph';
        html += me.htmlCls.divNowrapStr + buttonStrTmp + me.linegraphid + '_svg">SVG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.linegraphid + '_png">PNG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.linegraphid + '_json">JSON</button>' + me.htmlCls.space4;
        html += "<b>Scale</b>: <select id='" + me.linegraphid + "_scale'>";

        let optArray4 = ['0.1', '0.2', '0.4', '0.6', '0.8', '1', '2', '4', '6', '8', '10'];
        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray4, 5);

        html += "</select></div><br>";
        html += '<div id="' + me.pre + 'linegraphDiv"></div>';

        html += "</div>";

        html += me.htmlCls.divStr + "dl_scatterplot' style='background-color:white' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "Hold Ctrl key to select multiple nodes." + me.htmlCls.space3;

        html += '<div style="width:20px; margin-top:6px; display:inline-block;"><span id="'
          + me.pre + 'dl_scatterplotcolor_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="'
          + me.pre + 'dl_scatterplotcolor_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div></div>';
        html += me.htmlCls.divStr + "dl_scatterplotcolor' style='display:none;'>";

        html += me.htmlCls.setHtmlCls.setColorHints();

        html += "</div>";

        me.scatterplotid = me.pre + 'scatterplot';
        html += me.htmlCls.divNowrapStr + buttonStrTmp + me.scatterplotid + '_svg">SVG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.scatterplotid + '_png">PNG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.scatterplotid + '_json">JSON</button>' + me.htmlCls.space4;
        html += "<b>Scale</b>: <select id='" + me.scatterplotid + "_scale'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray4, 5);

        html += "</select></div><br>";
        html += '<div id="' + me.pre + 'scatterplotDiv"></div>';

        html += "</div>";

        html += me.htmlCls.divStr + "dl_contactmap' style='background-color:white' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "Hold Ctrl key to select multiple nodes." + me.htmlCls.space3 + "</div>";

        me.contactmapid = me.pre + 'contactmap';
        html += me.htmlCls.divNowrapStr + buttonStrTmp + me.contactmapid + '_svg">SVG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.contactmapid + '_png">PNG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.contactmapid + '_json">JSON</button>' + me.htmlCls.space4;
        html += "<b>Scale</b>: <select id='" + me.contactmapid + "_scale'>";

        let optArray5 = ['0.01', '0.02', '0.04', '0.06', '0.08', '0.1', '0.2', '0.4', '0.6', '0.8', '1'];
        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray5, 10);

        html += "</select></div><br>";
        html += '<div id="' + me.pre + 'contactmapDiv"></div>';

        html += "</div>";

        html += me.htmlCls.divStr + "dl_alignerrormap' style='background-color:white' class='" + dialogClass + "'>";

        html += me.htmlCls.divNowrapStr + "Hold Ctrl key to select multiple nodes." + me.htmlCls.space3 + "</div>";
      
        me.alignerrormapid = me.pre + 'alignerrormap';
        html += me.htmlCls.divNowrapStr + buttonStrTmp + me.alignerrormapid + '_svg">SVG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.alignerrormapid + '_png">PNG (slow)</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.alignerrormapid + '_json">JSON</button>' + me.htmlCls.space4;
        html += '<b>Scale</b>: <select id="' + me.alignerrormapid + '_scale">';

        //let optArray5 = ['0.01', '0.02', '0.04', '0.06', '0.08', '0.1', '0.2', '0.4', '0.6', '0.8', '1'];
        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray5, 2);

        html += "</select></div><br>";

        //min: 004d00, max: FFFFFF
        let startColorStr = '#004d00';
        let endColorStr = '#FFFFFF';
        let rangeStr = startColorStr + ' 0%, ' + endColorStr + ' 100%';

        html += "<div style='width:200px'><div style='height: 12px; border: 1px solid #000; background: linear-gradient(to right, " + rangeStr + ");'></div>";
        html += "<table width='100%' border='0' cellspacing='0' cellpadding='0'><tr><td width='15%'>0</td><td width='15%'>5</td><td width='15%'>10</td><td width='15%'>15</td><td width='15%'>20</td><td width='15%'>25</td><td>30</td></tr><tr><td colspan='7' align='center'>Expected position error (Angstroms)</td></tr></table></div><br>";
  
        html += '<div id="' + me.pre + 'alignerrormapDiv"></div>';

        html += "</div>";

        html += me.htmlCls.divStr + "dl_elecmap2fofc' class='" + dialogClass + "'>";
        html += "<span style='white-space:nowrap;font-weight:bold;'>Contour at: <select id='" + me.pre + "sigma2fofc'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray1, 3);

        html += "</select> &sigma;</span> <span style='white-space:nowrap; margin-left:30px;'>" + me.htmlCls.buttonStr + "applymap2fofc'>Display</button></span> <span style='white-space:nowrap; margin-left:30px;'>" + me.htmlCls.buttonStr + "elecmapNo2'>Remove Map</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_elecmapfofc' class='" + dialogClass + "'>";
        html += "<span style='white-space:nowrap;font-weight:bold;'>Contour at: <select id='" + me.pre + "sigmafofc'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(optArray1, 5);

        html += "</select> &sigma;</span> <span style='white-space:nowrap; margin-left:30px;'>" + me.htmlCls.buttonStr + "applymapfofc'>Display</button></span> <span style='white-space:nowrap; margin-left:30px;'>" + me.htmlCls.buttonStr + "elecmapNo3'>Remove Map</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_emmap' class='" + dialogClass + "'>";
        html += "<span style='white-space:nowrap;font-weight:bold;'>Contour at: <select id='" + me.pre + "empercentage'>";

        html += me.htmlCls.setHtmlCls.getOptionHtml(['0', '10', '20', '30', '40', '50', '60', '70', '80', '90', '100'], 3);

        html += "</select> % of maximum EM values</span><br><span style='white-space:nowrap; margin-left:30px;'>" + me.htmlCls.buttonStr + "applyemmap'>Display</button></span> <span style='white-space:nowrap; margin-left:30px;'>" + me.htmlCls.buttonStr + "emmapNo2'>Remove EM Map</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_aroundsphere' class='" + dialogClass + "'>";
        html += me.htmlCls.divNowrapStr + "1. Select the first set:</div>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomSphere2' multiple size='3' style='min-width:130px;'>";
        html += "</select></div><br>";
        html += me.htmlCls.divNowrapStr + "2. Sphere with a radius: " + me.htmlCls.inputTextStr + "id='" + me.pre + "radius_aroundsphere' value='4' size='2'> &#197;</div><br/>";

        html += me.htmlCls.divNowrapStr + "3. Select the second set to apply the sphere:</div>";
        html += "<div style='text-indent:1.1em'><select id='" + me.pre + "atomsCustomSphere' multiple size='3' style='min-width:130px;'>";
        html += "</select></div><br>";

        html += me.htmlCls.divNowrapStr + "4. " + me.htmlCls.buttonStr + "applypick_aroundsphere'>Display</button> the sphere around the first set of atoms</div><br>";
        html += "<div style='text-indent:1.1em'>" + me.htmlCls.buttonStr + "sphereExport'>Save</button> interacting/contacting residue pairs in a file</div>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_adjustmem' class='" + dialogClass + "'>";
        html += "<b>Note</b>: The membranes are parallel to the X-Y plane. The center of the membranes is at Z = 0. <br/><br/>";
        html += me.htmlCls.divNowrapStr + "1. Extracellular membrane Z-axis position: " + me.htmlCls.inputTextStr + "id='" + me.pre + "extra_mem_z' value='' size='3'> &#197;</div><br/>";
        html += me.htmlCls.divNowrapStr + "2. intracellular membrane Z-axis position: " + me.htmlCls.inputTextStr + "id='" + me.pre + "intra_mem_z' value='' size='3'> &#197;</div><br/>";
        html += me.htmlCls.divNowrapStr + "3. " + me.htmlCls.buttonStr + "apply_adjustmem'>Display</button> the adjusted membranes</div><br>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_selectplane' class='" + dialogClass + "'>";
        html += "<b>Note</b>: The membranes are parallel to the X-Y plane. The center of the membranes is at Z = 0. <br/><br/>";
        html += me.htmlCls.divNowrapStr + "1. Z-axis position of the first X-Y plane: " + me.htmlCls.inputTextStr + "id='" + me.pre + "selectplane_z1' value='15' size='3'> &#197;</div><br/>";
        html += me.htmlCls.divNowrapStr + "2. Z-axis position of the second X-Y plane: " + me.htmlCls.inputTextStr + "id='" + me.pre + "selectplane_z2' value='-15' size='3'> &#197;</div><br/>";
        html += me.htmlCls.divNowrapStr + "3. " + me.htmlCls.buttonStr + "apply_selectplane'>Save</button> the region between the planes to Defined Sets</div><br>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_addlabel' class='" + dialogClass + "'>";
        html += "1. Text: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labeltext' value='Text' size=4><br/>";
        html += "2. Size: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labelsize' value='18' size=4 maxlength=2><br/>";
        html += "3. Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labelcolor' value='" + defaultColor + "' size=4><br/>";
        //html += "4. Background: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labelbkgd' value='' size=4><br/>";
        if(me.utilsCls.isMobile()) {
            html += me.htmlCls.spanNowrapStr + "4. Touch TWO atoms</span><br/>";
        }
        else {
            html += me.htmlCls.spanNowrapStr + "4. Pick TWO atoms while holding \"Alt\" key</span><br/>";
        }
        html += me.htmlCls.spanNowrapStr + "5. " + me.htmlCls.buttonStr + "applypick_labels'>Display</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_addlabelselection' class='" + dialogClass + "'>";
        html += "1. Text: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labeltext2' value='Text' size=4><br/>";
        html += "2. Size: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labelsize2' value='18' size=4 maxlength=2><br/>";
        html += "3. Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labelcolor2' value='" + defaultColor + "' size=4><br/>";
        //html += "4. Background: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labelbkgd2' value='' size=4><br/>";
        html += me.htmlCls.spanNowrapStr + "4. " + me.htmlCls.buttonStr + "applyselection_labels'>Display</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_labelColor' class='" + dialogClass + "'>";
        html += "Color for all labels: " + me.htmlCls.inputTextStr + "id='" + me.pre + "labelcolorall' value='" + defaultColor + "' size=4><br/><br/>";
        html += me.htmlCls.spanNowrapStr + me.htmlCls.buttonStr + "applylabelcolor'>Display</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_distance' class='" + dialogClass + "'>";
        if(me.utilsCls.isMobile()) {
            html += me.htmlCls.spanNowrapStr + "1. Touch TWO atoms</span><br/>";
        }
        else {
            html += me.htmlCls.spanNowrapStr + "1. Pick TWO atoms while holding \"Alt\" key</span><br/>";
        }
        html += me.htmlCls.spanNowrapStr + "2. Line Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "distancecolor' value='" + defaultColor + "' size=4><br/>";
        html += me.htmlCls.spanNowrapStr + "3. " + me.htmlCls.buttonStr + "applypick_measuredistance'>Display</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_stabilizer' class='" + dialogClass + "'>";
        if(me.utilsCls.isMobile()) {
            html += me.htmlCls.spanNowrapStr + "1. Touch TWO atoms</span><br/>";
        }
        else {
            html += me.htmlCls.spanNowrapStr + "1. Pick TWO atoms while holding \"Alt\" key</span><br/>";
        }
        html += me.htmlCls.spanNowrapStr + "2. Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "stabilizercolor' value='ffffff' size=4><br/>";
        html += me.htmlCls.spanNowrapStr + "3. " + me.htmlCls.buttonStr + "applypick_stabilizer'>Add</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_disttwosets' class='" + dialogClass + "'>";
        html += me.htmlCls.spanNowrapStr + "1. Select two sets</span><br/>";
        html += "<table border=0 width=400 cellspacing=10><tr><td>";

        html += me.htmlCls.divNowrapStr + "First set:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "atomsCustomDist2' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td><td>";

        html += me.htmlCls.divNowrapStr + "Second set:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "atomsCustomDist' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td></tr></table>";

        html += me.htmlCls.spanNowrapStr + "2. Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "distancecolor2' value='" + defaultColor + "' size=4><br/><br/>";
        html += me.htmlCls.spanNowrapStr + "3. " + me.htmlCls.buttonStr + "applydist2'>Display</button></span>";
        html += "</div>";

        
        html += me.htmlCls.divStr + "dl_linebtwsets' class='" + dialogClass + "'>";
        html += me.htmlCls.spanNowrapStr + "1. Select two sets</span><br/>";
        html += "<table border=0 width=400 cellspacing=10><tr><td>";

        html += me.htmlCls.divNowrapStr + "First set:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "linebtwsets2' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td><td>";

        html += me.htmlCls.divNowrapStr + "Second set:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "linebtwsets' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td></tr></table>";

        html += me.htmlCls.divNowrapStr + "2. Line style: <select id='" + me.pre + "linebtwsets_style'>";
        html += me.htmlCls.setHtmlCls.getOptionHtml(['Solid', 'Dashed'], 0);
        html += "</select></div><br>";

        html += "3. Line radius: " + me.htmlCls.inputTextStr + "id='" + me.pre + "linebtwsets_radius' value='0.4' size=4><br/><br/>";
        
        html += "4. Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "linebtwsets_customcolor' value='" + defaultColor + "' size=4><br/><br/>";

        html += me.htmlCls.divNowrapStr + "5. Opacity: <select id='" + me.pre + "linebtwsets_opacity'>";
        html += me.htmlCls.setHtmlCls.getOptionHtml(['1.0', '0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.1'], 7);
        html += "</select></div><br>";

        html += me.htmlCls.spanNowrapStr + "6. " + me.htmlCls.buttonStr + "applylinebtwsets'>Display</button></span>";
        html += me.htmlCls.space3 + me.htmlCls.spanNowrapStr + me.htmlCls.buttonStr + "clearlinebtwsets'>Clear</button></span>";
        html += "</div>";


        html += me.htmlCls.divStr + "dl_cartoonshape' class='" + dialogClass + "'>";
        html += me.htmlCls.spanNowrapStr + "1. Select a set:</span><br/>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "cartoonshape' multiple size='5' style='min-width:130px;'>";
        html += "</select></div><br>";

        html += me.htmlCls.divNowrapStr + "2. Shape: <select id='" + me.pre + "cartoonshape_shape'>";
        html += me.htmlCls.setHtmlCls.getOptionHtml(['Sphere', 'Cube'], 0);
        html += "</select></div><br>";

        html += "3. Radius: " + me.htmlCls.inputTextStr + "id='" + me.pre + "cartoonshape_radius' value='1.5' size=4><br/><br/>";
        
        html += "4. Color: " + me.htmlCls.inputTextStr + "id='" + me.pre + "cartoonshape_customcolor' value='" + defaultColor + "' size=4><br/><br/>";

        html += me.htmlCls.divNowrapStr + "5. Opacity: <select id='" + me.pre + "cartoonshape_opacity'>";
        html += me.htmlCls.setHtmlCls.getOptionHtml(['1.0', '0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.1'], 7);
        html += "</select></div><br>";
        
        html += me.htmlCls.spanNowrapStr + "6. " + me.htmlCls.buttonStr + "applycartoonshape'>Display</button></span>";
        html += me.htmlCls.space3 + me.htmlCls.spanNowrapStr + me.htmlCls.buttonStr + "clearcartoonshape'>Clear</button></span>";
        html += "</div>";


        html += me.htmlCls.divStr + "dl_distmanysets' class='" + dialogClass + "'>";
        html += me.htmlCls.spanNowrapStr + "1. Select sets for pairwise distances</span><br/>";
        html += "<table border=0 width=400 cellspacing=10><tr><td>";

        html += me.htmlCls.divNowrapStr + "First sets:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "atomsCustomDistTable2' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td><td>";

        html += me.htmlCls.divNowrapStr + "Second sets:</div>";
        html += "<div style='text-indent:1.1em'><select style='max-width:200px' id='" + me.pre + "atomsCustomDistTable' multiple size='5' style='min-width:130px;'>";
        html += "</select></div>";

        html += "</td></tr></table>";

        html += me.htmlCls.spanNowrapStr + "2. " + me.htmlCls.buttonStr + "applydisttable'>Distances in Table</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_stabilizer_rm' class='" + dialogClass + "'>";
        if(me.utilsCls.isMobile()) {
            html += me.htmlCls.spanNowrapStr + "1. Touch TWO atoms</span><br/>";
        }
        else {
            html += me.htmlCls.spanNowrapStr + "1. Pick TWO atoms while holding \"Alt\" key</span><br/>";
        }
        html += me.htmlCls.spanNowrapStr + "2. " + me.htmlCls.buttonStr + "applypick_stabilizer_rm'>Remove</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_thickness' class='" + dialogClass + "'>";
        html += me.htmlCls.setHtmlCls.setThicknessHtml('3dprint');
        html += "</div>";

        html += me.htmlCls.divStr + "dl_thickness2' class='" + dialogClass + "'>";
        html += me.htmlCls.setHtmlCls.setThicknessHtml('style');
        html += "</div>";

        html += me.htmlCls.divStr + "dl_menupref' class='" + dialogClass + "'>";
        html += "<b>Note</b>: The following parameters will be saved in cache. You just need to set them once. <br><br>";
        html += "<div id='" + me.pre + "menulist' style='max-width:500px; max-height:300px; overflow:scroll'></div><br><br>";
        html += me.htmlCls.spanNowrapStr + "" + me.htmlCls.buttonStr + "apply_menupref'>Apply</button></span>";
        html += me.htmlCls.spanNowrapStr + "" + me.htmlCls.buttonStr + "reset_menupref' style='margin-left:30px'>Reset to Simple Menus</button></span>";
        html += me.htmlCls.spanNowrapStr + "" + me.htmlCls.buttonStr + "reset_menupref_all' style='margin-left:30px'>Reset to All Menus</button></span>";
        html += me.htmlCls.spanNowrapStr + "" + me.htmlCls.buttonStr + "savepref' style='margin-left:30px'>Save Preferences</button></span>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_addtrack' class='" + dialogClass + "'>";
        html += " <input type='hidden' id='" + me.pre + "track_chainid' value=''>";

        html += me.htmlCls.divStr + "dl_addtrack_tabs' style='border:0px;'>";
        html += "<ul>";
        html += "<li><a href='#" + me.pre + "tracktab1'>NCBI gi/Accession</a></li>";
        html += "<li><a href='#" + me.pre + "tracktab2'>FASTA</a></li>";
        html += "<li><a href='#" + me.pre + "tracktab2b'>FASTA Alignment</a></li>";
        html += "<li><a href='#" + me.pre + "tracktab3'>BED File</a></li>";
        html += "<li><a href='#" + me.pre + "tracktab4'>Custom</a></li>";
        html += "<li><a href='#" + me.pre + "tracktab5'>Current Selection</a></li>";
        html += "</ul>";
        html += me.htmlCls.divStr + "tracktab1'>";
        html += "NCBI gi/Accession: " + me.htmlCls.inputTextStr + "id='" + me.pre + "track_gi' placeholder='gi' size=16> <br><br>";
        html += me.htmlCls.buttonStr + "addtrack_button1'>Add Track</button>";
        html += "</div>";
        html += me.htmlCls.divStr + "tracktab2'>";
        html += "FASTA Title: " + me.htmlCls.inputTextStr + "id='" + me.pre + "fasta_title' placeholder='track title' size=16> <br><br>";
        html += "FASTA sequence: <br><textarea id='" + me.pre + "track_fasta' rows='5' style='width: 100%; height: " +(2*me.htmlCls.LOG_HEIGHT) + "px; padding: 0px; border: 0px;'></textarea><br><br>";
        html += me.htmlCls.buttonStr + "addtrack_button2'>Add Track</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "tracktab2b'>";
        html += "<div style='width:600px'>The full protein sequences with gaps are listed one by one. The sequence of the structure is listed at the top. If there are non-gap residues(e.g., from RefSeq) outside of the sequence of the structure, please remove them. Each sequence has a title line starting with \">\".</div><br>";
        html += "<b>FASTA alignment sequences</b>:<br>";
        html += "<textarea id='" + me.pre + "track_fastaalign' rows='5' style='width: 100%; height: " +(2*me.htmlCls.LOG_HEIGHT) + "px; padding: 0px; border: 0px;'></textarea><br><br>";
        html += "Position of the first residue in Sequences & Annotations window: " + me.htmlCls.inputTextStr + "id='" + me.pre + "fasta_startpos' value='1' size=2> <br><br>";

        html += "Color Sequence by: <select id='" + me.pre + "colorseqby'>";
        html += me.htmlCls.optionStr + "'identity' selected>Identity</option>";
        html += me.htmlCls.optionStr + "'conservation'>Conservation</option>";
        html += "</select> <br><br>";

        html += me.htmlCls.buttonStr + "addtrack_button2b'>Add Track(s)</button>";
        html += "</div>";

        html += me.htmlCls.divStr + "tracktab3'>";
        html += "BED file: " + me.htmlCls.inputFileStr + "id='" + me.pre + "track_bed' size=16> <br><br>";
        html += me.htmlCls.buttonStr + "addtrack_button3'>Add Track</button>";
        html += "</div>";
        html += me.htmlCls.divStr + "tracktab4'>";
        html += "Track Title: " + me.htmlCls.inputTextStr + "id='" + me.pre + "track_title' placeholder='track title' size=16> <br><br>";
        html += "Track Text (e.g., \"2 G, 5-6 RR\" defines a character \"G\" at the position 2 and two continuous characters \"RR\" at positions from 5 to 6. The starting position is 1): <br>";
        html += "<textarea id='" + me.pre + "track_text' rows='5' style='width: 100%; height: " +(2*me.htmlCls.MENU_HEIGHT) + "px; padding: 0px; border: 0px;'></textarea><br><br>";
        html += me.htmlCls.buttonStr + "addtrack_button4'>Add Track</button>";
        html += "</div>";
        html += me.htmlCls.divStr + "tracktab5'>";
        html += "Track Title: " + me.htmlCls.inputTextStr + "id='" + me.pre + "track_selection' placeholder='track title' size=16> <br><br>";
        html += me.htmlCls.buttonStr + "addtrack_button5'>Add Track</button>";
        html += "</div>";

        html += "</div>";

        html += "</div>";

        html += me.htmlCls.divStr + "dl_saveselection' class='" + dialogClass + "'>";
        let index =(ic && ic.defNames2Atoms) ? Object.keys(ic.defNames2Atoms).length : 1;
        let suffix = '';
        html += "Name: " + me.htmlCls.inputTextStr + "id='" + me.pre + "seq_command_name" + suffix + "' value='seq_" + index + "' size='5'> <br>";
        //html += "Description: " + me.htmlCls.inputTextStr + "id='" + me.pre + "seq_command_desc" + suffix + "' value='seq_desc_" + index + "' size='10'> <br>";
        html += "<button style='white-space:nowrap;' id='" + me.pre + "seq_saveselection" + suffix + "'>Save</button> <button style='white-space:nowrap; margin-left:20px;' id='" + me.pre + "seq_clearselection" + suffix + "'>Clear</button><br/><br/>";
        html += "</div>";


        html += me.htmlCls.divStr + "dl_copyurl' style='width:520px;' class='" + dialogClass + "'>";
        html += "Please copy one of the URLs below. They show the same result.<br>(To add a title to share link, click \"Windows > Your Note\" and click \"File > Share Link\" again.)<br><br>";
        html += "Original URL with commands: <br><textarea id='" + me.pre + "ori_url' rows='4' style='width:100%'></textarea><br><br>";
        html += "Lifelong Short URL:(To replace this URL, send a pull request to update share.html at <a href='https://github.com/ncbi/icn3d' target='_blank'>iCn3D GitHub</a>)<br>" + me.htmlCls.inputTextStr + "id='" + me.pre + "short_url' value='' style='width:100%'><br><br>";
        html += "Lifelong Short URL + Window Title:(To update the window title, click \"Analysis > Your Note/Window Title\".)<br>" + me.htmlCls.inputTextStr + "id='" + me.pre + "short_url_title' value='' style='width:100%'><br><br>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_selectannotations' class='" + dialogClass + " icn3d-annotation' style='background-color:white;'>";

        html += me.htmlCls.divStr + "dl_annotations_tabs'>";

        html += me.htmlCls.divStr + "dl_anno_view_tabs' style='border:0px; height:33px;'>";
        html += "<ul>";
        html += "<li><a href='#" + me.pre + "anno_tmp1' id='" + me.pre + "anno_summary'>Summary</a></li>";
        html += "<li><a href='#" + me.pre + "anno_tmp2' id='" + me.pre + "anno_details'>Details</a></li>";
        html += "</ul>";
        html += me.htmlCls.divStr + "anno_tmp1'>";
        html += "</div>";
        html += me.htmlCls.divStr + "anno_tmp2'>";
        html += "</div>";
        html += "</div>";

        html += this.getAnnoHeader();

        html += "<button style='white-space:nowrap; margin-left:5px;' id='" + me.pre + "showallchains'>Show All Chains</button><br>";

        html += me.htmlCls.divStr + "seqguide_wrapper' style='display:none'><br>" + me.htmlCls.setHtmlCls.setSequenceGuide("2") + "</div>";

        html += "</div><br/><hr><br>";

        html += me.htmlCls.divStr + "dl_annotations'>";
        html += "</div>";

        html += "</div>";

        html += me.htmlCls.divStr + "dl_graph' style='background-color:white;' class='" + dialogClass + "'>";
        me.svgid = me.pre + 'icn3d_viz';
        html += '<style>';
        html += '#' + me.svgid + ' svg { border: 1px solid; font: 13px sans-serif; text-anchor: end; }';
        html += '#' + me.svgid + ' .node { stroke: #eee; stroke-width: 1.5px; }';
        html += '.node .selected { stroke: ' + me.htmlCls.ORANGE + '; }';
        html += '.link { stroke: #999; stroke-opacity: 0.6; }';

        html += '</style>';

        html += me.htmlCls.divNowrapStr + '<b>Zoom</b>: mouse wheel; ' + me.htmlCls.space3 + ' <b>Move</b>: left button; ' + me.htmlCls.space3 + ' <b>Select Multiple Nodes</b>: Ctrl Key and drag an Area' + me.htmlCls.space3;
        html += '<div id="' + me.pre + 'interactionDesc" style="width:20px; margin-top:6px; display:inline-block;"><span id="'
          + me.pre + 'dl_svgcolor_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="'
          + me.pre + 'dl_svgcolor_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div></div>';
        html += me.htmlCls.divStr + "dl_svgcolor' style='display:none;'>";
        html += me.htmlCls.divNowrapStr + '<span style="margin-left:33px">Click "View > H-Bonds & Interactions" to adjust parameters and relaunch the graph</span></div>';
        html += me.htmlCls.divNowrapStr + '<span style="margin-left:33px; color:#00FF00; font-weight:bold">Green</span>: H-Bonds; ';
        html += '<span style="color:#00FFFF; font-weight:bold">Cyan</span>: Salt Bridge/Ionic; ';
        html += '<span style="font-weight:bold">Grey</span>: contacts; ';
        html += '<span style="color:' + me.htmlCls.ORANGE + '; font-weight:bold">Orange</span>: disulfide bonds</div>';
        html += me.htmlCls.divNowrapStr + '<span style="margin-left:33px; color:#FF00FF; font-weight:bold">Magenta</span>: Halogen Bonds; ';
        html += '<span style="color:#FF0000; font-weight:bold">Red</span>: &pi;-Cation; ';
        html += '<span style="color:#0000FF; font-weight:bold">Blue</span>: &pi;-Stacking</div>';
        html += "</div>";

        html += me.htmlCls.divNowrapStr + buttonStrTmp + me.svgid + '_svg">SVG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.svgid + '_png">PNG</button>' + me.htmlCls.space2;
        html += buttonStrTmp + me.svgid + '_json">JSON</button>';
        html += me.htmlCls.space3 + "<div id='" + me.pre + "force' style='display:inline-block;'><b>Force on Nodes</b>: <select id='" + me.svgid + "_force'>";
        html += me.htmlCls.optionStr + "'0'>No</option>";
        html += me.htmlCls.optionStr + "'1'>X-axis</option>";
        html += me.htmlCls.optionStr + "'2'>Y-axis</option>";
        html += me.htmlCls.optionStr + "'3'>Circle</option>";
        html += me.htmlCls.optionStr + "'4' selected>Random</option>";
        html += "</select></div>";
        html += me.htmlCls.space3 + "<b>Label Size</b>: <select id='" + me.svgid + "_label'>";
        tmpStr = 'icn3d-node-text';
        html += me.htmlCls.optionStr + "'" + tmpStr + "0'>No</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "4'>4px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "8' selected>8px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "12'>12px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "16'>16px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "24'>24px</option>";
        html += me.htmlCls.optionStr + "'" + tmpStr + "32'>32px</option>";
        html += "</select>";
        html += me.htmlCls.space3 + "<div id='" + me.pre + "internalEdges' style='display:inline-block;'><b>Internal Edges</b>: <select id='" + me.svgid + "_hideedges'>";
        html += me.htmlCls.optionStr + "'1' selected>Hide</option>";
        html += me.htmlCls.optionStr + "'0'>Show</option>";
        html += "</select></div>";
        html += "</div>";

        html += '<svg id="' + me.svgid + '" style="margin-top:6px;"></svg>';
        html += "</div>";

        html += me.htmlCls.divStr + "dl_area' class='" + dialogClass + "'>";
        html += "Solvent Accessible Surface Area(SASA) calculated using the <a href='https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0008140' target='_blank'>EDTSurf algorithm</a>: <br>";
        html += '(0-20% out is considered "in". 50-100% out is considered "out".)<br><br>';
        html += "<b>Toal</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "areavalue' value='' size='10'> &#8491;<sup>2</sup><br><br>";
        html += "<div id='" + me.pre + "areatable' style='max-height:400px; overflow:auto'></div>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_colorbyarea' class='" + dialogClass + "'>";
        html += "<div style='width:500px'>Color each residue based on the percentage of solvent accessilbe surface area. The color ranges from blue, to white, to red for a percentage of 0, 35(variable), and 100, respectively.</div><br>";
        html += "<b>Middle Percentage(White)</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "midpercent' value='35' size='10'>% <br><br>";
        html += "<button style='white-space:nowrap;' id='" + me.pre + "applycolorbyarea'>Color</button><br/><br/>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_rmsd' class='" + dialogClass + "' style='max-width:300px'>";
        
        html += "</div>";

        html += me.htmlCls.divStr + "dl_buriedarea' class='" + dialogClass + "'>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_propbypercentout' class='" + dialogClass + "'>";
        html += "<div style='width:400px'>Select residue based on the percentage of solvent accessilbe surface area. The values are in the range of 0-100.</div><br>";
        html += "<b>Min Percentage</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "minpercentout' value='0' size='10'>% <br>";
        html += "<b>Max Percentage</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "maxpercentout' value='100' size='10'>% <br>";
        html += "<button style='white-space:nowrap;' id='" + me.pre + "applypropbypercentout'>Apply</button><br/><br/>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_propbybfactor' class='" + dialogClass + "'>";
        html += "<div style='width:400px'>Select residue based on B-factor. The values are in the range of 0-100.</div><br>";
        html += "<b>Min B-factor</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "minbfactor' value='0' size='10'>% <br>";
        html += "<b>Max B-factor</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "maxbfactor' value='100' size='10'>% <br>";
        html += "<button style='white-space:nowrap;' id='" + me.pre + "applypropbybfactor'>Apply</button><br/><br/>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_legend' class='" + dialogClass + "' style='max-width:500px; background-color:white'>";
        html += "</div>";

        html += me.htmlCls.divStr + "dl_disttable' class='" + dialogClass + "'>";
        html += "</div>";

        html += "</div>";
        html += "<!--/form-->";

        return html;
    }

    getAnnoHeader() { let me = this.icn3dui; me.icn3d;
        let html = '';

        html += "<div id='" + me.pre + "annoHeaderSection' class='icn3d-box' style='width:520px;'><b>Annotations:&nbsp;</b><br>";
        html += "<div id='" + me.pre + "annoHeader'><table border=0><tr>";
        let tmpStr1 = "<td style='min-width:110px;'><span style='white-space:nowrap'>";
        let tmpStr2 = "<td style='min-width:130px;'><span style='white-space:nowrap'>";

        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_all'>All" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr2 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_cdd' checked>Conserved Domains" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_clinvar'>ClinVar" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_binding'>Functional Sites" + me.htmlCls.space2 + "</span></td>";
        html += "</tr><tr>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_custom'>Custom" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr2 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_3dd'>3D Domains" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_snp'>SNPs" + me.htmlCls.space2 + "</span></td>";
        
        // if(me.cfg.mmdbid != undefined || me.cfg.pdbid != undefined || me.cfg.mmtfid != undefined || me.cfg.mmcifid != undefined) { // PDB
        //     html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_ptm' disabled>PTM (UniProt)" + me.htmlCls.space2 + "</span></td>";
        // }
        // else {
            html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_ptm'>PTM (UniProt)" + me.htmlCls.space2 + "</span></td>";
        // }
        html += "<td></td>";
        html += "</tr><tr>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_ssbond'>Disulfide Bonds" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_interact'>Interactions" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_crosslink'>Cross-Linkages" + me.htmlCls.space2 + "</span></td>";
        html += tmpStr1 + me.htmlCls.inputCheckStr + "id='" + me.pre + "anno_transmem'>Transmembrane" + me.htmlCls.space2 + "</span></td>";

        html += "<td></td>";
        html += "</tr></table></div></div>";

        return html;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Events {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    // simplify setLogCmd from clickMenuCls
    setLogCmd(str, bSetCommand, bAddLogs) {var me = this.icn3dui; me.icn3d;
        me.htmlCls.clickMenuCls.setLogCmd(str, bSetCommand, bAddLogs);
    }

    // ====== events start ===============
    fullScreenChange() { let me = this.icn3dui, ic = me.icn3d, thisClass = this; // event handler uses ".bind(inputAsThis)" to define "this"
        if(me.bNode) return;

        let fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement
          || document.mozFullscreenElement || document.msFullscreenElement;
        if(!fullscreenElement) {
            thisClass.setLogCmd("exit full screen", false);
            ic.bFullscreen = false;
            me.utilsCls.setViewerWidthHeight(me, true);
            ic.applyCenterCls.setWidthHeight(me.htmlCls.WIDTH, me.htmlCls.HEIGHT);
            ic.drawCls.draw();
        }
    }

    convertUniProtInChains(alignment) { let me = this.icn3dui; me.icn3d;
        let idArray = alignment.split(',');
        let alignment_final = '';
        for(let i = 0, il = idArray.length; i < il; ++i) {
            alignment_final += (idArray[i].indexOf('_') != -1) ? idArray[i] : idArray[i] + '_A'; // AlphaFold ID
            if(i < il - 1) alignment_final += ',';
        }

        return alignment_final;
    }

    async searchSeq() { let me = this.icn3dui, ic = me.icn3d, thisClass = this;
       let select = $("#" + me.pre + "search_seq").val();
       if(isNaN(select) && select.indexOf('$') == -1 && select.indexOf('.') == -1 && select.indexOf(':') == -1 
       && select.indexOf('%') == -1 && select.indexOf('@') == -1) {
           select = ':' + select;
       }
       let commandname = select.replace(/\s+/g, '_');
       let commanddesc = commandname;
       await ic.selByCommCls.selectByCommand(select, commandname, commanddesc);
       thisClass.setLogCmd('select ' + select + ' | name ' + commandname, true);
    }

    readFile(bAppend, files, index, dataStrAll) { let me = this.icn3dui, ic = me.icn3d, thisClass = this;
        let file = files[index];
        let commandName = (bAppend) ? 'append': 'load';
        
        let reader = new FileReader();
        reader.onload = async function(e) {
            //++ic.loadedFileCnt;

            let dataStr = e.target.result; // or = reader.result;
            //thisClass.setLogCmd(commandName + ' pdb file ' + $("#" + me.pre + fileId).val(), false);
            thisClass.setLogCmd(commandName + ' pdb file ' + file.name, false);

            if(!bAppend) {
                ic.init();
            }
            else {
                ic.resetConfig();
                //ic.hAtoms = {};
                //ic.dAtoms = {};
                ic.bResetAnno = true;
                ic.bResetSets = true;
            }

            ic.bInputfile = true;
            ic.InputfileType = 'pdb';
            ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + dataStr : dataStr;

            dataStrAll = (index > 0) ? dataStrAll + '\nENDMDL\n' + dataStr : dataStr;

            if(Object.keys(files).length == index + 1) {
                if(bAppend) {
                    ic.hAtoms = {};
                    ic.dAtoms = {};
                }
                await ic.pdbParserCls.loadPdbData(dataStrAll, undefined, undefined, bAppend);
            }
            else {
                thisClass.readFile(bAppend, files, index + 1, dataStrAll);
            }
        };

        if (typeof file === "object") {
            reader.readAsText(file);
        }
    }

    async loadPdbFile(bAppend) { let me = this.icn3dui, ic = me.icn3d;
       let fileId = (bAppend) ? 'pdbfile_app' : 'pdbfile';

       //me = ic.setIcn3dui(this.id);
       ic.bInitial = true;
       if(!me.cfg.notebook) dialog.dialog( "close" );
       //close all dialog
       if(!me.cfg.notebook) {
           $(".ui-dialog-content").dialog("close");
       }
       else {
           ic.resizeCanvasCls.closeDialogs();
       }
       let files = $("#" + me.pre + fileId)[0].files;
       if(!files[0]) {
         alert("Please select a file before clicking 'Load'");
       }
       else {
            me.htmlCls.setHtmlCls.fileSupport();
            ic.molTitle = "";

            //ic.fileCnt = Object.keys(files).length;
            //ic.loadedFileCnt = 0;

            ic.dataStrAll = '';

            this.readFile(bAppend, files, 0, '');

            if(bAppend) {
                if(ic.bSetChainsAdvancedMenu) ic.definedSetsCls.showSets();
                //if(ic.bSetChainsAdvancedMenu) ic.legendTableCls.showSets();
                if(ic.bAnnoShown) await ic.showAnnoCls.showAnnotations();
            }
       }
    }

    saveHtml(id) { let me = this.icn3dui, ic = me.icn3d;
        let html = '';
        html += '<link rel="stylesheet" href="https:///structure.ncbi.nlm.nih.gov/icn3d/lib/jquery-ui-1.13.2.min.css">\n';
        html += '<link rel="stylesheet" href="https:///structure.ncbi.nlm.nih.gov/icn3d/icn3d_full_ui.css">\n';
        html += $("#" + id).html();
        let idArray = id.split('_');
        let idStr =(idArray.length > 2) ? idArray[2] : id;
        let structureStr = Object.keys(ic.structures)[0];
        if(Object.keys(ic.structures).length > 1) structureStr += '-' + Object.keys(ic.structures)[1];
        ic.saveFileCls.saveFile(structureStr + '-' + idStr + '.html', 'html', encodeURIComponent(html));
    }

    setPredefinedMenu(id) { let me = this.icn3dui, ic = me.icn3d;
        if(Object.keys(ic.chains).length < 2) {
            alert("At least two chains are required for alignment...");
            return;
        }
        me.htmlCls.clickMenuCls.SetChainsAdvancedMenu();
        let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(['protein']);
        if($("#" + me.pre + id).length) {
            $("#" + me.pre + id).html(definedAtomsHtml);
        }
        
        $("#" + me.pre + id).resizable();
    }

    launchMmdb(ids, bBiounit, hostUrl) { let me = this.icn3dui, ic = me.icn3d, thisClass = this;
        let flag = bBiounit ? '1' : '0';

        ids = ids.replace(/,/g, ' ').replace(/\s+/g, ',').trim();

        if(!ids) {
            alert("Please enter a list of PDB IDs or AlphaFold UniProt IDs...");
            return;
        }

        let idArray = ids.split(',');
        if(idArray.length == 1 && (idArray[0].length == 4 || !isNaN(idArray[0])) ) {
            thisClass.setLogCmd("load mmdb" + flag + " " + ids, false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?mmdbid=' + ids + '&bu=' + flag, urlTarget);
        }
        else {
            thisClass.setLogCmd("load mmdbaf" + flag + " " + ids, false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?mmdbafid=' + ids + '&bu=' + flag, urlTarget);
        }
    }

    //Hold all functions related to click events.
    allEventFunctions() { let me = this.icn3dui, ic = me.icn3d;
        let thisClass = this;

        if(me.bNode) return;

        let hostUrl = document.URL;
        let pos = hostUrl.indexOf("?");
        hostUrl = (pos == -1) ? hostUrl : hostUrl.substr(0, pos);

        // some URLs from VAST search are like https://www.ncbi.nlm.nih.gov/Structure/vast/icn3d/
        if(hostUrl == 'https://www.ncbi.nlm.nih.gov/Structure/vast/icn3d/') {
            hostUrl = 'https://www.ncbi.nlm.nih.gov/Structure/icn3d/';
        }

        ic.definedSetsCls.clickCustomAtoms();
        ic.definedSetsCls.clickCommand_apply();
        ic.definedSetsCls.clickModeswitch();

        ic.selectionCls.clickShow_selected();
        ic.selectionCls.clickHide_selected();

        ic.diagram2dCls.click2Ddgm();
        ic.cartoon2dCls.click2Dcartoon();
        ic.addTrackCls.clickAddTrackButton();
        ic.resizeCanvasCls.windowResize();
        ic.annotationCls.setTabs();
        ic.resid2specCls.switchHighlightLevel();

        if(! me.utilsCls.isMobile()) {
            ic.hlSeqCls.selectSequenceNonMobile();
        }
        else {
            ic.hlSeqCls.selectSequenceMobile();
            ic.hlSeqCls.selectChainMobile();
        }

        me.htmlCls.clickMenuCls.clickMenu1();
        me.htmlCls.clickMenuCls.clickMenu2();
        me.htmlCls.clickMenuCls.clickMenu3();
        me.htmlCls.clickMenuCls.clickMenu4();
        me.htmlCls.clickMenuCls.clickMenu5();
        me.htmlCls.clickMenuCls.clickMenu6();

        // back and forward arrows
        me.myEventCls.onIds(["#" + me.pre + "back", "#" + me.pre + "mn6_back"], "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           thisClass.setLogCmd("back", false);
           await ic.resizeCanvasCls.back();
        });

        me.myEventCls.onIds(["#" + me.pre + "forward", "#" + me.pre + "mn6_forward"], "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           thisClass.setLogCmd("forward", false);
           await ic.resizeCanvasCls.forward();
        });

        me.myEventCls.onIds(["#" + me.pre + "fullscreen", "#" + me.pre + "mn6_fullscreen"], "click", function(e) { let ic = me.icn3d; // from expand icon for mobilemenu
           e.preventDefault();
           //me = ic.setIcn3dui($(this).attr('id'));
           thisClass.setLogCmd("enter full screen", false);
           ic.bFullscreen = true;
           me.htmlCls.WIDTH = $( window ).width();
           me.htmlCls.HEIGHT = $( window ).height();
           ic.applyCenterCls.setWidthHeight(me.htmlCls.WIDTH, me.htmlCls.HEIGHT);
           ic.drawCls.draw();
           ic.resizeCanvasCls.openFullscreen($("#" + me.pre + "canvas")[0]);
        });

        document.addEventListener('fullscreenchange', this.fullScreenChange.bind(this));
        document.addEventListener('webkitfullscreenchange', this.fullScreenChange.bind(this));
        document.addEventListener('mozfullscreenchange', this.fullScreenChange.bind(this));
        document.addEventListener('msfullscreenchange', this.fullScreenChange.bind(this));


        me.myEventCls.onIds(["#" + me.pre + "toggle", "#" + me.pre + "mn2_toggle"], "click", function(e) { let ic = me.icn3d;
           //thisClass.setLogCmd("toggle selection", true);
           ic.selectionCls.toggleSelection();
           thisClass.setLogCmd("toggle selection", true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_hl_clrYellow", "click", function(e) { let ic = me.icn3d;
           thisClass.setLogCmd("set highlight color yellow", true);
           ic.hColor = me.parasCls.thr(0xFFFF00);
           ic.matShader = ic.setColorCls.setOutlineColor('yellow');
           ic.drawCls.draw(); // required to make it work properly
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_hl_clrGreen", "click", function(e) { let ic = me.icn3d;
           thisClass.setLogCmd("set highlight color green", true);
           ic.hColor = me.parasCls.thr(0x00FF00);
           ic.matShader = ic.setColorCls.setOutlineColor('green');
           ic.drawCls.draw(); // required to make it work properly
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_hl_clrRed", "click", function(e) { let ic = me.icn3d;
           thisClass.setLogCmd("set highlight color red", true);
           ic.hColor = me.parasCls.thr(0xFF0000);
           ic.matShader = ic.setColorCls.setOutlineColor('red');
           ic.drawCls.draw(); // required to make it work properly
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_hl_styleOutline", "click", function(e) { let ic = me.icn3d;
           thisClass.setLogCmd("set highlight style outline", true);
           ic.bHighlight = 1;
           ic.hlUpdateCls.showHighlight();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_hl_styleObject", "click", function(e) { let ic = me.icn3d;
           thisClass.setLogCmd("set highlight style 3d", true);
           ic.bHighlight = 2;
           ic.hlUpdateCls.showHighlight();
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_hl_styleNone", "click", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            ic.hlUpdateCls.clearHighlight();
            thisClass.setLogCmd("clear selection", true);
        });

        me.myEventCls.onIds(["#" + me.pre + "alternate", "#" + me.pre + "mn2_alternate", "#" + me.pre + "alternate2"], "click", function(e) { let ic = me.icn3d;
           ic.bAlternate = true;
           ic.alternateCls.alternateStructures();
           ic.bAlternate = false;

           thisClass.setLogCmd("alternate structures", false);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_realignresbyres", "click", function(e) { me.icn3d;
            me.htmlCls.dialogCls.openDlg('dl_realignresbyres', 'Align multiple chains residue by residue');
        });

        me.myEventCls.onIds("#" + me.pre + "realignSelection", "click", function(e) { let ic = me.icn3d;
            if(Object.keys(ic.chains).length < 2) {
                alert("At least two chains are required for alignment...");
                return;
            }
            
           ic.realignParserCls.realign();
           thisClass.setLogCmd("realign", true);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_realignonseqalign", "click", function(e) { let ic = me.icn3d;
            if(ic.bRender) me.htmlCls.dialogCls.openDlg('dl_realign', 'Please select two sets in two chains to realign');

            thisClass.setPredefinedMenu('atomsCustomRealign');
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_realignonstruct", "click", function(e) { let ic = me.icn3d;
            if(ic.bRender) me.htmlCls.dialogCls.openDlg('dl_realignbystruct', 'Please select two sets in two chains to realign');

            thisClass.setPredefinedMenu('atomsCustomRealignByStruct');
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_realigntwostru", "click", function(e) { let ic = me.icn3d;
            if(ic.bRender) me.htmlCls.dialogCls.openDlg('dl_realigntwostru', 'Please select two sets in two structures to realign');

            thisClass.setPredefinedMenu('atomsCustomRealignByStruct2');
        });


        me.myEventCls.onIds("#" + me.pre + "applyRealign", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let nameArray = $("#" + me.pre + "atomsCustomRealign").val();
           if(nameArray.length > 0) {
               ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
           }

           await ic.realignParserCls.realignOnSeqAlign();

           if(nameArray.length > 0) {
               thisClass.setLogCmd("realign on seq align | " + nameArray, true);
           }
           else {
               thisClass.setLogCmd("realign on seq align", true);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "applyRealignByStruct", "click", async function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let nameArray = $("#" + me.pre + "atomsCustomRealignByStruct").val();
            if(nameArray.length > 0) {
                ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
            }
 
            me.cfg.aligntool = 'vast';
 
            await ic.realignParserCls.realignOnStructAlign();
            if(nameArray.length > 0) {
                thisClass.setLogCmd("realign on structure align | " + nameArray, true);
            }
            else {
                thisClass.setLogCmd("realign on structure align", true);
            }
         });

         me.myEventCls.onIds("#" + me.pre + "applyRealignByStruct_tmalign", "click", async function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let nameArray = $("#" + me.pre + "atomsCustomRealignByStruct").val();
            if(nameArray.length > 0) {
                ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
            }

            me.cfg.aligntool = 'tmalign';
 
            await ic.realignParserCls.realignOnStructAlign();
            if(nameArray.length > 0) {
                thisClass.setLogCmd("realign on tmalign | " + nameArray, true);
            }
            else {
                thisClass.setLogCmd("realign on tmalign", true);
            }
         });

         me.myEventCls.onIds("#" + me.pre + "applyRealignByStruct_vastplus", "click", async function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let nameArray = $("#" + me.pre + "atomsCustomRealignByStruct2").val();
            if(nameArray.length > 0) {
                ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
            }

            //me.cfg.aligntool = 'tmalign';

            await ic.vastplusCls.realignOnVastplus();

            if(nameArray.length > 0) {
                thisClass.setLogCmd("realign on vastplus | " + nameArray, true);
            }
            else {
                thisClass.setLogCmd("realign on vastplus", true);
            }
         });


        me.myEventCls.onIds("#" + me.pre + "applyColorSpectrumAcrossSets", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let nameArray = $("#" + me.pre + "atomsCustomColorSpectrumAcross").val();
            if(nameArray.length == 0) {
                alert("Please select some sets");
                return;
            }

            let bSpectrum = true;
            ic.setColorCls.setColorAcrossSets(nameArray, bSpectrum);

            thisClass.setLogCmd("set color spectrum | " + nameArray, true);
        });

        me.myEventCls.onIds("#" + me.pre + "applyColorSpectrumBySets", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let nameArray = $("#" + me.pre + "atomsCustomColorSpectrum").val();
            if(nameArray.length == 0) {
                alert("Please select some sets");
                return;
            }

            let bSpectrum = true;
            ic.setColorCls.setColorBySets(nameArray, bSpectrum);

            thisClass.setLogCmd("set residues color spectrum | " + nameArray, true);
        });

        me.myEventCls.onIds("#" + me.pre + "applyColorRainbowAcrossSets", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let nameArray = $("#" + me.pre + "atomsCustomColorRainbowAcross").val();
            if(nameArray.length == 0) {
                alert("Please select some sets");
                return;
            }

            let bSpectrum = false;
            ic.setColorCls.setColorAcrossSets(nameArray, bSpectrum);

            thisClass.setLogCmd("set color rainbow | " + nameArray, true);
        });

        me.myEventCls.onIds("#" + me.pre + "applyColorRainbowBySets", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let nameArray = $("#" + me.pre + "atomsCustomColorRainbow").val();
            if(nameArray.length == 0) {
                alert("Please select some sets");
                return;
            }

            let bSpectrum = false;
            ic.setColorCls.setColorBySets(nameArray, bSpectrum);

            thisClass.setLogCmd("set residues color rainbow | " + nameArray, true);
        });

        // other
        me.myEventCls.onIds("#" + me.pre + "anno_summary", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            ic.annotationCls.setAnnoViewAndDisplay('overview');
            thisClass.setLogCmd("set view overview", true);
        });
        me.myEventCls.onIds("#" + me.pre + "anno_details", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            ic.annotationCls.setAnnoViewAndDisplay('detailed view');
            thisClass.setLogCmd("set view detailed view", true);
        });

        me.myEventCls.onIds("#" + me.pre + "show_annotations", "click", async function(e) { let ic = me.icn3d;
            await ic.showAnnoCls.showAnnotations();
            thisClass.setLogCmd("view annotations", true);
        });

        me.myEventCls.onIds("#" + me.pre + "showallchains", "click", function(e) { let ic = me.icn3d;
           ic.annotationCls.showAnnoAllChains();
           thisClass.setLogCmd("show annotations all chains", true);
        });

        me.myEventCls.onIds("#" + me.pre + "show_alignsequences", "click", function(e) { me.icn3d;
             me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');
        });

        me.myEventCls.onIds(["#" + me.pre + "show_2ddgm", "#" + me.pre + "mn2_2ddgm"], "click", async function(e) { let ic = me.icn3d;
             me.htmlCls.dialogCls.openDlg('dl_2ddgm', '2D Diagram');
             await ic.viewInterPairsCls.retrieveInteractionData();
             thisClass.setLogCmd("view interactions", true);
        });

        me.myEventCls.onIds("#" + me.pre + "search_seq_button", "click", async function(e) { me.icn3d;
           e.stopImmediatePropagation();
           await thisClass.searchSeq();
        });

        me.myEventCls.onIds("#" + me.pre + "search_seq", "keyup", async function(e) { me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               await thisClass.searchSeq();
           }
        });


        me.myEventCls.onIds("#" + me.pre + "reload_vastplus", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            thisClass.setLogCmd("vast+ search " + $("#" + me.pre + "vastpluspdbid").val(), false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open('https://www.ncbi.nlm.nih.gov/Structure/vastplus/vastplus.cgi?uid=' + $("#" + me.pre + "vastpluspdbid").val(), urlTarget);
         });

        me.myEventCls.onIds("#" + me.pre + "reload_vast", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            thisClass.setLogCmd("vast search " + $("#" + me.pre + "vastpdbid").val() + "_" + $("#" + me.pre + "vastchainid").val(), false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open('https://www.ncbi.nlm.nih.gov/Structure/vast/vastsrv.cgi?pdbid=' + $("#" + me.pre + "vastpdbid").val() + '&chain=' + $("#" + me.pre + "vastchainid").val(), urlTarget);
         });

        me.myEventCls.onIds("#" + me.pre + "reload_foldseek", "click", function(e) { me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );

            let alignment = $("#" + me.pre + "foldseekchainids").val();
            let alignment_final = thisClass.convertUniProtInChains(alignment);

            thisClass.setLogCmd("load chainalignment " + alignment_final, true);
            window.open(hostUrl + '?chainalign=' + alignment_final + '&aligntool=tmalign&showalignseq=1&bu=0', '_self');
         });

        me.myEventCls.onIds("#" + me.pre + "reload_mmtf", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load mmtf " + $("#" + me.pre + "mmtfid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?mmtfid=' + $("#" + me.pre + "mmtfid").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "mmtfid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load mmtf " + $("#" + me.pre + "mmtfid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?mmtfid=' + $("#" + me.pre + "mmtfid").val(), urlTarget);
           }
        });


        me.myEventCls.onIds("#" + me.pre + "reload_pdb", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load pdb " + $("#" + me.pre + "pdbid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?pdbid=' + $("#" + me.pre + "pdbid").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "pdbid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load pdb " + $("#" + me.pre + "pdbid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?pdbid=' + $("#" + me.pre + "pdbid").val(), urlTarget);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_af", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load af " + $("#" + me.pre + "afid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?afid=' + $("#" + me.pre + "afid").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_afmap", "click", async function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let afid = me.cfg.afid ? me.cfg.afid : $("#" + me.pre + "afid").val();

            thisClass.setLogCmd("set half pae map " + afid, true);
            
            await ic.contactMapCls.afErrorMap(afid);
        });
        me.myEventCls.onIds("#" + me.pre + "reload_afmapfull", "click", async function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let afid = me.cfg.afid ? me.cfg.afid : $("#" + me.pre + "afid").val();

            thisClass.setLogCmd("set full pae map " + afid, true);
            
            await ic.contactMapCls.afErrorMap(afid, true);
        });

        me.myEventCls.onIds("#" + me.pre + "afid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load af " + $("#" + me.pre + "afid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?afid=' + $("#" + me.pre + "afid").val(), urlTarget);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_opm", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load opm " + $("#" + me.pre + "opmid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?opmid=' + $("#" + me.pre + "opmid").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "opmid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load opm " + $("#" + me.pre + "opmid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?opmid=' + $("#" + me.pre + "opmid").val(), urlTarget);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_align_refined", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let alignment = $("#" + me.pre + "alignid1").val() + "," + $("#" + me.pre + "alignid2").val();
            thisClass.setLogCmd("load alignment " + alignment + ' | parameters &atype=1&bu=1', false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?align=' + alignment + '&showalignseq=1&atype=1&bu=1', urlTarget);
         });

        me.myEventCls.onIds("#" + me.pre + "reload_align_ori", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let alignment = $("#" + me.pre + "alignid1").val() + "," + $("#" + me.pre + "alignid2").val();
            thisClass.setLogCmd("load alignment " + alignment + ' | parameters &atype=0&bu=1', false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?align=' + alignment + '&showalignseq=1&atype=0&bu=1', urlTarget);
         });

        me.myEventCls.onIds("#" + me.pre + "reload_align_tmalign", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let alignment = $("#" + me.pre + "alignid1").val() + "," + $("#" + me.pre + "alignid2").val();
            thisClass.setLogCmd("load alignment " + alignment + ' | parameters &atype=2&bu=1', false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?align=' + alignment + '&showalignseq=1&atype=2&bu=1', urlTarget);
         });

        me.myEventCls.onIds("#" + me.pre + "reload_alignaf", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let alignment = $("#" + me.pre + "alignafid1").val() + "_A," + $("#" + me.pre + "alignafid2").val() + "_A";
            thisClass.setLogCmd("load chains " + alignment + " | residues | resdef ", false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?chainalign=' + alignment + '&resnum=&resdef=&showalignseq=1', urlTarget);
          });

        me.myEventCls.onIds("#" + me.pre + "reload_alignaf_tmalign", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let alignment = $("#" + me.pre + "alignafid1").val() + "_A," + $("#" + me.pre + "alignafid2").val() + "_A";
            thisClass.setLogCmd("load chains " + alignment + " | residues | resdef | align tmalign", false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?chainalign=' + alignment + '&aligntool=tmalign&resnum=&resdef=&showalignseq=1', urlTarget);
          });


        me.myEventCls.onIds("#" + me.pre + "reload_chainalign_asym", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );

           let alignment = $("#" + me.pre + "chainalignids").val();
           let alignment_final = thisClass.convertUniProtInChains(alignment);

           thisClass.setLogCmd("load chains " + alignment_final + " on asymmetric unit | residues | resdef ", false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?chainalign=' + alignment_final + '&resnum=&resdef=&showalignseq=1&bu=0', urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_chainalign_asym2", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
 
            let alignment = $("#" + me.pre + "chainalignids2").val();
            let alignment_final = thisClass.convertUniProtInChains(alignment);
            let resalign = $("#" + me.pre + "resalignids").val();
 
            thisClass.setLogCmd("load chains " + alignment_final + " on asymmetric unit | residues " + resalign + " | resdef ", false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?chainalign=' + alignment_final + '&resnum=' + resalign + '&resdef=&showalignseq=1&bu=0', urlTarget);
         });

         me.myEventCls.onIds("#" + me.pre + "reload_chainalign_asym3", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
 
            let alignment = $("#" + me.pre + "chainalignids3").val();
            let alignment_final = thisClass.convertUniProtInChains(alignment);

            let predefinedres = $("#" + me.pre + "predefinedres").val().trim().replace(/\n/g, ': ');
            if(predefinedres && alignment_final.split(',').length - 1 != predefinedres.split(': ').length) {
                alert("Please make sure the number of chains and the lines of predefined residues are the same...");
                return;
            }
 
            thisClass.setLogCmd("load chains " + alignment_final + " on asymmetric unit | residues | resdef " + predefinedres, false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?chainalign=' + alignment_final + '&resnum=&resdef=' + predefinedres + '&showalignseq=1&bu=0', urlTarget);
         });

         me.myEventCls.onIds("#" + me.pre + "reload_chainalign_asym4", "click", async function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
 
            let alignment = $("#" + me.pre + "chainalignids4").val();
            let alignment_final = thisClass.convertUniProtInChains(alignment);

            let predefinedres = $("#" + me.pre + "predefinedres2").val().trim().replace(/\n/g, ': ');
            if(predefinedres && alignment_final.split(',').length - 1 != predefinedres.split(': ').length) {
                alert("Please make sure the number of chains and the lines of predefined residues are the same...");
                return;
            }

            me.cfg.resdef = predefinedres.replace(/:/gi, ';');

            let bRealign = true, bPredefined = true;
            let chainidArray = alignment_final.split(',');
            await ic.realignParserCls.realignChainOnSeqAlign(undefined, chainidArray, bRealign, bPredefined);
 
            thisClass.setLogCmd("realign predefined " + alignment_final + " " + predefinedres, true);
         });

        me.myEventCls.onIds("#" + me.pre + "reload_chainalign_tmalign", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );

            let alignment = $("#" + me.pre + "chainalignids").val();
            let alignment_final = thisClass.convertUniProtInChains(alignment);
 
            thisClass.setLogCmd("load chains " + alignment_final + " on asymmetric unit | residues | resdef | align tmalign", false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?chainalign=' + alignment_final + '&aligntool=tmalign&resnum=&resdef=&showalignseq=1&bu=0', urlTarget);
         });

        me.myEventCls.onIds("#" + me.pre + "reload_mutation_3d", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let mutationids = $("#" + me.pre + "mutationids").val();
           //let idsource = $("#" + me.pre + "idsource").val();
           let idsource, pdbsource;
           if($("#" + me.pre + "type_mmdbid").is(":checked")) {
                idsource = 'mmdbid';
           }
           else {
                idsource = 'afid';
           }
           if($("#" + me.pre + "showin_currentpage").is(":checked")) {
                pdbsource = 'currentpage';
            }
            else {
                pdbsource = 'newpage';
            }

           if(pdbsource == 'currentpage') {
                let snp = mutationids;

                await ic.scapCls.retrieveScap(snp);
                thisClass.setLogCmd('scap 3d ' + snp, true);
                thisClass.setLogCmd("select displayed set", true);
           }
           else {
                let mmdbid = mutationids.substr(0, mutationids.indexOf('_'));           
                thisClass.setLogCmd("3d of mutation " + mutationids, false);
                let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
                window.open(hostUrl + '?' + idsource + '=' + mmdbid + '&command=scap 3d ' + mutationids + '; select displayed set', urlTarget);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_mutation_pdb", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let mutationids = $("#" + me.pre + "mutationids").val();
           //let idsource = $("#" + me.pre + "idsource").val();
           let idsource, pdbsource;
           if($("#" + me.pre + "type_mmdbid").is(":checked")) {
                idsource = 'mmdbid';
           }
           else {
                idsource = 'afid';
           }
           if($("#" + me.pre + "showin_currentpage").is(":checked")) {
                pdbsource = 'currentpage';
            }
            else {
                pdbsource = 'newpage';
            }

           if(pdbsource == 'currentpage') {
                let snp = mutationids;

                let bPdb = true;
                await ic.scapCls.retrieveScap(snp, undefined, bPdb);
                thisClass.setLogCmd('scap pdb ' + snp, true);
           }
           else {
                let mmdbid = mutationids.substr(0, mutationids.indexOf('_'));
                thisClass.setLogCmd("pdb of mutation " + mutationids, false);
                let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
                window.open(hostUrl + '?' + idsource + '=' + mmdbid + '&command=scap pdb ' + mutationids + '; select displayed set', urlTarget);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_mutation_inter", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let mutationids = $("#" + me.pre + "mutationids").val();
           //let idsource = $("#" + me.pre + "idsource").val();
           let idsource, pdbsource;
           if($("#" + me.pre + "type_mmdbid").is(":checked")) {
                idsource = 'mmdbid';
           }
           else {
                idsource = 'afid';
           }
           if($("#" + me.pre + "showin_currentpage").is(":checked")) {
                pdbsource = 'currentpage';
            }
            else {
                pdbsource = 'newpage';
            }

           if(pdbsource == 'currentpage') {
                let snp = mutationids;

                let bInteraction = true;
                await ic.scapCls.retrieveScap(snp, bInteraction);
                thisClass.setLogCmd('scap interaction ' + snp, true);

                let idArray = snp.split('_'); //stru_chain_resi_snp
                let select = '.' + idArray[1] + ':' + idArray[2];
                let name = 'snp_' + idArray[1] + '_' + idArray[2];
                thisClass.setLogCmd("select " + select + " | name " + name, true);
                thisClass.setLogCmd("line graph interaction pairs | selected non-selected | hbonds,salt bridge,interactions,halogen,pi-cation,pi-stacking | false | threshold 3.8 6 4 3.8 6 5.5", true);
                thisClass.setLogCmd("adjust dialog dl_linegraph", true);
                thisClass.setLogCmd("select displayed set", true);
           }
           else {
                let mutationArray = mutationids.split(',');
                let residArray = [];
                for(let i = 0, il = mutationArray.length; i < il; ++i) {
                    let pos = mutationArray[i].lastIndexOf('_');
                    let resid = mutationArray[i].substr(0, pos);
                    residArray.push(resid);
                }

                let mmdbid = mutationids.substr(0, mutationids.indexOf('_'));

                // if no structures are loaded yet
                if(!ic.structures) {
                    ic.structures = {};
                    ic.structures[mmdbid] = 1;
                }
                ic.resid2specCls.residueids2spec(residArray);

                thisClass.setLogCmd("interaction change of mutation " + mutationids, false);
                let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
                window.open(hostUrl + '?' + idsource + '=' + mmdbid + '&command=scap interaction ' + mutationids, urlTarget);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_mmcif", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load mmcif " + $("#" + me.pre + "mmcifid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?mmcifid=' + $("#" + me.pre + "mmcifid").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "mmcifid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load mmcif " + $("#" + me.pre + "mmcifid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?mmcifid=' + $("#" + me.pre + "mmcifid").val(), urlTarget);
           }
        });


        me.myEventCls.onIds("#" + me.pre + "reload_mmdb", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           thisClass.setLogCmd("load mmdb1 " + $("#" + me.pre + "mmdbid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?mmdbid=' + $("#" + me.pre + "mmdbid").val() + '&bu=1', urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_mmdb_asym", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            thisClass.setLogCmd("load mmdb0 " + $("#" + me.pre + "mmdbid").val(), false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?mmdbid=' + $("#" + me.pre + "mmdbid").val() + '&bu=0', urlTarget);
        });

         me.myEventCls.onIds("#" + me.pre + "reload_mmdbaf", "click", function(e) { me.icn3d;
            e.preventDefault();
            

            // remove space
            let ids = $("#" + me.pre + "mmdbafid").val();

            thisClass.launchMmdb(ids, 1, hostUrl);
        });
 
         me.myEventCls.onIds("#" + me.pre + "reload_mmdbaf_asym", "click", function(e) { me.icn3d;
            e.preventDefault();
            

            // remove space
            let ids = $("#" + me.pre + "mmdbafid").val();
            thisClass.launchMmdb(ids, 0, hostUrl);
        });

        me.myEventCls.onIds("#" + me.pre + "mmdbid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               
               thisClass.setLogCmd("load mmdb1 " + $("#" + me.pre + "mmdbid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?mmdbid=' + $("#" + me.pre + "mmdbid").val() + '&bu=1', urlTarget);
              }
        });

        me.myEventCls.onIds("#" + me.pre + "mmdbafid", "keyup", function(e) { me.icn3d;
            if (e.keyCode === 13) {
                e.preventDefault();
                
                let ids = $("#" + me.pre + "mmdbafid").val();
                thisClass.launchMmdb(ids, 1, hostUrl);
               }
         });


        me.myEventCls.onIds("#" + me.pre + "reload_blast_rep_id", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let query_id = $("#" + me.pre + "query_id").val();
           let query_fasta = encodeURIComponent($("#" + me.pre + "query_fasta").val());
           let blast_rep_id = $("#" + me.pre + "blast_rep_id").val();
           thisClass.setLogCmd("load seq_struct_ids " + query_id + "," + blast_rep_id, false);
           query_id =(query_id !== '' && query_id !== undefined) ? query_id : query_fasta;
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?from=icn3d&alg=blast&blast_rep_id=' + blast_rep_id
             + '&query_id=' + query_id
             + '&command=view annotations; set annotation cdd; set annotation site; set view detailed view; select chain '
             + blast_rep_id + '; show selection', urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_alignsw", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let query_id = $("#" + me.pre + "query_id").val();
            let query_fasta = encodeURIComponent($("#" + me.pre + "query_fasta").val());
            let blast_rep_id = $("#" + me.pre + "blast_rep_id").val();
            thisClass.setLogCmd("load seq_struct_ids_smithwm " + query_id + "," + blast_rep_id, false);
            query_id =(query_id !== '' && query_id !== undefined) ? query_id : query_fasta;
            
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?from=icn3d&alg=smithwm&blast_rep_id=' + blast_rep_id
              + '&query_id=' + query_id
              + '&command=view annotations; set annotation cdd; set annotation site; set view detailed view; select chain '
              + blast_rep_id + '; show selection', urlTarget);
         });

         me.myEventCls.onIds("#" + me.pre + "reload_alignswlocal", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let query_id = $("#" + me.pre + "query_id").val();
            let query_fasta = encodeURIComponent($("#" + me.pre + "query_fasta").val());
            let blast_rep_id = $("#" + me.pre + "blast_rep_id").val();
            thisClass.setLogCmd("load seq_struct_ids_local_smithwm " + query_id + "," + blast_rep_id, false);
            query_id =(query_id !== '' && query_id !== undefined) ? query_id : query_fasta;
            
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?from=icn3d&alg=local_smithwm&blast_rep_id=' + blast_rep_id
              + '&query_id=' + query_id
              + '&command=view annotations; set annotation cdd; set annotation site; set view detailed view; select chain '
              + blast_rep_id + '; show selection', urlTarget);
         });


        me.myEventCls.onIds("#" + me.pre + "reload_gi", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load gi " + $("#" + me.pre + "gi").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?gi=' + $("#" + me.pre + "gi").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_refseq", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            thisClass.setLogCmd("load refseq " + $("#" + me.pre + "refseqid").val(), false);
            let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
            window.open(hostUrl + '?refseqid=' + $("#" + me.pre + "refseqid").val(), urlTarget);
         });

        me.myEventCls.onIds("#" + me.pre + "gi", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load gi " + $("#" + me.pre + "gi").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?gi=' + $("#" + me.pre + "gi").val(), urlTarget);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_uniprotid", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load uniprotid " + $("#" + me.pre + "uniprotid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?uniprotid=' + $("#" + me.pre + "uniprotid").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "uniprotid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load uniprotid " + $("#" + me.pre + "uniprotid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?uniprotid=' + $("#" + me.pre + "uniprotid").val(), urlTarget);
           }
        });


        me.myEventCls.onIds("#" + me.pre 
        + "reload_cid", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           thisClass.setLogCmd("load cid " + $("#" + me.pre + "cid").val(), false);
           let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
           window.open(hostUrl + '?cid=' + $("#" + me.pre + "cid").val(), urlTarget);
        });

        me.myEventCls.onIds("#" + me.pre + "cid", "keyup", function(e) { let ic = me.icn3d;
           if (e.keyCode === 13) {
               e.preventDefault();
               if(!me.cfg.notebook) dialog.dialog( "close" );
               thisClass.setLogCmd("load cid " + $("#" + me.pre + "cid").val(), false);
               let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
               window.open(hostUrl + '?cid=' + $("#" + me.pre + "cid").val(), urlTarget);
           }
        });


        me.htmlCls.setHtmlCls.clickReload_pngimage();

        me.myEventCls.onIds("#" + me.pre + "reload_state", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           //close all dialog
           if(!me.cfg.notebook) {
               $(".ui-dialog-content").dialog("close");
           }
           else {
               ic.resizeCanvasCls.closeDialogs();
           }
           // initialize icn3dui
           //Do NOT clear data if iCn3D loads a pdb or other data file and then load a state file
           if(!ic.bInputfile) {
               //ic.initUI();
               ic.init();
           }
           let file = $("#" + me.pre + "state")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             me.htmlCls.setHtmlCls.fileSupport();
             let reader = new FileReader();
             reader.onload = async function(e) {
               ic.bStatefile = true;

               let dataStr = e.target.result; // or = reader.result;
               thisClass.setLogCmd('load state file ' + $("#" + me.pre + "state").val(), false);
               ic.commands = [];
               ic.optsHistory = [];
               await ic.loadScriptCls.loadScript(dataStr, true);
             };
             reader.readAsText(file);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_selectionfile", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let file = $("#" + me.pre + "selectionfile")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             me.htmlCls.setHtmlCls.fileSupport();
             let reader = new FileReader();
             reader.onload = async function(e) {
               let dataStr = e.target.result; // or = reader.result;
               await ic.selectionCls.loadSelection(dataStr);
               thisClass.setLogCmd('load selection file ' + $("#" + me.pre + "selectionfile").val(), false);
             };
             reader.readAsText(file);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_dsn6file2fofc", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.dsn6ParserCls.loadDsn6File('2fofc');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_dsn6filefofc", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.dsn6ParserCls.loadDsn6File('fofc');
        });

        me.myEventCls.onIds("#" + me.pre + "reload_delphifile", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.delphiCls.loadDelphiFile('delphi');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_pqrfile", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.delphiCls.loadPhiFile('pqr');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_phifile", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.delphiCls.loadPhiFile('phi');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_cubefile", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.delphiCls.loadPhiFile('cube');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_pqrurlfile", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.delphiCls.loadPhiFileUrl('pqrurl');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_phiurlfile", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.delphiCls.loadPhiFileUrl('phiurl');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_cubeurlfile", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.delphiCls.loadPhiFileUrl('cubeurl');
        });

        me.myEventCls.onIds("#" + me.pre + "reload_delphifile2", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           me.htmlCls.setHtmlCls.updateSurfPara('delphi');

           if(!me.cfg.notebook) dialog.dialog( "close" );

           await ic.delphiCls.loadDelphiFile('delphi2');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_pqrfile2", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           me.htmlCls.setHtmlCls.updateSurfPara('phi');

           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.delphiCls.loadPhiFile('pqr2');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_phifile2", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           me.htmlCls.setHtmlCls.updateSurfPara('phi');

           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.delphiCls.loadPhiFile('phi2');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_cubefile2", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           me.htmlCls.setHtmlCls.updateSurfPara('phi');

           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.delphiCls.loadPhiFile('cube2');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_pqrurlfile2", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           me.htmlCls.setHtmlCls.updateSurfPara('phiurl');

           if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.delphiCls.loadPhiFileUrl('pqrurl2');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_phiurlfile2", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           me.htmlCls.setHtmlCls.updateSurfPara('phiurl');

           if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.delphiCls.loadPhiFileUrl('phiurl2');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_cubeurlfile2", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           me.htmlCls.setHtmlCls.updateSurfPara('phiurl');

           if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.delphiCls.loadPhiFileUrl('cubeurl2');
        });

        me.myEventCls.onIds("#" + me.pre + "reload_dsn6fileurl2fofc", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.dsn6ParserCls.loadDsn6FileUrl('2fofc');
        });
        me.myEventCls.onIds("#" + me.pre + "reload_dsn6fileurlfofc", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.dsn6ParserCls.loadDsn6FileUrl('fofc');
        });

        me.myEventCls.onIds("#" + me.pre + "reload_pdbfile", "click", async function(e) { me.icn3d;
           e.preventDefault();

           let bAppend = false;
           await thisClass.loadPdbFile(bAppend);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_pdbfile_app", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();

           ic.bAppend = true;
           await thisClass.loadPdbFile(ic.bAppend);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_mol2file", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           ic.bInitial = true;
           if(!me.cfg.notebook) dialog.dialog( "close" );
           //close all dialog
           if(!me.cfg.notebook) {
               $(".ui-dialog-content").dialog("close");
           }
           else {
               ic.resizeCanvasCls.closeDialogs();
           }
           let file = $("#" + me.pre + "mol2file")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             me.htmlCls.setHtmlCls.fileSupport();
             let reader = new FileReader();
             reader.onload = async function(e) {
               let dataStr = e.target.result; // or = reader.result;
               thisClass.setLogCmd('load mol2 file ' + $("#" + me.pre + "mol2file").val(), false);
               ic.molTitle = "";
               ic.inputid = undefined;
               //ic.initUI();
               ic.init();
               ic.bInputfile = true;
               ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + dataStr : dataStr;
               ic.InputfileType = 'mol2';
               await ic.mol2ParserCls.loadMol2Data(dataStr);
             };
             reader.readAsText(file);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_sdffile", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           ic.bInitial = true;
           if(!me.cfg.notebook) dialog.dialog( "close" );
           //close all dialog
           if(!me.cfg.notebook) {
               $(".ui-dialog-content").dialog("close");
           }
           else {
               ic.resizeCanvasCls.closeDialogs();
           }
           let file = $("#" + me.pre + "sdffile")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             me.htmlCls.setHtmlCls.fileSupport();
             let reader = new FileReader();
             reader.onload = async function(e) {
               let dataStr = e.target.result; // or = reader.result;
               thisClass.setLogCmd('load sdf file ' + $("#" + me.pre + "sdffile").val(), false);
               ic.molTitle = "";
               ic.inputid = undefined;
               //ic.initUI();
               ic.init();
               ic.bInputfile = true;
               ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + dataStr : dataStr;
               ic.InputfileType = 'sdf';
               await ic.sdfParserCls.loadSdfData(dataStr);
             };
             reader.readAsText(file);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_xyzfile", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           ic.bInitial = true;
           if(!me.cfg.notebook) dialog.dialog( "close" );
           //close all dialog
           if(!me.cfg.notebook) {
               $(".ui-dialog-content").dialog("close");
           }
           else {
               ic.resizeCanvasCls.closeDialogs();
           }
           let file = $("#" + me.pre + "xyzfile")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             me.htmlCls.setHtmlCls.fileSupport();
             let reader = new FileReader();
             reader.onload = async function(e) {
               let dataStr = e.target.result; // or = reader.result;
               thisClass.setLogCmd('load xyz file ' + $("#" + me.pre + "xyzfile").val(), false);
               ic.molTitle = "";
               ic.inputid = undefined;
               //ic.initUI();
               ic.init();
               ic.bInputfile = true;
               ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + dataStr : dataStr;
               ic.InputfileType = 'xyz';
               await ic.xyzParserCls.loadXyzData(dataStr);
             };
             reader.readAsText(file);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "reload_afmapfile", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            ic.bInitial = true;
            if(!me.cfg.notebook) dialog.dialog( "close" );
            //close all dialog
            if(!me.cfg.notebook) {
                $(".ui-dialog-content").dialog("close");
            }
            else {
                ic.resizeCanvasCls.closeDialogs();
            }
            let file = $("#" + me.pre + "afmapfile")[0].files[0];
            if(!file) {
              alert("Please select a file before clicking 'Load'");
            }
            else {
              me.htmlCls.setHtmlCls.fileSupport();
              let reader = new FileReader();
              reader.onload = function(e) {
                let dataStr = e.target.result; // or = reader.result;
                thisClass.setLogCmd('load AlphaFold PAE file ' + $("#" + me.pre + "afmapfile").val(), false);
                
                me.htmlCls.dialogCls.openDlg('dl_alignerrormap', 'Show Predicted Aligned Error (PAE) map');
                ic.contactMapCls.processAfErrorMap(JSON.parse(dataStr));
              };
              reader.readAsText(file);
            }
         });

         me.myEventCls.onIds("#" + me.pre + "reload_afmapfilefull", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            ic.bInitial = true;
            if(!me.cfg.notebook) dialog.dialog( "close" );
            //close all dialog
            if(!me.cfg.notebook) {
                $(".ui-dialog-content").dialog("close");
            }
            else {
                ic.resizeCanvasCls.closeDialogs();
            }
            let file = $("#" + me.pre + "afmapfile")[0].files[0];
            if(!file) {
              alert("Please select a file before clicking 'Load'");
            }
            else {
              me.htmlCls.setHtmlCls.fileSupport();
              let reader = new FileReader();
              reader.onload = function(e) {
                let dataStr = e.target.result; // or = reader.result;
                thisClass.setLogCmd('load AlphaFold PAE file ' + $("#" + me.pre + "afmapfile").val(), false);
                
                me.htmlCls.dialogCls.openDlg('dl_alignerrormap', 'Show Predicted Aligned Error (PAE) map');
                ic.contactMapCls.processAfErrorMap(JSON.parse(dataStr), true);
              };
              reader.readAsText(file);
            }
         });

        me.myEventCls.onIds("#" + me.pre + "reload_urlfile", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           ic.bInitial = true;
           if(!me.cfg.notebook) dialog.dialog( "close" );
           //close all dialog
           if(!me.cfg.notebook) {
               $(".ui-dialog-content").dialog("close");
           }
           else {
               ic.resizeCanvasCls.closeDialogs();
           }
           let type = $("#" + me.pre + "filetype").val();
           let url = $("#" + me.pre + "urlfile").val();
           ic.inputurl = 'type=' + type + '&url=' + encodeURIComponent(url);
           //ic.initUI();
           ic.init();
           ic.bInputfile = true;
           ic.bInputUrlfile = true;
           await ic.pdbParserCls.downloadUrl(url, type);
        });

        me.myEventCls.onIds("#" + me.pre + "reload_mmciffile", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           ic.bInitial = true;
           if(!me.cfg.notebook) dialog.dialog( "close" );
           //close all dialog
           if(!me.cfg.notebook) {
               $(".ui-dialog-content").dialog("close");
           }
           else {
               ic.resizeCanvasCls.closeDialogs();
           }
           let file = $("#" + me.pre + "mmciffile")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             me.htmlCls.setHtmlCls.fileSupport();
             let reader = new FileReader();
             reader.onload = async function(e) {
                let dataStr = e.target.result; // or = reader.result;
                thisClass.setLogCmd('load mmcif file ' + $("#" + me.pre + "mmciffile").val(), false);
                ic.molTitle = "";
                let url = me.htmlCls.baseUrl + "mmcifparser/mmcifparser.cgi";
                //ic.bCid = undefined;

                let dataObj = {'mmciffile': dataStr};
                let data = await me.getAjaxPostPromise(url, dataObj, true);

                //ic.initUI();
                ic.init();
                ic.bInputfile = true;
                ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + data : data;
                ic.InputfileType = 'mmcif';
                await ic.mmcifParserCls.loadMmcifData(data); //await 
             };
             reader.readAsText(file);
           }
        });

        me.myEventCls.onIds("#" + me.pre + "applycustomcolor", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.setOptionCls.setOption("color", $("#" + me.pre + "colorcustom").val());
           thisClass.setLogCmd("color " + $("#" + me.pre + "colorcustom").val(), true);
        });

        me.myEventCls.onIds(["#" + me.pre + "atomsCustomSphere2", "#" + me.pre + "atomsCustomSphere", "#" + me.pre + "radius_aroundsphere"], "change", function(e) { let ic = me.icn3d;
            ic.bSphereCalc = false;
            //thisClass.setLogCmd('set calculate sphere false', true);
        });
        me.myEventCls.onIds("#" + me.pre + "applypick_aroundsphere", "click", function(e) { let ic = me.icn3d;
            //e.preventDefault();
            
            let radius = parseFloat($("#" + me.pre + "radius_aroundsphere").val());
            let nameArray = $("#" + me.pre + "atomsCustomSphere").val();
            let nameArray2 = $("#" + me.pre + "atomsCustomSphere2").val();
            if(nameArray2.length == 0) {
                alert("Please select the first set at step #1");
            }
            else {
                let select = "select zone cutoff " + radius + " | sets " + nameArray2 + " " + nameArray + " | " + ic.bSphereCalc;
                if(!ic.bSphereCalc) ic.showInterCls.pickCustomSphere(radius, nameArray2, nameArray, ic.bSphereCalc);
                ic.bSphereCalc = true;
                //thisClass.setLogCmd('set calculate sphere true', true);
                ic.hlUpdateCls.updateHlAll();
                thisClass.setLogCmd(select, true);
            }
        });
        me.myEventCls.onIds("#" + me.pre + "sphereExport", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let radius = parseFloat($("#" + me.pre + "radius_aroundsphere").val());
            let nameArray = $("#" + me.pre + "atomsCustomSphere").val();
            let nameArray2 = $("#" + me.pre + "atomsCustomSphere2").val();
            if(nameArray2.length == 0) {
                alert("Please select the first set at step #1");
            }
            else {
                ic.showInterCls.pickCustomSphere(radius, nameArray2, nameArray, ic.bSphereCalc);
                ic.bSphereCalc = true;
                let text = ic.viewInterPairsCls.exportSpherePairs();
                let file_pref =(ic.inputid) ? ic.inputid : "custom";
                ic.saveFileCls.saveFile(file_pref + '_sphere_pairs.html', 'html', text);

                thisClass.setLogCmd("export pairs | " + nameArray2 + " " + nameArray + " | dist " + radius, true);
            }
        });

        me.myEventCls.onIds("#" + me.pre + "apply_adjustmem", "click", function(e) { let ic = me.icn3d;
            //e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let extra_mem_z = parseFloat($("#" + me.pre + "extra_mem_z").val());
            let intra_mem_z = parseFloat($("#" + me.pre + "intra_mem_z").val());
            ic.selectionCls.adjustMembrane(extra_mem_z, intra_mem_z);
            let select = "adjust membrane z-axis " + extra_mem_z + " " + intra_mem_z;
            thisClass.setLogCmd(select, true);
        });

        me.myEventCls.onIds("#" + me.pre + "apply_selectplane", "click", function(e) { let ic = me.icn3d;
            //e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            let large = parseFloat($("#" + me.pre + "selectplane_z1").val());
            let small = parseFloat($("#" + me.pre + "selectplane_z2").val());
            ic.selectionCls.selectBtwPlanes(large, small);
            let select = "select planes z-axis " + large + " " + small;
            thisClass.setLogCmd(select, true);
        });

        me.myEventCls.onIds(["#" + me.pre + "atomsCustomHbond2", "#" + me.pre + "atomsCustomHbond", "#" + me.pre + "analysis_hbond", "#" + me.pre + "analysis_saltbridge", "#" + me.pre + "analysis_contact", "#" + me.pre + "hbondthreshold", "#" + me.pre + "saltbridgethreshold", "#" + me.pre + "contactthreshold"], "change", function(e) { let ic = me.icn3d;
            ic.bHbondCalc = false;
            //thisClass.setLogCmd('set calculate hbond false', true);
        });
        me.myEventCls.onIds("#" + me.pre + "crossstrucinter", "change", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.crossstrucinter = parseInt($("#" + me.pre + "crossstrucinter").val());
           thisClass.setLogCmd("cross structure interaction " + ic.crossstrucinter, true);
        });
        me.myEventCls.onIds("#" + me.pre + "applyhbonds", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           await ic.showInterCls.showInteractions('3d');
        });
        me.myEventCls.onIds("#" + me.pre + "applycontactmap", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );

           let contactdist = parseFloat($("#" + ic.pre + "contactdist").val());
           let contacttype = $("#" + ic.pre + "contacttype").val();

           await ic.contactMapCls.contactMap(contactdist, contacttype);
           thisClass.setLogCmd('contact map | dist ' + contactdist + ' | type ' + contacttype, true);
        });
        me.myEventCls.onIds("#" + me.pre + "hbondWindow", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           await ic.showInterCls.showInteractions('view');
        });
        me.myEventCls.onIds("#" + me.pre + "areaWindow", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           let nameArray = $("#" + me.pre + "atomsCustomHbond").val();
           let nameArray2 = $("#" + me.pre + "atomsCustomHbond2").val();
           ic.analysisCls.calcBuriedSurface(nameArray2, nameArray);
           thisClass.setLogCmd("calc buried surface | " + nameArray2 + " " + nameArray, true);
        });
        me.myEventCls.onIds("#" + me.pre + "sortSet1", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           await ic.showInterCls.showInteractions('save1');
        });
        $(document).on("click", "." + me.pre + "showintercntonly", function(e) { me.icn3d;
            e.stopImmediatePropagation();
            $(".icn3d-border").hide();
            thisClass.setLogCmd("table inter count only", true);
        });
        $(document).on("click", "." + me.pre + "showinterdetails", function(e) { me.icn3d;
            e.stopImmediatePropagation();
            $(".icn3d-border").show();
            thisClass.setLogCmd("table inter details", true);
        });
        me.myEventCls.onIds("#" + me.pre + "sortSet2", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           await ic.showInterCls.showInteractions('save2');
        });
        me.myEventCls.onIds("#" + me.pre + "hbondGraph", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           await ic.showInterCls.showInteractions('graph');
        });
        me.myEventCls.onIds("#" + me.pre + "hbondLineGraph", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           await ic.showInterCls.showInteractions('linegraph');
        });
        me.myEventCls.onIds("#" + me.pre + "hbondScatterplot", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           await ic.showInterCls.showInteractions('scatterplot');
        });
        // select residues
        $(document).on("click", "#" + me.svgid + " circle.selected", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            let id = $(this).attr('res');
            if(ic.bSelectResidue === false && !ic.bShift && !ic.bCtrl) {
              ic.selectionCls.removeSelection();
            }
            if(id !== undefined) {
               ic.hlSeqCls.selectResidues(id, this);
               ic.hlObjectsCls.addHlObjects();  // render() is called
            }
        });
        me.myEventCls.onIds("#" + me.svgid + "_svg", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.saveSvg(me.svgid, ic.inputid + "_force_directed_graph.svg");
        });
        me.myEventCls.onIds("#" + me.svgid + "_png", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.savePng(me.svgid, ic.inputid + "_force_directed_graph.png");
        });
        me.myEventCls.onIds("#" + me.svgid + "_json", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let graphStr2 = ic.graphStr.substr(0, ic.graphStr.lastIndexOf('}'));
            graphStr2 += me.htmlCls.setHtmlCls.getLinkColor();

            ic.saveFileCls.saveFile(ic.inputid + "_force_directed_graph.json", "text", [graphStr2]);
        });

        $(document).on("click", "#" + me.svgid_ct + "_svg", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.saveSvg(me.svgid_ct, ic.inputid + "_cartoon.svg");
        });
        $(document).on("click", "#" + me.svgid_ct + "_png", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.savePng(me.svgid_ct, ic.inputid + "_cartoon.png");
        });
        $(document).on("click", "#" + me.svgid_ct + "_json", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            //let graphStr2 = ic.graphStr.substr(0, ic.graphStr.lastIndexOf('}'));

            ic.saveFileCls.saveFile(ic.inputid + "_cartoon.json", "text", [ic.graphStr]);
        });
        $(document).on("change", "#" + me.svgid_ct + "_label", function(e) { me.icn3d;
           e.preventDefault();
           
           let className = $("#" + me.svgid_ct + "_label").val();
           $("#" + me.svgid_ct + " text").removeClass();
           $("#" + me.svgid_ct + " text").addClass(className);
           thisClass.setLogCmd("cartoon label " + className, true);
        });

        me.myEventCls.onIds("#" + me.linegraphid + "_svg", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.saveSvg(me.linegraphid, ic.inputid + "_line_graph.svg");
        });
        me.myEventCls.onIds("#" + me.linegraphid + "_png", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.savePng(me.linegraphid, ic.inputid + "_line_graph.png");
        });
        me.myEventCls.onIds("#" + me.linegraphid + "_json", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let graphStr2 = ic.lineGraphStr.substr(0, ic.lineGraphStr.lastIndexOf('}'));

            graphStr2 += me.htmlCls.setHtmlCls.getLinkColor();

            ic.saveFileCls.saveFile(ic.inputid + "_line_graph.json", "text", [graphStr2]);
        });
        me.myEventCls.onIds("#" + me.linegraphid + "_scale", "change", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           let scale = $("#" + me.linegraphid + "_scale").val();
           $("#" + me.linegraphid).attr("width",(ic.linegraphWidth * parseFloat(scale)).toString() + "px");
           thisClass.setLogCmd("line graph scale " + scale, true);
        });
        me.myEventCls.onIds("#" + me.scatterplotid + "_svg", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.saveSvg(me.scatterplotid, ic.inputid + "_scatterplot.svg");
        });
        me.myEventCls.onIds("#" + me.scatterplotid + "_png", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.savePng(me.scatterplotid, ic.inputid + "_scatterplot.png");
        });
        me.myEventCls.onIds("#" + me.scatterplotid + "_json", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let graphStr2 = ic.scatterplotStr.substr(0, ic.scatterplotStr.lastIndexOf('}'));

            graphStr2 += me.htmlCls.setHtmlCls.getLinkColor();

            ic.saveFileCls.saveFile(ic.inputid + "_scatterplot.json", "text", [graphStr2]);
        });
        me.myEventCls.onIds("#" + me.scatterplotid + "_scale", "change", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           let scale = $("#" + me.scatterplotid + "_scale").val();
           $("#" + me.scatterplotid).attr("width",(ic.scatterplotWidth * parseFloat(scale)).toString() + "px");
           thisClass.setLogCmd("scatterplot scale " + scale, true);
        });

        me.myEventCls.onIds("#" + me.contactmapid + "_svg", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.saveSvg(me.contactmapid, ic.inputid + "_contactmap.svg", true);
        });
        me.myEventCls.onIds("#" + me.contactmapid + "_png", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.saveFileCls.savePng(me.contactmapid, ic.inputid + "_contactmap.png", true);
        });
        me.myEventCls.onIds("#" + me.contactmapid + "_json", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let graphStr2 = ic.contactmapStr.substr(0, ic.contactmapStr.lastIndexOf('}'));

            graphStr2 += me.htmlCls.setHtmlCls.getLinkColor();

            ic.saveFileCls.saveFile(ic.inputid + "_contactmap.json", "text", [graphStr2]);
        });
        me.myEventCls.onIds("#" + me.contactmapid + "_scale", "change", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let scale = $("#" + me.contactmapid + "_scale").val();
            $("#" + me.contactmapid).attr("width",(ic.contactmapWidth * parseFloat(scale)).toString() + "px");
            thisClass.setLogCmd("contactmap scale " + scale, true);
         });

        me.myEventCls.onIds("#" + me.alignerrormapid + "_svg", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let scale = 1;
            $("#" + me.alignerrormapid + "_scale").val(scale);
            $("#" + me.alignerrormapid).attr("width",(ic.alignerrormapWidth * parseFloat(scale)).toString() + "px");
            
            ic.saveFileCls.saveSvg(me.alignerrormapid, ic.inputid + "_alignerrormap.svg", true);
         });
         me.myEventCls.onIds("#" + me.alignerrormapid + "_png", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            let scale = 1;
            $("#" + me.alignerrormapid + "_scale").val(scale);
            $("#" + me.alignerrormapid).attr("width",(ic.alignerrormapWidth * parseFloat(scale)).toString() + "px");
            
            ic.saveFileCls.savePng(me.alignerrormapid, ic.inputid + "_alignerrormap.png", true);
         });
         me.myEventCls.onIds("#" + me.alignerrormapid + "_full", "click", async function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            await ic.contactMapCls.afErrorMap(afid, true);
         });
         me.myEventCls.onIds("#" + me.alignerrormapid + "_json", "click", function(e) { let ic = me.icn3d;
             e.preventDefault();
             
             
             let graphStr2 = ic.alignerrormapStr.substr(0, ic.alignerrormapStr.lastIndexOf('}'));
 
             graphStr2 += me.htmlCls.setHtmlCls.getLinkColor();
 
             ic.saveFileCls.saveFile(ic.inputid + "_alignerrormap.json", "text", [graphStr2]);
         });

        me.myEventCls.onIds("#" + me.alignerrormapid + "_scale", "change", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           let scale = $("#" + me.alignerrormapid + "_scale").val();
           $("#" + me.alignerrormapid).attr("width",(ic.alignerrormapWidth * parseFloat(scale)).toString() + "px");
           thisClass.setLogCmd("alignerrormap scale " + scale, true);
        });

        me.myEventCls.onIds("#" + me.svgid + "_label", "change", function(e) { me.icn3d;
           e.preventDefault();
           
           let className = $("#" + me.svgid + "_label").val();
           $("#" + me.svgid + " text").removeClass();
           $("#" + me.svgid + " text").addClass(className);
           thisClass.setLogCmd("graph label " + className, true);
        });
        me.myEventCls.onIds("#" + me.svgid + "_hideedges", "change", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           me.htmlCls.hideedges = parseInt($("#" + me.svgid + "_hideedges").val());
           if(me.htmlCls.hideedges) {
                me.htmlCls.contactInsideColor = 'FFF';
                me.htmlCls.hbondInsideColor = 'FFF';
                me.htmlCls.ionicInsideColor = 'FFF';
           }
           else {
                me.htmlCls.contactInsideColor = 'DDD';
                me.htmlCls.hbondInsideColor = 'AFA';
                me.htmlCls.ionicInsideColor = '8FF';
           }
           if(ic.graphStr !== undefined) {
               if(ic.bRender && me.htmlCls.force) me.drawGraph(ic.graphStr, me.pre + 'dl_graph');
               thisClass.setLogCmd("hide edges " + me.htmlCls.hideedges, true);
           }
        });
        me.myEventCls.onIds("#" + me.svgid + "_force", "change", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           me.htmlCls.force = parseInt($("#" + me.svgid + "_force").val());
           if(ic.graphStr !== undefined) {
               thisClass.setLogCmd("graph force " + me.htmlCls.force, true);
               ic.getGraphCls.handleForce();
           }
        });
        me.myEventCls.onIds("#" + me.pre + "hbondReset", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           
           ic.viewInterPairsCls.resetInteractionPairs();
           thisClass.setLogCmd("reset interaction pairs", true);
        });

        me.myEventCls.onIds("#" + me.pre + "applypick_labels", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let text = $("#" + me.pre + "labeltext" ).val();
           let size = $("#" + me.pre + "labelsize" ).val();
           let color = $("#" + me.pre + "labelcolor" ).val();
           let background = $("#" + me.pre + "labelbkgd" ).val();
           if(size === '0' || size === '' || size === 'undefined') size = 0;
           if(color === '0' || color === '' || color === 'undefined') color = 0;
           if(background === '0' || background === '' || background === 'undefined') background = 0;
           if(ic.pAtom === undefined || ic.pAtom2 === undefined) {
             alert("Please pick another atom");
           }
           else {
             let x =(ic.pAtom.coord.x + ic.pAtom2.coord.x) / 2;
             let y =(ic.pAtom.coord.y + ic.pAtom2.coord.y) / 2;
             let z =(ic.pAtom.coord.z + ic.pAtom2.coord.z) / 2;
             ic.analysisCls.addLabel(text, x, y, z, size, color, background, 'custom');
             ic.pickpair = false;
             let sizeStr = '', colorStr = '', backgroundStr = '';
             if(size != 0) sizeStr = ' | size ' + size;
             if(color != 0) colorStr = ' | color ' + color;
             if(background != 0) backgroundStr = ' | background ' + background;
             thisClass.setLogCmd('add label ' + text + ' | x ' + x.toPrecision(4)  + ' y ' + y.toPrecision(4) + ' z ' + z.toPrecision(4) + sizeStr + colorStr + backgroundStr + ' | type custom', true);
             ic.drawCls.draw();
           }
        });

        me.myEventCls.onIds("#" + me.pre + "applyselection_labels", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           let text = $("#" + me.pre + "labeltext2" ).val();
           let size = $("#" + me.pre + "labelsize2" ).val();
           let color = $("#" + me.pre + "labelcolor2" ).val();
           let background = $("#" + me.pre + "labelbkgd2" ).val();
           if(size === '0' || size === '' || size === 'undefined') size = 0;
           if(color === '0' || color === '' || color === 'undefined') color = 0;
           if(background === '0' || background === '' || background === 'undefined') background = 0;
             let position = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.hAtoms, ic.atoms));
             let x = position.center.x;
             let y = position.center.y;
             let z = position.center.z;
             //thisClass.setLogCmd('add label ' + text + ' | size ' + size + ' | color ' + color + ' | background ' + background + ' | type custom', true);
             ic.analysisCls.addLabel(text, x, y, z, size, color, background, 'custom');
             let sizeStr = '', colorStr = '', backgroundStr = '';
             if(size != 0) sizeStr = ' | size ' + size;
             if(color != 0) colorStr = ' | color ' + color;
             if(background != 0) backgroundStr = ' | background ' + background;
             thisClass.setLogCmd('add label ' + text + ' | x ' + x.toPrecision(4)  + ' y ' + y.toPrecision(4) + ' z ' + z.toPrecision(4) + sizeStr + colorStr + backgroundStr + ' | type custom', true);
             ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "applylabelcolor", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            ic.labelcolor = $("#" + me.pre + "labelcolorall" ).val();

            thisClass.setLogCmd('set label color ' + ic.labelcolor, true);
            ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "applypick_stabilizer", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           if(ic.pAtom === undefined || ic.pAtom2 === undefined) {
             alert("Please pick another atom");
           }
           else {
             ic.pickpair = false;
             thisClass.setLogCmd('add one stabilizer | ' + ic.pAtom.serial + ' ' + ic.pAtom2.serial, true);
             if(ic.pairArray === undefined) ic.pairArray = [];
             ic.pairArray.push(ic.pAtom.serial);
             ic.pairArray.push(ic.pAtom2.serial);
             //ic.updateStabilizer();
             ic.threeDPrintCls.setThichknessFor3Dprint();
             ic.drawCls.draw();
           }
        });

    // https://github.com/tovic/color-picker
    // https://tovic.github.io/color-picker/color-picker.value-update.html
    //    pickColor: function() {
        let picker = new CP(document.querySelector("#" + me.pre + "colorcustom"));
        picker.on("change", function(color) {
            this.target.value = color;
        });
        me.myEventCls.onIds("#" + me.pre + "colorcustom", "input", function() {
            let color = $("#" + me.pre + "colorcustom").val();
            picker.set('#' + color).enter();
        });
        me.myEventCls.onIds("#" + me.pre + "colorcustom", "keyup", function() {
            let color = $("#" + me.pre + "colorcustom").val();
            picker.set('#' + color).enter();
        });
        me.myEventCls.onIds("#" + me.pre + "colorcustom", "paste", function() {
            let color = $("#" + me.pre + "colorcustom").val();
            picker.set('#' + color).enter();
        });
        me.myEventCls.onIds("#" + me.pre + "colorcustom", "cut", function() {
            let color = $("#" + me.pre + "colorcustom").val();
            picker.set('#' + color).enter();
        });

        let picker2 = new CP(document.querySelector("#" + me.pre + "labelcolorall"));
        picker2.on("change", function(color) {
            this.target.value = color;
        });
        me.myEventCls.onIds("#" + me.pre + "labelcolorall", "input", function() {
            let color = $("#" + me.pre + "labelcolorall").val();
            picker2.set('#' + color).enter();
        });
        me.myEventCls.onIds("#" + me.pre + "labelcolorall", "keyup", function() {
            let color = $("#" + me.pre + "labelcolorall").val();
            picker2.set('#' + color).enter();
        });
        me.myEventCls.onIds("#" + me.pre + "labelcolorall", "paste", function() {
            let color = $("#" + me.pre + "labelcolorall").val();
            picker2.set('#' + color).enter();
        });
        me.myEventCls.onIds("#" + me.pre + "labelcolorall", "cut", function() {
            let color = $("#" + me.pre + "labelcolorall").val();
            picker2.set('#' + color).enter();
        });    

        me.myEventCls.onIds("#" + me.pre + "applypick_stabilizer_rm", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           if(ic.pAtom === undefined || ic.pAtom2 === undefined) {
             alert("Please pick another atom");
           }
           else {
             ic.pickpair = false;
             thisClass.setLogCmd('remove one stabilizer | ' + ic.pAtom.serial + ' ' + ic.pAtom2.serial, true);
             let rmLineArray = [];
             rmLineArray.push(ic.pAtom.serial);
             rmLineArray.push(ic.pAtom2.serial);
             ic.threeDPrintCls.removeOneStabilizer(rmLineArray);
             //ic.updateStabilizer();
             ic.drawCls.draw();
           }
        });

        me.myEventCls.onIds("#" + me.pre + "applypick_measuredistance", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.bMeasureDistance = false;
           if(ic.pAtom === undefined || ic.pAtom2 === undefined) {
             alert("Please pick another atom");
           }
           else {
             let size = 0, background = 0;
             let color = $("#" + me.pre + "linecolor" ).val();
             let x =(ic.pAtom.coord.x + ic.pAtom2.coord.x) / 2;
             let y =(ic.pAtom.coord.y + ic.pAtom2.coord.y) / 2;
             let z =(ic.pAtom.coord.z + ic.pAtom2.coord.z) / 2;
             ic.analysisCls.addLineFromPicking('distance');
             let distance = parseInt(ic.pAtom.coord.distanceTo(ic.pAtom2.coord) * 10) / 10;
             let text = distance.toString() + " A";
             ic.analysisCls.addLabel(text, x, y, z, size, color, background, 'distance');
             let sizeStr = '', colorStr = '', backgroundStr = '';
             if(color != 0) colorStr = ' | color ' + color;
             thisClass.setLogCmd('add label ' + text + ' | x ' + x.toPrecision(4)  + ' y ' + y.toPrecision(4) + ' z ' + z.toPrecision(4) + sizeStr + colorStr + backgroundStr + ' | type distance', true);
             ic.drawCls.draw();
             ic.pk = 2;
           }
        });


        me.myEventCls.onIds("#" + me.pre + "applydist2", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.bMeasureDistance = false;

           let nameArray = $("#" + me.pre + "atomsCustomDist").val();
           let nameArray2 = $("#" + me.pre + "atomsCustomDist2").val();

           ic.analysisCls.measureDistTwoSets(nameArray, nameArray2);
           thisClass.setLogCmd("dist | " + nameArray2 + " " + nameArray, true);
        });

        $(document).on("click", ".icn3d-distance", function(e) { let ic = me.icn3d;
            e.preventDefault();
            ic.bMeasureDistance = false;

            ic.distPnts = [];
            ic.labels['distance'] = [];
            ic.lines['distance'] = [];

            let sets = $(this).attr('sets').split('|');
 
            let nameArray = [sets[0]];
            let nameArray2 = [sets[1]];
 
            ic.analysisCls.measureDistTwoSets(nameArray, nameArray2);
            thisClass.setLogCmd("dist | " + nameArray2 + " " + nameArray, true);
         });

        me.myEventCls.onIds("#" + me.pre + "applydisttable", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            ic.bMeasureDistance = false;
 
            let nameArray = $("#" + me.pre + "atomsCustomDistTable").val();
            let nameArray2 = $("#" + me.pre + "atomsCustomDistTable2").val();
 
            ic.analysisCls.measureDistManySets(nameArray, nameArray2);
            me.htmlCls.dialogCls.openDlg('dl_disttable', 'Distance among the sets');

            thisClass.setLogCmd("disttable | " + nameArray2 + " " + nameArray, true);
        });

        me.myEventCls.onIds("#" + me.pre + "applylinebtwsets", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            ic.bLinebtwsets = false;
 
            let nameArray = $("#" + me.pre + "linebtwsets").val();
            let nameArray2 = $("#" + me.pre + "linebtwsets2").val();
 
            let atomSet1 = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
            let atomSet2 = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);

            let posArray1 = ic.contactCls.getExtent(atomSet1);
            let posArray2 = ic.contactCls.getExtent(atomSet2);

            let pos1 = new THREE.Vector3(posArray1[2][0], posArray1[2][1], posArray1[2][2]);
            let pos2 = new THREE.Vector3(posArray2[2][0], posArray2[2][1], posArray2[2][2]);

            let radius = $("#" + me.pre + "linebtwsets_radius").val(); 
            let color = $("#" + me.pre + "linebtwsets_customcolor").val(); 
            let opacity = $("#" + me.pre + "linebtwsets_opacity").val();
            let dashed = ($("#" + me.pre + "linebtwsets_style").val() == 'Solid') ? false : true;
            let type = 'cylinder';

            let command = 'add line | x1 ' + pos1.x.toPrecision(4)  + ' y1 ' + pos1.y.toPrecision(4) + ' z1 ' + pos1.z.toPrecision(4) + ' | x2 ' + pos2.x.toPrecision(4)  + ' y2 ' + pos2.y.toPrecision(4) + ' z2 ' + pos2.z.toPrecision(4) + ' | color ' + color + ' | dashed ' + dashed + ' | type ' + type + ' | radius ' + radius + ' | opacity ' + opacity;

            thisClass.setLogCmd(command, true);

            ic.analysisCls.addLine(pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, color, dashed, type, radius, opacity);
            ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "applycartoonshape", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            
            ic.bCartoonshape = false;
 
            let nameArray = $("#" + me.pre + "cartoonshape").val();
            let atomSet1 = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
            let posArray1 = ic.contactCls.getExtent(atomSet1);
            let pos1 = new THREE.Vector3(posArray1[2][0], posArray1[2][1], posArray1[2][2]);

            let shape = $("#" + me.pre + "cartoonshape_shape").val(); // Sphere or Cube
            let radius = $("#" + me.pre + "cartoonshape_radius").val(); 
            let colorStr = $("#" + me.pre + "cartoonshape_customcolor").val(); 
            let opacity = $("#" + me.pre + "cartoonshape_opacity").val();

            colorStr = '#' + colorStr.replace(/\#/g, '');
            let color = me.parasCls.thr(colorStr);
         
            // draw the shape
            let command;
            if(shape == 'Sphere') {
                ic.sphereCls.createSphereBase(pos1, color, radius, undefined, undefined, undefined, opacity);
                command = 'add sphere | x1 ' + pos1.x.toPrecision(4)  + ' y1 ' + pos1.y.toPrecision(4) + ' z1 ' + pos1.z.toPrecision(4) + ' | color ' + colorStr + ' | opacity ' + opacity + ' | radius ' + radius;
            }
            else {
                ic.boxCls.createBox_base(pos1, radius, color, undefined, undefined, undefined, opacity);
                command = 'add cube | x1 ' + pos1.x.toPrecision(4)  + ' y1 ' + pos1.y.toPrecision(4) + ' z1 ' + pos1.z.toPrecision(4) + ' | color ' + colorStr + ' | opacity ' + opacity + ' | radius ' + radius;
            }

            thisClass.setLogCmd(command, true);
            ic.shapeCmdHash[command] = 1;

            ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "clearlinebtwsets", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            

            ic.lines['cylinder'] = [];
            thisClass.setLogCmd('clear line between sets', true);

            ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "clearcartoonshape", "click", function(e) { let ic = me.icn3d;
            e.preventDefault();
            

            ic.shapeCmdHash = {};
            thisClass.setLogCmd('clear shape', true);

            ic.drawCls.draw();
        });

        me.myEventCls.onIds("#" + me.pre + "apply_thickness_3dprint", "click", function(e) { me.icn3d;
            e.preventDefault();

            me.htmlCls.setHtmlCls.setLineThickness("3dprint");
        });
        me.myEventCls.onIds("#" + me.pre + "apply_thickness_style", "click", function(e) { me.icn3d;
            e.preventDefault();

            me.htmlCls.setHtmlCls.setLineThickness("style");
            me.htmlCls.setMenuCls.setLogWindow(true);
        });

        me.myEventCls.onIds("#" + me.pre + "reset_thickness_3dprint", "click", function(e) { me.icn3d;
            e.preventDefault();

            me.htmlCls.setHtmlCls.setLineThickness("3dprint", true);
        });
        me.myEventCls.onIds("#" + me.pre + "reset_thickness_style", "click", function(e) { me.icn3d;
            e.preventDefault();

            me.htmlCls.setHtmlCls.setLineThickness("style", true);
            me.htmlCls.setMenuCls.setLogWindow(true);
        });


        me.myEventCls.onIds("#" + me.pre + "reset", "click", function(e) { let ic = me.icn3d;
            ic.selectionCls.resetAll();

            // need to render
            if(ic.bRender) ic.drawCls.draw(); //ic.drawCls.render();
        });

        me.myEventCls.onIds(["#" + me.pre + "toggleHighlight", "#" + me.pre + "toggleHighlight2"], "click", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            ic.hlUpdateCls.toggleHighlight();
            thisClass.setLogCmd("toggle highlight", true);
        });
        me.myEventCls.onIds("#" + me.pre + "seq_clearselection", "click", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            ic.hlUpdateCls.clearHighlight();
            thisClass.setLogCmd("clear selection", true);
        });
        me.myEventCls.onIds("#" + me.pre + "seq_clearselection2", "click", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            e.preventDefault();
            ic.hlUpdateCls.clearHighlight();
            thisClass.setLogCmd("clear selection", true);
        });
        me.myEventCls.onIds("#" + me.pre + "alignseq_clearselection", "click", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            ic.hlUpdateCls.clearHighlight();
            thisClass.setLogCmd("clear selection", true);
        });

        me.myEventCls.onIds("#" + me.pre + "replay", "click", async function(e) { let ic = me.icn3d;
             e.stopImmediatePropagation();
             ic.CURRENTNUMBER++;
             let currentNumber =(me.cfg.replay) ? ic.STATENUMBER : ic.STATENUMBER - 1;

             if(ic.CURRENTNUMBER == currentNumber) {
                  ic.bReplay = 0;
                  $("#" + me.pre + "replay").hide();
             }
             else if(ic.commands.length > 0 && ic.commands[ic.CURRENTNUMBER]) {
                  await ic.loadScriptCls.execCommandsBase(ic.CURRENTNUMBER, ic.CURRENTNUMBER, ic.STATENUMBER);
                  let pos = ic.commands[ic.CURRENTNUMBER].indexOf('|||');
                  let cmdStrOri =(pos != -1) ? ic.commands[ic.CURRENTNUMBER].substr(0, pos) : ic.commands[ic.CURRENTNUMBER];
                  let maxLen = 30;
                  let cmdStr =(cmdStrOri.length > maxLen) ? cmdStrOri.substr(0, maxLen) + '...' : cmdStrOri;
                  let menuStr = ic.applyCommandCls.getMenuFromCmd(cmdStr);
                  $("#" + me.pre + "replay_cmd").html('Cmd: ' + cmdStr);
                  $("#" + me.pre + "replay_menu").html('Menu: ' + menuStr);

                  thisClass.setLogCmd(cmdStrOri, true);

                  ic.drawCls.draw();
             }
        });


        ic.loadScriptCls.pressCommandtext();

        me.myEventCls.onIds("#" + me.pre + "seq_saveselection", "click", function(e) { let ic = me.icn3d;
           e.stopImmediatePropagation();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           ic.selectionCls.saveSelectionPrep();
           let name = $("#" + me.pre + "seq_command_name").val().replace(/\s+/g, '_');
           //var description = $("#" + me.pre + "seq_command_desc").val();
           ic.selectionCls.saveSelection(name, name);
        });
        me.myEventCls.onIds("#" + me.pre + "seq_saveselection2", "click", function(e) { let ic = me.icn3d;
           e.stopImmediatePropagation();
           ic.selectionCls.saveSelectionPrep();
           let name = $("#" + me.pre + "seq_command_name2").val().replace(/\s+/g, '_');
           //var description = $("#" + me.pre + "seq_command_desc2").val();
           ic.selectionCls.saveSelection(name, name);
        });

        me.myEventCls.onIds("#" + me.pre + "mn2_saveresidue", "click", function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();
            if(!me.cfg.notebook) dialog.dialog( "close" );
            
            ic.selectionCls.saveEachResiInSel();

            thisClass.setLogCmd('select each residue', true);
         });


        me.myEventCls.onIds("#" + me.pre + "alignseq_saveselection", "click", function(e) { let ic = me.icn3d;
           e.stopImmediatePropagation();
           ic.selectionCls.saveSelectionPrep();
           let name = $("#" + me.pre + "alignseq_command_name").val().replace(/\s+/g, '_');
           //var description = $("#" + me.pre + "alignseq_command_desc").val();
           ic.selectionCls.saveSelection(name, name);
        });

        $(document).on("click", "." + me.pre + "outputselection", function(e) { let ic = me.icn3d;
              e.stopImmediatePropagation();
            ic.bSelectResidue = false;
            ic.bSelectAlignResidue = false;
            thisClass.setLogCmd('output selection', true);
            ic.threeDPrintCls.outputSelection();
        });

        $(document).on("click", ".icn3d-saveicon", function(e) { me.icn3d;
           e.stopImmediatePropagation();
           let id = $(this).attr('pid');

           thisClass.saveHtml(id);
           thisClass.setLogCmd("save html " + id, true);
        });

        $(document).on("click", ".icn3d-hideicon", function(e) { let ic = me.icn3d;
           e.stopImmediatePropagation();
           let id = $(this).attr('pid');
           if(!me.cfg.notebook) {
               if(ic.dialogHashHideDone === undefined) ic.dialogHashHideDone = {};
               if(ic.dialogHashPosToRight === undefined) ic.dialogHashPosToRight = {};
               if(!ic.dialogHashHideDone.hasOwnProperty(id)) {
                   ic.dialogHashHideDone[id] = {"width": $("#" + id).dialog( "option", "width"), "height": $("#" + id).dialog( "option", "height"), "position": $("#" + id).dialog( "option", "position")};
                   let dialogWidth = 160;
                   let dialogHeight = 80;
                   $("#" + id).dialog( "option", "width", dialogWidth );
                   $("#" + id).dialog( "option", "height", dialogHeight );
                   let posToRight;
                   if(ic.dialogHashPosToRight.hasOwnProperty(id)) {
                       posToRight = ic.dialogHashPosToRight[id];
                   }
                   else {
                       posToRight = Object.keys(ic.dialogHashPosToRight).length *(dialogWidth + 10);
                       ic.dialogHashPosToRight[id] = posToRight;
                   }
                   let position ={ my: "right bottom", at: "right-" + posToRight + " bottom+60", of: "#" + ic.divid, collision: "none" };
                   $("#" + id).dialog( "option", "position", position );
               }
               else {
                   let width = ic.dialogHashHideDone[id].width;
                   let height = ic.dialogHashHideDone[id].height;
                   let position = ic.dialogHashHideDone[id].position;
                   $("#" + id).dialog( "option", "width", width );
                   $("#" + id).dialog( "option", "height", height );
                   $("#" + id).dialog( "option", "position", position );
                   delete ic.dialogHashHideDone[id];
               }
           }
        });

        // highlight a pair residues
        $(document).on("click", "." + me.pre + "selres", function(e) { let ic = me.icn3d;
              e.stopImmediatePropagation();
              ic.bSelOneRes = false;
              let elems = $( "." + me.pre + "seloneres" );
              for(let i = 0, il = elems.length; i < il;  ++i) {
                  elems[i].checked = false;
              }
              let idArray = $(this).attr('resid').split('|');
              ic.hAtoms = {};
              ic.selectedResidues = {};
              let cmd = 'select ';
              for(let i = 0, il = idArray.length; i < il; ++i) {
                  let idStr = idArray[i]; // TYR $1KQ2.B:56@OH, or ASP $1KQ2.B:40
                  if(i > 0) cmd += ' or ';
                  cmd += ic.selectionCls.selectOneResid(idStr);
              }
              ic.hlUpdateCls.updateHlAll();
              thisClass.setLogCmd(cmd, true);
        });
        // highlight a residue
        $(document).on("click", "." + me.pre + "seloneres", function(e) { let ic = me.icn3d;
              e.stopImmediatePropagation();
              if(!ic.bSelOneRes) {
                  ic.hAtoms = {};
                  ic.selectedResidues = {};
                  ic.bSelOneRes = true;
              }
              let resid = $(this).attr('resid');
              let id = $(this).attr('id');
              if($("#" + id).length && $("#" + id)[0].checked) { // checked
                  ic.selectionCls.selectOneResid(resid);
              }
              else if($("#" + id).length && !$("#" + id)[0].checked) { // unchecked
                  ic.selectionCls.selectOneResid(resid, true);
              }
              ic.hlUpdateCls.updateHlAll();
        });
        // highlight a set of residues
        $(document).on("click", "." + me.pre + "selset", async function(e) { let ic = me.icn3d;
              e.stopImmediatePropagation();
              ic.bSelOneRes = false;
              let elems = $( "." + me.pre + "seloneres" );
              for(let i = 0, il = elems.length; i < il;  ++i) {
                  elems[i].checked = false;
              }
              let cmd = $(this).attr('cmd');
              await ic.selByCommCls.selectByCommand(cmd, '', '');
              ic.hlObjectsCls.removeHlObjects();  // render() is called
              ic.hlObjectsCls.addHlObjects();  // render() is called
              thisClass.setLogCmd(cmd, true);
        });


        $(document).on("click", ".icn3d-addtrack", function(e) { me.icn3d;
          e.stopImmediatePropagation();
          $("#" + me.pre + "anno_custom")[0].checked = true;
          $("[id^=" + me.pre + "custom]").show();
          //e.preventDefault();
          let chainid = $(this).attr('chainid');
          $("#" + me.pre + "track_chainid").val(chainid);
          me.htmlCls.dialogCls.openDlg('dl_addtrack', 'Add track for Chain: ' + chainid);
          $( "#" + me.pre + "track_gi" ).focus();
        });

        $(document).on("click", ".icn3d-customcolor", function(e) { me.icn3d;
          e.stopImmediatePropagation();
          //e.preventDefault();
          let chainid = $(this).attr('chainid');
          $("#" + me.pre + "customcolor_chainid").val(chainid);
          me.htmlCls.dialogCls.openDlg('dl_customcolor', 'Apply custom color or tube for Chain: ' + chainid);
        });

        $(document).on("click", ".icn3d-helixsets", function(e) { let ic = me.icn3d;
          e.stopImmediatePropagation();
          //e.preventDefault();
          let chainid = $(this).attr('chainid');
          ic.addTrackCls.defineSecondary(chainid, 'helix');
          thisClass.setLogCmd('define helix sets | chain ' + chainid, true);
        });

        $(document).on("click", ".icn3d-sheetsets", function(e) { let ic = me.icn3d;
          e.stopImmediatePropagation();
          //e.preventDefault();
          let chainid = $(this).attr('chainid');
          ic.addTrackCls.defineSecondary(chainid, 'sheet');
          thisClass.setLogCmd('define sheet sets | chain ' + chainid, true);
        });

        $(document).on("click", ".icn3d-coilsets", function(e) { let ic = me.icn3d;
          e.stopImmediatePropagation();
          //e.preventDefault();
          let chainid = $(this).attr('chainid');
          ic.addTrackCls.defineSecondary(chainid, 'coil');
          thisClass.setLogCmd('define coil sets | chain ' + chainid, true);
        });

        me.myEventCls.onIds("#" + me.pre + "deletesets", "click", function(e) { let ic = me.icn3d;
             ic.definedSetsCls.deleteSelectedSets();
             thisClass.setLogCmd("delete selected sets", true);
        });

        $(document).on('mouseup touchend', "accordion", function(e) { let ic = me.icn3d;
          if(ic.bControlGl && !me.bNode) {
              if(window.controls) {
                window.controls.noRotate = false;
                window.controls.noZoom = false;
                window.controls.noPan = false;
              }
          }
          else {
              if(ic.controls) {
                ic.controls.noRotate = false;
                ic.controls.noZoom = false;
                ic.controls.noPan = false;
              }
          }
        });

       $(document).on('mousedown touchstart', "accordion", function(e) { let ic = me.icn3d;
          if(ic.bControlGl && !me.bNode) {
              if(window.controls) {
                window.controls.noRotate = true;
                window.controls.noZoom = true;
                window.controls.noPan = true;
              }
          }
          else {
              if(ic.controls) {
                ic.controls.noRotate = true;
                ic.controls.noZoom = true;
                ic.controls.noPan = true;
              }
          }
        });

        //$("[id$=_cddseq_expand]").on('click', '.ui-icon-plus', function(e) { let ic = me.icn3d;
        $(document).on("click", ".icn3d-expand", function(e) { me.icn3d;
            e.stopImmediatePropagation();
            let oriId = $(this).attr('id');
            let pos = oriId.lastIndexOf('_');
            let id = oriId.substr(0, pos);
            $("#" + id).show();
            $("#" + id + "_expand").hide();
            $("#" + id + "_shrink").show();
        });
        //$("[id$=_cddseq_shrink]").on('click', '.ui-icon-minus', function(e) { let ic = me.icn3d;
        $(document).on("click", ".icn3d-shrink", function(e) { me.icn3d;
            e.stopImmediatePropagation();
            let oriId = $(this).attr('id');
            let pos = oriId.lastIndexOf('_');
            let id = oriId.substr(0, pos);
            $("#" + id).hide();
            $("#" + id + "_expand").show();
            $("#" + id + "_shrink").hide();
        });

        window.onscroll = function(e) { let ic = me.icn3d;
            if(ic.view == 'detailed view' && $(window).scrollTop() == 0 && $(window).scrollTop() == 0 && $("#" + me.pre + "dl_selectannotations").scrollTop() == 0) {
                // show fixed titles
                ic.annotationCls.showFixedTitle();
            }
            else {
                // remove fixed titles
                ic.annotationCls.hideFixedTitle();
            }
        } ;
        me.myEventCls.onIds( "#" + me.pre + "dl_selectannotations", "scroll", function() {
            if(ic.view == 'detailed view' && $(window).scrollTop() == 0 && $(window).scrollTop() == 0 && $("#" + me.pre + "dl_selectannotations").scrollTop() == 0) {
                // show fixed titles
                ic.annotationCls.showFixedTitle();
            }
            else {
                // remove fixed titles
                ic.annotationCls.hideFixedTitle();
            }
        });


        me.myEventCls.onIds("#" + me.pre + "mn6_themeBlue", "click", function(e) { me.icn3d;
           me.htmlCls.setMenuCls.setTheme('blue');
           thisClass.setLogCmd("set theme blue", true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn6_themeOrange", "click", function(e) { me.icn3d;
           me.htmlCls.setMenuCls.setTheme('orange');
           thisClass.setLogCmd("set theme orange", true);
        });
        me.myEventCls.onIds("#" + me.pre + "mn6_themeBlack", "click", function(e) { me.icn3d;
           me.htmlCls.setMenuCls.setTheme('black');
           thisClass.setLogCmd("set theme black", true);
        });

        $(document).on("click", "." + me.pre + "snpin3d", async function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();

            let snp = $(this).attr('snp');

            await ic.scapCls.retrieveScap(snp);
            thisClass.setLogCmd('scap 3d ' + snp, true);
            thisClass.setLogCmd("select displayed set", true);
        });

        $(document).on("click", "." + me.pre + "snpinter", async function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();

            let snp = $(this).attr('snp');

            let bInteraction = true;
            await ic.scapCls.retrieveScap(snp, bInteraction);
            thisClass.setLogCmd('scap interaction ' + snp, true);

            let idArray = snp.split('_'); //stru_chain_resi_snp
            let select = '.' + idArray[1] + ':' + idArray[2];
            let name = 'snp_' + idArray[1] + '_' + idArray[2];
            thisClass.setLogCmd("select " + select + " | name " + name, true);
            thisClass.setLogCmd("line graph interaction pairs | selected non-selected | hbonds,salt bridge,interactions,halogen,pi-cation,pi-stacking | false | threshold 3.8 6 4 3.8 6 5.5", true);
            thisClass.setLogCmd("adjust dialog dl_linegraph", true);
            thisClass.setLogCmd("select displayed set", true);
        });

        $(document).on("click", "." + me.pre + "snppdb", async function(e) { let ic = me.icn3d;
            e.stopImmediatePropagation();

            let snp = $(this).attr('snp');

            let bPdb = true;
            await ic.scapCls.retrieveScap(snp, undefined, bPdb);
            thisClass.setLogCmd('scap pdb ' + snp, true);
        });

    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AlignSeq {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    //Set up the sequence display with the aligned sequences. Either chains in "alignChainArray" or residues
    //in "residueArray" will be highlighted. "bUpdateHighlightAtoms" is a flag to update the highlight atoms
    //or not. "bShowHighlight" is a flag to show highlight or not.
    getAlignSequencesAnnotations(alignChainArray, bUpdateHighlightAtoms, residueArray, bShowHighlight, bOnechain, bReverse) {
        let me = this.icn3dui,
            ic = me.icn3d;
        let sequencesHtml = '';

        alignChainArray = Object.keys(ic.alnChains);

        if (bReverse) alignChainArray = alignChainArray.reverse();

        let maxSeqCnt = 0;

        let chainHash = {};
        if (alignChainArray !== undefined) {
            for (let i = 0, il = alignChainArray.length; i < il; ++i) {
                chainHash[alignChainArray[i]] = 1;
            }
        }

        //  let bModifyHAtoms = Object.keys(ic.hAtoms).length == Object.keys(ic.atoms).length && bHighlightChain &&(bUpdateHighlightAtoms === undefined || bUpdateHighlightAtoms);
        //  let bModifyHAtoms = Object.keys(ic.hAtoms).length == Object.keys(ic.atoms).length &&(bUpdateHighlightAtoms === undefined || bUpdateHighlightAtoms);
        let bModifyHAtoms = (bUpdateHighlightAtoms === undefined || bUpdateHighlightAtoms);

        if (bModifyHAtoms) {
            ic.hAtoms = {};
        }

        let bHighlightChain;
        let index = 0, prevResCnt2nd = 0;
        let firstChainid, oriChainid;

        //  for(let i in ic.alnChains) {
        for (let m = 0, ml = alignChainArray.length; m < ml; ++m) {
            let i = alignChainArray[m];
            
            if (index == 0) firstChainid = i;

            if (bOnechain && index > 0) {
                oriChainid = firstChainid;
            } else {
                oriChainid = i;
            }

            //bHighlightChain =(alignChainArray !== undefined && chainHash.hasOwnProperty(oriChainid)) ? true : false;

            //if( bHighlightChain &&(bUpdateHighlightAtoms === undefined || bUpdateHighlightAtoms) ) {
            // do not update isa subset is selected already
            if (bModifyHAtoms) {
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.alnChains[i]);
            }

            let resiHtmlArray = [], seqHtml = "";
            let seqLength = (ic.alnChainsSeq[i] !== undefined) ? ic.alnChainsSeq[i].length : 0;

            if (seqLength > maxSeqCnt) maxSeqCnt = seqLength;

            let dashPos = oriChainid.indexOf('_');
            let structure = oriChainid.substr(0, dashPos);
            let chain = oriChainid.substr(dashPos + 1);

            //let startResi = (ic.alnChainsSeq[i][0] !== undefined) ? ic.alnChainsSeq[i][0].resi : '';
            let startResi, endResi;
            for (let k = 0, kl = seqLength; k < kl; ++k) {
                if(ic.alnChainsSeq[i][k].resn != '-') {
                    startResi = ic.alnChainsSeq[i][k].resi;
                    break;
                }
            }

            for (let k = seqLength - 1; k >= 0; --k) {
                if(ic.alnChainsSeq[i][k].resn != '-') {
                    endResi = ic.alnChainsSeq[i][k].resi;
                    break;
                }
            }

            seqHtml += "<span class='icn3d-residueNum' title='starting residue number'>" + startResi + "</span>";
            bHighlightChain = (alignChainArray !== undefined && chainHash.hasOwnProperty(oriChainid)) ? true : false;

            for (let k = 0, kl = seqLength; k < kl; ++k) {
                // resiId is empty if it's gap
                let resiId = 'N/A', resIdFull = '';
                if (ic.alnChainsSeq[i][k].resi !== '' && !isNaN(ic.alnChainsSeq[i][k].resi)) {
                    resiId = ic.alnChainsSeq[i][k].resi;
                    resIdFull = structure + "_" + chain + "_" + resiId;
                    ic.alnChainsSeq[i][k].color;
                }

                let classForAlign = "class='icn3d-residue"; // used to identify a residue when clicking a residue in sequence

                //if((bShowHighlight === undefined || bShowHighlight) &&(bHighlightChain ||(ic.alnChainsSeq[i][k].aligned === 2 && residueArray !== undefined && resIdFull !== '' && residueArray.indexOf(resIdFull) !== -1) ) ) {
                if ((bShowHighlight === undefined || bShowHighlight) && (bHighlightChain || (residueArray !== undefined && resIdFull !== '' && residueArray.indexOf(resIdFull) !== -1))) {
                    classForAlign = "class='icn3d-residue icn3d-highlightSeq";
                }

                // class for alignment: cons, ncons, nalign
                if (resIdFull === '') {
                    classForAlign += "'";
                } else {
                    classForAlign += " " + ic.alnChainsSeq[i][k].class + "'";
                }

                let colorRes;

                if (!ic.residues.hasOwnProperty(resIdFull)) {                  
                    colorRes = '#000000;';
                } else {
                    let firstAtom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[resIdFull]);
                    colorRes = (firstAtom.color !== undefined) ? '#' + firstAtom.color.getHexString() + ';' : '#000000;';
                }

                if (colorRes.toUpperCase() === '#FFFFFF;') colorRes = me.htmlCls.GREYD;

                let bWithCoord = (resIdFull !== '') ? true : false;

                if (bOnechain && k == 0) {
                    let letterSpace = 10;
                    let empthWidth = prevResCnt2nd * letterSpace;
                    seqHtml += "<span style='width:" + empthWidth + "px'></span>";
                }

                if (bWithCoord) {
                    if (ic.alnChainsSeq[i][k].resi != -1) {
                        // add "align" in front of id so that full sequence and aligned sequence will not conflict
                        seqHtml += "<span id='align_" + me.pre + resIdFull + "' " + classForAlign + " style='color:" + colorRes + "' title='" + ic.alnChainsSeq[i][k].resn + ic.alnChainsSeq[i][k].resi + "'>" + ic.alnChainsSeq[i][k].resn + "</span>";
                    } else {
                        seqHtml += "<span>" + ic.alnChainsSeq[i][k].resn + "</span>";
                    }
                } else {
                    seqHtml += "<span title='" + ic.alnChainsSeq[i][k].resn + ic.alnChainsSeq[i][k].resi + "'>" + ic.alnChainsSeq[i][k].resn + "</span>";
                }

            }
            //let endResi = (ic.alnChainsSeq[i][seqLength - 1] !== undefined) ? ic.alnChainsSeq[i][seqLength - 1].resi : '';
            seqHtml += "<span class='icn3d-residueNum' title='ending residue number'>" + endResi + "</span>";

            let n = alignChainArray.length;

            // the first chain stores all annotations
            // secondary: n, labels: 2, title: n, empty line: 1
            let annoLength = (ic.alnChainsAnno[i] !== undefined) ? ic.alnChainsAnno[i].length : 0;

            for (let j = 0, jl = annoLength; j < jl; ++j) {
                resiHtmlArray[j] = "";

                //let chainid = (j == 0 && annoLength >= 7) ? ic.alnChainsAnTtl[i][4][0] : oriChainid; // bottom secondary, j == 0: chain2,  next secondary, j == 1: chain1,
                let chainid = (j < n) ?  alignChainArray[n - 1 - j] : oriChainid; // bottom secondary, j == 0: chain2,  next secondary, j == 1: chain1,

                resiHtmlArray[j] += "<span class='icn3d-residueNum'></span>"; // a spot corresponding to the starting and ending residue number
                for (let k = 0, kl = ic.alnChainsAnno[i][j].length; k < kl; ++k) {
                    let text = ic.alnChainsAnno[i][j][k];

                    if (text == 'H' || text == 'E' || text == 'c' || text == 'o') {

                        if (text == 'H') {
                            if (k % 2 == 0) {
                                resiHtmlArray[j] += '<span class="icn3d-helix">&nbsp;</span>';
                            } else {
                                resiHtmlArray[j] += '<span class="icn3d-helix2">&nbsp;</span>';
                            }
                        } else if (text == 'E') {
                            if (ic.alnChainsSeq[chainid][k] !== undefined) {
                                let resiId = ic.alnChainsSeq[chainid][k].resi;
                                let resIdFull = chainid + "_" + resiId;

                                if (ic.residues.hasOwnProperty(resIdFull)) {
                                    let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[resIdFull]);

                                    if (atom.ssend) {
                                        resiHtmlArray[j] += '<span class="icn3d-sheet2">&nbsp;</span>';
                                    } else {
                                        resiHtmlArray[j] += '<span class="icn3d-sheet">&nbsp;</span>';
                                    }
                                }
                                else {
                                    resiHtmlArray[j] += '<span class="icn3d-sheet">&nbsp;</span>';
                                }
                            }
                            else {
                                resiHtmlArray[j] += '<span class="icn3d-sheet">&nbsp;</span>';
                            }
                        } else if (text == 'c') {
                            resiHtmlArray[j] += '<span class="icn3d-coil">&nbsp;</span>';
                        } else if (text == 'o') {
                            resiHtmlArray[j] += '<span class="icn3d-other">&nbsp;</span>';
                        } else {                          
                            resiHtmlArray[j] += "<span></span>";
                        }
                    } else {
                        resiHtmlArray[j] += "<span>" + text + "</span>";
                    }
                    //resiHtmlArray[j] += "<span>" + ic.alnChainsAnno[i][j][k] + "</span>";
                }
                resiHtmlArray[j] += "<span class='icn3d-residueNum'></span>"; // a spot corresponding to the starting and ending residue number
            }

            let chainidTmp = i,
                title = (ic.pdbid_chain2title !== undefined) ? ic.pdbid_chain2title[oriChainid] : '';

            // add markers and residue numbers
            for (let j = annoLength - 1; j >= 0; --j) {
                let annotitle = ic.alnChainsAnTtl[i][j][0];
                if (annotitle == 'SS') annotitle = '';
                //sequencesHtml += "<div class='icn3d-residueLine' style='white-space:nowrap;'><div class='icn3d-seqTitle' chain='" + i + "' anno='" + j + "'>" + annotitle + "</div>" + resiHtmlArray[j] + "<br/></div>";
                sequencesHtml += "<div class='icn3d-residueLine' style='white-space:nowrap;'><div class='icn3d-seqTitle' anno='" + j + "'>" + annotitle + "</div>" + resiHtmlArray[j] + "<br/></div>";
            }

            sequencesHtml += '<div class="icn3d-seqTitle icn3d-link icn3d-blue" chain="' + i + '" anno="sequence" title="' + title + '">' + chainidTmp + ' </div><span class="icn3d-seqLine">' + seqHtml + '</span><br/>';

            if (index > 0) prevResCnt2nd += seqLength;

            ++index;
        }

        return { "sequencesHtml": sequencesHtml, "maxSeqCnt": maxSeqCnt }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SetHtml {
    constructor(icn3dui) {
        this.icn3dui = icn3dui;
    }

    getLink(id, text, bSimpleMenu, selType) { let me = this.icn3dui; me.icn3d;
        me.htmlCls.allMenus[id] = text;
        if(selType) me.htmlCls.allMenusSel[id] = selType;
        if(bSimpleMenu) me.htmlCls.simpleMenus[id] = 1;

        return "<li><span id='" + me.pre + id + "' class='icn3d-link'>" + text + "</span></li>";
    }

    // a group of menus
    getMenuText(id, text, classname, bSimpleMenu, selType) { let me = this.icn3dui; me.icn3d;
        me.htmlCls.allMenus[id] = text;
        if(selType) me.htmlCls.allMenusSel[id] = selType;
        if(bSimpleMenu) me.htmlCls.simpleMenus[id] = 1;

        let styleStr = (classname == 'icn3d-menupd') ? " style='padding-left:1.5em!important;'" : "";

        // no ending "</li>"" since this is usually the start of a group of menus
        return "<li><span id='" + me.pre + id + "'" + styleStr + ">" + text + "</span>"; 
    }

    getMenuUrl(id, url, text, bSimpleMenu, selType) { let me = this.icn3dui; me.icn3d;
        me.htmlCls.allMenus[id] = text;
        if(selType) me.htmlCls.allMenusSel[id] = selType;
        if(bSimpleMenu) me.htmlCls.simpleMenus[id] = 1;

        return "<li><a id='" + me.pre + id + "' href='" + url + "' target='_blank'>" + text + "</a></li>";
    }

    getMenuSep() { let me = this.icn3dui; me.icn3d;
        return "<li class='icn3d-menusep'>-</li>";
    }

    getLinkWrapper(id, text, wrapper, bSimpleMenu, selType, bHide) { let me = this.icn3dui; me.icn3d;
        me.htmlCls.allMenus[id] = text;
        if(selType) me.htmlCls.allMenusSel[id] = selType;
        if(bSimpleMenu) me.htmlCls.simpleMenus[id] = 1;

        let hideStr = (bHide) ? ' style="display:none"' : '';
        return "<li id='" + me.pre + wrapper + "'" + hideStr + "><span id='" + me.pre + id + "' class='icn3d-link'>" + text + "</span></li>";
    }

    getLinkWrapper2(id, text, wrapper, bSimpleMenu, selType) { let me = this.icn3dui; me.icn3d;
        me.htmlCls.allMenus[id] = text;
        if(selType) me.htmlCls.allMenusSel[id] = selType;
        if(bSimpleMenu) me.htmlCls.simpleMenus[id] = 1;

        return "<li id='" + me.pre + wrapper + "'><span id='" + me.pre + id + "' class='icn3d-link'>" + text + "</span>";
    }

    getRadio(radioid, id, text, bChecked, bSimpleMenu, selType) { let me = this.icn3dui; me.icn3d;
        me.htmlCls.allMenus[id] = text;
        if(selType) me.htmlCls.allMenusSel[id] = selType;
        if(bSimpleMenu) me.htmlCls.simpleMenus[id] = 1;

        let checkedStr =(bChecked) ? ' checked' : '';

        //https://stackoverflow.com/questions/17541614/use-images-instead-of-radio-buttons/17541916
        //return "<li><label for='" + me.pre + id + "' class='icn3d-rad'>" + me.htmlCls.inputRadioStr + "name='" + me.pre + radioid + "' " + "class='" + me.pre + radioid + "' " + "v='" + text + "' id='" + me.pre + id + "'" + checkedStr + "><span class='ui-icon ui-icon-blank'></span> <span class='icn3d-rad-text'>" + text + "</span></label></li>";
        return "<li><label id='" + me.pre + id + "' class='icn3d-rad'>" + me.htmlCls.inputRadioStr + "name='" + me.pre + radioid + "' " + "class='" + me.pre + radioid + "' " + "v='" + text + "'" + checkedStr + "><span class='ui-icon ui-icon-blank'></span> <span class='icn3d-rad-text'>" + text + "</span></label></li>";
    }

    getRadioColor(radioid, id, text, color, bChecked, bSimpleMenu, selType) { let me = this.icn3dui; me.icn3d;
        me.htmlCls.allMenus[id] = text;
        if(selType) me.htmlCls.allMenusSel[id] = selType;
        if(bSimpleMenu) me.htmlCls.simpleMenus[id] = 1;

        let checkedStr =(bChecked) ? ' checked' : '';

        //https://stackoverflow.com/questions/17541614/use-images-instead-of-radio-buttons/17541916
        //return "<li><label for='" + me.pre + id + "' class='icn3d-rad'>" + me.htmlCls.inputRadioStr + "name='" + me.pre + radioid + "' id='" + me.pre + id + "'" + checkedStr + "><span class='ui-icon ui-icon-blank'></span> <span class='icn3d-color-rad-text' color='" + color + "'><span style='background-color:#" + color + "'>" + me.htmlCls.space3 + "</span> " + text + "</span></label></li>";
        return "<li><label id='" + me.pre + id + "' class='icn3d-rad'>" + me.htmlCls.inputRadioStr + "name='" + me.pre + radioid + "'" + checkedStr + "><span class='ui-icon ui-icon-blank'></span> <span class='icn3d-color-rad-text' color='" + color + "'><span style='background-color:#" + color + "'>" + me.htmlCls.space3 + "</span> " + text + "</span></label></li>";
    }

    setAdvanced(index) { let me = this.icn3dui; me.icn3d;
        let indexStr =(index === undefined) ? '' : index;

        let dialogClass =(me.cfg.notebook) ? 'icn3d-hidden' : '';
        let html = me.htmlCls.divStr + "dl_advanced" + indexStr + "' class='" + dialogClass + "'>";

        html += "<table width='500'><tr><td valign='top'><table cellspacing='0'>";
        html += "<tr><td><b>Select:</b></td><td>" + me.htmlCls.inputTextStr + "id='" + me.pre + "command" + indexStr + "' placeholder='$[structures].[chains]:[residues]@[atoms]' size='60'></td></tr>";
        html += "<tr><td><b>Name:</b></td><td>" + me.htmlCls.inputTextStr + "id='" + me.pre + "command_name" + indexStr + "' placeholder='my_selection' size='60'></td></tr>";
        html += "<tr><td colspan='2' align='left'>" + me.htmlCls.space3 + me.htmlCls.buttonStr + "command_apply" + indexStr + "'><b>Save Selection to Defined Sets</b></button></td></tr>";
        html += "</table></td>";

        html += "</tr>";

        html += "<tr><td>";

        html += 'Specification Tips: <div style="width:20px; margin-top:6px; display:inline-block;"><span id="' + me.pre + 'specguide' + indexStr + '_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="' + me.pre + 'specguide' + indexStr + '_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div><br>';

        html += me.htmlCls.divStr + "specguide" + indexStr + "' style='display:none; width:500px' class='icn3d-box'>";

        html += "<b>Specification:</b> In the selection \"$1HHO,4N7N.A,B,C:5-10,LV,3AlaVal,chemicals@CA,C\":";
        html += "<ul><li>\"$1HHO,4N7N\" uses \"$\" to indicate structure selection.<br/>";
        html += "<li>\".A,B,C\" uses \".\" to indicate chain selection.<br/>";
        html += "<li>\":5-10,LV,3LeuVal,chemicals\" uses the colon \":\" to indicate residue selection. Residue selection could be residue number(5-10), one-letter IUPAC residue name abbreviations(LV), three-letter residue names(AlaVal, \"3\" indicates each residue name has three letters), or predefined names: \"proteins\", \"nucleotides\", \"chemicals\", \"ions\", and \"water\". IUPAC abbreviations can be written either as a contiguous string(e.g., \":LV\"), in order to find all instances of that sequence in the structure, or they can be separated by commas(e.g., \":L,V\") to select all residues of a given type in the structure(in the latter case, select all Leucine and Valine in the structure).<br/>";
        html += "<li>\"@CA,C\" uses \"@\" to indicate atom selection.<br/>";
        html += "<li>Partial definition is allowed, e.g., \":1-10\" selects all residue IDs 1-10 in all chains.<br/>";
        html += "<li>Different selections can be unioned(with \"<b>or</b>\", default), intersected(with \"<b>and</b>\"), or negated(with \"<b>not</b>\"). For example, \":1-10 or :K\" selects all residues 1-10 and all Lys residues. \":1-10 and :K\" selects all Lys residues in the range of residue number 1-10. \":1-10 or not :K\" selects all residues 1-10, which are not Lys residues.<br/>";
        html += "<li>The wild card character \"X\" or \"x\" can be used to represent any character.";
        html += "</ul>";
        html += "<b>Set Operation:</b>";
        html += "<ul><li>Users can select multiple sets in the menu \"Select > Defined Sets\".<br/>";
        html += "<li>Different sets can be unioned(with \"<b>or</b>\", default), intersected(with \"<b>and</b>\"), or negated(with \"<b>not</b>\"). For example, if the \"Defined Sets\" menu has four sets \":1-10\", \":11-20\", \":5-15\", and \":7-8\", the command \"saved atoms :1-10 or :11-20 and :5-15 not :7-8\" unions all residues 1-10 and 11-20 to get the residues 1-20, then intersects with the residues 5-15 to get the residues 5-15, then exclude the residues 7-8 to get the final residues 5-6 and 9-15.</ul>";
        html += "<b>Full commands in url or command window:</b>";
        html += "<ul><li>Select without saving the set: select $1HHO,4N7N.A,B,C:5-10,LV,chemicals@CA,C<br/>";
        //html += "<li>Select and save: select $1HHO,4N7N.A,B,C:5-10,LV,chemicals@CA,C | name my_name | description my_description</ul>";
        html += "<li>Select and save: select $1HHO,4N7N.A,B,C:5-10,LV,chemicals@CA,C | name my_name</ul>";

        html += "</div>";

        html += "</td></tr></table>";
        html += "</div>";

        return html;
    }

    getOptionHtml(optArray, selIndex) { let me = this.icn3dui; me.icn3d;
        let html = '';

        for(let i = 0, il = optArray.length; i < il; ++i) {
            let iStr = optArray[i];

            if(i == selIndex) {
                html += me.htmlCls.optionStr + "'" + iStr + "' selected>" + iStr + "</option>";
            }
            else {
                html += me.htmlCls.optionStr + "'" + iStr + "'>" + iStr + "</option>";
            }
        }

        return html;
    }

    setColorHints() { let me = this.icn3dui; me.icn3d;
        let html = '';

        html += me.htmlCls.divNowrapStr + '<span style="margin-left:33px; color:#00FF00; font-weight:bold">Green</span>: H-Bonds; ';
        html += '<span style="color:#00FFFF; font-weight:bold">Cyan</span>: Salt Bridge/Ionic; ';
        html += '<span style="font-weight:bold">Grey</span>: contacts</div>';
        html += me.htmlCls.divNowrapStr + '<span style="margin-left:33px; color:#FF00FF; font-weight:bold">Magenta</span>: Halogen Bonds; ';
        html += '<span style="color:#FF0000; font-weight:bold">Red</span>: &pi;-Cation; ';
        html += '<span style="color:#0000FF; font-weight:bold">Blue</span>: &pi;-Stacking</div>';

        return html;
    }

    setThicknessHtml(type) { let me = this.icn3dui, ic = me.icn3d;
        let html = '';

        // type == '3dprint' or 'style'
        let linerad =(type == '3dprint') ? '1' : '0.1';
        let coilrad =(type == '3dprint') ? '1.2' : '0.3';
        let stickrad =(type == '3dprint') ? '0.8' : '0.4';
        let crosslinkrad =(type == '3dprint') ? '0.8' : '0.4';
        let tracerad =(type == '3dprint') ? '1' : '0.4';
        let ballscale =(type == '3dprint') ? '0.6' : '0.3';
        let ribbonthick =(type == '3dprint') ? '1' : '0.2';
        let prtribbonwidth =(type == '3dprint') ? '2' : '1.3';
        let nucleotideribbonwidth =(type == '3dprint') ? '1.4' : '0.8';

        let shininess = 40;
        let light1 = 0.6;
        let light2 = 0.4;
        let light3 = 0.2;
        let bGlycansCartoon = 0;
        let bMembrane = 1;
        let bCmdWindow = 0;

        // retrieve from cache
        if(type == 'style') {
            if(this.getCookie('shininess') != '') {
                shininess = parseFloat(this.getCookie('shininess'));
            }

            if(this.getCookie('light1') != '') {
                light1 = parseFloat(this.getCookie('light1'));
                light2 = parseFloat(this.getCookie('light2'));
                light3 = parseFloat(this.getCookie('light3'));
            }

            if(this.getCookie('lineRadius') != '') {
                linerad = parseFloat(this.getCookie('lineRadius'));
                coilrad = parseFloat(this.getCookie('coilWidth'));
                stickrad = parseFloat(this.getCookie('cylinderRadius'));
                let clrad = this.getCookie('crosslinkRadius');
                crosslinkrad = (!isNaN(clrad)) ? parseFloat(clrad) : ic.crosslinkRadius;
                tracerad = parseFloat(this.getCookie('traceRadius'));
                ballscale = parseFloat(this.getCookie('dotSphereScale'));
                ribbonthick = parseFloat(this.getCookie('ribbonthickness'));
                prtribbonwidth = parseFloat(this.getCookie('helixSheetWidth'));
                nucleotideribbonwidth = parseFloat(this.getCookie('nucleicAcidWidth'));
            }

            if(this.getCookie('glycan') != '') {
                bGlycansCartoon = parseFloat(this.getCookie('glycan'));
            }

            if(this.getCookie('membrane') != '') {
                bMembrane = parseFloat(this.getCookie('membrane'));
            }

            if(this.getCookie('cmdwindow') != '') {
                bCmdWindow = parseFloat(this.getCookie('cmdwindow'));
            }

            html += "<b>Note</b>: The following parameters will be saved in cache. You just need to set them once. <br><br>";

            html += "<b>1. Shininess</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "shininess' value='" + shininess + "' size=4>" + me.htmlCls.space3 + "(for the shininess of the 3D objects, default 40)<br/><br/>";
            html += "<b>2. Three directional lights</b>: <br>";
            html += "<b>Key Light</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "light1' value='" + light1 + "' size=4>" + me.htmlCls.space3 + "(for the light strength of the key light, default 0.6)<br/>";
            html += "<b>Fill Light</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "light2' value='" + light2 + "' size=4>" + me.htmlCls.space3 + "(for the light strength of the fill light, default 0.4)<br/>";
            html += "<b>Back Light</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "light3' value='" + light3 + "' size=4>" + me.htmlCls.space3 + "(for the light strength of the back light, default 0.2)<br/><br/>";
            html += "<b>3. Thickness</b>: <br>";
        }

        html += "<b>Line Radius</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "linerad_" + type + "' value='" + linerad + "' size=4>" + me.htmlCls.space3 + "(for stabilizers, hydrogen bonds, distance lines, default 0.1)<br/>";
        html += "<b>Coil Radius</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "coilrad_" + type + "' value='" + coilrad + "' size=4>" + me.htmlCls.space3 + "(for coils, default 0.3)<br/>";
        html += "<b>Stick Radius</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "stickrad_" + type + "' value='" + stickrad + "' size=4>" + me.htmlCls.space3 + "(for sticks, default 0.4)<br/>";
        html += "<b>Cross-Linkage Radius</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "crosslinkrad_" + type + "' value='" + crosslinkrad + "' size=4>" + me.htmlCls.space3 + "(for cross-linkages, default 0.4)<br/>";
        html += "<b>Trace Radius</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "tracerad_" + type + "' value='" + tracerad + "' size=4>" + me.htmlCls.space3 + "(for C alpha trace, O3' trace, default 0.4)<br/>";

        html += "<b>Ribbon Thickness</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "ribbonthick_" + type + "' value='" + ribbonthick + "' size=4>" + me.htmlCls.space3 + "(for helix and sheet ribbons, nucleotide ribbons, default 0.2)<br/>";
        html += "<b>Protein Ribbon Width</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "prtribbonwidth_" + type + "' value='" + prtribbonwidth + "' size=4>" + me.htmlCls.space3 + "(for helix and sheet ribbons, default 1.3)<br/>";
        html += "<b>Nucleotide Ribbon Width</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "nucleotideribbonwidth_" + type + "' value='" + nucleotideribbonwidth + "' size=4>" + me.htmlCls.space3 + "(for nucleotide ribbons, default 0.8)<br/>";

        html += "<b>Ball Scale</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "ballscale_" + type + "' value='" + ballscale + "' size=4>" + me.htmlCls.space3 + "(for styles 'Ball and Stick' and 'Dot', default 0.3)<br/>";

        if(type == 'style') {
            html += "<br><b>4. Show Glycan Cartoon</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "glycan' value='" + bGlycansCartoon + "' size=4>" + me.htmlCls.space3 + "(0: hide, 1: show, default 0)<br/>";

            html += "<br><b>5. Show Membrane</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "membrane' value='" + bMembrane + "' size=4>" + me.htmlCls.space3 + "(0: hide, 1: show, default 1)<br/>";

            html += "<br><b>6. Enlarge Command Window</b>: " + me.htmlCls.inputTextStr + "id='" + me.pre + "cmdwindow' value='" + bCmdWindow + "' size=4>" + me.htmlCls.space3 + "(0: Regular, 1: Large, default 0)<br/><br/>";
        }

        html += me.htmlCls.spanNowrapStr + "" + me.htmlCls.buttonStr + "apply_thickness_" + type + "'>Apply</button></span>&nbsp;&nbsp;&nbsp;";

        html += me.htmlCls.spanNowrapStr + "" + me.htmlCls.buttonStr + "reset_thickness_" + type + "'>Reset</button></span>";

        return html;
    }

    getCookie(cname) {
      let name = cname + "=";
      let decodedCookie = decodeURIComponent(document.cookie);
      let ca = decodedCookie.split(';');
      for(let i = 0; i <ca.length; i++) {
        let c = ca[i];
        while (c.charAt(0) == ' ') {
          c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
          return c.substring(name.length, c.length);
        }
      }
      return "";
    }

    setSequenceGuide(suffix, bShown) { let me = this.icn3dui, ic = me.icn3d;
      let sequencesHtml = '';

      let index =(ic && ic.defNames2Atoms) ? Object.keys(ic.defNames2Atoms).length : 1;

      if(bShown) {
         sequencesHtml += me.htmlCls.divStr + "seqguide" + suffix + "'>";
     }
     else {
         sequencesHtml += '<div style="width:20px; margin-left:3px; display:inline-block;"><span id="' + me.pre + 'seqguide' + suffix + '_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="' + me.pre + 'seqguide' + suffix + '_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div> ';

         sequencesHtml += "<div style='min-width:200px; display:inline-block;'><b>Selection:</b> Name: " + me.htmlCls.inputTextStr + "id='" + me.pre + "seq_command_name" + suffix + "' value='seq_" + index + "' size='5'> " + me.htmlCls.space2 + "<button style='white-space:nowrap;' id='" + me.pre + "seq_saveselection" + suffix + "'>Save</button> <button style='white-space:nowrap; margin-left:20px;' id='" + me.pre + "seq_clearselection" + suffix + "'>Clear</button></div><br/>";

         sequencesHtml += me.htmlCls.divStr + "seqguide" + suffix + "' style='display:none; white-space:normal;' class='icn3d-box'>";
     }

      sequencesHtml += this.getSelectionHints();

      let resCategories = "<b>Residue labeling:</b> standard residue with coordinates: UPPER case letter; nonstandard residue with coordinates: the first UPPER case letter plus a period except that water residue uses the letter 'O'; residue missing coordinates: lower case letter.";
      let scroll =(me.utilsCls.isMac() && !me.utilsCls.isMobile()) ? "<br/><br/><b>Turn on scroll bar:</b> System preferences -> General -> show scroll bars -> check Always" : "";

      sequencesHtml += resCategories + scroll + "<br/></div>";

      return sequencesHtml;
    }

    setAlignSequenceGuide(suffix, bShown) { let me = this.icn3dui, ic = me.icn3d;
      let sequencesHtml = '';
      suffix = '';

      let index =(ic && ic.defNames2Atoms) ? Object.keys(ic.defNames2Atoms).length : 1;

      sequencesHtml += '<div style="width:20px; margin-left:3px; display:inline-block;"><span id="' + me.pre + 'alignseqguide' + suffix + '_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="' + me.pre + 'alignseqguide' + suffix + '_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div> ';

      sequencesHtml += "<div style='min-width:200px; display:inline-block;''><b>Selection:</b> Name: " + me.htmlCls.inputTextStr + "id='" + me.pre + "alignseq_command_name' value='alseq_" + index + "' size='10'> " + me.htmlCls.space2 + "<button style='white-space:nowrap;' id='" + me.pre + "alignseq_saveselection'>Save</button> <button style='white-space:nowrap; margin-left:20px;' id='" + me.pre + "alignseq_clearselection'>Clear</button></div><br/>";

      sequencesHtml += me.htmlCls.divStr + "alignseqguide" + suffix + "' style='display:none; white-space:normal;' class='icn3d-box'>";

      sequencesHtml += this.getSelectionHints();

      let resCategories = "<b>Residue labeling:</b> aligned residue with coordinates: UPPER case letter; non-aligned residue with coordinates: lower case letter which can be highlighted; residue missing coordinates: lower case letter which can NOT be highlighted.";
      let scroll =(me.utilsCls.isMac() && !me.utilsCls.isMobile()) ? "<br/><br/><b>Turn on scroll bar:</b> System preferences -> General -> show scroll bars -> check Always" : "";

      sequencesHtml += resCategories + scroll + "<br/></div>";

      return sequencesHtml;
    }

    getSelectionHints() { let me = this.icn3dui; me.icn3d;
      let sequencesHtml = '';

      if(!me.utilsCls.isMobile()) {
          sequencesHtml += "<b>Select on 1D sequences:</b> drag to select, drag again to deselect, multiple selection is allowed without Ctrl key, click \"Save Selection\" to save the current selection.<br/><br/>";

          sequencesHtml += "<b>Select on 2D interaction diagram:</b> click on the nodes or lines. The nodes are chains and can be united with the Ctrl key. The lines are interactions and can NOT be united. Each click on the lines selects half of the lines, i.e., select the interacting residues in one of the two chains.<br/><br/>";

          let tmpStr = me.utilsCls.isMobile() ? 'use finger to pick' : 'hold "Alt" and use mouse to pick';
          sequencesHtml += "<b>Select on 3D structures:</b> " + tmpStr + ", click the second time to deselect, hold \"Ctrl\" to union selection, hold \"Shift\" to select a range, press the up/down arrow to switch among atom/residue/strand/chain/structure, click \"Save Selection\" to save the current selection.<br/><br/>";

          sequencesHtml += "<b>Save the current selection</b>(either on 3D structure, 2D interactions, or 1D sequence): open the menu \"Select -> Save Selection\", specify the name and description for the selection, and click \"Save\".<br/><br/>";
      }
      else {
            sequencesHtml += "<b>Select Aligned Sequences:</b> touch to select, touch again to deselect, multiple selection is allowed without Ctrl key, click \"Save Selection\" to save the current selection.<br/>";
      }

      return sequencesHtml;
    }

    addGsizeSalt(name) { let me = this.icn3dui; me.icn3d;
        let html = "";

        html += "<span style='white-space:nowrap;font-weight:bold;'>Grid Size: <select id='" + me.pre + name + "gsize'>";

        let optArray1c = ['65', '97', '129'];
        html += this.getOptionHtml(optArray1c, 0);

        html += "</select></span>";

        html += "<span style='white-space:nowrap;font-weight:bold;margin-left:30px;'>Salt Concentration: <select id='" + me.pre + name + "salt'>";

        let optArray1d = ['0', '0.15'];
        html += this.getOptionHtml(optArray1d, 1);

        html += "</select> M</span><br/>";

        return html;
    }

    getFootHtml(type, tabName) { let me = this.icn3dui; me.icn3d;
        let footHtml = "<div style='width:500px;'>";

        if(type == 'delphi') {
            if(me.cfg.cid) {
                footHtml += "<b>Note</b>: Partial charges(MMFF94) are from PubChem Compound SDF files.<br/><br/>";
            }
            else {
                footHtml += "<b>Note</b>: Only the selected residues are used for <a href='http://honig.c2b2.columbia.edu/delphi'>DelPhi</a> potential calculation by solving linear Poisson-Boltzmann equation.";

                footHtml += '<div style="width:20px; margin-top:6px; display:inline-block;"><span id="'
                  + me.pre + tabName + '_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="'
                  + me.pre + tabName + '_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div><br>';
                footHtml += me.htmlCls.divStr + tabName + "' style='display:none;'>";

                footHtml += "<br>The hydrogens and partial charges of proteins and nucleotides are added using <a href='http://compbio.clemson.edu/pka_webserver'>DelPhiPKa</a> with the Amber charge and size files. The hydrogens of ligands are added using <a href='http://openbabel.org/wiki/Main_Page'>Open Babel</a>. The partial charges of ligands are calculated using <a href='http://ambermd.org/antechamber/ac.html'>Antechamber</a> with the Gasteiger charge method. All partial charges are calculated at pH 7.<br/><br/>";

                footHtml += "Lipids are treated as ligands. Please use \"HETATM\" instead of \"ATOM  \" for each lipid atom in your PDB file. Each phosphate in lipids is assigned with a charge of -1. You can download PQR and modify it, or prepare your PQR file using other tools. Then load the PQR file at the menu \"Analysis > Load PQR/Potential\".<br/><br/>";

                footHtml += "</div>";
            }
        }
        else {
            footHtml += "<b>Note</b>: Always load a PDB file before loading a PQR or DelPhi potential file.";

            footHtml += '<div style="width:20px; margin-top:6px; display:inline-block;"><span id="'
              + me.pre + tabName + '_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="'
              + me.pre + tabName + '_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div><br>';
            footHtml += me.htmlCls.divStr + tabName + "' style='display:none;'>";

            footHtml += "The PDB file can be loaded in the URL with \"pdbid=\" or at \"File > Open File\". The PQR file can be prepared at the menu \"Analysis > Download PQR\" with your modification or using other tools. The DelPhi potential file can be calculated at <a href='http://compbio.clemson.edu/sapp/delphi_webserver/'>DelPhi Web Server</a> and be exported as a Cube file. ";

            if(type == 'url') footHtml += "The PQR or potential file can be accessed in a URL if it is located in the same host as iCn3D.";

            footHtml += "<br/><br/>";

            footHtml += "</div>";
        }
        footHtml += "</div>";

        return footHtml;
    }

    getPotentialHtml(type, dialogClass) { let me = this.icn3dui; me.icn3d;
        let html = '';

        let name0, name1, name2;
        let tab1, tab2;
        tab1 = 'Equipotential Map';
        tab2 = 'Surface with Potential';
        //tab3 = 'Download PQR';

        if(type == 'delphi') {
            name1 = 'delphi';
        }
        else if(type == 'local') {
            name0 = 'pqr';
            name1 = 'phi';
            name2 = 'cube';
        }
        else if(type == 'url') {
            name0 = 'pqrurl';
            name1 = 'phiurl';
            name2 = 'cubeurl';
        }

        html += me.htmlCls.divStr + "dl_" + name1 + "' class='" + dialogClass + "'>";
        html += me.htmlCls.divStr + "dl_" + name1 + "_tabs' style='border:0px;'>";
        html += "<ul>";
        html += "<li><a href='#" + me.pre + name1 + "tab1'>" + tab1 + "</a></li>";
        html += "<li><a href='#" + me.pre + name1 + "tab2'>" + tab2 + "</a></li>";
        //html += "<li><a href='#" + me.pre + name1 + "tab3'>" + tab3 + "</a></li>";
        html += "</ul>";

        html += me.htmlCls.divStr + name1 + "tab1'>";
        if(type == 'delphi') html += this.addGsizeSalt(name1) + "<br>";

        html += "<span style='white-space:nowrap;font-weight:bold;'>Potential contour at: <select id='" + me.pre + name1 + "contour'>";

        let optArray1b = ['0.5', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
        html += this.getOptionHtml(optArray1b, 2);

        html += "</select> kT/e(25.6mV at 298K)</span><br/><br/>";

        let htmlTmp;

        // tab1: equipotential map
        if(type == 'delphi') {
            html += me.htmlCls.buttonStr + "reload_" + name1 + "file' style='margin-top: 6px;'>Equipotential Map</button>";
            html += me.htmlCls.buttonStr + name1 + "mapNo' style='margin-left:30px;'>Remove Map</button><br>";
        }
        else if(type == 'local') {
            html += me.htmlCls.divStr + name1 + "tab1_tabs' style='border:0px;'>";
            html += "<ul>";
            html += "<li><a href='#" + me.pre + name1 + "tab1_" + name0 + "'>PQR</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab1_" + name1 + "'>Phi</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab1_" + name2 + "'>Cube</a></li>";
            html += "</ul>";

            htmlTmp = "<span style='margin-left:30px'>" + me.htmlCls.buttonStr + name1 + "mapNo'>Remove Map</button></span></div>";

            html += me.htmlCls.divStr + name1 + "tab1_" + name0 + "'>";
            html += this.addGsizeSalt(name0) + "<br>";
            html += "<b>PQR File</b>: " + me.htmlCls.inputFileStr + "id='" + me.pre + name0 + "file'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name0 + "file' style='margin-top: 6px;'>Equipotential Map</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab1_" + name1 + "'>";
            html += "<b>Phi File</b>: " + me.htmlCls.inputFileStr + "id='" + me.pre + name1 + "file'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name1 + "file' style='margin-top: 6px;'>Equipotential Map</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab1_" + name2 + "'>";
            html += "<b>Cube File</b>: " + me.htmlCls.inputFileStr + "id='" + me.pre + name2 + "file'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name2 + "file' style='margin-top: 6px;'>Equipotential Map</button>" + htmlTmp;

            html += "</div>";
        }
        else if(type == 'url') {
            html += me.htmlCls.divStr + name1 + "tab1_tabs' style='border:0px;'>";
            html += "<ul>";
            html += "<li><a href='#" + me.pre + name1 + "tab1_" + name0 + "2'>PQR</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab1_" + name1 + "2'>Phi</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab1_" + name2 + "2'>Cube</a></li>";
            html += "</ul>";

            htmlTmp = "<span style='margin-left:30px'>" + me.htmlCls.buttonStr + name1 + "mapNo'>Remove Map</button></span></div>";

            html += me.htmlCls.divStr + name1 + "tab1_" + name0 + "2'>";
            html += this.addGsizeSalt(name0) + "<br>";
            html += "<b>PQR URL</b> in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + name0 + "file'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name0 + "file' style='margin-top: 6px;'>Equipotential Map</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab1_" + name1 + "2'>";
            html += "<b>Phi URL</b> in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + name1 + "file'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name1 + "file' style='margin-top: 6px;'>Equipotential Map</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab1_" + name2 + "2'>";
            html += "<b>Cube URL</b> in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + name2 + "file'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name2 + "file' style='margin-top: 6px;'>Equipotential Map</button>" + htmlTmp;

            html += "</div>";
        }

        html += "<br>" + this.getFootHtml(type, name1 + "tab1_foot");
        html += "</div>";

        html += me.htmlCls.divStr + name1 + "tab2'>";
        if(type == 'delphi') html += this.addGsizeSalt(name1) + "<br>";

        html += "<span style='white-space:nowrap;font-weight:bold;'>Surface with max potential at: <select id='" + me.pre + name1 + "contour2'>";

        let optArray1c = ['0.5', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
        html += this.getOptionHtml(optArray1c, 2);

        html += "</select> kT/e(25.6mV at 298K)</span><br/><br/>";

        html += "<b>Surface</b>: <select id='" + me.pre + name1 + "surftype'>";
        html += "<option value='21'>Van der Waals</option>";
        html += "<option value='22' selected>Molecular Surface</option>";
        html += "<option value='23'>Solvent Accessible</option>";
        html += "</select>";

        html += "<span style='margin-left:20px'><b>Opacity</b>: <select id='" + me.pre + name1 + "surfop'>";
        let surfOp = ['1.0', '0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.1'];
        html += this.getOptionHtml(surfOp, 0);
        html += "</select></span>";

        html += "<span style='margin-left:20px'><b>Wireframe</b>: <select id='" + me.pre + name1 + "surfwf'>";
        html += "<option value='yes'>Yes</option>";
        html += "<option value='no' selected>No</option>";
        html += "</select></span><br/>";

        html += "<br/>";

        // tab2: surface with potential
        if(type == 'delphi') {
            html += me.htmlCls.buttonStr + "reload_" + name1 + "file2' style='margin-top: 6px;'>Surface with Potential</button>";
            html += me.htmlCls.buttonStr + name1 + "mapNo2' style='margin-left:30px;'>Remove Surface</button><br>";
        }
        else if(type == 'local') {
            html += me.htmlCls.divStr + name1 + "tab2_tabs' style='border:0px;'>";
            html += "<ul>";
            html += "<li><a href='#" + me.pre + name1 + "tab2_" + name0 + "'>PQR</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab2_" + name1 + "'>Phi</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab2_" + name2 + "'>Cube</a></li>";
            html += "</ul>";

            htmlTmp = "<span style='margin-left:30px'>" + me.htmlCls.buttonStr + name1 + "mapNo2'>Remove Surface</button></span></div>";

            html += me.htmlCls.divStr + name1 + "tab2_" + name0 + "'>";
            html += this.addGsizeSalt(name0 + "2") + "<br>";
            html += "<b>PQR File</b>: " + me.htmlCls.inputFileStr + "id='" + me.pre + name0 + "file2'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name0 + "file2' style='margin-top: 6px;'>Surface with Potential</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab2_" + name1 + "'>";
            html += "<b>Phi File</b>: " + me.htmlCls.inputFileStr + "id='" + me.pre + name1 + "file2'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name1 + "file2' style='margin-top: 6px;'>Surface with Potential</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab2_" + name2 + "'>";
            html += "<b>Cube File</b>: " + me.htmlCls.inputFileStr + "id='" + me.pre + name2 + "file2'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name2 + "file2' style='margin-top: 6px;'>Surface with Potential</button>" + htmlTmp;

            html += "</div>";
        }
        else if(type == 'url') {
            html += me.htmlCls.divStr + name1 + "tab2_tabs' style='border:0px;'>";
            html += "<ul>";
            html += "<li><a href='#" + me.pre + name1 + "tab2_" + name0 + "2'>PQR</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab2_" + name1 + "2'>Phi</a></li>";
            html += "<li><a href='#" + me.pre + name1 + "tab2_" + name2 + "2'>Cube</a></li>";
            html += "</ul>";

            htmlTmp = "<span style='margin-left:30px'>" + me.htmlCls.buttonStr + name1 + "mapNo2'>Remove Surface</button></span></div>";

            html += me.htmlCls.divStr + name1 + "tab2_" + name0 + "2'>";
            html += this.addGsizeSalt(name0 + "2") + "<br>";
            html += "<b>PQR URL</b> in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + name0 + "file2'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name0 + "file2' style='margin-top: 6px;'>Surface with Potential</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab2_" + name1 + "2'>";
            html += "<b>Phi URL</b> in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + name1 + "file2'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name1 + "file2' style='margin-top: 6px;'>Surface with Potential</button>" + htmlTmp;

            html += me.htmlCls.divStr + name1 + "tab2_" + name2 + "2'>";
            html += "<b>Cube URL</b> in the same host: " + me.htmlCls.inputTextStr + "id='" + me.pre + name2 + "file2'> <br><br>" + me.htmlCls.buttonStr + "reload_" + name2 + "file2' style='margin-top: 6px;'>Surface with Potential</button>" + htmlTmp;

            html += "</div>";
        }

        html += "<br>" + this.getFootHtml(type, name1 + "tab2_foot");
        html += "</div>";

        html += "</div>";
        html += "</div>";

        return html;
    }

    async exportPqr(bPdb) { let me = this.icn3dui, ic = me.icn3d;
       let ionHash = {};
       let atomHash = {};

       let atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);
       for(let i in atoms) {
           ic.atoms[i];

           if(ic.ions.hasOwnProperty(i)) {
             ionHash[i] = 1;
           }
           else {
             atomHash[i] = 1;
           }
       }

       let fileExt = (bPdb) ? 'pdb' : 'pqr';
       if(me.cfg.cid) {
          let pqrStr = '';
          
          let bPqr = (bPdb) ? false : true;
          pqrStr += ic.saveFileCls.getAtomPDB(atomHash, bPqr) + ic.saveFileCls.getAtomPDB(ionHash, bPqr);

          let file_pref =(ic.inputid) ? ic.inputid : "custom";
          ic.saveFileCls.saveFile(file_pref + '_icn3d.' + fileExt, 'text', [pqrStr]);
       }
       else {
            let bCalphaOnly = me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms));
            if(bCalphaOnly) {
                alert("The potential will not be shown because the side chains are missing in the structure...");
                return;
            }

            let pdbstr = '';

            pdbstr += ic.saveFileCls.getAtomPDB(atomHash);
            pdbstr += ic.saveFileCls.getAtomPDB(ionHash, true, undefined, true);

            let url = "https://www.ncbi.nlm.nih.gov/Structure/delphi/delphi.fcgi";

            let pdbid =(me.cfg.cid) ? me.cfg.cid : Object.keys(ic.structures).toString();

            let dataObj = {'pdb2pqr': pdbstr, 'pdbid': pdbid};
            let data = await me.getAjaxPostPromise(url, dataObj, true, undefined, undefined, true, 'text');

            let pqrStr = data;

            if(bPdb) {
            let lineArray = pqrStr.split('\n');

            let pdbStr = '';
            for(let i = 0, il = lineArray.length; i < il; ++i) {
                let line = lineArray[i];
                if(line.substr(0, 6) == 'ATOM  ' || line.substr(0, 6) == 'HETATM') {
                    let atomName = line.substr(12, 4).trim();
                    let elem;
                    if(line.substr(0, 6) == 'ATOM  ') {
                        elem = atomName.substr(0, 1);
                    }
                    else {
                        let twochar = atomName.substr(0, 2);
                        if(me.parasCls.vdwRadii.hasOwnProperty(twochar)) {
                            elem = twochar;
                        }
                        else {
                            elem = atomName.substr(0, 1);
                        }
                    }

                    pdbStr += line.substr(0, 54) + '                      ' + elem.padStart(2, ' ') + '\n';
                }
                else {
                    pdbStr += line + '\n';
                }
            }

            pqrStr = pdbStr;
            }

            let file_pref =(ic.inputid) ? ic.inputid : "custom";
            ic.saveFileCls.saveFile(file_pref + '_icn3d_residues.' + fileExt, 'text', [pqrStr]);
        }
    }

    clickReload_pngimage() { let me = this.icn3dui; me.icn3d;
        if(me.bNode) return;

        let thisClass = this;
        me.myEventCls.onIds("#" + me.pre + "reload_pngimage", "click", function(e) { let ic = me.icn3d;
           e.preventDefault();
           if(!me.cfg.notebook) dialog.dialog( "close" );
           //close all dialog
           if(!me.cfg.notebook) {
               $(".ui-dialog-content").dialog("close");
           }
           else {
               ic.resizeCanvasCls.closeDialogs();
           }
           // initialize icn3dui
           //me.initUI();
           ic.init();
           let file = $("#" + me.pre + "pngimage")[0].files[0];
           if(!file) {
             alert("Please select a file before clicking 'Load'");
           }
           else {
             thisClass.fileSupport();
             let reader = new FileReader();
             reader.onload = async function(e) {
               let imageStr = e.target.result; // or = reader.result;
               await thisClass.loadPng(imageStr);
             };
             reader.readAsText(file);
           }
        });
    }

    async loadPng(imageStr, command) { let me = this.icn3dui, ic = me.icn3d;
       let matchedStr = 'Share Link: ';
       let pos = imageStr.indexOf(matchedStr);
       let matchedStrState = "Start of state file======\n";
       let posState = imageStr.indexOf(matchedStrState);
       if(pos == -1 && posState == -1) {
           alert('Please load a PNG image saved by clicking "Save Datas > PNG Image" in the Data menu...');
       }
       else if(pos != -1) {
           let url = imageStr.substr(pos + matchedStr.length);
           me.htmlCls.clickMenuCls.setLogCmd('load iCn3D PNG image ' + $("#" + me.pre + "pngimage").val(), false);
           window.open(url);
       }
       else if(posState != -1) {
           let matchedStrData = "Start of data file======\n";
           let posData = imageStr.indexOf(matchedStrData);
           ic.bInputfile =(posData == -1) ? false : true;
           let commandStr = (command) ? command.replace(/;/g, "\n") : '';

           if(ic.bInputfile) {
               let posDataEnd = imageStr.indexOf("End of data file======\n");
               let data = imageStr.substr(posData + matchedStrData.length, posDataEnd - posData - matchedStrData.length);
               ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + data : data;

               let matchedStrType = "Start of type file======\n";
               let posType = imageStr.indexOf(matchedStrType);
               let posTypeEnd = imageStr.indexOf("End of type file======\n");
               let type = imageStr.substr(posType + matchedStrType.length, posTypeEnd - posType - matchedStrType.length - 1); // remove the new line char
               ic.InputfileType = type;

               //var matchedStrState = "Start of state file======\n";
               //var posState = imageStr.indexOf(matchedStrState);
               let posStateEnd = imageStr.indexOf("End of state file======\n");
               let statefile = imageStr.substr(posState + matchedStrState.length, posStateEnd - posState- matchedStrState.length);
               //statefile = decodeURIComponent(statefile);
               statefile = decodeURIComponent(statefile + "\n" + commandStr);

                if(type === 'pdb') {
                    await ic.pdbParserCls.loadPdbData(data);

                    ic.commands = [];
                    ic.optsHistory = [];
                    await ic.loadScriptCls.loadScript(statefile, true);
                }
                else {
                    if(type === 'mol2') {
                        await ic.mol2ParserCls.loadMol2Data(data);
                    }
                    else if(type === 'sdf') {
                        await ic.sdfParserCls.loadSdfData(data);
                    }
                    else if(type === 'xyz') {
                        await ic.xyzParserCls.loadXyzData(data);
                    }
                    else if(type === 'mmcif') {
                        await ic.mmcifParserCls.loadMmcifData(data);
                    }
                   ic.commands = [];
                   ic.optsHistory = [];
                   await ic.loadScriptCls.loadScript(statefile, true);
               }
           }
           else { // url length > 4000
               //var matchedStrState = "Start of state file======\n";
               //var posState = imageStr.indexOf(matchedStrState);
               let posStateEnd = imageStr.indexOf("End of state file======\n");
               let statefile = imageStr.substr(posState + matchedStrState.length, posStateEnd - posState- matchedStrState.length);
               //statefile = decodeURIComponent(statefile);
               statefile = decodeURIComponent(statefile + "\n" + commandStr);

               ic.commands = [];
               ic.optsHistory = [];
               await  ic.loadScriptCls.loadScript(statefile, true);
           }
           me.htmlCls.clickMenuCls.setLogCmd('load iCn3D PNG image ' + $("#" + me.pre + "pngimage").val(), false);
       }
    }

    fileSupport() {
         if(!window.File || !window.FileReader || !window.FileList || !window.Blob) {
            alert('The File APIs are not fully supported in this browser.');
         }
    }

    getLinkColor() {
        let graphStr2 = '';
        graphStr2 += ', linkmap: {\n';
        graphStr2 += '3: {"type": "peptidebond", "c":""},\n';
        graphStr2 += '4: {"type": "ssbond", "c":"FFA500"},\n';
        graphStr2 += '5: {"type": "ionic", "c":"0FF"},\n';
        graphStr2 += '6: {"type": "ionicInside", "c":"FFF"},\n';
        graphStr2 += '11: {"type": "contact", "c":"888"},\n';
        graphStr2 += '12: {"type": "contactInside", "c":"FFF"},\n';
        graphStr2 += '13: {"type": "hbond", "c":"0F0"},\n';
        graphStr2 += '14: {"type": "hbondInside", "c":"FFF"},\n';
        graphStr2 += '15: {"type": "clbond", "c":"006400"},\n';
        graphStr2 += '17: {"type": "halogen", "c":"F0F"},\n';
        graphStr2 += '18: {"type": "halogenInside", "c":"FFF"},\n';
        graphStr2 += '19: {"type": "pication", "c":"F00"},\n';
        graphStr2 += '20: {"type": "picationInside", "c":"FFF"},\n';
        graphStr2 += '21: {"type": "pistacking", "c":"00F"},\n';
        graphStr2 += '22: {"type": "pistackingInside", "c":"FFF"}\n';
        graphStr2 += '}}\n';

        return graphStr2;
    }

    setCookieForThickness() { let me = this.icn3dui, ic = me.icn3d;
        if(!me.bNode) { // && postfix == 'style') {
            let exdays = 3650; // 10 years

            this.setCookie('lineRadius', ic.lineRadius, exdays);
            this.setCookie('coilWidth', ic.coilWidth, exdays);
            this.setCookie('cylinderRadius', ic.cylinderRadius, exdays);
            this.setCookie('crosslinkRadius', ic.crosslinkRadius, exdays);
            this.setCookie('traceRadius', ic.traceRadius, exdays);
            this.setCookie('dotSphereScale', ic.dotSphereScale, exdays);
            this.setCookie('ribbonthickness', ic.ribbonthickness, exdays);
            this.setCookie('helixSheetWidth', ic.helixSheetWidth, exdays);
            this.setCookie('nucleicAcidWidth', ic.nucleicAcidWidth, exdays);
        }
    }

    setLineThickness(postfix, bReset) { let me = this.icn3dui, ic = me.icn3d;
        ic.bSetThickness = true;

        if(postfix == 'style') {
            if(bReset) {
                $("#" + me.pre + "shininess").val('40');
                $("#" + me.pre + "light1").val('0.6');
                $("#" + me.pre + "light2").val('0.4');
                $("#" + me.pre + "light3").val('0.2');
                $("#" + me.pre + "glycan").val('0');
                $("#" + me.pre + "membrane").val('1');
                $("#" + me.pre + "cmdwindow").val('0');
            }

            ic.shininess = parseFloat($("#" + me.pre + "shininess").val()); //40;
            ic.light1 = parseFloat($("#" + me.pre + "light1").val()); //0.6;
            ic.light2 = parseFloat($("#" + me.pre + "light2").val()); //0.4;
            ic.light3 = parseFloat($("#" + me.pre + "light3").val()); //0.2;
            ic.bGlycansCartoon = parseInt($("#" + me.pre + "glycan").val()); //0;
            ic.bMembrane = parseInt($("#" + me.pre + "membrane").val()); //1;
            ic.bCmdWindow = parseInt($("#" + me.pre + "cmdwindow").val()); //0;
        }

        if(bReset) {
            $("#" + me.pre + "linerad_" + postfix ).val(0.1); //0.1; // hbonds, distance lines
            $("#" + me.pre + "coilrad_" + postfix ).val(0.3); //0.3; // style cartoon-coil
            $("#" + me.pre + "stickrad_" + postfix ).val(0.4); //0.4; // style stick
            $("#" + me.pre + "crosslinkrad_" + postfix ).val(0.4); //0.4; // cross-linkage
            $("#" + me.pre + "tracerad_" + postfix ).val(0.4); //0.4; // style c alpha trace, nucleotide stick
            $("#" + me.pre + "ballscale_" + postfix ).val(0.3); //0.3; // style ball and stick, dot
            $("#" + me.pre + "ribbonthick_" + postfix ).val(0.2); //0.2; // style ribbon, nucleotide cartoon, stand thickness
            $("#" + me.pre + "prtribbonwidth_" + postfix ).val(1.3); //1.3; // style ribbon, stand thickness
            $("#" + me.pre + "nucleotideribbonwidth_" + postfix ).val(0.8); //0.8; // nucleotide cartoon
        }

        ic.lineRadius = parseFloat($("#" + me.pre + "linerad_" + postfix ).val()); //0.1; // hbonds, distance lines
        ic.coilWidth = parseFloat($("#" + me.pre + "coilrad_" + postfix ).val()); //0.4; // style cartoon-coil
        ic.cylinderRadius = parseFloat($("#" + me.pre + "stickrad_" + postfix ).val()); //0.4; // style stick
        ic.crosslinkRadius = parseFloat($("#" + me.pre + "crosslinkrad_" + postfix ).val()); //0.4; // cross-linkage
        ic.traceRadius = parseFloat($("#" + me.pre + "tracerad_" + postfix ).val()); //0.4; // style c alpha trace, nucleotide stick
        ic.dotSphereScale = parseFloat($("#" + me.pre + "ballscale_" + postfix ).val()); //0.3; // style ball and stick, dot
        ic.ribbonthickness = parseFloat($("#" + me.pre + "ribbonthick_" + postfix ).val()); //0.4; // style ribbon, nucleotide cartoon, stand thickness
        ic.helixSheetWidth = parseFloat($("#" + me.pre + "prtribbonwidth_" + postfix ).val()); //1.3; // style ribbon, stand thickness
        ic.nucleicAcidWidth = parseFloat($("#" + me.pre + "nucleotideribbonwidth_" + postfix ).val()); //0.8; // nucleotide cartoon

        // save to cache
        if(!me.bNode) { // && postfix == 'style') {
            let exdays = 3650; // 10 years
            this.setCookie('shininess', ic.shininess, exdays);
            this.setCookie('light1', ic.light1, exdays);
            this.setCookie('light2', ic.light2, exdays);
            this.setCookie('light3', ic.light3, exdays);
            this.setCookie('glycan', ic.bGlycansCartoon, exdays);
            this.setCookie('membrane', ic.bMembrane, exdays);
            this.setCookie('cmdwindow', ic.bCmdWindow, exdays);
        }

        this.setCookieForThickness();

        if(postfix = bReset) {
           let select = "reset thickness";
           me.htmlCls.clickMenuCls.setLogCmd(select, true);
           ic.bSetThickness = false;
           ic.threeDPrintCls.resetAfter3Dprint();
        }
        else {
            me.htmlCls.clickMenuCls.setLogCmd('set thickness | linerad ' + ic.lineRadius + ' | coilrad ' + ic.coilWidth + ' | stickrad ' + ic.cylinderRadius + ' | crosslinkrad ' + ic.crosslinkRadius + ' | tracerad ' + ic.traceRadius + ' | ribbonthick ' + ic.ribbonthickness + ' | proteinwidth ' + ic.helixSheetWidth + ' | nucleotidewidth ' + ic.nucleicAcidWidth  + ' | ballscale ' + ic.dotSphereScale, true);

            me.htmlCls.clickMenuCls.setLogCmd('set glycan ' + ic.bGlycansCartoon, true);
            me.htmlCls.clickMenuCls.setLogCmd('set membrane ' + ic.bMembrane, true);
            me.htmlCls.clickMenuCls.setLogCmd('set cmdwindow ' + ic.bCmdWindow, true);
        }

        ic.drawCls.draw();
    }

    setCookie(cname, cvalue, exdays) {
      let d = new Date();
      d.setTime(d.getTime() + (exdays*24*60*60*1000));
      let expires = "expires="+ d.toUTCString();
      document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
    }

    updateSurfPara(type) { let me = this.icn3dui, ic = me.icn3d;
       ic.phisurftype = $("#" + me.pre + type + "surftype").val();
       ic.phisurfop = $("#" + me.pre + type + "surfop").val();
       ic.phisurfwf = $("#" + me.pre + type + "surfwf").val();
    }

    exportPdb() { let me = this.icn3dui, ic = me.icn3d;
       let pdbStr = '';
///       pdbStr += ic.saveFileCls.getPDBHeader();
       let atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);
       pdbStr += ic.saveFileCls.getAtomPDB(atoms);

       let file_pref =(ic.inputid) ? ic.inputid : "custom";
       ic.saveFileCls.saveFile(file_pref + '_icn3d.pdb', 'text', [pdbStr]);
    }

    exportSecondary() { let me = this.icn3dui, ic = me.icn3d;
       let secondaryStr = '';
       let atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);
       secondaryStr += ic.saveFileCls.getSecondary(atoms);

       let file_pref =(ic.inputid) ? ic.inputid : "custom";
       ic.saveFileCls.saveFile(file_pref + '_icn3d_ss.txt', 'text', [secondaryStr]);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Html {
  constructor(icn3dui) { let me = icn3dui;
    this.icn3dui = icn3dui;

    this.cfg = this.icn3dui.cfg;

    this.opts = {};
    this.opts['background']         = 'black';        //transparent, black, grey, white

    this.allMenus = {};
    this.allMenusSel= {}; // Selectable menus
    this.simpleMenus = {};
    this.shownMenus = {};

    this.WIDTH = 400; // total width of view area
    this.HEIGHT = 400; // total height of view area
    this.RESIDUE_WIDTH = 10;  // sequences
    if(me.utilsCls.isMobile() || this.cfg.mobilemenu) {
        this.MENU_HEIGHT = 0;
    }
    else {
        this.MENU_HEIGHT = 40;
    }
    this.LOG_HEIGHT = 65; //65;

    // used to set the position for the log/command textarea
    this.MENU_WIDTH = 750;
    //The width (in px) that was left empty by the 3D viewer. The default is 20px.
    this.LESSWIDTH = 20;
    this.LESSWIDTH_RESIZE = 20;
    //The height (in px) that was left empty by the 3D viewer. The default is 20px.
    this.LESSHEIGHT = 20;

    // size of 2D cartoons
    this.width2d = 200;

    this.CMD_HEIGHT = 0.8*this.LOG_HEIGHT;
    //this.EXTRAHEIGHT = 2*this.MENU_HEIGHT + this.CMD_HEIGHT;
    this.EXTRAHEIGHT = this.MENU_HEIGHT + this.CMD_HEIGHT;
    if(this.cfg.showmenu != undefined && this.cfg.showmenu == false) {
        //this.EXTRAHEIGHT -= 2*this.MENU_HEIGHT;
        this.EXTRAHEIGHT -= this.MENU_HEIGHT;
    }
    if(this.cfg.showcommand != undefined && this.cfg.showcommand == false) {
        this.EXTRAHEIGHT -= this.CMD_HEIGHT;
    }

    this.GREY8 = "#AAAAAA"; //"#888888"; // style protein grey
    this.GREYB = "#CCCCCC"; //"#BBBBBB";
    this.GREYC = "#DDDDDD"; //"#CCCCCC"; // grey background
    this.GREYD = "#EEEEEE"; //"#DDDDDD";
    this.ORANGE = "#FFA500";

    this.themecolor = 'blue';

    // used in graph
    this.defaultValue = 1;
    this.ssValue = 3;
    this.coilValue = 3;
    this.contactValue = 11;
    this.contactInsideValue = 12;
    this.hbondValue = 13;
    this.hbondInsideValue = 14;
    this.ssbondValue = 4;
    this.ionicValue = 5;
    this.ionicInsideValue = 6;
    this.clbondValue = 15;
    this.halogenValue = 17;
    this.halogenInsideValue = 18;
    this.picationValue = 19;
    this.picationInsideValue = 20;
    this.pistackingValue = 21;
    this.pistackingInsideValue = 22;
    this.contactColor = '888';
    this.contactInsideColor = 'FFF'; //'DDD';
    this.hbondColor = '0F0';
    this.hbondInsideColor = 'FFF'; //'AFA';
    this.ssbondColor = 'FFA500';
    this.ionicColor = '0FF';
    this.ionicInsideColor = 'FFF'; //'8FF';
    this.clbondColor = '006400';
    this.halogenColor = 'F0F';
    this.halogenInsideColor = 'FFF';
    this.picationColor = 'F00';
    this.picationInsideColor = 'FFF';
    this.pistackingColor = '00F';
    this.pistackingInsideColor = 'FFF';
    this.hideedges = 1;
    //this.pushcenter = 0;
    this.force = 4;
    this.simulation = undefined;

    //this.baseUrl = "https://structure.ncbi.nlm.nih.gov/";
    this.baseUrl = (window && window.location && window.location.hostname == 'structure.ncbi.nlm.nih.gov') 
        ? "https://structure.ncbi.nlm.nih.gov/Structure/" : "https://www.ncbi.nlm.nih.gov/Structure/";
    this.divStr = "<div id='" + this.icn3dui.pre;
    this.divNowrapStr = "<div style='white-space:nowrap'>";
    this.spanNowrapStr = "<span style='white-space:nowrap'>";
    this.inputTextStr = "<input type='text' ";
    this.inputFileStr = "<input type='file' ";
    this.inputRadioStr = "<input type='radio' ";
    this.inputCheckStr = "<input type='checkbox' ";
    this.optionStr = "<option value=";
    this.buttonStr = "<button id='" + this.icn3dui.pre;
    this.postfix = "2"; // add postfix for the structure of the query protein when align two chains in one protein
    this.space2 = "&nbsp;&nbsp;";
    this.space3 = this.space2 + "&nbsp;";
    this.space4 = this.space2 + this.space2;
    //this.wifiStr = '<i class="icn3d-wifi" title="requires internet">&nbsp;</i>';
    this.wifiStr = '';
    //this.licenseStr = '<i class="icn3d-license" title="requires license">&nbsp;</i>';
    this.licenseStr = '';
    this.closeAc = {collapsible: true, active: false}; // close accordion

    this.clickMenuCls = new ClickMenu(this.icn3dui);
    this.setMenuCls = new SetMenu(this.icn3dui);
    this.dialogCls = new Dialog(this.icn3dui);
    this.setDialogCls = new SetDialog(this.icn3dui);
    this.eventsCls = new Events(this.icn3dui);
    this.alignSeqCls = new AlignSeq(this.icn3dui);
    this.setHtmlCls = new SetHtml(this.icn3dui);
  }
}

/*
import {
    AnimationClip,
    Bone,
    Box3,
    BufferAttribute,
    BufferGeometry,
    ClampToEdgeWrapping,
    Color,
    DirectionalLight,
    DoubleSide,
    FileLoader,
    FrontSide,
    Group,
    ImageBitmapLoader,
    InterleavedBuffer,
    InterleavedBufferAttribute,
    Interpolant,
    InterpolateDiscrete,
    InterpolateLinear,
    Line,
    LineBasicMaterial,
    LineLoop,
    LineSegments,
    LinearFilter,
    LinearMipmapLinearFilter,
    LinearMipmapNearestFilter,
    Loader,
    LoaderUtils,
    Material,
    MathUtils,
    Matrix4,
    Mesh,
    MeshBasicMaterial,
    MeshPhysicalMaterial,
    MeshStandardMaterial,
    MirroredRepeatWrapping,
    NearestFilter,
    NearestMipmapLinearFilter,
    NearestMipmapNearestFilter,
    NumberKeyframeTrack,
    Object3D,
    OrthographicCamera,
    PerspectiveCamera,
    PointLight,
    Points,
    PointsMaterial,
    PropertyBinding,
    Quaternion,
    QuaternionKeyframeTrack,
    RepeatWrapping,
    Skeleton,
    SkinnedMesh,
    Sphere,
    SpotLight,
    TangentSpaceNormalMap,
    Texture,
    TextureLoader,
    TriangleFanDrawMode,
    TriangleStripDrawMode,
    Vector2,
    Vector3,
    VectorKeyframeTrack,
    sRGBEncoding
} from 'three';
*/

class GLTFLoader extends THREE.Loader {

    constructor( manager ) {

        super( manager );

        this.dracoLoader = null;
        this.ktx2Loader = null;
        this.meshoptDecoder = null;

        this.pluginCallbacks = [];

        this.register( function ( parser ) {

            return new GLTFMaterialsClearcoatExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFTextureBasisUExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFTextureWebPExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFMaterialsSheenExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFMaterialsTransmissionExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFMaterialsVolumeExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFMaterialsIorExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFMaterialsSpecularExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFLightsExtension( parser );

        } );

        this.register( function ( parser ) {

            return new GLTFMeshoptCompression( parser );

        } );

    }

    load( url, onLoad, onProgress, onError ) {

        const scope = this;

        let resourcePath;

        if ( this.resourcePath !== '' ) {

            resourcePath = this.resourcePath;

        } else if ( this.path !== '' ) {

            resourcePath = this.path;

        } else {

            resourcePath = LoaderUtils.extractUrlBase( url );

        }

        // Tells the LoadingManager to track an extra item, which resolves after
        // the model is fully loaded. This means the count of items loaded will
        // be incorrect, but ensures manager.onLoad() does not fire early.
        this.manager.itemStart( url );

        const _onError = function ( e ) {

            if ( onError ) {

                onError( e );

            } else {

                console.error( e );

            }

            scope.manager.itemError( url );
            scope.manager.itemEnd( url );

        };

        const loader = new FileLoader( this.manager );

        loader.setPath( this.path );
        loader.setResponseType( 'arraybuffer' );
        loader.setRequestHeader( this.requestHeader );
        loader.setWithCredentials( this.withCredentials );

        loader.load( url, function ( data ) {

            try {

                scope.parse( data, resourcePath, function ( gltf ) {

                    onLoad( gltf );

                    scope.manager.itemEnd( url );

                }, _onError );

            } catch ( e ) {

                _onError( e );

            }

        }, onProgress, _onError );

    }

    setDRACOLoader( dracoLoader ) {

        this.dracoLoader = dracoLoader;
        return this;

    }

    setDDSLoader() {

        throw new Error(

            'THREE.GLTFLoader: "MSFT_texture_dds" no longer supported. Please update to "KHR_texture_basisu".'

        );

    }

    setKTX2Loader( ktx2Loader ) {

        this.ktx2Loader = ktx2Loader;
        return this;

    }

    setMeshoptDecoder( meshoptDecoder ) {

        this.meshoptDecoder = meshoptDecoder;
        return this;

    }

    register( callback ) {

        if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) {

            this.pluginCallbacks.push( callback );

        }

        return this;

    }

    unregister( callback ) {

        if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) {

            this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 );

        }

        return this;

    }

    parse( data, path, onLoad, onError ) {

        let content;
        const extensions = {};
        const plugins = {};

        if ( typeof data === 'string' ) {

            content = data;

        } else {

            const magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );

            if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {

                try {

                    extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data );

                } catch ( error ) {

                    if ( onError ) onError( error );
                    return;

                }

                content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content;

            } else {

                content = LoaderUtils.decodeText( new Uint8Array( data ) );

            }

        }

        const json = JSON.parse( content );

        if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) {

            if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) );
            return;

        }

        const parser = new GLTFParser( json, {

            path: path || this.resourcePath || '',
            crossOrigin: this.crossOrigin,
            requestHeader: this.requestHeader,
            manager: this.manager,
            ktx2Loader: this.ktx2Loader,
            meshoptDecoder: this.meshoptDecoder

        } );

        parser.fileLoader.setRequestHeader( this.requestHeader );

        for ( let i = 0; i < this.pluginCallbacks.length; i ++ ) {

            const plugin = this.pluginCallbacks[ i ]( parser );
            plugins[ plugin.name ] = plugin;

            // Workaround to avoid determining as unknown extension
            // in addUnknownExtensionsToUserData().
            // Remove this workaround if we move all the existing
            // extension handlers to plugin system
            extensions[ plugin.name ] = true;

        }

        if ( json.extensionsUsed ) {

            for ( let i = 0; i < json.extensionsUsed.length; ++ i ) {

                const extensionName = json.extensionsUsed[ i ];
                const extensionsRequired = json.extensionsRequired || [];

                switch ( extensionName ) {

                    case EXTENSIONS.KHR_MATERIALS_UNLIT:
                        extensions[ extensionName ] = new GLTFMaterialsUnlitExtension();
                        break;

                    case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS:
                        extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension();
                        break;

                    case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION:
                        extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader );
                        break;

                    case EXTENSIONS.KHR_TEXTURE_TRANSFORM:
                        extensions[ extensionName ] = new GLTFTextureTransformExtension();
                        break;

                    case EXTENSIONS.KHR_MESH_QUANTIZATION:
                        extensions[ extensionName ] = new GLTFMeshQuantizationExtension();
                        break;

                    default:

                        if ( extensionsRequired.indexOf( extensionName ) >= 0 && plugins[ extensionName ] === undefined ) {

                            console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' );

                        }

                }

            }

        }

        parser.setExtensions( extensions );
        parser.setPlugins( plugins );
        parser.parse( onLoad, onError );

    }

    parseAsync( data, path ) {

        const scope = this;

        return new Promise( function ( resolve, reject ) {

            scope.parse( data, path, resolve, reject );

        } );

    }

}

/* GLTFREGISTRY */

function GLTFRegistry() {

    let objects = {};

    return  {

        get: function ( key ) {

            return objects[ key ];

        },

        add: function ( key, object ) {

            objects[ key ] = object;

        },

        remove: function ( key ) {

            delete objects[ key ];

        },

        removeAll: function () {

            objects = {};

        }

    };

}

/*********************************/
/********** EXTENSIONS ***********/
/*********************************/

const EXTENSIONS = {
    KHR_BINARY_GLTF: 'KHR_binary_glTF',
    KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
    KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual',
    KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat',
    KHR_MATERIALS_IOR: 'KHR_materials_ior',
    KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
    KHR_MATERIALS_SHEEN: 'KHR_materials_sheen',
    KHR_MATERIALS_SPECULAR: 'KHR_materials_specular',
    KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission',
    KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
    KHR_MATERIALS_VOLUME: 'KHR_materials_volume',
    KHR_TEXTURE_BASISU: 'KHR_texture_basisu',
    KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform',
    KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization',
    EXT_TEXTURE_WEBP: 'EXT_texture_webp',
    EXT_MESHOPT_COMPRESSION: 'EXT_meshopt_compression'
};

/**
 * Punctual Lights Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
 */
class GLTFLightsExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL;

        // Object3D instance caches
        this.cache = { refs: {}, uses: {} };

    }

    _markDefs() {

        const parser = this.parser;
        const nodeDefs = this.parser.json.nodes || [];

        for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) {

            const nodeDef = nodeDefs[ nodeIndex ];

            if ( nodeDef.extensions
                    && nodeDef.extensions[ this.name ]
                    && nodeDef.extensions[ this.name ].light !== undefined ) {

                parser._addNodeRef( this.cache, nodeDef.extensions[ this.name ].light );

            }

        }

    }

    _loadLight( lightIndex ) {

        const parser = this.parser;
        const cacheKey = 'light:' + lightIndex;
        let dependency = parser.cache.get( cacheKey );

        if ( dependency ) return dependency;

        const json = parser.json;
        const extensions = ( json.extensions && json.extensions[ this.name ] ) || {};
        const lightDefs = extensions.lights || [];
        const lightDef = lightDefs[ lightIndex ];
        let lightNode;

        const color = new Color( 0xffffff );

        if ( lightDef.color !== undefined ) color.fromArray( lightDef.color );

        const range = lightDef.range !== undefined ? lightDef.range : 0;

        switch ( lightDef.type ) {

            case 'directional':
                lightNode = new DirectionalLight( color );
                lightNode.target.position.set( 0, 0, - 1 );
                lightNode.add( lightNode.target );
                break;

            case 'point':
                lightNode = new PointLight( color );
                lightNode.distance = range;
                break;

            case 'spot':
                lightNode = new SpotLight( color );
                lightNode.distance = range;
                // Handle spotlight properties.
                lightDef.spot = lightDef.spot || {};
                lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0;
                lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0;
                lightNode.angle = lightDef.spot.outerConeAngle;
                lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle;
                lightNode.target.position.set( 0, 0, - 1 );
                lightNode.add( lightNode.target );
                break;

            default:
                throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type );

        }

        // Some lights (e.g. spot) default to a position other than the origin. Reset the position
        // here, because node-level parsing will only override position if explicitly specified.
        lightNode.position.set( 0, 0, 0 );

        lightNode.decay = 2;

        if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity;

        lightNode.name = parser.createUniqueName( lightDef.name || ( 'light_' + lightIndex ) );

        dependency = Promise.resolve( lightNode );

        parser.cache.add( cacheKey, dependency );

        return dependency;

    }

    createNodeAttachment( nodeIndex ) {

        const self = this;
        const parser = this.parser;
        const json = parser.json;
        const nodeDef = json.nodes[ nodeIndex ];
        const lightDef = ( nodeDef.extensions && nodeDef.extensions[ this.name ] ) || {};
        const lightIndex = lightDef.light;

        if ( lightIndex === undefined ) return null;

        return this._loadLight( lightIndex ).then( function ( light ) {

            return parser._getNodeRef( self.cache, lightIndex, light );

        } );

    }

}

/**
 * Unlit Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit
 */
class GLTFMaterialsUnlitExtension {

    constructor() {

        this.name = EXTENSIONS.KHR_MATERIALS_UNLIT;

    }

    getMaterialType() {

        return MeshBasicMaterial;

    }

    extendParams( materialParams, materialDef, parser ) {

        const pending = [];

        materialParams.color = new Color( 1.0, 1.0, 1.0 );
        materialParams.opacity = 1.0;

        const metallicRoughness = materialDef.pbrMetallicRoughness;

        if ( metallicRoughness ) {

            if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {

                const array = metallicRoughness.baseColorFactor;

                materialParams.color.fromArray( array );
                materialParams.opacity = array[ 3 ];

            }

            if ( metallicRoughness.baseColorTexture !== undefined ) {

                pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture, sRGBEncoding ) );

            }

        }

        return Promise.all( pending );

    }

}

/**
 * Clearcoat Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat
 */
class GLTFMaterialsClearcoatExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT;

    }

    getMaterialType( materialIndex ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

        return MeshPhysicalMaterial;

    }

    extendMaterialParams( materialIndex, materialParams ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

            return Promise.resolve();

        }

        const pending = [];

        const extension = materialDef.extensions[ this.name ];

        if ( extension.clearcoatFactor !== undefined ) {

            materialParams.clearcoat = extension.clearcoatFactor;

        }

        if ( extension.clearcoatTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) );

        }

        if ( extension.clearcoatRoughnessFactor !== undefined ) {

            materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor;

        }

        if ( extension.clearcoatRoughnessTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) );

        }

        if ( extension.clearcoatNormalTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) );

            if ( extension.clearcoatNormalTexture.scale !== undefined ) {

                const scale = extension.clearcoatNormalTexture.scale;

                materialParams.clearcoatNormalScale = new Vector2( scale, scale );

            }

        }

        return Promise.all( pending );

    }

}

/**
 * Sheen Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen
 */
class GLTFMaterialsSheenExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_MATERIALS_SHEEN;

    }

    getMaterialType( materialIndex ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

        return MeshPhysicalMaterial;

    }

    extendMaterialParams( materialIndex, materialParams ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

            return Promise.resolve();

        }

        const pending = [];

        materialParams.sheenColor = new Color( 0, 0, 0 );
        materialParams.sheenRoughness = 0;
        materialParams.sheen = 1;

        const extension = materialDef.extensions[ this.name ];

        if ( extension.sheenColorFactor !== undefined ) {

            materialParams.sheenColor.fromArray( extension.sheenColorFactor );

        }

        if ( extension.sheenRoughnessFactor !== undefined ) {

            materialParams.sheenRoughness = extension.sheenRoughnessFactor;

        }

        if ( extension.sheenColorTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'sheenColorMap', extension.sheenColorTexture, sRGBEncoding ) );

        }

        if ( extension.sheenRoughnessTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'sheenRoughnessMap', extension.sheenRoughnessTexture ) );

        }

        return Promise.all( pending );

    }

}

/**
 * Transmission Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission
 * Draft: https://github.com/KhronosGroup/glTF/pull/1698
 */
class GLTFMaterialsTransmissionExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION;

    }

    getMaterialType( materialIndex ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

        return MeshPhysicalMaterial;

    }

    extendMaterialParams( materialIndex, materialParams ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

            return Promise.resolve();

        }

        const pending = [];

        const extension = materialDef.extensions[ this.name ];

        if ( extension.transmissionFactor !== undefined ) {

            materialParams.transmission = extension.transmissionFactor;

        }

        if ( extension.transmissionTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) );

        }

        return Promise.all( pending );

    }

}

/**
 * Materials Volume Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume
 */
class GLTFMaterialsVolumeExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_MATERIALS_VOLUME;

    }

    getMaterialType( materialIndex ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

        return MeshPhysicalMaterial;

    }

    extendMaterialParams( materialIndex, materialParams ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

            return Promise.resolve();

        }

        const pending = [];

        const extension = materialDef.extensions[ this.name ];

        materialParams.thickness = extension.thicknessFactor !== undefined ? extension.thicknessFactor : 0;

        if ( extension.thicknessTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'thicknessMap', extension.thicknessTexture ) );

        }

        materialParams.attenuationDistance = extension.attenuationDistance || 0;

        const colorArray = extension.attenuationColor || [ 1, 1, 1 ];
        materialParams.attenuationColor = new Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] );

        return Promise.all( pending );

    }

}

/**
 * Materials ior Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior
 */
class GLTFMaterialsIorExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_MATERIALS_IOR;

    }

    getMaterialType( materialIndex ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

        return MeshPhysicalMaterial;

    }

    extendMaterialParams( materialIndex, materialParams ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

            return Promise.resolve();

        }

        const extension = materialDef.extensions[ this.name ];

        materialParams.ior = extension.ior !== undefined ? extension.ior : 1.5;

        return Promise.resolve();

    }

}

/**
 * Materials specular Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular
 */
class GLTFMaterialsSpecularExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_MATERIALS_SPECULAR;

    }

    getMaterialType( materialIndex ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

        return MeshPhysicalMaterial;

    }

    extendMaterialParams( materialIndex, materialParams ) {

        const parser = this.parser;
        const materialDef = parser.json.materials[ materialIndex ];

        if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

            return Promise.resolve();

        }

        const pending = [];

        const extension = materialDef.extensions[ this.name ];

        materialParams.specularIntensity = extension.specularFactor !== undefined ? extension.specularFactor : 1.0;

        if ( extension.specularTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'specularIntensityMap', extension.specularTexture ) );

        }

        const colorArray = extension.specularColorFactor || [ 1, 1, 1 ];
        materialParams.specularColor = new Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] );

        if ( extension.specularColorTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'specularColorMap', extension.specularColorTexture, sRGBEncoding ) );

        }

        return Promise.all( pending );

    }

}

/**
 * BasisU Texture Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu
 */
class GLTFTextureBasisUExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.KHR_TEXTURE_BASISU;

    }

    loadTexture( textureIndex ) {

        const parser = this.parser;
        const json = parser.json;

        const textureDef = json.textures[ textureIndex ];

        if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) {

            return null;

        }

        const extension = textureDef.extensions[ this.name ];
        const loader = parser.options.ktx2Loader;

        if ( ! loader ) {

            if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) {

                throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' );

            } else {

                // Assumes that the extension is optional and that a fallback texture is present
                return null;

            }

        }

        return parser.loadTextureImage( textureIndex, extension.source, loader );

    }

}

/**
 * WebP Texture Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp
 */
class GLTFTextureWebPExtension {

    constructor( parser ) {

        this.parser = parser;
        this.name = EXTENSIONS.EXT_TEXTURE_WEBP;
        this.isSupported = null;

    }

    loadTexture( textureIndex ) {

        const name = this.name;
        const parser = this.parser;
        const json = parser.json;

        const textureDef = json.textures[ textureIndex ];

        if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) {

            return null;

        }

        const extension = textureDef.extensions[ name ];
        const source = json.images[ extension.source ];

        let loader = parser.textureLoader;
        if ( source.uri ) {

            const handler = parser.options.manager.getHandler( source.uri );
            if ( handler !== null ) loader = handler;

        }

        return this.detectSupport().then( function ( isSupported ) {

            if ( isSupported ) return parser.loadTextureImage( textureIndex, extension.source, loader );

            if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) {

                throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' );

            }

            // Fall back to PNG or JPEG.
            return parser.loadTexture( textureIndex );

        } );

    }

    detectSupport() {

        if ( ! this.isSupported ) {

            this.isSupported = new Promise( function ( resolve ) {

                const image = new Image();

                // Lossy test image. Support for lossy images doesn't guarantee support for all
                // WebP images, unfortunately.
                image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA';

                image.onload = image.onerror = function () {

                    resolve( image.height === 1 );

                };

            } );

        }

        return this.isSupported;

    }

}

/**
 * meshopt BufferView Compression Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression
 */
class GLTFMeshoptCompression {

    constructor( parser ) {

        this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION;
        this.parser = parser;

    }

    loadBufferView( index ) {

        const json = this.parser.json;
        const bufferView = json.bufferViews[ index ];

        if ( bufferView.extensions && bufferView.extensions[ this.name ] ) {

            const extensionDef = bufferView.extensions[ this.name ];

            const buffer = this.parser.getDependency( 'buffer', extensionDef.buffer );
            const decoder = this.parser.options.meshoptDecoder;

            if ( ! decoder || ! decoder.supported ) {

                if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) {

                    throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' );

                } else {

                    // Assumes that the extension is optional and that fallback buffer data is present
                    return null;

                }

            }

            return Promise.all( [ buffer, decoder.ready ] ).then( function ( res ) {

                const byteOffset = extensionDef.byteOffset || 0;
                const byteLength = extensionDef.byteLength || 0;

                const count = extensionDef.count;
                const stride = extensionDef.byteStride;

                const result = new ArrayBuffer( count * stride );
                const source = new Uint8Array( res[ 0 ], byteOffset, byteLength );

                decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter );
                return result;

            } );

        } else {

            return null;

        }

    }

}

/* BINARY EXTENSION */
const BINARY_EXTENSION_HEADER_MAGIC = 'glTF';
const BINARY_EXTENSION_HEADER_LENGTH = 12;
const BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };

class GLTFBinaryExtension {

    constructor( data ) {

        this.name = EXTENSIONS.KHR_BINARY_GLTF;
        this.content = null;
        this.body = null;

        const headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH );

        this.header = {
            magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ),
            version: headerView.getUint32( 4, true ),
            length: headerView.getUint32( 8, true )
        };

        if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) {

            throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' );

        } else if ( this.header.version < 2.0 ) {

            throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' );

        }

        const chunkContentsLength = this.header.length - BINARY_EXTENSION_HEADER_LENGTH;
        const chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH );
        let chunkIndex = 0;

        while ( chunkIndex < chunkContentsLength ) {

            const chunkLength = chunkView.getUint32( chunkIndex, true );
            chunkIndex += 4;

            const chunkType = chunkView.getUint32( chunkIndex, true );
            chunkIndex += 4;

            if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) {

                const contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength );
                this.content = LoaderUtils.decodeText( contentArray );

            } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) {

                const byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex;
                this.body = data.slice( byteOffset, byteOffset + chunkLength );

            }

            // Clients must ignore chunks with unknown types.

            chunkIndex += chunkLength;

        }

        if ( this.content === null ) {

            throw new Error( 'THREE.GLTFLoader: JSON content not found.' );

        }

    }

}

/**
 * DRACO Mesh Compression Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression
 */
class GLTFDracoMeshCompressionExtension {

    constructor( json, dracoLoader ) {

        if ( ! dracoLoader ) {

            throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' );

        }

        this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION;
        this.json = json;
        this.dracoLoader = dracoLoader;
        this.dracoLoader.preload();

    }

    decodePrimitive( primitive, parser ) {

        const json = this.json;
        const dracoLoader = this.dracoLoader;
        const bufferViewIndex = primitive.extensions[ this.name ].bufferView;
        const gltfAttributeMap = primitive.extensions[ this.name ].attributes;
        const threeAttributeMap = {};
        const attributeNormalizedMap = {};
        const attributeTypeMap = {};

        for ( const attributeName in gltfAttributeMap ) {

            const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();

            threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ];

        }

        for ( const attributeName in primitive.attributes ) {

            const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();

            if ( gltfAttributeMap[ attributeName ] !== undefined ) {

                const accessorDef = json.accessors[ primitive.attributes[ attributeName ] ];
                const componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];

                attributeTypeMap[ threeAttributeName ] = componentType;
                attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true;

            }

        }

        return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) {

            return new Promise( function ( resolve ) {

                dracoLoader.decodeDracoFile( bufferView, function ( geometry ) {

                    for ( const attributeName in geometry.attributes ) {

                        const attribute = geometry.attributes[ attributeName ];
                        const normalized = attributeNormalizedMap[ attributeName ];

                        if ( normalized !== undefined ) attribute.normalized = normalized;

                    }

                    resolve( geometry );

                }, threeAttributeMap, attributeTypeMap );

            } );

        } );

    }

}

/**
 * Texture Transform Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform
 */
class GLTFTextureTransformExtension {

    constructor() {

        this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM;

    }

    extendTexture( texture, transform ) {

        if ( transform.texCoord !== undefined ) {

            console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' );

        }

        if ( transform.offset === undefined && transform.rotation === undefined && transform.scale === undefined ) {

            // See https://github.com/mrdoob/three.js/issues/21819.
            return texture;

        }

        texture = texture.clone();

        if ( transform.offset !== undefined ) {

            texture.offset.fromArray( transform.offset );

        }

        if ( transform.rotation !== undefined ) {

            texture.rotation = transform.rotation;

        }

        if ( transform.scale !== undefined ) {

            texture.repeat.fromArray( transform.scale );

        }

        texture.needsUpdate = true;

        return texture;

    }

}

/**
 * Specular-Glossiness Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness
 */

/**
 * A sub class of StandardMaterial with some of the functionality
 * changed via the `onBeforeCompile` callback
 * @pailhead
 */
class GLTFMeshStandardSGMaterial extends THREE.MeshStandardMaterial {

    constructor( params ) {

        super();

        this.isGLTFSpecularGlossinessMaterial = true;

        //various chunks that need replacing
        const specularMapParsFragmentChunk = [
            '#ifdef USE_SPECULARMAP',
            '   uniform sampler2D specularMap;',
            '#endif'
        ].join( '\n' );

        const glossinessMapParsFragmentChunk = [
            '#ifdef USE_GLOSSINESSMAP',
            '   uniform sampler2D glossinessMap;',
            '#endif'
        ].join( '\n' );

        const specularMapFragmentChunk = [
            'vec3 specularFactor = specular;',
            '#ifdef USE_SPECULARMAP',
            '   vec4 texelSpecular = texture2D( specularMap, vUv );',
            '   // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture',
            '   specularFactor *= texelSpecular.rgb;',
            '#endif'
        ].join( '\n' );

        const glossinessMapFragmentChunk = [
            'float glossinessFactor = glossiness;',
            '#ifdef USE_GLOSSINESSMAP',
            '   vec4 texelGlossiness = texture2D( glossinessMap, vUv );',
            '   // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture',
            '   glossinessFactor *= texelGlossiness.a;',
            '#endif'
        ].join( '\n' );

        const lightPhysicalFragmentChunk = [
            'PhysicalMaterial material;',
            'material.diffuseColor = diffuseColor.rgb * ( 1. - max( specularFactor.r, max( specularFactor.g, specularFactor.b ) ) );',
            'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );',
            'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );',
            'material.roughness = max( 1.0 - glossinessFactor, 0.0525 ); // 0.0525 corresponds to the base mip of a 256 cubemap.',
            'material.roughness += geometryRoughness;',
            'material.roughness = min( material.roughness, 1.0 );',
            'material.specularColor = specularFactor;',
        ].join( '\n' );

        const uniforms = {
            specular: { value: new Color().setHex( 0xffffff ) },
            glossiness: { value: 1 },
            specularMap: { value: null },
            glossinessMap: { value: null }
        };

        this._extraUniforms = uniforms;

        this.onBeforeCompile = function ( shader ) {

            for ( const uniformName in uniforms ) {

                shader.uniforms[ uniformName ] = uniforms[ uniformName ];

            }

            shader.fragmentShader = shader.fragmentShader
                .replace( 'uniform float roughness;', 'uniform vec3 specular;' )
                .replace( 'uniform float metalness;', 'uniform float glossiness;' )
                .replace( '#include <roughnessmap_pars_fragment>', specularMapParsFragmentChunk )
                .replace( '#include <metalnessmap_pars_fragment>', glossinessMapParsFragmentChunk )
                .replace( '#include <roughnessmap_fragment>', specularMapFragmentChunk )
                .replace( '#include <metalnessmap_fragment>', glossinessMapFragmentChunk )
                .replace( '#include <lights_physical_fragment>', lightPhysicalFragmentChunk );

        };

        Object.defineProperties( this, {

            specular: {
                get: function () {

                    return uniforms.specular.value;

                },
                set: function ( v ) {

                    uniforms.specular.value = v;

                }
            },

            specularMap: {
                get: function () {

                    return uniforms.specularMap.value;

                },
                set: function ( v ) {

                    uniforms.specularMap.value = v;

                    if ( v ) {

                        this.defines.USE_SPECULARMAP = ''; // USE_UV is set by the renderer for specular maps

                    } else {

                        delete this.defines.USE_SPECULARMAP;

                    }

                }
            },

            glossiness: {
                get: function () {

                    return uniforms.glossiness.value;

                },
                set: function ( v ) {

                    uniforms.glossiness.value = v;

                }
            },

            glossinessMap: {
                get: function () {

                    return uniforms.glossinessMap.value;

                },
                set: function ( v ) {

                    uniforms.glossinessMap.value = v;

                    if ( v ) {

                        this.defines.USE_GLOSSINESSMAP = '';
                        this.defines.USE_UV = '';

                    } else {

                        delete this.defines.USE_GLOSSINESSMAP;
                        delete this.defines.USE_UV;

                    }

                }
            }

        } );

        delete this.metalness;
        delete this.roughness;
        delete this.metalnessMap;
        delete this.roughnessMap;

        this.setValues( params );

    }

    copy( source ) {

        super.copy( source );

        this.specularMap = source.specularMap;
        this.specular.copy( source.specular );
        this.glossinessMap = source.glossinessMap;
        this.glossiness = source.glossiness;
        delete this.metalness;
        delete this.roughness;
        delete this.metalnessMap;
        delete this.roughnessMap;
        return this;

    }

}


class GLTFMaterialsPbrSpecularGlossinessExtension {

    constructor() {

        this.name = EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS;

        this.specularGlossinessParams = [
            'color',
            'map',
            'lightMap',
            'lightMapIntensity',
            'aoMap',
            'aoMapIntensity',
            'emissive',
            'emissiveIntensity',
            'emissiveMap',
            'bumpMap',
            'bumpScale',
            'normalMap',
            'normalMapType',
            'displacementMap',
            'displacementScale',
            'displacementBias',
            'specularMap',
            'specular',
            'glossinessMap',
            'glossiness',
            'alphaMap',
            'envMap',
            'envMapIntensity'
        ];

    }

    getMaterialType() {

        return GLTFMeshStandardSGMaterial;

    }

    extendParams( materialParams, materialDef, parser ) {

        const pbrSpecularGlossiness = materialDef.extensions[ this.name ];

        materialParams.color = new Color( 1.0, 1.0, 1.0 );
        materialParams.opacity = 1.0;

        const pending = [];

        if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) {

            const array = pbrSpecularGlossiness.diffuseFactor;

            materialParams.color.fromArray( array );
            materialParams.opacity = array[ 3 ];

        }

        if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) {

            pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture, sRGBEncoding ) );

        }

        materialParams.emissive = new Color( 0.0, 0.0, 0.0 );
        materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0;
        materialParams.specular = new Color( 1.0, 1.0, 1.0 );

        if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) {

            materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor );

        }

        if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) {

            const specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture;
            pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) );
            pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef, sRGBEncoding ) );

        }

        return Promise.all( pending );

    }

    createMaterial( materialParams ) {

        const material = new GLTFMeshStandardSGMaterial( materialParams );
        material.fog = true;

        material.color = materialParams.color;

        material.map = materialParams.map === undefined ? null : materialParams.map;

        material.lightMap = null;
        material.lightMapIntensity = 1.0;

        material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap;
        material.aoMapIntensity = 1.0;

        material.emissive = materialParams.emissive;
        material.emissiveIntensity = 1.0;
        material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap;

        material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap;
        material.bumpScale = 1;

        material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap;
        material.normalMapType = TangentSpaceNormalMap;

        if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale;

        material.displacementMap = null;
        material.displacementScale = 1;
        material.displacementBias = 0;

        material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap;
        material.specular = materialParams.specular;

        material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap;
        material.glossiness = materialParams.glossiness;

        material.alphaMap = null;

        material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap;
        material.envMapIntensity = 1.0;

        return material;

    }

}

/**
 * Mesh Quantization Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization
 */
class GLTFMeshQuantizationExtension {

    constructor() {

        this.name = EXTENSIONS.KHR_MESH_QUANTIZATION;

    }

}

/*********************************/
/********** INTERPOLATION ********/
/*********************************/

// Spline Interpolation
// Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation
class GLTFCubicSplineInterpolant extends THREE.Interpolant {

    constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

        super( parameterPositions, sampleValues, sampleSize, resultBuffer );

    }

    copySampleValue_( index ) {

        // Copies a sample value to the result buffer. See description of glTF
        // CUBICSPLINE values layout in interpolate_() function below.

        const result = this.resultBuffer,
            values = this.sampleValues,
            valueSize = this.valueSize,
            offset = index * valueSize * 3 + valueSize;

        for ( let i = 0; i !== valueSize; i ++ ) {

            result[ i ] = values[ offset + i ];

        }

        return result;

    }

}

GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_;

GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_;

GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) {

    const result = this.resultBuffer;
    const values = this.sampleValues;
    const stride = this.valueSize;

    const stride2 = stride * 2;
    const stride3 = stride * 3;

    const td = t1 - t0;

    const p = ( t - t0 ) / td;
    const pp = p * p;
    const ppp = pp * p;

    const offset1 = i1 * stride3;
    const offset0 = offset1 - stride3;

    const s2 = - 2 * ppp + 3 * pp;
    const s3 = ppp - pp;
    const s0 = 1 - s2;
    const s1 = s3 - pp + p;

    // Layout of keyframe output values for CUBICSPLINE animations:
    //   [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ]
    for ( let i = 0; i !== stride; i ++ ) {

        const p0 = values[ offset0 + i + stride ]; // splineVertex_k
        const m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k)
        const p1 = values[ offset1 + i + stride ]; // splineVertex_k+1
        const m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k)

        result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1;

    }

    return result;

};

const _q = new THREE.Quaternion();

class GLTFCubicSplineQuaternionInterpolant extends GLTFCubicSplineInterpolant {

    interpolate_( i1, t0, t, t1 ) {

        const result = super.interpolate_( i1, t0, t, t1 );

        _q.fromArray( result ).normalize().toArray( result );

        return result;

    }

}


/*********************************/
/********** INTERNALS ************/
/*********************************/

/* CONSTANTS */

const WEBGL_CONSTANTS = {
    FLOAT: 5126,
    //FLOAT_MAT2: 35674,
    FLOAT_MAT3: 35675,
    FLOAT_MAT4: 35676,
    FLOAT_VEC2: 35664,
    FLOAT_VEC3: 35665,
    FLOAT_VEC4: 35666,
    LINEAR: 9729,
    REPEAT: 10497,
    SAMPLER_2D: 35678,
    POINTS: 0,
    LINES: 1,
    LINE_LOOP: 2,
    LINE_STRIP: 3,
    TRIANGLES: 4,
    TRIANGLE_STRIP: 5,
    TRIANGLE_FAN: 6,
    UNSIGNED_BYTE: 5121,
    UNSIGNED_SHORT: 5123
};

const WEBGL_COMPONENT_TYPES = {
    5120: Int8Array,
    5121: Uint8Array,
    5122: Int16Array,
    5123: Uint16Array,
    5125: Uint32Array,
    5126: Float32Array
};

const WEBGL_FILTERS = {
    9728: THREE.NearestFilter,
    9729: THREE.LinearFilter,
    9984: THREE.NearestMipmapNearestFilter,
    9985: THREE.LinearMipmapNearestFilter,
    9986: THREE.NearestMipmapLinearFilter,
    9987: THREE.LinearMipmapLinearFilter
};

const WEBGL_WRAPPINGS = {
    33071: THREE.ClampToEdgeWrapping,
    33648: THREE.MirroredRepeatWrapping,
    10497: THREE.RepeatWrapping
};

const WEBGL_TYPE_SIZES = {
    'SCALAR': 1,
    'VEC2': 2,
    'VEC3': 3,
    'VEC4': 4,
    'MAT2': 4,
    'MAT3': 9,
    'MAT4': 16
};

const ATTRIBUTES = {
    POSITION: 'position',
    NORMAL: 'normal',
    TANGENT: 'tangent',
    TEXCOORD_0: 'uv',
    TEXCOORD_1: 'uv2',
    COLOR_0: 'color',
    WEIGHTS_0: 'skinWeight',
    JOINTS_0: 'skinIndex',
};

const PATH_PROPERTIES = {
    scale: 'scale',
    translation: 'position',
    rotation: 'quaternion',
    weights: 'morphTargetInfluences'
};

const INTERPOLATION = {
    CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
                                // keyframe track will be initialized with a default interpolation type, then modified.
    LINEAR: THREE.InterpolateLinear,
    STEP: THREE.InterpolateDiscrete
};

const ALPHA_MODES = {
    OPAQUE: 'OPAQUE',
    MASK: 'MASK',
    BLEND: 'BLEND'
};

/**
 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
 */
function createDefaultMaterial( cache ) {

    if ( cache[ 'DefaultMaterial' ] === undefined ) {

        cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( {
            color: 0xFFFFFF,
            emissive: 0x000000,
            metalness: 1,
            roughness: 1,
            transparent: false,
            depthTest: true,
            side: FrontSide
        } );

    }

    return cache[ 'DefaultMaterial' ];

}

function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) {

    // Add unknown glTF extensions to an object's userData.

    for ( const name in objectDef.extensions ) {

        if ( knownExtensions[ name ] === undefined ) {

            object.userData.gltfExtensions = object.userData.gltfExtensions || {};
            object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ];

        }

    }

}

/**
 * @param {Object3D|Material|BufferGeometry} object
 * @param {GLTF.definition} gltfDef
 */
function assignExtrasToUserData( object, gltfDef ) {

    if ( gltfDef.extras !== undefined ) {

        if ( typeof gltfDef.extras === 'object' ) {

            Object.assign( object.userData, gltfDef.extras );

        } else {

            console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras );

        }

    }

}

/**
 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
 *
 * @param {BufferGeometry} geometry
 * @param {Array<GLTF.Target>} targets
 * @param {GLTFParser} parser
 * @return {Promise<BufferGeometry>}
 */
function addMorphTargets( geometry, targets, parser ) {

    let hasMorphPosition = false;
    let hasMorphNormal = false;
    let hasMorphColor = false;

    for ( let i = 0, il = targets.length; i < il; i ++ ) {

        const target = targets[ i ];

        if ( target.POSITION !== undefined ) hasMorphPosition = true;
        if ( target.NORMAL !== undefined ) hasMorphNormal = true;
        if ( target.COLOR_0 !== undefined ) hasMorphColor = true;

        if ( hasMorphPosition && hasMorphNormal && hasMorphColor ) break;

    }

    if ( ! hasMorphPosition && ! hasMorphNormal && ! hasMorphColor ) return Promise.resolve( geometry );

    const pendingPositionAccessors = [];
    const pendingNormalAccessors = [];
    const pendingColorAccessors = [];

    for ( let i = 0, il = targets.length; i < il; i ++ ) {

        const target = targets[ i ];

        if ( hasMorphPosition ) {

            const pendingAccessor = target.POSITION !== undefined
                ? parser.getDependency( 'accessor', target.POSITION )
                : geometry.attributes.position;

            pendingPositionAccessors.push( pendingAccessor );

        }

        if ( hasMorphNormal ) {

            const pendingAccessor = target.NORMAL !== undefined
                ? parser.getDependency( 'accessor', target.NORMAL )
                : geometry.attributes.normal;

            pendingNormalAccessors.push( pendingAccessor );

        }

        if ( hasMorphColor ) {

            const pendingAccessor = target.COLOR_0 !== undefined
                ? parser.getDependency( 'accessor', target.COLOR_0 )
                : geometry.attributes.color;

            pendingColorAccessors.push( pendingAccessor );

        }

    }

    return Promise.all( [
        Promise.all( pendingPositionAccessors ),
        Promise.all( pendingNormalAccessors ),
        Promise.all( pendingColorAccessors )
    ] ).then( function ( accessors ) {

        const morphPositions = accessors[ 0 ];
        const morphNormals = accessors[ 1 ];
        const morphColors = accessors[ 2 ];

        if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions;
        if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals;
        if ( hasMorphColor ) geometry.morphAttributes.color = morphColors;
        geometry.morphTargetsRelative = true;

        return geometry;

    } );

}

/**
 * @param {Mesh} mesh
 * @param {GLTF.Mesh} meshDef
 */
function updateMorphTargets( mesh, meshDef ) {

    mesh.updateMorphTargets();

    if ( meshDef.weights !== undefined ) {

        for ( let i = 0, il = meshDef.weights.length; i < il; i ++ ) {

            mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ];

        }

    }

    // .extras has user-defined data, so check that .extras.targetNames is an array.
    if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) {

        const targetNames = meshDef.extras.targetNames;

        if ( mesh.morphTargetInfluences.length === targetNames.length ) {

            mesh.morphTargetDictionary = {};

            for ( let i = 0, il = targetNames.length; i < il; i ++ ) {

                mesh.morphTargetDictionary[ targetNames[ i ] ] = i;

            }

        } else {

            console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' );

        }

    }

}

function createPrimitiveKey( primitiveDef ) {

    const dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ];
    let geometryKey;

    if ( dracoExtension ) {

        geometryKey = 'draco:' + dracoExtension.bufferView
                + ':' + dracoExtension.indices
                + ':' + createAttributesKey( dracoExtension.attributes );

    } else {

        geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode;

    }

    return geometryKey;

}

function createAttributesKey( attributes ) {

    let attributesKey = '';

    const keys = Object.keys( attributes ).sort();

    for ( let i = 0, il = keys.length; i < il; i ++ ) {

        attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';';

    }

    return attributesKey;

}

function getNormalizedComponentScale( constructor ) {

    // Reference:
    // https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#encoding-quantized-data

    switch ( constructor ) {

        case Int8Array:
            return 1 / 127;

        case Uint8Array:
            return 1 / 255;

        case Int16Array:
            return 1 / 32767;

        case Uint16Array:
            return 1 / 65535;

        default:
            throw new Error( 'THREE.GLTFLoader: Unsupported normalized accessor component type.' );

    }

}

function getImageURIMimeType( uri ) {

    if ( uri.search( /\.jpe?g($|\?)/i ) > 0 || uri.search( /^data\:image\/jpeg/ ) === 0 ) return 'image/jpeg';
    if ( uri.search( /\.webp($|\?)/i ) > 0 || uri.search( /^data\:image\/webp/ ) === 0 ) return 'image/webp';

    return 'image/png';

}

/* GLTF PARSER */

class GLTFParser {

    constructor( json = {}, options = {} ) {

        this.json = json;
        this.extensions = {};
        this.plugins = {};
        this.options = options;

        // loader object cache
        this.cache = new GLTFRegistry();

        // associations between Three.js objects and glTF elements
        this.associations = new Map();

        // BufferGeometry caching
        this.primitiveCache = {};

        // Object3D instance caches
        this.meshCache = { refs: {}, uses: {} };
        this.cameraCache = { refs: {}, uses: {} };
        this.lightCache = { refs: {}, uses: {} };

        this.sourceCache = {};
        this.textureCache = {};

        // Track node names, to ensure no duplicates
        this.nodeNamesUsed = {};

        // Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the
        // expensive work of uploading a texture to the GPU off the main thread.
        if ( typeof createImageBitmap !== 'undefined' && /^((?!chrome|android).)*safari/i.test( navigator.userAgent ) === false ) {

            this.textureLoader = new ImageBitmapLoader( this.options.manager );

        } else {

            this.textureLoader = new TextureLoader( this.options.manager );

        }

        this.textureLoader.setCrossOrigin( this.options.crossOrigin );
        this.textureLoader.setRequestHeader( this.options.requestHeader );

        this.fileLoader = new FileLoader( this.options.manager );
        this.fileLoader.setResponseType( 'arraybuffer' );

        if ( this.options.crossOrigin === 'use-credentials' ) {

            this.fileLoader.setWithCredentials( true );

        }

    }

    setExtensions( extensions ) {

        this.extensions = extensions;

    }

    setPlugins( plugins ) {

        this.plugins = plugins;

    }

    parse( onLoad, onError ) {

        const parser = this;
        const json = this.json;
        const extensions = this.extensions;

        // Clear the loader cache
        this.cache.removeAll();

        // Mark the special nodes/meshes in json for efficient parse
        this._invokeAll( function ( ext ) {

            return ext._markDefs && ext._markDefs();

        } );

        Promise.all( this._invokeAll( function ( ext ) {

            return ext.beforeRoot && ext.beforeRoot();

        } ) ).then( function () {

            return Promise.all( [

                parser.getDependencies( 'scene' ),
                parser.getDependencies( 'animation' ),
                parser.getDependencies( 'camera' ),

            ] );

        } ).then( function ( dependencies ) {

            const result = {
                scene: dependencies[ 0 ][ json.scene || 0 ],
                scenes: dependencies[ 0 ],
                animations: dependencies[ 1 ],
                cameras: dependencies[ 2 ],
                asset: json.asset,
                parser: parser,
                userData: {}
            };

            addUnknownExtensionsToUserData( extensions, result, json );

            assignExtrasToUserData( result, json );

            Promise.all( parser._invokeAll( function ( ext ) {

                return ext.afterRoot && ext.afterRoot( result );

            } ) ).then( function () {

                onLoad( result );

            } );

        } ).catch( onError );

    }

    /**
     * Marks the special nodes/meshes in json for efficient parse.
     */
    _markDefs() {

        const nodeDefs = this.json.nodes || [];
        const skinDefs = this.json.skins || [];
        const meshDefs = this.json.meshes || [];

        // Nothing in the node definition indicates whether it is a Bone or an
        // Object3D. Use the skins' joint references to mark bones.
        for ( let skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) {

            const joints = skinDefs[ skinIndex ].joints;

            for ( let i = 0, il = joints.length; i < il; i ++ ) {

                nodeDefs[ joints[ i ] ].isBone = true;

            }

        }

        // Iterate over all nodes, marking references to shared resources,
        // as well as skeleton joints.
        for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) {

            const nodeDef = nodeDefs[ nodeIndex ];

            if ( nodeDef.mesh !== undefined ) {

                this._addNodeRef( this.meshCache, nodeDef.mesh );

                // Nothing in the mesh definition indicates whether it is
                // a SkinnedMesh or Mesh. Use the node's mesh reference
                // to mark SkinnedMesh if node has skin.
                if ( nodeDef.skin !== undefined ) {

                    meshDefs[ nodeDef.mesh ].isSkinnedMesh = true;

                }

            }

            if ( nodeDef.camera !== undefined ) {

                this._addNodeRef( this.cameraCache, nodeDef.camera );

            }

        }

    }

    /**
     * Counts references to shared node / Object3D resources. These resources
     * can be reused, or "instantiated", at multiple nodes in the scene
     * hierarchy. Mesh, Camera, and Light instances are instantiated and must
     * be marked. Non-scenegraph resources (like Materials, Geometries, and
     * Textures) can be reused directly and are not marked here.
     *
     * Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
     */
    _addNodeRef( cache, index ) {

        if ( index === undefined ) return;

        if ( cache.refs[ index ] === undefined ) {

            cache.refs[ index ] = cache.uses[ index ] = 0;

        }

        cache.refs[ index ] ++;

    }

    /** Returns a reference to a shared resource, cloning it if necessary. */
    _getNodeRef( cache, index, object ) {

        if ( cache.refs[ index ] <= 1 ) return object;

        const ref = object.clone();

        // Propagates mappings to the cloned object, prevents mappings on the
        // original object from being lost.
        const updateMappings = ( original, clone ) => {

            const mappings = this.associations.get( original );
            if ( mappings != null ) {

                this.associations.set( clone, mappings );

            }

            for ( const [ i, child ] of original.children.entries() ) {

                updateMappings( child, clone.children[ i ] );

            }

        };

        updateMappings( object, ref );

        ref.name += '_instance_' + ( cache.uses[ index ] ++ );

        return ref;

    }

    _invokeOne( func ) {

        const extensions = Object.values( this.plugins );
        extensions.push( this );

        for ( let i = 0; i < extensions.length; i ++ ) {

            const result = func( extensions[ i ] );

            if ( result ) return result;

        }

        return null;

    }

    _invokeAll( func ) {

        const extensions = Object.values( this.plugins );
        extensions.unshift( this );

        const pending = [];

        for ( let i = 0; i < extensions.length; i ++ ) {

            const result = func( extensions[ i ] );

            if ( result ) pending.push( result );

        }

        return pending;

    }

    /**
     * Requests the specified dependency asynchronously, with caching.
     * @param {string} type
     * @param {number} index
     * @return {Promise<Object3D|Material|THREE.Texture|AnimationClip|ArrayBuffer|Object>}
     */
    getDependency( type, index ) {

        const cacheKey = type + ':' + index;
        let dependency = this.cache.get( cacheKey );

        if ( ! dependency ) {

            switch ( type ) {

                case 'scene':
                    dependency = this.loadScene( index );
                    break;

                case 'node':
                    dependency = this.loadNode( index );
                    break;

                case 'mesh':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadMesh && ext.loadMesh( index );

                    } );
                    break;

                case 'accessor':
                    dependency = this.loadAccessor( index );
                    break;

                case 'bufferView':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadBufferView && ext.loadBufferView( index );

                    } );
                    break;

                case 'buffer':
                    dependency = this.loadBuffer( index );
                    break;

                case 'material':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadMaterial && ext.loadMaterial( index );

                    } );
                    break;

                case 'texture':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadTexture && ext.loadTexture( index );

                    } );
                    break;

                case 'skin':
                    dependency = this.loadSkin( index );
                    break;

                case 'animation':
                    dependency = this.loadAnimation( index );
                    break;

                case 'camera':
                    dependency = this.loadCamera( index );
                    break;

                default:
                    throw new Error( 'Unknown type: ' + type );

            }

            this.cache.add( cacheKey, dependency );

        }

        return dependency;

    }

    /**
     * Requests all dependencies of the specified type asynchronously, with caching.
     * @param {string} type
     * @return {Promise<Array<Object>>}
     */
    getDependencies( type ) {

        let dependencies = this.cache.get( type );

        if ( ! dependencies ) {

            const parser = this;
            const defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || [];

            dependencies = Promise.all( defs.map( function ( def, index ) {

                return parser.getDependency( type, index );

            } ) );

            this.cache.add( type, dependencies );

        }

        return dependencies;

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
     * @param {number} bufferIndex
     * @return {Promise<ArrayBuffer>}
     */
    loadBuffer( bufferIndex ) {

        const bufferDef = this.json.buffers[ bufferIndex ];
        const loader = this.fileLoader;

        if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) {

            throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' );

        }

        // If present, GLB container is required to be the first buffer.
        if ( bufferDef.uri === undefined && bufferIndex === 0 ) {

            return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body );

        }

        const options = this.options;

        return new Promise( function ( resolve, reject ) {

            loader.load( LoaderUtils.resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () {

                reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) );

            } );

        } );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
     * @param {number} bufferViewIndex
     * @return {Promise<ArrayBuffer>}
     */
    loadBufferView( bufferViewIndex ) {

        const bufferViewDef = this.json.bufferViews[ bufferViewIndex ];

        return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) {

            const byteLength = bufferViewDef.byteLength || 0;
            const byteOffset = bufferViewDef.byteOffset || 0;
            return buffer.slice( byteOffset, byteOffset + byteLength );

        } );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors
     * @param {number} accessorIndex
     * @return {Promise<BufferAttribute|InterleavedBufferAttribute>}
     */
    loadAccessor( accessorIndex ) {

        const parser = this;
        const json = this.json;

        const accessorDef = this.json.accessors[ accessorIndex ];

        if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) {

            // Ignore empty accessors, which may be used to declare runtime
            // information about attributes coming from another source (e.g. Draco
            // compression extension).
            return Promise.resolve( null );

        }

        const pendingBufferViews = [];

        if ( accessorDef.bufferView !== undefined ) {

            pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) );

        } else {

            pendingBufferViews.push( null );

        }

        if ( accessorDef.sparse !== undefined ) {

            pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) );
            pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) );

        }

        return Promise.all( pendingBufferViews ).then( function ( bufferViews ) {

            const bufferView = bufferViews[ 0 ];

            const itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ];
            const TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];

            // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12.
            const elementBytes = TypedArray.BYTES_PER_ELEMENT;
            const itemBytes = elementBytes * itemSize;
            const byteOffset = accessorDef.byteOffset || 0;
            const byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined;
            const normalized = accessorDef.normalized === true;
            let array, bufferAttribute;

            // The buffer is not interleaved if the stride is the item size in bytes.
            if ( byteStride && byteStride !== itemBytes ) {

                // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer
                // This makes sure that IBA.count reflects accessor.count properly
                const ibSlice = Math.floor( byteOffset / byteStride );
                const ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count;
                let ib = parser.cache.get( ibCacheKey );

                if ( ! ib ) {

                    array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes );

                    // Integer parameters to IB/IBA are in array elements, not bytes.
                    ib = new InterleavedBuffer( array, byteStride / elementBytes );

                    parser.cache.add( ibCacheKey, ib );

                }

                bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized );

            } else {

                if ( bufferView === null ) {

                    array = new TypedArray( accessorDef.count * itemSize );

                } else {

                    array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize );

                }

                bufferAttribute = new BufferAttribute( array, itemSize, normalized );

            }

            // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors
            if ( accessorDef.sparse !== undefined ) {

                const itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR;
                const TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ];

                const byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0;
                const byteOffsetValues = accessorDef.sparse.values.byteOffset || 0;

                const sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices );
                const sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize );

                if ( bufferView !== null ) {

                    // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes.
                    bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized );

                }

                for ( let i = 0, il = sparseIndices.length; i < il; i ++ ) {

                    const index = sparseIndices[ i ];

                    bufferAttribute.setX( index, sparseValues[ i * itemSize ] );
                    if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] );
                    if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] );
                    if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] );
                    if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' );

                }

            }

            return bufferAttribute;

        } );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures
     * @param {number} textureIndex
     * @return {Promise<THREE.Texture>}
     */
    loadTexture( textureIndex ) {

        const json = this.json;
        const options = this.options;
        const textureDef = json.textures[ textureIndex ];
        const sourceIndex = textureDef.source;
        const sourceDef = json.images[ sourceIndex ];

        let loader = this.textureLoader;

        if ( sourceDef.uri ) {

            const handler = options.manager.getHandler( sourceDef.uri );
            if ( handler !== null ) loader = handler;

        }

        return this.loadTextureImage( textureIndex, sourceIndex, loader );

    }

    loadTextureImage( textureIndex, sourceIndex, loader ) {

        const parser = this;
        const json = this.json;

        const textureDef = json.textures[ textureIndex ];
        const sourceDef = json.images[ sourceIndex ];

        const cacheKey = ( sourceDef.uri || sourceDef.bufferView ) + ':' + textureDef.sampler;

        if ( this.textureCache[ cacheKey ] ) {

            // See https://github.com/mrdoob/three.js/issues/21559.
            return this.textureCache[ cacheKey ];

        }

        const promise = this.loadImageSource( sourceIndex, loader ).then( function ( texture ) {

            texture.flipY = false;

            if ( textureDef.name ) texture.name = textureDef.name;

            const samplers = json.samplers || {};
            const sampler = samplers[ textureDef.sampler ] || {};

            texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter;
            texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter;
            texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping;
            texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping;

            parser.associations.set( texture, { textures: textureIndex } );

            return texture;

        } ).catch( function () {

            return null;

        } );

        this.textureCache[ cacheKey ] = promise;

        return promise;

    }

    loadImageSource( sourceIndex, loader ) {

        const parser = this;
        const json = this.json;
        const options = this.options;

        if ( this.sourceCache[ sourceIndex ] !== undefined ) {

            return this.sourceCache[ sourceIndex ].then( ( texture ) => texture.clone() );

        }

        const sourceDef = json.images[ sourceIndex ];

        const URL = self.URL || self.webkitURL;

        let sourceURI = sourceDef.uri || '';
        let isObjectURL = false;

        if ( sourceDef.bufferView !== undefined ) {

            // Load binary image data from bufferView, if provided.

            sourceURI = parser.getDependency( 'bufferView', sourceDef.bufferView ).then( function ( bufferView ) {

                isObjectURL = true;
                const blob = new Blob( [ bufferView ], { type: sourceDef.mimeType } );
                sourceURI = URL.createObjectURL( blob );
                return sourceURI;

            } );

        } else if ( sourceDef.uri === undefined ) {

            throw new Error( 'THREE.GLTFLoader: Image ' + sourceIndex + ' is missing URI and bufferView' );

        }

        const promise = Promise.resolve( sourceURI ).then( function ( sourceURI ) {

            return new Promise( function ( resolve, reject ) {

                let onLoad = resolve;

                if ( loader.isImageBitmapLoader === true ) {

                    onLoad = function ( imageBitmap ) {

                        const texture = new Texture( imageBitmap );
                        texture.needsUpdate = true;

                        resolve( texture );

                    };

                }

                loader.load( LoaderUtils.resolveURL( sourceURI, options.path ), onLoad, undefined, reject );

            } );

        } ).then( function ( texture ) {

            // Clean up resources and configure Texture.

            if ( isObjectURL === true ) {

                URL.revokeObjectURL( sourceURI );

            }

            texture.userData.mimeType = sourceDef.mimeType || getImageURIMimeType( sourceDef.uri );

            return texture;

        } ).catch( function ( error ) {

            console.error( 'THREE.GLTFLoader: Couldn\'t load texture', sourceURI );
            throw error;

        } );

        this.sourceCache[ sourceIndex ] = promise;
        return promise;

    }

    /**
     * Asynchronously assigns a texture to the given material parameters.
     * @param {Object} materialParams
     * @param {string} mapName
     * @param {Object} mapDef
     * @return {Promise<Texture>}
     */
    assignTexture( materialParams, mapName, mapDef, encoding ) {

        const parser = this;

        return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) {

            // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured
            // However, we will copy UV set 0 to UV set 1 on demand for aoMap
            if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) {

                console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' );

            }

            if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) {

                const transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined;

                if ( transform ) {

                    const gltfReference = parser.associations.get( texture );
                    texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform );
                    parser.associations.set( texture, gltfReference );

                }

            }

            if ( encoding !== undefined ) {

                texture.encoding = encoding;

            }

            materialParams[ mapName ] = texture;

            return texture;

        } );

    }

    /**
     * Assigns final material to a Mesh, Line, or Points instance. The instance
     * already has a material (generated from the glTF material options alone)
     * but reuse of the same glTF material may require multiple threejs materials
     * to accommodate different primitive types, defines, etc. New materials will
     * be created if necessary, and reused from a cache.
     * @param  {Object3D} mesh Mesh, Line, or Points instance.
     */
    assignFinalMaterial( mesh ) {

        const geometry = mesh.geometry;
        let material = mesh.material;

        const useDerivativeTangents = geometry.attributes.tangent === undefined;
        const useVertexColors = geometry.attributes.color !== undefined;
        const useFlatShading = geometry.attributes.normal === undefined;

        if ( mesh.isPoints ) {

            const cacheKey = 'PointsMaterial:' + material.uuid;

            let pointsMaterial = this.cache.get( cacheKey );

            if ( ! pointsMaterial ) {

                pointsMaterial = new PointsMaterial();
                Material.prototype.copy.call( pointsMaterial, material );
                pointsMaterial.color.copy( material.color );
                pointsMaterial.map = material.map;
                pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px

                this.cache.add( cacheKey, pointsMaterial );

            }

            material = pointsMaterial;

        } else if ( mesh.isLine ) {

            const cacheKey = 'LineBasicMaterial:' + material.uuid;

            let lineMaterial = this.cache.get( cacheKey );

            if ( ! lineMaterial ) {

                lineMaterial = new LineBasicMaterial();
                Material.prototype.copy.call( lineMaterial, material );
                lineMaterial.color.copy( material.color );

                this.cache.add( cacheKey, lineMaterial );

            }

            material = lineMaterial;

        }

        // Clone the material if it will be modified
        if ( useDerivativeTangents || useVertexColors || useFlatShading ) {

            let cacheKey = 'ClonedMaterial:' + material.uuid + ':';

            if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:';
            if ( useDerivativeTangents ) cacheKey += 'derivative-tangents:';
            if ( useVertexColors ) cacheKey += 'vertex-colors:';
            if ( useFlatShading ) cacheKey += 'flat-shading:';

            let cachedMaterial = this.cache.get( cacheKey );

            if ( ! cachedMaterial ) {

                cachedMaterial = material.clone();

                if ( useVertexColors ) cachedMaterial.vertexColors = true;
                if ( useFlatShading ) cachedMaterial.flatShading = true;

                if ( useDerivativeTangents ) {

                    // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995
                    if ( cachedMaterial.normalScale ) cachedMaterial.normalScale.y *= - 1;
                    if ( cachedMaterial.clearcoatNormalScale ) cachedMaterial.clearcoatNormalScale.y *= - 1;

                }

                this.cache.add( cacheKey, cachedMaterial );

                this.associations.set( cachedMaterial, this.associations.get( material ) );

            }

            material = cachedMaterial;

        }

        // workarounds for mesh and geometry

        if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) {

            geometry.setAttribute( 'uv2', geometry.attributes.uv );

        }

        mesh.material = material;

    }

    getMaterialType( /* materialIndex */ ) {

        return MeshStandardMaterial;

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials
     * @param {number} materialIndex
     * @return {Promise<Material>}
     */
    loadMaterial( materialIndex ) {

        const parser = this;
        const json = this.json;
        const extensions = this.extensions;
        const materialDef = json.materials[ materialIndex ];

        let materialType;
        const materialParams = {};
        const materialExtensions = materialDef.extensions || {};

        const pending = [];

        if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) {

            const sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
            materialType = sgExtension.getMaterialType();
            pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) );

        } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) {

            const kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ];
            materialType = kmuExtension.getMaterialType();
            pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) );

        } else {

            // Specification:
            // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material

            const metallicRoughness = materialDef.pbrMetallicRoughness || {};

            materialParams.color = new Color( 1.0, 1.0, 1.0 );
            materialParams.opacity = 1.0;

            if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {

                const array = metallicRoughness.baseColorFactor;

                materialParams.color.fromArray( array );
                materialParams.opacity = array[ 3 ];

            }

            if ( metallicRoughness.baseColorTexture !== undefined ) {

                pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture, sRGBEncoding ) );

            }

            materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
            materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;

            if ( metallicRoughness.metallicRoughnessTexture !== undefined ) {

                pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) );
                pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) );

            }

            materialType = this._invokeOne( function ( ext ) {

                return ext.getMaterialType && ext.getMaterialType( materialIndex );

            } );

            pending.push( Promise.all( this._invokeAll( function ( ext ) {

                return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams );

            } ) ) );

        }

        if ( materialDef.doubleSided === true ) {

            materialParams.side = DoubleSide;

        }

        const alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE;

        if ( alphaMode === ALPHA_MODES.BLEND ) {

            materialParams.transparent = true;

            // See: https://github.com/mrdoob/three.js/issues/17706
            materialParams.depthWrite = false;

        } else {

            materialParams.transparent = false;

            if ( alphaMode === ALPHA_MODES.MASK ) {

                materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;

            }

        }

        if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) {

            pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) );

            materialParams.normalScale = new Vector2( 1, 1 );

            if ( materialDef.normalTexture.scale !== undefined ) {

                const scale = materialDef.normalTexture.scale;

                materialParams.normalScale.set( scale, scale );

            }

        }

        if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) {

            pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) );

            if ( materialDef.occlusionTexture.strength !== undefined ) {

                materialParams.aoMapIntensity = materialDef.occlusionTexture.strength;

            }

        }

        if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) {

            materialParams.emissive = new Color().fromArray( materialDef.emissiveFactor );

        }

        if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) {

            pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture, sRGBEncoding ) );

        }

        return Promise.all( pending ).then( function () {

            let material;

            if ( materialType === GLTFMeshStandardSGMaterial ) {

                material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams );

            } else {

                material = new materialType( materialParams );

            }

            if ( materialDef.name ) material.name = materialDef.name;

            assignExtrasToUserData( material, materialDef );

            parser.associations.set( material, { materials: materialIndex } );

            if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef );

            return material;

        } );

    }

    /** When Object3D instances are targeted by animation, they need unique names. */
    createUniqueName( originalName ) {

        const sanitizedName = PropertyBinding.sanitizeNodeName( originalName || '' );

        let name = sanitizedName;

        for ( let i = 1; this.nodeNamesUsed[ name ]; ++ i ) {

            name = sanitizedName + '_' + i;

        }

        this.nodeNamesUsed[ name ] = true;

        return name;

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
     *
     * Creates BufferGeometries from primitives.
     *
     * @param {Array<GLTF.Primitive>} primitives
     * @return {Promise<Array<BufferGeometry>>}
     */
    loadGeometries( primitives ) {

        const parser = this;
        const extensions = this.extensions;
        const cache = this.primitiveCache;

        function createDracoPrimitive( primitive ) {

            return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]
                .decodePrimitive( primitive, parser )
                .then( function ( geometry ) {

                    return addPrimitiveAttributes( geometry, primitive, parser );

                } );

        }

        const pending = [];

        for ( let i = 0, il = primitives.length; i < il; i ++ ) {

            const primitive = primitives[ i ];
            const cacheKey = createPrimitiveKey( primitive );

            // See if we've already created this geometry
            const cached = cache[ cacheKey ];

            if ( cached ) {

                // Use the cached geometry if it exists
                pending.push( cached.promise );

            } else {

                let geometryPromise;

                if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) {

                    // Use DRACO geometry if available
                    geometryPromise = createDracoPrimitive( primitive );

                } else {

                    // Otherwise create a new geometry
                    geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser );

                }

                // Cache this geometry
                cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise };

                pending.push( geometryPromise );

            }

        }

        return Promise.all( pending );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
     * @param {number} meshIndex
     * @return {Promise<Group|Mesh|SkinnedMesh>}
     */
    loadMesh( meshIndex ) {

        const parser = this;
        const json = this.json;
        const extensions = this.extensions;

        const meshDef = json.meshes[ meshIndex ];
        const primitives = meshDef.primitives;

        const pending = [];

        for ( let i = 0, il = primitives.length; i < il; i ++ ) {

            const material = primitives[ i ].material === undefined
                ? createDefaultMaterial( this.cache )
                : this.getDependency( 'material', primitives[ i ].material );

            pending.push( material );

        }

        pending.push( parser.loadGeometries( primitives ) );

        return Promise.all( pending ).then( function ( results ) {

            const materials = results.slice( 0, results.length - 1 );
            const geometries = results[ results.length - 1 ];

            const meshes = [];

            for ( let i = 0, il = geometries.length; i < il; i ++ ) {

                const geometry = geometries[ i ];
                const primitive = primitives[ i ];

                // 1. create Mesh

                let mesh;

                const material = materials[ i ];

                if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
                        primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
                        primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
                        primitive.mode === undefined ) {

                    // .isSkinnedMesh isn't in glTF spec. See ._markDefs()
                    mesh = meshDef.isSkinnedMesh === true
                        ? new SkinnedMesh( geometry, material )
                        : new Mesh( geometry, material );

                    if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) {

                        // we normalize floating point skin weight array to fix malformed assets (see #15319)
                        // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs
                        mesh.normalizeSkinWeights();

                    }

                    if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {

                        mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode );

                    } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) {

                        mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode );

                    }

                } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {

                    mesh = new LineSegments( geometry, material );

                } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) {

                    mesh = new Line( geometry, material );

                } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {

                    mesh = new LineLoop( geometry, material );

                } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) {

                    mesh = new Points( geometry, material );

                } else {

                    throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode );

                }

                if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) {

                    updateMorphTargets( mesh, meshDef );

                }

                mesh.name = parser.createUniqueName( meshDef.name || ( 'mesh_' + meshIndex ) );

                assignExtrasToUserData( mesh, meshDef );

                if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive );

                parser.assignFinalMaterial( mesh );

                meshes.push( mesh );

            }

            for ( let i = 0, il = meshes.length; i < il; i ++ ) {

                parser.associations.set( meshes[ i ], {
                    meshes: meshIndex,
                    primitives: i
                } );

            }

            if ( meshes.length === 1 ) {

                return meshes[ 0 ];

            }

            const group = new Group();

            parser.associations.set( group, { meshes: meshIndex } );

            for ( let i = 0, il = meshes.length; i < il; i ++ ) {

                group.add( meshes[ i ] );

            }

            return group;

        } );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras
     * @param {number} cameraIndex
     * @return {Promise<THREE.Camera>}
     */
    loadCamera( cameraIndex ) {

        let camera;
        const cameraDef = this.json.cameras[ cameraIndex ];
        const params = cameraDef[ cameraDef.type ];

        if ( ! params ) {

            console.warn( 'THREE.GLTFLoader: Missing camera parameters.' );
            return;

        }

        if ( cameraDef.type === 'perspective' ) {

            camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 );

        } else if ( cameraDef.type === 'orthographic' ) {

            camera = new OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar );

        }

        if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name );

        assignExtrasToUserData( camera, cameraDef );

        return Promise.resolve( camera );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins
     * @param {number} skinIndex
     * @return {Promise<Object>}
     */
    loadSkin( skinIndex ) {

        const skinDef = this.json.skins[ skinIndex ];

        const skinEntry = { joints: skinDef.joints };

        if ( skinDef.inverseBindMatrices === undefined ) {

            return Promise.resolve( skinEntry );

        }

        return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) {

            skinEntry.inverseBindMatrices = accessor;

            return skinEntry;

        } );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
     * @param {number} animationIndex
     * @return {Promise<AnimationClip>}
     */
    loadAnimation( animationIndex ) {

        const json = this.json;

        const animationDef = json.animations[ animationIndex ];

        const pendingNodes = [];
        const pendingInputAccessors = [];
        const pendingOutputAccessors = [];
        const pendingSamplers = [];
        const pendingTargets = [];

        for ( let i = 0, il = animationDef.channels.length; i < il; i ++ ) {

            const channel = animationDef.channels[ i ];
            const sampler = animationDef.samplers[ channel.sampler ];
            const target = channel.target;
            const name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
            const input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input;
            const output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output;

            pendingNodes.push( this.getDependency( 'node', name ) );
            pendingInputAccessors.push( this.getDependency( 'accessor', input ) );
            pendingOutputAccessors.push( this.getDependency( 'accessor', output ) );
            pendingSamplers.push( sampler );
            pendingTargets.push( target );

        }

        return Promise.all( [

            Promise.all( pendingNodes ),
            Promise.all( pendingInputAccessors ),
            Promise.all( pendingOutputAccessors ),
            Promise.all( pendingSamplers ),
            Promise.all( pendingTargets )

        ] ).then( function ( dependencies ) {

            const nodes = dependencies[ 0 ];
            const inputAccessors = dependencies[ 1 ];
            const outputAccessors = dependencies[ 2 ];
            const samplers = dependencies[ 3 ];
            const targets = dependencies[ 4 ];

            const tracks = [];

            for ( let i = 0, il = nodes.length; i < il; i ++ ) {

                const node = nodes[ i ];
                const inputAccessor = inputAccessors[ i ];
                const outputAccessor = outputAccessors[ i ];
                const sampler = samplers[ i ];
                const target = targets[ i ];

                if ( node === undefined ) continue;

                node.updateMatrix();
                node.matrixAutoUpdate = true;

                let TypedKeyframeTrack;

                switch ( PATH_PROPERTIES[ target.path ] ) {

                    case PATH_PROPERTIES.weights:

                        TypedKeyframeTrack = NumberKeyframeTrack;
                        break;

                    case PATH_PROPERTIES.rotation:

                        TypedKeyframeTrack = QuaternionKeyframeTrack;
                        break;

                    case PATH_PROPERTIES.position:
                    case PATH_PROPERTIES.scale:
                    default:

                        TypedKeyframeTrack = VectorKeyframeTrack;
                        break;

                }

                const targetName = node.name ? node.name : node.uuid;

                const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear;

                const targetNames = [];

                if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) {

                    node.traverse( function ( object ) {

                        if ( object.morphTargetInfluences ) {

                            targetNames.push( object.name ? object.name : object.uuid );

                        }

                    } );

                } else {

                    targetNames.push( targetName );

                }

                let outputArray = outputAccessor.array;

                if ( outputAccessor.normalized ) {

                    const scale = getNormalizedComponentScale( outputArray.constructor );
                    const scaled = new Float32Array( outputArray.length );

                    for ( let j = 0, jl = outputArray.length; j < jl; j ++ ) {

                        scaled[ j ] = outputArray[ j ] * scale;

                    }

                    outputArray = scaled;

                }

                for ( let j = 0, jl = targetNames.length; j < jl; j ++ ) {

                    const track = new TypedKeyframeTrack(
                        targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ],
                        inputAccessor.array,
                        outputArray,
                        interpolation
                    );

                    // Override interpolation with custom factory method.
                    if ( sampler.interpolation === 'CUBICSPLINE' ) {

                        track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) {

                            // A CUBICSPLINE keyframe in glTF has three output values for each input value,
                            // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize()
                            // must be divided by three to get the interpolant's sampleSize argument.

                            const interpolantType = ( this instanceof QuaternionKeyframeTrack ) ? GLTFCubicSplineQuaternionInterpolant : GLTFCubicSplineInterpolant;

                            return new interpolantType( this.times, this.values, this.getValueSize() / 3, result );

                        };

                        // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants.
                        track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;

                    }

                    tracks.push( track );

                }

            }

            const name = animationDef.name ? animationDef.name : 'animation_' + animationIndex;

            return new AnimationClip( name, undefined, tracks );

        } );

    }

    createNodeMesh( nodeIndex ) {

        const json = this.json;
        const parser = this;
        const nodeDef = json.nodes[ nodeIndex ];

        if ( nodeDef.mesh === undefined ) return null;

        return parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) {

            const node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh );

            // if weights are provided on the node, override weights on the mesh.
            if ( nodeDef.weights !== undefined ) {

                node.traverse( function ( o ) {

                    if ( ! o.isMesh ) return;

                    for ( let i = 0, il = nodeDef.weights.length; i < il; i ++ ) {

                        o.morphTargetInfluences[ i ] = nodeDef.weights[ i ];

                    }

                } );

            }

            return node;

        } );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
     * @param {number} nodeIndex
     * @return {Promise<Object3D>}
     */
    loadNode( nodeIndex ) {

        const json = this.json;
        const extensions = this.extensions;
        const parser = this;

        const nodeDef = json.nodes[ nodeIndex ];

        // reserve node's name before its dependencies, so the root has the intended name.
        const nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : '';

        return ( function () {

            const pending = [];

            const meshPromise = parser._invokeOne( function ( ext ) {

                return ext.createNodeMesh && ext.createNodeMesh( nodeIndex );

            } );

            if ( meshPromise ) {

                pending.push( meshPromise );

            }

            if ( nodeDef.camera !== undefined ) {

                pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) {

                    return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera );

                } ) );

            }

            parser._invokeAll( function ( ext ) {

                return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex );

            } ).forEach( function ( promise ) {

                pending.push( promise );

            } );

            return Promise.all( pending );

        }() ).then( function ( objects ) {

            let node;

            // .isBone isn't in glTF spec. See ._markDefs
            if ( nodeDef.isBone === true ) {

                node = new Bone();

            } else if ( objects.length > 1 ) {

                node = new Group();

            } else if ( objects.length === 1 ) {

                node = objects[ 0 ];

            } else {

                node = new Object3D();

            }

            if ( node !== objects[ 0 ] ) {

                for ( let i = 0, il = objects.length; i < il; i ++ ) {

                    node.add( objects[ i ] );

                }

            }

            if ( nodeDef.name ) {

                node.userData.name = nodeDef.name;
                node.name = nodeName;

            }

            assignExtrasToUserData( node, nodeDef );

            if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef );

            if ( nodeDef.matrix !== undefined ) {

                const matrix = new Matrix4();
                matrix.fromArray( nodeDef.matrix );
                node.applyMatrix4( matrix );

            } else {

                if ( nodeDef.translation !== undefined ) {

                    node.position.fromArray( nodeDef.translation );

                }

                if ( nodeDef.rotation !== undefined ) {

                    node.quaternion.fromArray( nodeDef.rotation );

                }

                if ( nodeDef.scale !== undefined ) {

                    node.scale.fromArray( nodeDef.scale );

                }

            }

            if ( ! parser.associations.has( node ) ) {

                parser.associations.set( node, {} );

            }

            parser.associations.get( node ).nodes = nodeIndex;

            return node;

        } );

    }

    /**
     * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
     * @param {number} sceneIndex
     * @return {Promise<Group>}
     */
    loadScene( sceneIndex ) {

        const json = this.json;
        const extensions = this.extensions;
        const sceneDef = this.json.scenes[ sceneIndex ];
        const parser = this;

        // Loader returns Group, not Scene.
        // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172
        const scene = new Group();
        if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name );

        assignExtrasToUserData( scene, sceneDef );

        if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef );

        const nodeIds = sceneDef.nodes || [];

        const pending = [];

        for ( let i = 0, il = nodeIds.length; i < il; i ++ ) {

            pending.push( buildNodeHierarchy( nodeIds[ i ], scene, json, parser ) );

        }

        return Promise.all( pending ).then( function () {

            // Removes dangling associations, associations that reference a node that
            // didn't make it into the scene.
            const reduceAssociations = ( node ) => {

                const reducedAssociations = new Map();

                for ( const [ key, value ] of parser.associations ) {

                    if ( key instanceof Material || key instanceof Texture ) {

                        reducedAssociations.set( key, value );

                    }

                }

                node.traverse( ( node ) => {

                    const mappings = parser.associations.get( node );

                    if ( mappings != null ) {

                        reducedAssociations.set( node, mappings );

                    }

                } );

                return reducedAssociations;

            };

            parser.associations = reduceAssociations( scene );

            return scene;

        } );

    }

}

function buildNodeHierarchy( nodeId, parentObject, json, parser ) {

    const nodeDef = json.nodes[ nodeId ];

    return parser.getDependency( 'node', nodeId ).then( function ( node ) {

        if ( nodeDef.skin === undefined ) return node;

        // build skeleton here as well

        let skinEntry;

        return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) {

            skinEntry = skin;

            const pendingJoints = [];

            for ( let i = 0, il = skinEntry.joints.length; i < il; i ++ ) {

                pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) );

            }

            return Promise.all( pendingJoints );

        } ).then( function ( jointNodes ) {

            node.traverse( function ( mesh ) {

                if ( ! mesh.isMesh ) return;

                const bones = [];
                const boneInverses = [];

                for ( let j = 0, jl = jointNodes.length; j < jl; j ++ ) {

                    const jointNode = jointNodes[ j ];

                    if ( jointNode ) {

                        bones.push( jointNode );

                        const mat = new Matrix4();

                        if ( skinEntry.inverseBindMatrices !== undefined ) {

                            mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 );

                        }

                        boneInverses.push( mat );

                    } else {

                        console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] );

                    }

                }

                mesh.bind( new Skeleton( bones, boneInverses ), mesh.matrixWorld );

            } );

            return node;

        } );

    } ).then( function ( node ) {

        // build node hierachy

        parentObject.add( node );

        const pending = [];

        if ( nodeDef.children ) {

            const children = nodeDef.children;

            for ( let i = 0, il = children.length; i < il; i ++ ) {

                const child = children[ i ];
                pending.push( buildNodeHierarchy( child, node, json, parser ) );

            }

        }

        return Promise.all( pending );

    } );

}

/**
 * @param {BufferGeometry} geometry
 * @param {GLTF.Primitive} primitiveDef
 * @param {GLTFParser} parser
 */
function computeBounds( geometry, primitiveDef, parser ) {

    const attributes = primitiveDef.attributes;

    const box = new Box3();

    if ( attributes.POSITION !== undefined ) {

        const accessor = parser.json.accessors[ attributes.POSITION ];

        const min = accessor.min;
        const max = accessor.max;

        // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement.

        if ( min !== undefined && max !== undefined ) {

            box.set(
                new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ),
                new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] )
            );

            if ( accessor.normalized ) {

                const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] );
                box.min.multiplyScalar( boxScale );
                box.max.multiplyScalar( boxScale );

            }

        } else {

            console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' );

            return;

        }

    } else {

        return;

    }

    const targets = primitiveDef.targets;

    if ( targets !== undefined ) {

        const maxDisplacement = new Vector3();
        const vector = new Vector3();

        for ( let i = 0, il = targets.length; i < il; i ++ ) {

            const target = targets[ i ];

            if ( target.POSITION !== undefined ) {

                const accessor = parser.json.accessors[ target.POSITION ];
                const min = accessor.min;
                const max = accessor.max;

                // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement.

                if ( min !== undefined && max !== undefined ) {

                    // we need to get max of absolute components because target weight is [-1,1]
                    vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) );
                    vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) );
                    vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) );


                    if ( accessor.normalized ) {

                        const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] );
                        vector.multiplyScalar( boxScale );

                    }

                    // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative
                    // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets
                    // are used to implement key-frame animations and as such only two are active at a time - this results in very large
                    // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size.
                    maxDisplacement.max( vector );

                } else {

                    console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' );

                }

            }

        }

        // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets.
        box.expandByVector( maxDisplacement );

    }

    geometry.boundingBox = box;

    const sphere = new Sphere();

    box.getCenter( sphere.center );
    sphere.radius = box.min.distanceTo( box.max ) / 2;

    geometry.boundingSphere = sphere;

}

/**
 * @param {BufferGeometry} geometry
 * @param {GLTF.Primitive} primitiveDef
 * @param {GLTFParser} parser
 * @return {Promise<BufferGeometry>}
 */
function addPrimitiveAttributes( geometry, primitiveDef, parser ) {

    const attributes = primitiveDef.attributes;

    const pending = [];

    function assignAttributeAccessor( accessorIndex, attributeName ) {

        return parser.getDependency( 'accessor', accessorIndex )
            .then( function ( accessor ) {

                geometry.setAttribute( attributeName, accessor );

            } );

    }

    for ( const gltfAttributeName in attributes ) {

        const threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase();

        // Skip attributes already provided by e.g. Draco extension.
        if ( threeAttributeName in geometry.attributes ) continue;

        pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) );

    }

    if ( primitiveDef.indices !== undefined && ! geometry.index ) {

        const accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) {

            geometry.setIndex( accessor );

        } );

        pending.push( accessor );

    }

    assignExtrasToUserData( geometry, primitiveDef );

    computeBounds( geometry, primitiveDef, parser );

    return Promise.all( pending ).then( function () {

        return primitiveDef.targets !== undefined
            ? addMorphTargets( geometry, primitiveDef.targets, parser )
            : geometry;

    } );

}

/**
 * @param {BufferGeometry} geometry
 * @param {Number} drawMode
 * @return {BufferGeometry}
 */
function toTrianglesDrawMode( geometry, drawMode ) {

    let index = geometry.getIndex();

    // generate index if not present

    if ( index === null ) {

        const indices = [];

        const position = geometry.getAttribute( 'position' );

        if ( position !== undefined ) {

            for ( let i = 0; i < position.count; i ++ ) {

                indices.push( i );

            }

            geometry.setIndex( indices );
            index = geometry.getIndex();

        } else {

            console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' );
            return geometry;

        }

    }

    //

    const numberOfTriangles = index.count - 2;
    const newIndices = [];

    if ( drawMode === TriangleFanDrawMode ) {

        // gl.TRIANGLE_FAN

        for ( let i = 1; i <= numberOfTriangles; i ++ ) {

            newIndices.push( index.getX( 0 ) );
            newIndices.push( index.getX( i ) );
            newIndices.push( index.getX( i + 1 ) );

        }

    } else {

        // gl.TRIANGLE_STRIP

        for ( let i = 0; i < numberOfTriangles; i ++ ) {

            if ( i % 2 === 0 ) {

                newIndices.push( index.getX( i ) );
                newIndices.push( index.getX( i + 1 ) );
                newIndices.push( index.getX( i + 2 ) );


            } else {

                newIndices.push( index.getX( i + 2 ) );
                newIndices.push( index.getX( i + 1 ) );
                newIndices.push( index.getX( i ) );

            }

        }

    }

    if ( ( newIndices.length / 3 ) !== numberOfTriangles ) {

        console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' );

    }

    // build final geometry

    const newGeometry = geometry.clone();
    newGeometry.setIndex( newIndices );

    return newGeometry;

}

/**
 * @webxr-input-profiles/motion-controllers 1.0.0 https://github.com/immersive-web/webxr-input-profiles
 */

const Constants = {
  Handedness: Object.freeze({
    NONE: 'none',
    LEFT: 'left',
    RIGHT: 'right'
  }),

  ComponentState: Object.freeze({
    DEFAULT: 'default',
    TOUCHED: 'touched',
    PRESSED: 'pressed'
  }),

  ComponentProperty: Object.freeze({
    BUTTON: 'button',
    X_AXIS: 'xAxis',
    Y_AXIS: 'yAxis',
    STATE: 'state'
  }),

  ComponentType: Object.freeze({
    TRIGGER: 'trigger',
    SQUEEZE: 'squeeze',
    TOUCHPAD: 'touchpad',
    THUMBSTICK: 'thumbstick',
    BUTTON: 'button'
  }),

  ButtonTouchThreshold: 0.05,

  AxisTouchThreshold: 0.1,

  VisualResponseProperty: Object.freeze({
    TRANSFORM: 'transform',
    VISIBILITY: 'visibility'
  })
};

/**
 * @description Static helper function to fetch a JSON file and turn it into a JS object
 * @param {string} path - Path to JSON file to be fetched
 */
async function fetchJsonFile(path) {
  const response = await fetch(path);
  if (!response.ok) {
    throw new Error(response.statusText);
  } else {
    return response.json();
  }
}

async function fetchProfilesList(basePath) {
  if (!basePath) {
    throw new Error('No basePath supplied');
  }

  const profileListFileName = 'profilesList.json';
  const profilesList = await fetchJsonFile(`${basePath}/${profileListFileName}`);
  return profilesList;
}

async function fetchProfile(xrInputSource, basePath, defaultProfile = null, getAssetPath = true) {
  if (!xrInputSource) {
    throw new Error('No xrInputSource supplied');
  }

  if (!basePath) {
    throw new Error('No basePath supplied');
  }

  // Get the list of profiles
  const supportedProfilesList = await fetchProfilesList(basePath);

  // Find the relative path to the first requested profile that is recognized
  let match;
  xrInputSource.profiles.some((profileId) => {
    const supportedProfile = supportedProfilesList[profileId];
    if (supportedProfile) {
      match = {
        profileId,
        profilePath: `${basePath}/${supportedProfile.path}`,
        deprecated: !!supportedProfile.deprecated
      };
    }
    return !!match;
  });

  if (!match) {
    if (!defaultProfile) {
      throw new Error('No matching profile name found');
    }

    const supportedProfile = supportedProfilesList[defaultProfile];
    if (!supportedProfile) {
      throw new Error(`No matching profile name found and default profile "${defaultProfile}" missing.`);
    }

    match = {
      profileId: defaultProfile,
      profilePath: `${basePath}/${supportedProfile.path}`,
      deprecated: !!supportedProfile.deprecated
    };
  }

  const profile = await fetchJsonFile(match.profilePath);

  let assetPath;
  if (getAssetPath) {
    let layout;
    if (xrInputSource.handedness === 'any') {
      layout = profile.layouts[Object.keys(profile.layouts)[0]];
    } else {
      layout = profile.layouts[xrInputSource.handedness];
    }
    if (!layout) {
      throw new Error(
        `No matching handedness, ${xrInputSource.handedness}, in profile ${match.profileId}`
      );
    }

    if (layout.assetPath) {
      assetPath = match.profilePath.replace('profile.json', layout.assetPath);
    }
  }

  return { profile, assetPath };
}

/** @constant {Object} */
const defaultComponentValues = {
  xAxis: 0,
  yAxis: 0,
  button: 0,
  state: Constants.ComponentState.DEFAULT
};

/**
 * @description Converts an X, Y coordinate from the range -1 to 1 (as reported by the Gamepad
 * API) to the range 0 to 1 (for interpolation). Also caps the X, Y values to be bounded within
 * a circle. This ensures that thumbsticks are not animated outside the bounds of their physical
 * range of motion and touchpads do not report touch locations off their physical bounds.
 * @param {number} x The original x coordinate in the range -1 to 1
 * @param {number} y The original y coordinate in the range -1 to 1
 */
function normalizeAxes(x = 0, y = 0) {
  let xAxis = x;
  let yAxis = y;

  // Determine if the point is outside the bounds of the circle
  // and, if so, place it on the edge of the circle
  const hypotenuse = Math.sqrt((x * x) + (y * y));
  if (hypotenuse > 1) {
    const theta = Math.atan2(y, x);
    xAxis = Math.cos(theta);
    yAxis = Math.sin(theta);
  }

  // Scale and move the circle so values are in the interpolation range.  The circle's origin moves
  // from (0, 0) to (0.5, 0.5). The circle's radius scales from 1 to be 0.5.
  const result = {
    normalizedXAxis: (xAxis * 0.5) + 0.5,
    normalizedYAxis: (yAxis * 0.5) + 0.5
  };
  return result;
}

/**
 * Contains the description of how the 3D model should visually respond to a specific user input.
 * This is accomplished by initializing the object with the name of a node in the 3D model and
 * property that need to be modified in response to user input, the name of the nodes representing
 * the allowable range of motion, and the name of the input which triggers the change. In response
 * to the named input changing, this object computes the appropriate weighting to use for
 * interpolating between the range of motion nodes.
 */
class VisualResponse {
  constructor(visualResponseDescription) {
    this.componentProperty = visualResponseDescription.componentProperty;
    this.states = visualResponseDescription.states;
    this.valueNodeName = visualResponseDescription.valueNodeName;
    this.valueNodeProperty = visualResponseDescription.valueNodeProperty;

    if (this.valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM) {
      this.minNodeName = visualResponseDescription.minNodeName;
      this.maxNodeName = visualResponseDescription.maxNodeName;
    }

    // Initializes the response's current value based on default data
    this.value = 0;
    this.updateFromComponent(defaultComponentValues);
  }

  /**
   * Computes the visual response's interpolation weight based on component state
   * @param {Object} componentValues - The component from which to update
   * @param {number} xAxis - The reported X axis value of the component
   * @param {number} yAxis - The reported Y axis value of the component
   * @param {number} button - The reported value of the component's button
   * @param {string} state - The component's active state
   */
  updateFromComponent({
    xAxis, yAxis, button, state
  }) {
    const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis);
    switch (this.componentProperty) {
      case Constants.ComponentProperty.X_AXIS:
        this.value = (this.states.includes(state)) ? normalizedXAxis : 0.5;
        break;
      case Constants.ComponentProperty.Y_AXIS:
        this.value = (this.states.includes(state)) ? normalizedYAxis : 0.5;
        break;
      case Constants.ComponentProperty.BUTTON:
        this.value = (this.states.includes(state)) ? button : 0;
        break;
      case Constants.ComponentProperty.STATE:
        if (this.valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY) {
          this.value = (this.states.includes(state));
        } else {
          this.value = this.states.includes(state) ? 1.0 : 0.0;
        }
        break;
      default:
        throw new Error(`Unexpected visualResponse componentProperty ${this.componentProperty}`);
    }
  }
}

class Component {
  /**
   * @param {Object} componentId - Id of the component
   * @param {Object} componentDescription - Description of the component to be created
   */
  constructor(componentId, componentDescription) {
    if (!componentId
     || !componentDescription
     || !componentDescription.visualResponses
     || !componentDescription.gamepadIndices
     || Object.keys(componentDescription.gamepadIndices).length === 0) {
      throw new Error('Invalid arguments supplied');
    }

    this.id = componentId;
    this.type = componentDescription.type;
    this.rootNodeName = componentDescription.rootNodeName;
    this.touchPointNodeName = componentDescription.touchPointNodeName;

    // Build all the visual responses for this component
    this.visualResponses = {};
    Object.keys(componentDescription.visualResponses).forEach((responseName) => {
      const visualResponse = new VisualResponse(componentDescription.visualResponses[responseName]);
      this.visualResponses[responseName] = visualResponse;
    });

    // Set default values
    this.gamepadIndices = Object.assign({}, componentDescription.gamepadIndices);

    this.values = {
      state: Constants.ComponentState.DEFAULT,
      button: (this.gamepadIndices.button !== undefined) ? 0 : undefined,
      xAxis: (this.gamepadIndices.xAxis !== undefined) ? 0 : undefined,
      yAxis: (this.gamepadIndices.yAxis !== undefined) ? 0 : undefined
    };
  }

  get data() {
    const data = { id: this.id, ...this.values };
    return data;
  }

  /**
   * @description Poll for updated data based on current gamepad state
   * @param {Object} gamepad - The gamepad object from which the component data should be polled
   */
  updateFromGamepad(gamepad) {
    // Set the state to default before processing other data sources
    this.values.state = Constants.ComponentState.DEFAULT;

    // Get and normalize button
    if (this.gamepadIndices.button !== undefined
        && gamepad.buttons.length > this.gamepadIndices.button) {
      const gamepadButton = gamepad.buttons[this.gamepadIndices.button];
      this.values.button = gamepadButton.value;
      this.values.button = (this.values.button < 0) ? 0 : this.values.button;
      this.values.button = (this.values.button > 1) ? 1 : this.values.button;

      // Set the state based on the button
      if (gamepadButton.pressed || this.values.button === 1) {
        this.values.state = Constants.ComponentState.PRESSED;
      } else if (gamepadButton.touched || this.values.button > Constants.ButtonTouchThreshold) {
        this.values.state = Constants.ComponentState.TOUCHED;
      }
    }

    // Get and normalize x axis value
    if (this.gamepadIndices.xAxis !== undefined
        && gamepad.axes.length > this.gamepadIndices.xAxis) {
      this.values.xAxis = gamepad.axes[this.gamepadIndices.xAxis];
      this.values.xAxis = (this.values.xAxis < -1) ? -1 : this.values.xAxis;
      this.values.xAxis = (this.values.xAxis > 1) ? 1 : this.values.xAxis;

      // If the state is still default, check if the xAxis makes it touched
      if (this.values.state === Constants.ComponentState.DEFAULT
        && Math.abs(this.values.xAxis) > Constants.AxisTouchThreshold) {
        this.values.state = Constants.ComponentState.TOUCHED;
      }
    }

    // Get and normalize Y axis value
    if (this.gamepadIndices.yAxis !== undefined
        && gamepad.axes.length > this.gamepadIndices.yAxis) {
      this.values.yAxis = gamepad.axes[this.gamepadIndices.yAxis];
      this.values.yAxis = (this.values.yAxis < -1) ? -1 : this.values.yAxis;
      this.values.yAxis = (this.values.yAxis > 1) ? 1 : this.values.yAxis;

      // If the state is still default, check if the yAxis makes it touched
      if (this.values.state === Constants.ComponentState.DEFAULT
        && Math.abs(this.values.yAxis) > Constants.AxisTouchThreshold) {
        this.values.state = Constants.ComponentState.TOUCHED;
      }
    }

    // Update the visual response weights based on the current component data
    Object.values(this.visualResponses).forEach((visualResponse) => {
      visualResponse.updateFromComponent(this.values);
    });
  }
}

/**
  * @description Builds a motion controller with components and visual responses based on the
  * supplied profile description. Data is polled from the xrInputSource's gamepad.
  * @author Nell Waliczek / https://github.com/NellWaliczek
*/
class MotionController {
  /**
   * @param {Object} xrInputSource - The XRInputSource to build the MotionController around
   * @param {Object} profile - The best matched profile description for the supplied xrInputSource
   * @param {Object} assetUrl
   */
  constructor(xrInputSource, profile, assetUrl) {
    if (!xrInputSource) {
      throw new Error('No xrInputSource supplied');
    }

    if (!profile) {
      throw new Error('No profile supplied');
    }

    this.xrInputSource = xrInputSource;
    this.assetUrl = assetUrl;
    this.id = profile.profileId;

    // Build child components as described in the profile description
    this.layoutDescription = profile.layouts[xrInputSource.handedness];
    this.components = {};
    Object.keys(this.layoutDescription.components).forEach((componentId) => {
      const componentDescription = this.layoutDescription.components[componentId];
      this.components[componentId] = new Component(componentId, componentDescription);
    });

    // Initialize components based on current gamepad state
    this.updateFromGamepad();
  }

  get gripSpace() {
    return this.xrInputSource.gripSpace;
  }

  get targetRaySpace() {
    return this.xrInputSource.targetRaySpace;
  }

  /**
   * @description Returns a subset of component data for simplified debugging
   */
  get data() {
    const data = [];
    Object.values(this.components).forEach((component) => {
      data.push(component.data);
    });
    return data;
  }

  /**
   * @description Poll for updated data based on current gamepad state
   */
  updateFromGamepad() {
    Object.values(this.components).forEach((component) => {
      component.updateFromGamepad(this.xrInputSource.gamepad);
    });
  }
}

/*
import {
    Mesh,
    MeshBasicMaterial,
    Object3D,
    SphereGeometry,
} from 'three';
*/

const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles';
const DEFAULT_PROFILE = 'generic-trigger';

class XRControllerModel extends THREE.Object3D {

    constructor() {

        super();

        this.motionController = null;
        this.envMap = null;

    }

    setEnvironmentMap( envMap ) {

        if ( this.envMap == envMap ) {

            return this;

        }

        this.envMap = envMap;
        this.traverse( ( child ) => {

            if ( child.isMesh ) {

                child.material.envMap = this.envMap;
                child.material.needsUpdate = true;

            }

        } );

        return this;

    }

    /**
     * Polls data from the XRInputSource and updates the model's components to match
     * the real world data
     */
    updateMatrixWorld( force ) {

        super.updateMatrixWorld( force );

        if ( ! this.motionController ) return;

        // Cause the MotionController to poll the Gamepad for data
        this.motionController.updateFromGamepad();

        // Update the 3D model to reflect the button, thumbstick, and touchpad state
        Object.values( this.motionController.components ).forEach( ( component ) => {

            // Update node data based on the visual responses' current states
            Object.values( component.visualResponses ).forEach( ( visualResponse ) => {

                const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse;

                // Skip if the visual response node is not found. No error is needed,
                // because it will have been reported at load time.
                if ( ! valueNode ) return;

                // Calculate the new properties based on the weight supplied
                if ( valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY ) {

                    valueNode.visible = value;

                } else if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) {

                    valueNode.quaternion.slerpQuaternions(
                        minNode.quaternion,
                        maxNode.quaternion,
                        value
                    );

                    valueNode.position.lerpVectors(
                        minNode.position,
                        maxNode.position,
                        value
                    );

                }

            } );

        } );

    }

}

/**
 * Walks the model's tree to find the nodes needed to animate the components and
 * saves them to the motionContoller components for use in the frame loop. When
 * touchpads are found, attaches a touch dot to them.
 */
function findNodes( motionController, scene ) {

    // Loop through the components and find the nodes needed for each components' visual responses
    Object.values( motionController.components ).forEach( ( component ) => {

        const { type, touchPointNodeName, visualResponses } = component;

        if ( type === Constants.ComponentType.TOUCHPAD ) {

            component.touchPointNode = scene.getObjectByName( touchPointNodeName );
            if ( component.touchPointNode ) {

                // Attach a touch dot to the touchpad.
                const sphereGeometry = new SphereGeometry( 0.001 );
                const material = new MeshBasicMaterial( { color: 0x0000FF } );
                const sphere = new Mesh( sphereGeometry, material );
                component.touchPointNode.add( sphere );

            } else {

                console.warn( `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` );

            }

        }

        // Loop through all the visual responses to be applied to this component
        Object.values( visualResponses ).forEach( ( visualResponse ) => {

            const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse;

            // If animating a transform, find the two nodes to be interpolated between.
            if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) {

                visualResponse.minNode = scene.getObjectByName( minNodeName );
                visualResponse.maxNode = scene.getObjectByName( maxNodeName );

                // If the extents cannot be found, skip this animation
                if ( ! visualResponse.minNode ) {

                    console.warn( `Could not find ${minNodeName} in the model` );
                    return;

                }

                if ( ! visualResponse.maxNode ) {

                    console.warn( `Could not find ${maxNodeName} in the model` );
                    return;

                }

            }

            // If the target node cannot be found, skip this animation
            visualResponse.valueNode = scene.getObjectByName( valueNodeName );
            if ( ! visualResponse.valueNode ) {

                console.warn( `Could not find ${valueNodeName} in the model` );

            }

        } );

    } );

}

function addAssetSceneToControllerModel( controllerModel, scene ) {

    // Find the nodes needed for animation and cache them on the motionController.
    findNodes( controllerModel.motionController, scene );

    // Apply any environment map that the mesh already has set.
    if ( controllerModel.envMap ) {

        scene.traverse( ( child ) => {

            if ( child.isMesh ) {

                child.material.envMap = controllerModel.envMap;
                child.material.needsUpdate = true;

            }

        } );

    }

    // Add the glTF scene to the controllerModel.
    controllerModel.add( scene );

}

class XRControllerModelFactory {

    constructor( gltfLoader = null ) {

        this.gltfLoader = gltfLoader;
        this.path = DEFAULT_PROFILES_PATH;
        this._assetCache = {};

        // If a GLTFLoader wasn't supplied to the constructor create a new one.
        if ( ! this.gltfLoader ) {

            this.gltfLoader = new GLTFLoader();

        }

    }

    createControllerModel( controller ) {

        const controllerModel = new XRControllerModel();
        let scene = null;

        controller.addEventListener( 'connected', ( event ) => {

            const xrInputSource = event.data;

            if ( xrInputSource.targetRayMode !== 'tracked-pointer' || ! xrInputSource.gamepad ) return;

            fetchProfile( xrInputSource, this.path, DEFAULT_PROFILE ).then( ( { profile, assetPath } ) => {

                controllerModel.motionController = new MotionController(
                    xrInputSource,
                    profile,
                    assetPath
                );

                const cachedAsset = this._assetCache[ controllerModel.motionController.assetUrl ];
                if ( cachedAsset ) {

                    scene = cachedAsset.scene.clone();

                    addAssetSceneToControllerModel( controllerModel, scene );

                } else {

                    if ( ! this.gltfLoader ) {

                        throw new Error( 'GLTFLoader not set.' );

                    }

                    this.gltfLoader.setPath( '' );
                    this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => {

                        this._assetCache[ controllerModel.motionController.assetUrl ] = asset;

                        scene = asset.scene.clone();

                        addAssetSceneToControllerModel( controllerModel, scene );

                    },
                    null,
                    () => {

                        throw new Error( `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` );

                    } );

                }

            } ).catch( ( err ) => {

                console.warn( err );

            } );

        } );

        controller.addEventListener( 'disconnected', () => {

            controllerModel.motionController = null;
            controllerModel.remove( scene );
            scene = null;

        } );

        return controllerModel;

    }

}

//import * as THREE from './three/three.module.js';

// copied from https://github.com/NikLever/Learn-WebXR/blob/master/libs/ControllerGestures.js
// created by Nik Lever

class ControllerGestures extends THREE.EventDispatcher{
    constructor( renderer ){
        super();
        
        if (renderer === undefined){
            console.error('ControllerGestures must be passed a renderer');
            return;
        }
        
        const clock = new THREE.Clock();
        
        this.controller1 = renderer.xr.getController(0);
        this.controller1.userData.gestures = { index: 0 };
        this.controller1.userData.selectPressed = false;
        this.controller1.addEventListener( 'selectstart', onSelectStart );
        this.controller1.addEventListener( 'selectend', onSelectEnd );
        
        this.controller2 = renderer.xr.getController(1);
        this.controller2.userData.gestures = { index: 1 };
        this.controller2.userData.selectPressed = false;
        this.controller2.addEventListener( 'selectstart', onSelectStart );
        this.controller2.addEventListener( 'selectend', onSelectEnd );
        
        this.doubleClickLimit = 0.2;
        this.pressMinimum = 0.4;
        this.right = new THREE.Vector3(1,0,0);
        this.up = new THREE.Vector3(0,1,0);
        
        this.type = 'unknown';
        this.touchCount = 0;
        
        this.clock = clock;
        
        const self = this;
        
        function onSelectStart( ){
            const data = this.userData.gestures;
            
            data.startPosition = undefined;
            data.startTime = clock.getElapsedTime();
            
            if ( self.type.indexOf('tap') == -1) data.taps = 0;
            
            self.type = 'unknown';
            this.userData.selectPressed = true;
            
            self.touchCount++;
            
            console.log( `onSelectStart touchCount: ${ self.touchCount }` );
        }
        
        function onSelectEnd( ){
            const data = this.userData.gestures;
            
            data.endTime = clock.getElapsedTime();
            const startToEnd = data.endTime - data.startTime;
            
            //console.log(`ControllerGestures.onSelectEnd: startToEnd:${startToEnd.toFixed(2)} taps:${data.taps}`);
            
            if (self.type === 'swipe'){
                const direction = ( self.controller1.position.y < data.startPosition.y) ? "DOWN" : "UP";
                self.dispatchEvent( { type:'swipe', direction } );
                self.type = 'unknown';
            }else if (self.type !== "pinch" && self.type !== "rotate" && self.type !== 'pan'){
                if ( startToEnd < self.doubleClickLimit ){
                    self.type = "tap";
                    data.taps++;
                }else if ( startToEnd > self.pressMinimum ){
                    self.dispatchEvent( { type: 'press', position: self.controller1.position, matrixWorld: self.controller1.matrixWorld }   );
                    self.type = 'unknown';
                }
            }else {
                self.type = 'unknown';
            }
            
            this.userData.selectPressed = false;
            data.startPosition = undefined;
            
            self.touchCount--;
        }
    }
    
    get multiTouch(){
        let result;
        if ( this.controller1 === undefined || this.controller2 === undefined ){   
            result = false;
        }else {
            result = this.controller1.userData.selectPressed && this.controller2.userData.selectPressed;
        }
        const self = this;
        console.log( `ControllerGestures multiTouch: ${result} touchCount:${self.touchCount}`);
        return result;
    }
    
    get touch(){
        let result;
        if ( this.controller1 === undefined || this.controller2 === undefined ){   
            result = false;
        }else {
            result = this.controller1.userData.selectPressed || this.controller2.userData.selectPressed;
        }
        //console.log( `ControllerGestures touch: ${result}`);
        return result;
    }
    
    get debugMsg(){
        return this.type;
    }
    
    update(){
        const data1 = this.controller1.userData.gestures;
        const data2 = this.controller2.userData.gestures;
        const currentTime = this.clock.getElapsedTime();
        
        let elapsedTime;
        
        if (this.controller1.userData.selectPressed && data1.startPosition === undefined){
            elapsedTime = currentTime - data1.startTime;
            if (elapsedTime > 0.05 ) data1.startPosition = this.controller1.position.clone();
        }
        
        if (this.controller2.userData.selectPressed && data2.startPosition === undefined){
            elapsedTime = currentTime - data2.startTime;
            if (elapsedTime > 0.05 ) data2.startPosition = this.controller2.position.clone();
        }
        
        if (!this.controller1.userData.selectPressed && this.type === 'tap' ){
            //Only dispatch event after double click limit is passed
            elapsedTime = this.clock.getElapsedTime() - data1.endTime;
            if (elapsedTime > this.doubleClickLimit){
                console.log( `ControllerGestures.update dispatchEvent taps:${data1.taps}` );
                switch( data1.taps ){
                    case 1:
                        this.dispatchEvent( { type: 'tap', position: this.controller1.position, matrixWorld: this.controller1.matrixWorld } );
                        break;
                    case 2:
                        this.dispatchEvent( { type: 'doubletap', position: this.controller1.position, matrixWorld: this.controller1.matrixWorld } );
                        break;
                    case 3:
                        this.dispatchEvent( { type: 'tripletap', position: this.controller1.position, matrixWorld: this.controller1.matrixWorld } );
                        break;
                    case 4:
                        this.dispatchEvent( { type: 'quadtap', position: this.controller1.position, matrixWorld: this.controller1.matrixWorld }  );
                        break;
                }
                this.type = "unknown";
                data1.taps = 0;
            }
        }
        
        if (this.type === 'unknown' && this.touch){
            if (data1.startPosition !== undefined){
                if (this.multiTouch){
                    if (data2.startPosition !== undefined){
                        //startPosition is undefined for 1/20 sec
                        //test for pinch or rotate
                        const startDistance = data1.startPosition.distanceTo( data2.startPosition );
                        const currentDistance = this.controller1.position.distanceTo( this.controller2.position );
                        const delta = currentDistance - startDistance;
                        if ( Math.abs(delta) > 0.01 ){
                            this.type = 'pinch';
                            this.startDistance = this.controller1.position.distanceTo( this.controller2.position );
                            this.dispatchEvent( { type: 'pinch', delta: 0, scale: 1, initialise: true } );
                        }else {
                            const v1 = data2.startPosition.clone().sub( data1.startPosition ).normalize();
                            const v2 = this.controller2.position.clone().sub( this.controller1.position ).normalize();
                            const theta = v1.angleTo( v2 );
                            if (Math.abs(theta) > 0.2){
                                this.type = 'rotate';
                                this.startVector = v2.clone();
                                this.dispatchEvent( { type: 'rotate', theta: 0, initialise: true } );
                            }
                        }
                    }
                }else {
                    //test for swipe or pan
                    let dist = data1.startPosition.distanceTo( this.controller1.position );
                    elapsedTime = this.clock.getElapsedTime() - data1.startTime;
                    const velocity = dist/elapsedTime;
                    //console.log(`dist:${dist.toFixed(3)} velocity:${velocity.toFixed(3)}`);
                    if ( dist > 0.01 && velocity > 0.1 ){
                        const v = this.controller1.position.clone().sub( data1.startPosition );
                        let maxY = (Math.abs(v.y) > Math.abs(v.x)) && (Math.abs(v.y) > Math.abs(v.z));
                        if ( maxY )this.type = "swipe";
                    }else if (dist > 0.006 && velocity < 0.03){
                        this.type = "pan";
                        this.startPosition = this.controller1.position.clone();
                        this.dispatchEvent( { type: 'pan', delta: new THREE.Vector3(), initialise: true } );
                    }
                }
            }
        }else if (this.type === 'pinch'){
            const currentDistance = this.controller1.position.distanceTo( this.controller2.position );
            const delta = currentDistance - this.startDistance;
            const scale = currentDistance/this.startDistance;
            this.dispatchEvent( { type: 'pinch', delta, scale });
        }else if (this.type === 'rotate'){
            const v = this.controller2.position.clone().sub( this.controller1.position ).normalize();
            let theta = this.startVector.angleTo( v );
            const cross = this.startVector.clone().cross( v );
            if (this.up.dot(cross) > 0) theta = -theta;
            this.dispatchEvent( { type: 'rotate', theta } );
        }else if (this.type === 'pan'){
            const delta = this.controller1.position.clone().sub( this.startPosition );
            this.dispatchEvent( { type: 'pan', delta } );
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Scene {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //This core function sets up the scene and display the structure according to the input
    //options (shown above), which is a hash containing values for different keys.
    rebuildScene(options) { let ic = this.icn3d; ic.icn3dui;
        if(options === undefined) options = ic.opts;

        this.rebuildSceneBase(options);

        ic.fogCls.setFog();

        ic.cameraCls.setCamera();

        this.setVrAr();

        if(ic.bSkipChemicalbinding === undefined || !ic.bSkipChemicalbinding) {
            ic.applyOtherCls.applyChemicalbindingOptions();
        }

        ic.bSkipChemicalbinding = true;

        if (options.chemicalbinding === 'show') {
            ic.opts["hbonds"] = "yes";
        }

        // show disulfide bonds, set side chains
        ic.applySsbondsCls.applySsbondsOptions();

        // show cross-linkages, set side chains
        ic.applyClbondsCls.applyClbondsOptions();

        ic.applyDisplayCls.applyDisplayOptions(ic.opts, ic.dAtoms);

        ic.applyOtherCls.applyOtherOptions();

        //ic.setFog();

        //ic.setCamera();

        //https://stackoverflow.com/questions/15726560/three-js-raycaster-intersection-empty-when-objects-not-part-of-scene
        ic.scene_ghost.updateMatrixWorld(true);
    }

    rebuildSceneBase(options) { let ic = this.icn3d, me = ic.icn3dui;
        $.extend(ic.opts, options);

        ic.cam_z = ic.maxD * 2;
        //ic.cam_z = -ic.maxD * 2;

        if(ic.scene !== undefined) {
            for(let i = ic.scene.children.length - 1; i >= 0; i--) {
                 let obj = ic.scene.children[i];
                 ic.scene.remove(obj);
            }
        }
        else {
            ic.scene = new THREE.Scene();
        }

        if(ic.scene_ghost !== undefined) {
            for(let i = ic.scene_ghost.children.length - 1; i >= 0; i--) {
                 let obj = ic.scene_ghost.children[i];
                 ic.scene_ghost.remove(obj);
            }
        }
        else {
            ic.scene_ghost = new THREE.Scene();
        }

        // get parameters from cookies
        if(me.htmlCls.setHtmlCls.getCookie('shininess') != '') {
            let shininess = parseFloat(me.htmlCls.setHtmlCls.getCookie('shininess'));

            if(ic.shininess != shininess) {
                me.htmlCls.clickMenuCls.setLogCmd('set shininess ' + shininess, true);
            }

            ic.shininess = shininess;
        }

        if(!me.bNode && me.htmlCls.setHtmlCls.getCookie('light1') != '') {
            let light1 = parseFloat(me.htmlCls.setHtmlCls.getCookie('light1'));
            let light2 = parseFloat(me.htmlCls.setHtmlCls.getCookie('light2'));
            let light3 = parseFloat(me.htmlCls.setHtmlCls.getCookie('light3'));

            if(ic.light1 != light1 || ic.light2 != light2 || ic.light3 != light3) {
                me.htmlCls.clickMenuCls.setLogCmd('set light | light1 ' + light1 + ' | light2 ' + light2 + ' | light3 ' + light3, true);
            }

            ic.light1 = light1;
            ic.light2 = light2;
            ic.light3 = light3;
        }

        ic.directionalLight = new THREE.DirectionalLight(0xFFFFFF, ic.light1); //1.0);
        ic.directionalLight2 = new THREE.DirectionalLight(0xFFFFFF, ic.light2);
        ic.directionalLight3 = new THREE.DirectionalLight(0xFFFFFF, ic.light3);

        if(ic.cam_z > 0) {
          ic.directionalLight.position.set(-1, 1, 1); //(0, 1, 1);
          ic.directionalLight2.position.set(1, 1, 1); //(0, -1, 1);
          ic.directionalLight3.position.set(1, 1, -1); //(0, 1, -1);

          ic.lightPos = new THREE.Vector3(-1, 1, 1); //(0, 1, 1);
          ic.lightPos2 = new THREE.Vector3(1, 1, 1); //(0, -1, 1);
          ic.lightPos3 = new THREE.Vector3(1, 1, -1); //(0, 1, -1);
        }
        else {
          ic.directionalLight.position.set(-1, 1, -1); //(0, 1, -1);
          ic.directionalLight2.position.set(1, 1, -1); //(0, -1, -1);
          ic.directionalLight3.position.set(1, 1, 1); //(0, 1, 1);

          ic.lightPos = new THREE.Vector3(-1, 1, -1); //(0, 1, -1);
          ic.lightPos2 = new THREE.Vector3(1, 1, -1); //(0, -1, -1);
          ic.lightPos3 = new THREE.Vector3(1, 1, 1); //(0, 1, 1);
        }

        let ambientLight = new THREE.AmbientLight(0x888888); //(0x404040);

        ic.scene.add(ic.directionalLight);
        ic.scene.add(ambientLight);

        if(ic.mdl !== undefined) {
            for(let i = ic.mdl.children.length - 1; i >= 0; i--) {
                 let obj = ic.mdl.children[i];
                 if(obj.geometry) obj.geometry.dispose();
                 if(obj.material) obj.material.dispose();
                 ic.mdl.remove(obj);
            }
        }

        if(ic.mdlImpostor !== undefined) {
            for(let i = ic.mdlImpostor.children.length - 1; i >= 0; i--) {
                 let obj = ic.mdlImpostor.children[i];
                 if(obj.geometry) obj.geometry.dispose();
                 if(obj.material) obj.material.dispose();
                 ic.mdlImpostor.remove(obj);
            }

            ic.mdlImpostor.children.length = 0;
        }

        // https://discourse.threejs.org/t/correctly-remove-mesh-from-scene-and-dispose-material-and-geometry/5448/2
        // clear memory
        if(!me.bNode) ic.renderer.renderLists.dispose();

        ic.mdl = new THREE.Object3D();  // regular display
        ic.mdlImpostor = new THREE.Object3D();  // Impostor display

        ic.scene.add(ic.mdl);
        ic.scene.add(ic.mdlImpostor);

        // highlight on impostors
        ic.mdl_ghost = new THREE.Object3D();  // Impostor display
        ic.scene_ghost.add(ic.mdl_ghost);
 
        // related to pk
        ic.objects = []; // define objects for pk, not all elements are used for pk
        ic.objects_ghost = []; // define objects for pk, not all elements are used for pk

        ic.raycaster = new THREE.Raycaster();
        ic.projector = new THREE.Projector();
        ic.mouse = new THREE.Vector2();

        let background = me.parasCls.backgroundColors[ic.opts.background.toLowerCase()];

        if(!me.bNode) {
            if(ic.opts.background.toLowerCase() === 'transparent') {
                ic.renderer.setClearColor(background, 0);
            }
            else {
                ic.renderer.setClearColor(background, 1);
            }
        }

        ic.perspectiveCamera = new THREE.PerspectiveCamera(20, ic.container.whratio, 0.1, 10000);
        ic.perspectiveCamera.position.set(0, 0, ic.cam_z);
        ic.perspectiveCamera.lookAt(new THREE.Vector3(0, 0, 0));

        ic.orthographicCamera = new THREE.OrthographicCamera();
        ic.orthographicCamera.position.set(0, 0, ic.cam_z);
        ic.orthographicCamera.lookAt(new THREE.Vector3(0, 0, 0));

        ic.cams = {
            perspective: ic.perspectiveCamera,
            orthographic: ic.orthographicCamera,
        };       
    };

    setVrAr() { let ic = this.icn3d, me = ic.icn3dui;

        // https://github.com/NikLever/Learn-WebXR/tree/master/start
        // https://github.com/mrdoob/three.js/blob/master/examples/webxr_ar_cones.html
        // https://github.com/mrdoob/three.js/blob/master/examples/webxr_vr_cubes.html

        if(ic.bVr) {
/*            
            ic.raycasterVR = new THREE.Raycaster();
            ic.workingMatrix = new THREE.Matrix4();
            ic.workingVector = new THREE.Vector3();
            ic.origin = new THREE.Vector3();

            let radius = 0.08;
            let geometry = new THREE.IcosahedronBufferGeometry( radius, 2 );
            ic.highlightVR = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: 0xffffff, side: THREE.BackSide } ) );
            ic.highlightVR.scale.set(1.2, 1.2, 1.2);        
*/
            // modified from https://github.com/NikLever/Learn-WebXR/blob/master/complete/lecture3_7/app.js
            // add dolly to move camera
            ic.dolly = new THREE.Object3D();
            ic.dolly.position.z = 5;
            ic.dolly.add(ic.cam);
            ic.scene.add(ic.dolly);

            ic.dummyCam = new THREE.Object3D();
            ic.cam.add(ic.dummyCam);

            ic.clock = new THREE.Clock();

            //controllers
            ic.controllers = this.getControllers();

            ic.getInputSources = true; // default

            function onSelectStart() {
//                this.children[0].scale.z = 10;
                this.userData.selectPressed = true;
            }
    
            function onSelectEnd() {
//                this.children[0].scale.z = 0;
//                ic.highlightVR.visible = false;
                this.userData.selectPressed = false;
            }
/*
            function buildController( data ) {
                let geometry, material;
            
                switch ( data.targetRayMode ) {
                    case 'tracked-pointer':
                        geometry = new THREE.BufferGeometry();
                        geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 0, - 1 ], 3 ) );
                        geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( [ 0.5, 0.5, 0.5, 0, 0, 0 ], 3 ) );
            
                        material = new THREE.LineBasicMaterial( { vertexColors: true, blending: THREE.AdditiveBlending } );
            
                        return new THREE.Line( geometry, material );
            
                    case 'gaze':
                        geometry = new THREE.RingGeometry( 0.02, 0.04, 32 ).translate( 0, 0, - 1 );
                        material = new THREE.MeshBasicMaterial( { opacity: 0.5, transparent: true } );
                        return new THREE.Mesh( geometry, material );
                }
            }
*/
            ic.controllers.forEach( (controller) => {
                controller.addEventListener( 'selectstart', onSelectStart );
                controller.addEventListener( 'selectend', onSelectEnd );
/*                
                controller.addEventListener( 'connected', function ( event ) {
                    const mesh = buildController(event.data);
                    mesh.scale.z = 0;
                    this.add( mesh );
                } );
                controller.addEventListener( 'disconnected', function () {
                    this.remove( this.children[ 0 ] );
                    ic.controllers.forEach( (controllerTmp) => {
                        controllerTmp = null;
                    });
                    //self.controllerGrip = null;
                } );
*/                
            });         
        }      
        else if(ic.bAr) {
            //Add gestures here
            ic.gestures = new ControllerGestures(ic.renderer);
            ic.scene.add(ic.gestures.controller1);
            ic.scene.add(ic.gestures.controller2);

            ic.gestures.addEventListener('tap', (ev) => {
                //if(!ic.mdl.visible) {
                //    ic.mdl.visible = true;
                //}

                const controller = ic.gestures.controller1; 
                //ic.mdl.position.set( 0, 0, - 0.3 ).applyMatrix4( controller.matrixWorld );
                ic.mdl.position.set( -0.03, 0, - 0.3 ).applyMatrix4( controller.matrixWorld );
                //ic.mdl.scale.copy(ic.mdl.scale.multiplyScalar(0.1));
                ic.mdl.scale.copy(new THREE.Vector3( 0.001, 0.001, 0.001 ));  
            });

            ic.gestures.addEventListener('doubletap', (ev) => {
                const controller = ic.gestures.controller1; 
                //ic.mdl.position.set( 0, 0, - 0.3 ).applyMatrix4( controller.matrixWorld );
                ic.mdl.position.set( -0.06, 0, - 0.6 ).applyMatrix4( controller.matrixWorld );
                //ic.mdl.scale.copy(ic.mdl.scale.multiplyScalar(10));
                ic.mdl.scale.copy(new THREE.Vector3( 0.005, 0.005, 0.005 )); 
            });
/*
            ic.gestures.addEventListener('swipe', (ev) => {
                // if(ic.mdl.visible) {
                //     ic.mdl.visible = false;
                // }
            });
  
            ic.gestures.addEventListener('pan', (ev) => {
                // if(ev.initialise !== undefined) {
                //     thisClass.startPosition = ic.mdl.position.clone();
                // }
                // else {
                //     const pos = thisClass.startPosition.clone().add(ev.delta.multiplyScalar(3));
                //     ic.mdl.position.copy(pos);
                // }
            });

            ic.gestures.addEventListener('pinch', (ev) => {
                // if(ev.initialise !== undefined) {
                //     thisClass.startScale = ic.mdl.scale.clone();                   
                // }
                // else {
                //     const scale = thisClass.startScale.clone().multiplyScalar(ev.scale);                  
                //     ic.mdl.scale.copy(scale);
                // }
            });
 
            ic.gestures.addEventListener('rotate', (ev) => {
                // if(ev.initialise !== undefined) {
                //     thisClass.startQuaternion = ic.mdl.quaternion.clone();
                // }
                // else {
                //     ic.mdl.quaternion.copy(thisClass.startQuaternion);
                //     ic.mdl.rotateY(ev.theta);
                // }
            });  
*/                            
        }

        if(!me.bNode) {
            $("#" + me.pre + "VRButton").remove();
            $("#" + me.pre + "viewer").get(0).appendChild( ic.VRButtonCls.createButton( ic.renderer ) );

            $("#" + me.pre + "ARButton").remove();
            $("#" + me.pre + "viewer").get(0).appendChild( ic.ARButtonCls.createButton( ic.renderer ) );
        }
    }

    getControllers() { let ic = this.icn3d; ic.icn3dui;
        const controllerModelFactory = new XRControllerModelFactory();
/*        
        const geometry = new THREE.BufferGeometry().setFromPoints( [
            new THREE.Vector3(0,0,0),
            new THREE.Vector3(0,0,-1)
        ]);
        const line = new THREE.Line( geometry );
        line.name = 'line';
        line.scale.z = 0;
*/

        const controllers = [];
        
        for(let i=0; i<=1; i++){
            const controller = ic.renderer.xr.getController( i );
            ic.dolly.add( controller );

//            controller.add( line.clone() );
            controller.userData.selectPressed = false;
            ic.scene.add(controller);
            
            controllers.push( controller );
            
            const grip = ic.renderer.xr.getControllerGrip( i );
            grip.add( controllerModelFactory.createControllerModel( grip ));
            ic.scene.add( grip );
        }
        
        return controllers;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Camera {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Set the camera according to the size of the structure.
    setCamera() { let ic = this.icn3d, me = ic.icn3dui;
        if(ic.bControlGl && !me.bNode) {
            window.cam = ic.cams[ic.opts.camera.toLowerCase()];

            let maxD = ic.maxD;

            if(window.cam === ic.perspectiveCamera) {
                let bInstance = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > ic.maxatomcnt) ? true : false;
                //var factor = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) ? 1 : 2;
                //var factor = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) ? 1 : 3;
                if(bInstance) {
                    window.camMaxDFactor = 1;
                }
                else if(window.camMaxDFactorFog !== undefined) {
                    window.camMaxDFactor = window.camMaxDFactorFog; // 3
                }
                else {
                    window.camMaxDFactor = 3; //2;
                }

                if(window.cam_z > 0) {
                  window.cam.position.z = maxD * window.camMaxDFactor; // for perspective, the z position should be large enough to see the whole molecule
                }
                else {
                  window.cam.position.z = -maxD * window.camMaxDFactor; // for perspective, the z position should be large enough to see the whole molecule
                }

                if(ic.opts['slab'] === 'yes') {
                    if(bInstance) {
                        window.cam.near = 0.1;
                    }
                    else if(window.camMaxDFactorFog !== undefined) {
                        window.cam.near = maxD * window.camMaxDFactorFog - 10; // keep some surrounding residues
                    }
                    else {
                        window.cam.near = maxD * window.camMaxDFactor;
                    }
                }
                else {
                    window.cam.near = 0.1;
                }
                window.cam.far = 10000;

                if(ic.bControlGl && !me.bNode) {
                    window.controls = new THREE.TrackballControls( window.cam, undefined, ic );
                }
                else {
                    if(!me.bNode) {
                        ic.controls = new THREE.TrackballControls( ic.cam, document.getElementById(ic.id), ic );
                    }
                    else {
                        ic.controls = new THREE.TrackballControls( ic.cam, document, ic );
                    }
                }
            }
            else if (window.cam === ic.orthographicCamera){
                if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) {
                    window.cam.right = ic.maxD/2 * 1.5;
                }
                else {
                    window.cam.right = ic.maxD/2 * 2.5;
                }

                window.cam.left = -window.cam.right;
                window.cam.top = window.cam.right /ic.container.whratio;
                window.cam.bottom = -window.cam.right /ic.container.whratio;

                  if(ic.opts['slab'] === 'yes') {
                      window.cam.near = ic.maxD * 2;
                  }
                  else {
                    window.cam.near = 0;
                  }

                  window.cam.far = 10000;

                if(ic.bControlGl && !me.bNode) {
                    window.controls = new THREE.OrthographicTrackballControls( window.cam, undefined, ic );
                }
                else {
                    if(!me.bNode) {
                        ic.controls = new THREE.OrthographicTrackballControls( ic.cam, document.getElementById(ic.id), ic );
                    }
                    else {
                        ic.controls = new THREE.OrthographicTrackballControls( ic.cam, document, ic );
                    }
                }
            }

            window.cam.updateProjectionMatrix();
        }
    //    else {
            // also set its own camera for picking purpose

            ic.cam = ic.cams[ic.opts.camera.toLowerCase()];

            let maxD = ic.maxD;

            if(ic.cam === ic.perspectiveCamera) {
                let bInstance = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > ic.maxatomcnt) ? true : false;
                //var factor = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) ? 1 : 2;
                //var factor = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) ? 1 : 3;
                if(bInstance) {
                    ic.camMaxDFactor = 1;
                }
                else if(ic.camMaxDFactorFog !== undefined) {
                    ic.camMaxDFactor = ic.camMaxDFactorFog; // 3
                }
                else {
                    ic.camMaxDFactor = 3; //2;
                }

                if(ic.cam_z > 0) {
                  ic.cam.position.z = maxD * ic.camMaxDFactor; // forperspective, the z positionshould be large enough to see the whole molecule
                }
                else {
                  ic.cam.position.z = -maxD * ic.camMaxDFactor; // forperspective, the z positionshould be large enough to see the whole molecule
                }

                if(ic.opts['slab'] === 'yes') {
                    if(bInstance) {
                        ic.cam.near = 0.1;
                    }
                    else if(ic.camMaxDFactorFog !== undefined) {
                        ic.cam.near = maxD * ic.camMaxDFactorFog - 10; // keep some surrounding residues
                    }
                    else {
                        ic.cam.near = maxD * ic.camMaxDFactor;
                    }
                }
                else {
                    ic.cam.near = 0.1;
                }
                ic.cam.far = 10000;

                if(ic.bControlGl && !me.bNode) {
                    window.controls = new THREE.TrackballControls( ic.cam, undefined, ic );
                }
                else {
                    if(!me.bNode) {
                        ic.controls = new THREE.TrackballControls( ic.cam, document.getElementById(ic.id), ic );
                    }
                    else {
                        ic.controls = new THREE.TrackballControls( ic.cam, document, ic );
                    }
                }
            }
            else if (ic.cam === ic.orthographicCamera){
                if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) {
                    ic.cam.right = ic.maxD/2 * 1.5;
                }
                else {
                    ic.cam.right = ic.maxD/2 * 2.5;
                }

                ic.cam.left = -ic.cam.right;
                ic.cam.top = ic.cam.right /ic.container.whratio;
                ic.cam.bottom = -ic.cam.right /ic.container.whratio;

                  if(ic.opts['slab'] === 'yes') {
                      ic.cam.near = ic.maxD * 2;
                  }
                  else {
                    ic.cam.near = 0;
                  }

                  ic.cam.far = 10000;

                if(ic.bControlGl && !me.bNode) {
                    window.controls = new THREE.OrthographicTrackballControls( ic.cam, undefined, ic );
                }
                else {
                    if(!me.bNode) {
                        ic.controls = new THREE.OrthographicTrackballControls( ic.cam, document.getElementById(ic.id), ic );
                    }
                    else {
                        ic.controls = new THREE.OrthographicTrackballControls( ic.cam, document, ic );
                    }
                }
            }

            ic.cam.updateProjectionMatrix();
    //    }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Fog {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    setFog(bZoomin) { let ic = this.icn3d, me = ic.icn3dui;
        let background = me.parasCls.backgroundColors[ic.opts.background.toLowerCase()];

        if(bZoomin) {
            let centerAtomsResults = ic.applyCenterCls.centerAtoms(ic.hAtoms);
            ic.maxD = centerAtomsResults.maxD;
            //if (ic.maxD < 5) ic.maxD = 5;
            if (ic.maxD < 25) ic.maxD = 25;
        }

        let bInstance = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > ic.maxatomcnt) ? true : false;

        // apply fog
        if(ic.opts['fog'] === 'yes') {
            if(ic.opts['camera'] === 'perspective') {        //perspective, orthographic
                //ic.scene.fog = new THREE.Fog(background, ic.cam_z, ic.cam_z + 0.5 * ic.maxD);
                //ic.scene.fog = new THREE.Fog(background, 2 * ic.maxD, 2.5 * ic.maxD);
                //ic.scene.fog = new THREE.Fog(background, 1.5 * ic.maxD, 3 * ic.maxD);

                if(bInstance) {
                    ic.scene.fog = undefined;
                    ic.bSetFog = false;
                }
                else {
                    // adjust
                    let zoomFactor = (ic._zoomFactor > 1) ? ic._zoomFactor * 1.0 : ic._zoomFactor;
                    ic.scene.fog = new THREE.Fog(background, 2.5 * ic.maxD * zoomFactor, 4 * ic.maxD * zoomFactor);
                    ic.bSetFog = true;
                    ic.camMaxDFactorFog = 3;
                }
            }
            else if(ic.opts['camera'] === 'orthographic') {
                //ic.scene.fog = new THREE.FogExp2(background, 2);
                //ic.scene.fog.near = 1.5 * ic.maxD;
                //ic.scene.fog.far = 3 * ic.maxD;

                ic.scene.fog = undefined;
                ic.bSetFog = false;
            }
        }
        else {
            ic.scene.fog = undefined;
            ic.bSetFog = false;
        }

        //if(bZoomin && !bInstance) {
        //    ic.transformCls.zoominSelection();
        //}
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Box {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Create a cube for "atom" with the "defaultRadius". "forceDefault" means to use the default radius.
    //"scale" means scale on the radius. "color" means the color of the cube. "bHighlight" is an option
    //to draw the highlight for the atom.
    createBox(atom, defaultRadius, forceDefault, scale, color, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        if(defaultRadius === undefined) defaultRadius = 0.8;
        if(forceDefault === undefined) forceDefault = false;
        if(scale === undefined) scale = 0.8;

        if(bHighlight) {
            if(color === undefined) color = ic.hColor;
        }
        else {
            if(color === undefined) color = atom.color;
        }

        let radius = forceDefault ? defaultRadius
          : (me.parasCls.vdwRadii[atom.elem.toUpperCase()] || defaultRadius) * (scale ? scale : 1);

        this.createBox_base(atom.coord, radius, color, bHighlight);
    }

    createBox_base(coord, radius, color, bHighlight, bOther, bGlycan, opacity) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let mesh;

        if(opacity === undefined) opacity = (bGlycan) ? 0.5 : 1.0;

        new THREE.BoxGeometry(1, 1, 1);

        //if(bHighlight || bGlycan) {
          mesh = new THREE.Mesh(ic.boxGeometry, new THREE.MeshPhongMaterial({ transparent: true, opacity: opacity,
              specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));
        // }
        // else {
        //   mesh = new THREE.Mesh(ic.boxGeometry, new THREE.MeshPhongMaterial({
        //       specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));
        // }

        mesh.scale.x = mesh.scale.y = mesh.scale.z = radius;

        mesh.position.copy(coord);
        ic.mdl.add(mesh);

        if(bHighlight) {
            ic.prevHighlightObjects.push(mesh);
        }
        else if(bOther) {
            ic.prevOtherMesh.push(mesh);
        }
        else {
            ic.objects.push(mesh);
        }
    }

    createBoxRepresentation_P_CA(atoms, scale, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let thisClass = this;
        ic.reprSubCls.createRepresentationSub(atoms, function (atom0) {
            if(atom0.name === 'CA' || atom0.name === "O3'" || atom0.name === "O3*") {
                thisClass.createBox(atom0, undefined, undefined, scale, undefined, bHighlight);
            }
        });
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Brick {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    createBrick(p0, p1, radius, color) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let cylinderGeometry = new THREE.CylinderGeometry(1, 1, 1, 4, 1);

        let mesh = new THREE.Mesh(cylinderGeometry, new THREE.MeshPhongMaterial(
            { specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));

        mesh.position.copy(p0).add(p1).multiplyScalar(0.5);
        mesh.matrixAutoUpdate = false;
        mesh.lookAt(p1.clone().sub(p0));
        mesh.updateMatrix();

        mesh.matrix.multiply(new THREE.Matrix4().makeScale(radius, radius,
          p0.distanceTo(p1))).multiply(new THREE.Matrix4().makeRotationX(Math.PI * 0.5));

        ic.mdl.add(mesh);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class CurveStripArrow {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    createCurveSubArrow(p, width, colors, div, bHighlight, bRibbon, num, positionIndex,
      pntsCA, prevCOArray, bShowArray, calphaIdArray, bShowArrow, prevone, nexttwo) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let divPoints = [], positions = [];

        divPoints.push(p);
        positions.push(positionIndex);

        this.prepareStrand(divPoints, positions, width, colors, div, undefined, bHighlight, bRibbon, num,
          pntsCA, prevCOArray, false, bShowArray, calphaIdArray, bShowArrow, prevone, nexttwo);

        divPoints = [];
        positions = [];
    }

    createStripArrow(p0, p1, colors, div, thickness, bHighlight, num, start, end,
      pntsCA, prevCOArray, bShowArray, calphaIdArray, bShowArrow, prevone, nexttwo) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let divPoints = [], positions = [];

        divPoints.push(p0);
        divPoints.push(p1);
        positions.push(start);
        positions.push(end);

        this.prepareStrand(divPoints, positions, undefined, colors, div, thickness, bHighlight, undefined, num,
          pntsCA, prevCOArray, true, bShowArray, calphaIdArray, bShowArrow, prevone, nexttwo);

        divPoints = [];
        positions = [];
    }

    /**
     * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
     */

    prepareStrand(divPoints, positions, width, colors, div, thickness, bHighlight, bRibbon, num,
      pntsCA, prevCOArray, bStrip, bShowArray, calphaIdArray, bShowArrow, prevone, nexttwo) { let ic = this.icn3d, me = ic.icn3dui;
        if(pntsCA.length === 1) {
            return;
        }

        let oriColors = colors;
        let bHelix = (bShowArrow) ? false : true;

        let colorsLastTwo = [];
        colorsLastTwo.push(colors[colors.length - 2]);
        colorsLastTwo.push(colors[colors.length - 1]);

        div = div || ic.axisDIV;
        let numM1Inv2 = 2 / (num - 1);
        let delta, lastCAIndex, lastPrevCOIndex, v;

        let pnts = {};
        for(let i = 0, il = positions.length; i < il; ++i) pnts[i] = [];

        // smooth C-alpha
        let pnts_clrs = me.subdivideCls.subdivide(pntsCA, colors, div, undefined, undefined, prevone, nexttwo);
        let pntsCASmooth = pnts_clrs[0]; // get all smoothen pnts, do not use 'bShowArray'
        //colors = pnts_clrs[2];

        if(pntsCASmooth.length === 1) {
            return;
        }

        // draw the sheet without the last residue
        // use the sheet coord for n-2 residues
        let colorsTmp = [];
        let i, lastIndex = (bShowArrow === undefined || bShowArrow) ? pntsCA.length - 2 : pntsCA.length;

        let il = lastIndex;
        for (i = 0; i < il; ++i) {
            for(let index = 0, indexl = positions.length; index < indexl; ++index) {
                pnts[index].push(divPoints[index][i]);
            }
            colorsTmp.push(colors[i]);
        }
        colorsTmp.push(colors[i]);

        if(bShowArrow === undefined || bShowArrow) {
            // assign the sheet coord from C-alpha for the 2nd to the last residue of the sheet
            for(let i = 0, il = positions.length; i < il; ++i) {
                delta = -1 + numM1Inv2 * positions[i];
                lastCAIndex = pntsCASmooth.length - 1 - div;
                lastPrevCOIndex = pntsCA.length - 2;
                v = new THREE.Vector3(pntsCASmooth[lastCAIndex].x + prevCOArray[lastPrevCOIndex].x * delta,
                  pntsCASmooth[lastCAIndex].y + prevCOArray[lastPrevCOIndex].y * delta,
                  pntsCASmooth[lastCAIndex].z + prevCOArray[lastPrevCOIndex].z * delta);
                pnts[i].push(v);
            }
        }

        let posIndex = [];
        let results;
        for(let i = 0, il = positions.length; i < il; ++i) {
            results = me.subdivideCls.subdivide(pnts[i], colorsTmp, div, bShowArray, bHighlight);
            pnts[i] = results[0];
            colors = results[2];
            if(i === 0) {
                posIndex = results[1];
            }
        }

        if(bStrip) {
            if(bHelix) {
                if(!ic.bDoublecolor) {
                    ic.stripCls.createStrip(pnts[0], pnts[1], colors, div, thickness, bHighlight, true,
                      undefined, calphaIdArray, posIndex, prevone, nexttwo, pntsCA, prevCOArray);
                }
                else {
                    ic.stripCls.createStrip(pnts[0], pnts[1], oriColors, div, thickness, bHighlight, true,
                      undefined, calphaIdArray, posIndex, prevone, nexttwo, pntsCA, prevCOArray);
                }
            }
            else {
                ic.stripCls.createStrip(pnts[0], pnts[1], colors, div, thickness, bHighlight, true,
                  undefined, calphaIdArray, posIndex, prevone, nexttwo);
            }
        }
        else {
            ic.curveCls.createCurveSub(pnts[0], width, colors, div, bHighlight, bRibbon, true,
              undefined, calphaIdArray, posIndex, prevone, nexttwo);
        }

        if(bShowArrow === undefined || bShowArrow) {
            // draw the arrow
            colorsTmp = [];

            posIndex = [];
            for(let index = 0, indexl = positions.length; index < indexl; ++index) {
                pnts[index] = [];

                for (let i = div * (pntsCA.length - 2), il = div * (pntsCA.length - 1);
                  bShowArray[parseInt(i/div)] && i < il; i = i + div) {
                    let pos = parseInt(i/div);
                    for (let j = 0; j < div; ++j) {
                        let delta = -1 + numM1Inv2 * positions[index];
                        let scale = 1.8; // scale of the arrow width
                        delta = delta * scale * (div - j) / div;
                        let oriIndex = parseInt(i/div);

                        let v = new THREE.Vector3(pntsCASmooth[i+j].x + prevCOArray[oriIndex].x * delta,
                          pntsCASmooth[i+j].y + prevCOArray[oriIndex].y * delta,
                          pntsCASmooth[i+j].z + prevCOArray[oriIndex].z * delta);
                        v.smoothen = true;
                        pnts[index].push(v);
                        colorsTmp.push(colorsLastTwo[0]);
                        if(index === 0) posIndex.push(pos);
                    }
                }

                // last residue
                // make the arrow end with 0
                let delta = 0;
                let lastCAIndex = pntsCASmooth.length - 1;
                let lastPrevCOIndex = pntsCA.length - 1;

                //if(bShowArray[lastPrevCOIndex]) {
                    let v = new THREE.Vector3(pntsCASmooth[lastCAIndex].x + prevCOArray[lastPrevCOIndex].x * delta,
                      pntsCASmooth[lastCAIndex].y + prevCOArray[lastPrevCOIndex].y * delta,
                      pntsCASmooth[lastCAIndex].z + prevCOArray[lastPrevCOIndex].z * delta);
                    v.smoothen = true;
                    pnts[index].push(v);
                    colorsTmp.push(colorsLastTwo[1]);
                    if(index === 0) posIndex.push(lastCAIndex);
                //}
            }

            pntsCASmooth = [];

            //colorsTmp.push(colors[colors.length - 2]);
            //colorsTmp.push(colors[colors.length - 1]);

            if(bStrip) {
                ic.stripCls.createStrip(pnts[0], pnts[1], colorsTmp, div, thickness, bHighlight, true,
                  undefined, undefined, posIndex, prevone, nexttwo);
            }
            else {
                ic.curveCls.createCurveSub(pnts[0], width, colorsTmp, div, bHighlight, bRibbon, true,
                  undefined, undefined, posIndex, prevone, nexttwo);
            }
        }

        for(let i in pnts) {
            for(let j = 0, jl = pnts[i].length; j < jl; ++j) {
                pnts[i][j] = null;
            }
            pnts[i] = [];
        }

        pnts = {};
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Curve {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://star.cse.cuhk.edu.hk/iview/)
    createCurveSub(_pnts, width, colors, div, bHighlight, bRibbon, bNoSmoothen, bShowArray, calphaIdArray, positions, prevone, nexttwo) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        if (_pnts.length === 0) return;
        div = div || 5;
        let pnts;
        if(!bNoSmoothen) {
            let bExtendLastRes = true;
            let pnts_clrs = me.subdivideCls.subdivide(_pnts, colors, div, bShowArray, bHighlight, prevone, nexttwo, bExtendLastRes);
            pnts = pnts_clrs[0];
            colors = pnts_clrs[2];
        }
        else {
            pnts = _pnts;
        }
        if (pnts.length === 0) return;

        ic.stripCls.setCalphaDrawnCoord(pnts, div, calphaIdArray);

        if(bHighlight === 1) {
            let radius = ic.coilWidth / 2;
            //var radiusSegments = 8;
            let radiusSegments = 4; // save memory
            let closed = false;

            if(pnts.length > 1) {
                if(positions !== undefined) {
                    let currPos, prevPos;
                    let currPoints = [];
                    for(let i = 0, il = pnts.length; i < il; ++i) {
                        currPos = positions[i];

                        if( (currPos !== prevPos && parseInt(currPos) !== parseInt(prevPos) + 1 && prevPos !== undefined) || (i === il -1) ) {
                            // first tube
                            let geometry0 = new THREE.TubeGeometry(
                                new THREE.CatmullRomCurve3(currPoints), // path
                                currPoints.length, // segments
                                radius,
                                radiusSegments,
                                closed
                            );

                            let mesh = new THREE.Mesh(geometry0, ic.matShader);
                            mesh.renderOrder = ic.renderOrderPicking;
                            //ic.mdlPicking.add(mesh);
                            ic.mdl.add(mesh);

                            ic.prevHighlightObjects.push(mesh);

                            geometry0 = null;

                            currPoints = [];
                        }

                        currPoints.push(pnts[i]);

                        prevPos = currPos;
                    }

                    currPoints = [];
                }
                else {
                    let geometry0 = new THREE.TubeGeometry(
                        new THREE.CatmullRomCurve3(pnts), // path
                        pnts.length, // segments
                        radius,
                        radiusSegments,
                        closed
                    );

                    let mesh = new THREE.Mesh(geometry0, ic.matShader);
                    mesh.renderOrder = ic.renderOrderPicking;
                    //ic.mdlPicking.add(mesh);
                    ic.mdl.add(mesh);

                    ic.prevHighlightObjects.push(mesh);

                    geometry0 = null;
                }
            }
        }
        else {
            //var geo = new THREE.Geometry();
            let geo = new THREE.BufferGeometry();

            let verticeArray = [], colorArray = [];

            let offset = 0, color;
            if(bHighlight === 2 && bRibbon) {
                for (let i = 0; i < pnts.length; ++i, offset += 3) {
                    // shift the highlight a little bit to avoid the overlap with ribbon
                    pnts[i].addScalar(0.6); // ic.ribbonthickness is 0.4
                    //geo.vertices.push(pnts[i]);
                    //geo.colors.push(me.parasCls.thr(colors[i]));

                    //vertices = vertices.concat(pnts[i].toArray());
                    verticeArray[offset] = pnts[i].x;
                    verticeArray[offset+1] = pnts[i].y;
                    verticeArray[offset+2] = pnts[i].z;

                    //colors = colors.concat(me.parasCls.thr(colors[i]).toArray());
                    color = me.parasCls.thr(colors[i]);

                    colorArray[offset] = color.r;
                    colorArray[offset+1] = color.g;
                    colorArray[offset+2] = color.b;
                }
            }
            else {
                for (let i = 0; i < pnts.length; ++i, offset += 3) {
                    //geo.vertices.push(pnts[i]);
                    //geo.colors.push(me.parasCls.thr(colors[i]));

                    //vertices = vertices.concat(pnts[i].toArray());
                    verticeArray[offset] = pnts[i].x;
                    verticeArray[offset+1] = pnts[i].y;
                    verticeArray[offset+2] = pnts[i].z;

                    //colors = colors.concat(me.parasCls.thr(colors[i]).toArray());
                    color = me.parasCls.thr(colors[i]);

                    colorArray[offset] = color.r;
                    colorArray[offset+1] = color.g;
                    colorArray[offset+2] = color.b;
                }
            }

            let nComp = 3;
            geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(verticeArray), nComp));
            geo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), nComp));

            //geo.computeVertexNormals();

            //var line = new THREE.Line(geo, new THREE.LineBasicMaterial({ linewidth: width, vertexColors: true }), THREE.LineStrip);
            let line = new THREE.Line(geo, new THREE.LineBasicMaterial({ linewidth: width, vertexColors: true }));
            ic.mdl.add(line);
            if(bHighlight === 2) {
                ic.prevHighlightObjects.push(line);
            }
            else {
                ic.objects.push(line);
            }
        }

        pnts = null;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Cylinder {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    createCylinder(p0, p1, radius, color, bHighlight, color2, bPicking, bGlycan, opacity) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let opacity_ori = opacity;
        if(opacity === undefined) opacity = (bGlycan) ? 0.5 : 1.0;

        let mesh;
        if(bHighlight === 1) {
            mesh = new THREE.Mesh(ic.cylinderGeometryOutline, ic.matShader);

            mesh.position.copy(p0).add(p1).multiplyScalar(0.5);
            mesh.matrixAutoUpdate = false;
            mesh.lookAt(p1.clone().sub(p0));
            mesh.updateMatrix();

            mesh.matrix.multiply(new THREE.Matrix4().makeScale(radius, radius,
              p0.distanceTo(p1))).multiply(new THREE.Matrix4().makeRotationX(Math.PI * 0.5));

            mesh.renderOrder = ic.renderOrderPicking;
            ic.mdl.add(mesh);

            ic.prevHighlightObjects.push(mesh);
        }
        else {
            if(bHighlight === 2) {
              mesh = new THREE.Mesh(ic.cylinderGeometry, new THREE.MeshPhongMaterial(
                  { transparent: true, opacity: opacity, specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));

              radius *= 1.5;
            }
            //else if(bGlycan) {
            else {
              mesh = new THREE.Mesh(ic.cylinderGeometry, new THREE.MeshPhongMaterial(
                  { transparent: true, opacity: opacity, specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));
            }
            // else {
            //   mesh = new THREE.Mesh(ic.cylinderGeometry, new THREE.MeshPhongMaterial(
            //       { specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));
            // }

            mesh.position.copy(p0).add(p1).multiplyScalar(0.5);
            mesh.matrixAutoUpdate = false;
            mesh.lookAt(p1.clone().sub(p0));
            mesh.updateMatrix();

            mesh.matrix.multiply(new THREE.Matrix4().makeScale(radius, radius, p0.distanceTo(p1))).multiply(
                new THREE.Matrix4().makeRotationX(Math.PI * 0.5));

            if(ic.bImpo && !opacity_ori && !bGlycan) {
              ic.posArray.push(p0.x);
              ic.posArray.push(p0.y);
              ic.posArray.push(p0.z);

              if(!color) color = me.parasCls.thr(0xFFFFFF);
              ic.colorArray.push(color.r);
              ic.colorArray.push(color.g);
              ic.colorArray.push(color.b);

              ic.pos2Array.push(p1.x);
              ic.pos2Array.push(p1.y);
              ic.pos2Array.push(p1.z);

              if(color2 !== undefined) {
                  ic.color2Array.push(color2.r);
                  ic.color2Array.push(color2.g);
                  ic.color2Array.push(color2.b);
              }
              else {
                  ic.color2Array.push(color.r);
                  ic.color2Array.push(color.g);
                  ic.color2Array.push(color.b);
              }

              ic.radiusArray.push(radius);

              if(ic.cnt <= ic.maxatomcnt) ic.mdl_ghost.add(mesh);
            }
            else {
                ic.mdl.add(mesh);
            }

            if(bHighlight === 2) {
                if(ic.bImpo && !opacity_ori) {
                    if(ic.cnt <= ic.maxatomcnt) ic.prevHighlightObjects_ghost.push(mesh);
                }
                else {
                    ic.prevHighlightObjects.push(mesh);
                }
            }
            else {
                if(ic.bImpo && !opacity_ori) {
                    if(ic.cnt <= ic.maxatomcnt) ic.objects_ghost.push(mesh);
                }
                else {
                    if(bPicking === undefined || bPicking) ic.objects.push(mesh);
                }
            }
        }
    }

    createCylinder_base(p0, p1, radius, color, bHighlight, color2, bPicking) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let mesh = new THREE.Mesh(ic.cylinderGeometry, new THREE.MeshPhongMaterial(
            { specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));

        mesh.position.copy(p0).add(p1).multiplyScalar(0.5);
        mesh.matrixAutoUpdate = false;
        mesh.lookAt(p1.clone().sub(p0));
        mesh.updateMatrix();

        mesh.matrix.multiply(new THREE.Matrix4().makeScale(radius, radius, p0.distanceTo(p1))).multiply(
            new THREE.Matrix4().makeRotationX(Math.PI * 0.5));

        return mesh;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create cylinders for alpha helices and ribbons for beta strands in "atoms".
    //"radius" is radius of the cylinders. "bHighlight" is an option to draw the highlight for these atoms.
    //The highlight could be outlines with bHighlight=1 and 3D objects with bHighlight=2 as mentioned above.
    createCylinderHelix(atoms, radius, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let start = null;
        let currentChain, currentResi;
        let others = {}, beta = {};
        let i;
        for (i in atoms) {
            let atom = atoms[i];
            if (atom.het) continue;
            if ((atom.ss !== 'helix' && atom.ss !== 'sheet') || atom.ssend || atom.ssbegin) others[atom.serial] = atom;
            if (atom.ss === 'sheet') beta[atom.serial] = atom;
            if (atom.name !== 'CA') continue;
            if (atom.ss === 'helix' && atom.ssend) {
                if (start !== null && currentChain === atom.chain && parseInt(currentResi) < parseInt(atom.resi)) {
                    if(bHighlight === 1 || bHighlight === 2) {
                        this.createCylinder(start.coord, atom.coord, radius, ic.hColor, bHighlight);
                    }
                    else {                
                        this.createCylinder(start.coord, atom.coord, radius, atom.color);
                    }
                }

                start = null;
            }

            if (start === null && atom.ss === 'helix' && atom.ssbegin) {
                start = atom;

                currentChain = atom.chain;
                currentResi = atom.resi;
            }
        }

        if(bHighlight === 1 || bHighlight === 2) {
            if(Object.keys(others).length > 0) ic.tubeCls.createTube(others, 'CA', ic.coilWidth, bHighlight);
            if(Object.keys(beta).length > 0) ic.strandCls.createStrand(beta, undefined, undefined, true, 0,
                ic.helixSheetWidth, false, ic.ribbonthickness * 2, bHighlight);
        }
        else {
            if(Object.keys(others).length > 0) ic.tubeCls.createTube(others, 'CA', ic.coilWidth);
            if(Object.keys(beta).length > 0) ic.strandCls.createStrand(beta, undefined, undefined, true, 0,
                ic.helixSheetWidth, false, ic.ribbonthickness * 2);
        }
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create small cylinders (thick lines) for "atoms", whose atom name should be in the array atomNameArray.
    //"radius" is radius of the small cylinders. "bLine" is an option to show the cylinders as lines.
    //"bHighlight" is an option to draw the highlight for these atoms. The highlight could be outlines
    //with bHighlight=1 and 3D objects with bHighlight=2 as mentioned above.
    createCylinderCurve(atoms, atomNameArray, radius, bLines, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let start = null;
        let currentChain, currentResi;
        let i;

        let atom, maxDistance = 8.0; // max residue-residue (or nucleitide-nucleitide) distance allowed

        let chainid, currentChainid;

        for (i in atoms) {
            atom = atoms[i];
            if (atom.het) continue;

            chainid = atom.structure + '_' + atom.chain;
            currentChainid = atom.structure + '_' + currentChain;

            //if (atom.name !== atomName) continue;
            if(atomNameArray.indexOf(atom.name) == -1) continue;

            if (start !== null && currentChain === atom.chain 
                && ic.ParserUtilsCls.getResiNCBI(currentChainid, currentResi) + 1 === ic.ParserUtilsCls.getResiNCBI(chainid, atom.resi)
                && Math.abs(start.coord.x - atom.coord.x) < maxDistance
                && Math.abs(start.coord.y - atom.coord.y) < maxDistance
                && Math.abs(start.coord.z - atom.coord.z) < maxDistance ) {
                let middleCoord = start.coord.clone().add(atom.coord).multiplyScalar(0.5);

                if(!bHighlight) {
                    if(bLines) {
                        let line = ic.lineCls.createSingleLine( start.coord, middleCoord, start.color, false);
                        ic.mdl.add(line);
                        ic.objects.push(line);
                        line = ic.lineCls.createSingleLine( middleCoord, atom.coord, atom.color, false);
                        ic.mdl.add(line);
                        ic.objects.push(line);
                    }
                    else {
                        this.createCylinder(start.coord, middleCoord, radius, start.color);
                        this.createCylinder(middleCoord, atom.coord, radius, atom.color);
                        ic.sphereCls.createSphere(atom, radius, true, 1, bHighlight);
                    }
                }
                else if(bHighlight === 1) {
                    this.createCylinder(start.coord, middleCoord, radius, start.color, bHighlight);
                    this.createCylinder(middleCoord, atom.coord, radius, atom.color, bHighlight);
                    ic.sphereCls.createSphere(atom, radius, true, 1, bHighlight);
                }
            }

            start = atom;
            currentChain = atom.chain;
            currentResi = atom.resi;

            // create a sphere for each c-alpha
            ic.sphereCls.createSphere(atom, radius, true, 1, bHighlight);

            if(bHighlight === 2) ic.boxCls.createBox(atom, undefined, undefined, undefined, undefined, bHighlight);
        }
        if (start !== null && currentChain === atom.chain 
            && ic.ParserUtilsCls.getResiNCBI(currentChainid, currentResi) + 1 === ic.ParserUtilsCls.getResiNCBI(chainid, atom.resi)
            && Math.abs(start.coord.x - atom.coord.x) < maxDistance
            && Math.abs(start.coord.y - atom.coord.y) < maxDistance
            && Math.abs(start.coord.z - atom.coord.z) < maxDistance ) {
            let middleCoord = start.coord.add(atom.coord).multiplyScalar(0.5);
            if(!bHighlight) {
                if(bLines) {
                    let line = ic.lineCls.createSingleLine( start.coord, middleCoord, start.color, false);
                    ic.mdl.add(line);
                    ic.objects.push(line);
                    line = ic.lineCls.createSingleLine( middleCoord, atom.coord, atom.color, false);
                    ic.mdl.add(line);
                    ic.objects.push(line);
                }
                else {
                    this.createCylinder(start.coord, middleCoord, radius, start.color);
                    this.createCylinder(middleCoord, atom.coord, radius, atom.color);
                }
            }
            else if(bHighlight === 1) {
                this.createCylinder(start.coord, middleCoord, radius, start.color, bHighlight);
                this.createCylinder(middleCoord, atom.coord, radius, atom.color, bHighlight);
                ic.sphereCls.createSphere(atom, radius, true, 1, bHighlight);
            }
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Line$1 {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create lines for "atoms". "bHighlight" is an option to draw the highlight for these atoms.
    //The highlight could be outlines with bHighlight=1 and 3D objects with bHighlight=2.
    createLineRepresentation(atoms, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        //var geo = new THREE.Geometry();
        let geo = new THREE.BufferGeometry();
        let vertices = [], colors = [], offset = 0, offset2 = 0;

        ic.reprSubCls.createRepresentationSub(atoms, undefined, function (atom0, atom1) {
            if (atom0.color === atom1.color) {
                vertices[offset++] = atom0.coord.x;
                vertices[offset++] = atom0.coord.y;
                vertices[offset++] = atom0.coord.z;
                vertices[offset++] = atom1.coord.x;
                vertices[offset++] = atom1.coord.y;
                vertices[offset++] = atom1.coord.z;

                colors[offset2++] = atom0.color.r;
                colors[offset2++] = atom0.color.g;
                colors[offset2++] = atom0.color.b;
                colors[offset2++] = atom1.color.r;
                colors[offset2++] = atom1.color.g;
                colors[offset2++] = atom1.color.b;
            } else {
                let mp = atom0.coord.clone().add(atom1.coord).multiplyScalar(0.5);
                vertices[offset++] = atom0.coord.x;
                vertices[offset++] = atom0.coord.y;
                vertices[offset++] = atom0.coord.z;
                vertices[offset++] = mp.x;
                vertices[offset++] = mp.y;
                vertices[offset++] = mp.z;
                vertices[offset++] = atom1.coord.x;
                vertices[offset++] = atom1.coord.y;
                vertices[offset++] = atom1.coord.z;
                vertices[offset++] = mp.x;
                vertices[offset++] = mp.y;
                vertices[offset++] = mp.z;

                colors[offset2++] = atom0.color.r;
                colors[offset2++] = atom0.color.g;
                colors[offset2++] = atom0.color.b;
                colors[offset2++] = atom0.color.r;
                colors[offset2++] = atom0.color.g;
                colors[offset2++] = atom0.color.b;
                colors[offset2++] = atom1.color.r;
                colors[offset2++] = atom1.color.g;
                colors[offset2++] = atom1.color.b;
                colors[offset2++] = atom1.color.r;
                colors[offset2++] = atom1.color.g;
                colors[offset2++] = atom1.color.b;
            }
        });

        let nComp = 3;
        geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), nComp));
        geo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), nComp));

        //geo.computeVertexNormals();

        if(bHighlight !== 2) {
            let line;
            if(bHighlight === 1) ;
            else {
                line = new THREE.LineSegments(geo, new THREE.LineBasicMaterial(
                    { linewidth: ic.linewidth, vertexColors: true }));
                ic.mdl.add(line);
            }

            if(bHighlight === 1) {
                ic.prevHighlightObjects.push(line);
            }
            else {
                ic.objects.push(line);
            }
        }
        else if(bHighlight === 2) {
            ic.boxCls.createBoxRepresentation_P_CA(atoms, 0.8, bHighlight);
        }
    }

    createConnCalphSidechain(atoms, style) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        // find all residues with style2 as 'nothing' or undefined
        let residueHash = {};
        for(let i in atoms) {
            let atom = atoms[i];
            if(!atom.het && atom.style2 === style) {
                let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
                residueHash[resid] = 1;
            }
        }

        let coordArray = [];
        let colorArray = [];
        for(let resid in residueHash) {
            let atom = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid], 'CA');

            if(atom !== undefined) {
                for(let i = 0, il = atom.bonds.length; i < il; ++i) {
                    let bondAtom = ic.atoms[atom.bonds[i]];
                    // hydrogen connected to Calpha: HA
                    //if(bondAtom.name === 'HA' || (bondAtom.name !== 'C' && bondAtom.name !== 'N'
                    //  && bondAtom.elem !== 'H' && bondAtom.resi == atom.resi) ) {
                    if(bondAtom.name !== 'C' && bondAtom.name !== 'N'
                        && bondAtom.elem !== 'H' && bondAtom.resi == atom.resi) {
                        coordArray.push(atom.coord);
                        coordArray.push(bondAtom.coord);

                        colorArray.push(atom.color);
                        colorArray.push(bondAtom.color);
                    }
                }
            }
/*
            // hydrogen connected to N: H
            atom = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid], 'N');

            if(atom !== undefined) {
                for(let i = 0, il = atom.bonds.length; i < il; ++i) {
                    let bondAtom = ic.atoms[atom.bonds[i]];
                    // hydrogen connected to N: H
                    if(bondAtom.name === 'H') {
                        coordArray.push(atom.coord);
                        coordArray.push(bondAtom.coord);

                        colorArray.push(atom.color);
                        colorArray.push(bondAtom.color);
                    }
                }
            }
*/            
        }

        for(let i = 0, il = coordArray.length; i < il; i += 2) {
            if(style === 'ball and stick' || style === 'stick' || style === 'ball and stick2' || style === 'stick2') {
                let radius = (style === 'stick' || style === 'stick2') ? ic.cylinderRadius : ic.cylinderRadius * 0.5;
                ic.cylinderCls.createCylinder(coordArray[i], coordArray[i+1], radius, colorArray[i+1]);
            }
            else if(style === 'lines' || style === 'lines2') {
                let line = this.createSingleLine(coordArray[i], coordArray[i+1], colorArray[i+1], false, 0.5);
                ic.mdl.add(line);
            }
        }
    }

    createSingleLine( src, dst, colorHex, dashed, dashSize ) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        //var geom = new THREE.Geometry();
        let geo = new THREE.BufferGeometry();
        let vertices = [];

        let mat;

        if(dashed) {
            mat = new THREE.LineDashedMaterial({ linewidth: 1, color: colorHex, dashSize: dashSize, gapSize: 0.5*dashSize });
        } else {
            mat = new THREE.LineBasicMaterial({ linewidth: 1, color: colorHex });
        }

        vertices[0] = src.x;
        vertices[1] = src.y;
        vertices[2] = src.z;
        vertices[3] = dst.x;
        vertices[4] = dst.y;
        vertices[5] = dst.z;

        let nComp = 3;
        geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), nComp));

        //geo.computeVertexNormals();

        //if(dashed) geo.computeLineDistances(); // This one is SUPER important, otherwise dashed lines will appear as simple plain lines
        let axis = new THREE.LineSegments( geo, mat );
        if(dashed) axis.computeLineDistances(); // This one is SUPER important, otherwise dashed lines will appear as simple plain lines

        return axis;
    }

    // show extra lines, not used for pk, so no ic.objects
    //Create lines for a list of "lines", each of which has the properties 'position1', 'position2',
    //'color', and a boolean of 'dashed'.
    createLines(lines) {  let ic = this.icn3d, me = ic.icn3dui;
       if(me.bNode) return;

       if(lines !== undefined) {
         for(let name in lines) {
             let lineArray = lines[name];

             for(let i = 0, il = lineArray.length; i < il; ++i) {
               let line = lineArray[i];

               let p1 = line.position1;
               let p2 = line.position2;

               let dashed = (line.dashed) ? line.dashed : false;
               let dashSize = 0.3;

               let radius = (line.radius) ? line.radius : ic.lineRadius;
               let opacity = (line.opacity) ? line.opacity : 1.0;

               let colorStr = '#' + line.color.replace(/\#/g, '');

               let color = me.parasCls.thr(colorStr);

               if(!dashed) {
                    if(name == 'stabilizer') {
                        ic.brickCls.createBrick(p1, p2, radius, color);
                    }
                    else {
                        ic.cylinderCls.createCylinder(p1, p2, radius, color, undefined, undefined, undefined, undefined, opacity);
                    }
               }
               else {
                 let distance = p1.distanceTo(p2);

                 let nsteps = parseInt(distance / dashSize);
                 let step = p2.clone().sub(p1).multiplyScalar(dashSize/distance);

                 let start, end;
                 for(let j = 0; j < nsteps; ++j) {
                     if(j % 2 == 1) {
                          start = p1.clone().add(step.clone().multiplyScalar(j));
                          end = p1.clone().add(step.clone().multiplyScalar(j + 1));

                          if(name == 'stabilizer') {
                            ic.brickCls.createBrick(start, end, radius, color);
                          }
                          else {
                            ic.cylinderCls.createCylinder(start, end, radius, color, undefined, undefined, undefined, undefined, opacity);
                          }
                      }
                 }
               }
             }
         }
       }

       // do not add the artificial lines to raycasting objects
    };

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ReprSub {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // from iview (http://istar.cse.cuhk.edu.hk/iview/)
    createRepresentationSub(atoms, f0, f01) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;
        for (let i in atoms) {
            let atom0 = atoms[i];
            f0 && f0(atom0);

            for (let j in atom0.bonds) {
                let atom1 = this.icn3d.atoms[atom0.bonds[j]];
                if (atom1 === undefined || atom1.serial < atom0.serial) continue;
                if (atom1.chain === atom0.chain && ((atom1.resi === atom0.resi)
                  || (atom0.name === 'C' && atom1.name === 'N') || (atom0.name === 'O3\'' && atom1.name === 'P')
                  || (atom0.name === 'O3*' && atom1.name === 'P') || (atom0.name === 'SG' && atom1.name === 'SG'))) {
                    f01 && f01(atom0, atom1);
                }
            }
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Sphere$1 {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    createSphere(atom, defaultRadius, forceDefault, scale, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        if(defaultRadius === undefined) defaultRadius = 0.8;
        if(forceDefault === undefined) forceDefault = false;

        let radius = (me.parasCls.vdwRadii[atom.elem.toUpperCase()] || defaultRadius);
        if(forceDefault) {
            radius = defaultRadius;
            scale = 1;
        }

        this.createSphereBase(atom.coord, atom.color, radius, scale, bHighlight);
    }

    createSphereBase(pos, color, radius, scale, bHighlight, bGlycan, opacity) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let mesh;

        if(scale === undefined) scale = 1.0;

        let opacity_ori = opacity;
        if(opacity === undefined) opacity = (bGlycan) ? 0.5 : 1.0;

        if(bHighlight === 2) {
          scale *= 1.5;

          color = ic.hColor;

          mesh = new THREE.Mesh(ic.sphereGeometry, new THREE.MeshPhongMaterial({ transparent: true, opacity: opacity, specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));

          mesh.scale.x = mesh.scale.y = mesh.scale.z = radius * (scale ? scale : 1);
          mesh.position.copy(pos);
          ic.mdl.add(mesh);
        }
        else if(bHighlight === 1) {
          mesh = new THREE.Mesh(ic.sphereGeometry, ic.matShader);

          mesh.scale.x = mesh.scale.y = mesh.scale.z = radius * (scale ? scale : 1);
          mesh.position.copy(pos);
          mesh.renderOrder = ic.renderOrderPicking;
          ic.mdl.add(mesh);
        }
        else {
          if(color === undefined) {
              color = me.parasCls.defaultAtomColor;
          }
          
          //if(bGlycan) {
              mesh = new THREE.Mesh(ic.sphereGeometry, new THREE.MeshPhongMaterial({ transparent: true, opacity: opacity, specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));
        //   }
        //   else {
        //       mesh = new THREE.Mesh(ic.sphereGeometry, new THREE.MeshPhongMaterial({ specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color }));
        //   }

          mesh.scale.x = mesh.scale.y = mesh.scale.z = radius * (scale ? scale : 1);
          mesh.position.copy(pos);

          if(ic.bImpo && !opacity_ori && !bGlycan) {
              ic.posArraySphere.push(pos.x);
              ic.posArraySphere.push(pos.y);
              ic.posArraySphere.push(pos.z);

              ic.colorArraySphere.push(color.r);
              ic.colorArraySphere.push(color.g);
              ic.colorArraySphere.push(color.b);

              let realRadius = radius * (scale ? scale : 1);
              ic.radiusArraySphere.push(realRadius);

              if(ic.cnt <= ic.maxatomcnt) ic.mdl_ghost.add(mesh);
          }
          else {
              ic.mdl.add(mesh);
          }
        }

        if(bHighlight === 1 || bHighlight === 2) {
            if(ic.bImpo) {
                if(ic.cnt <= ic.maxatomcnt) ic.prevHighlightObjects_ghost.push(mesh);
            }
            else {
                ic.prevHighlightObjects.push(mesh);
            }
        }
        else {
            if(ic.bImpo && !opacity_ori) { // imposter didn't work with transparency yet in iCn3D
                if(ic.cnt <= ic.maxatomcnt) ic.objects_ghost.push(mesh);
            }
            else {
                ic.objects.push(mesh);
            }
        }
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create spheres for "atoms" with the "radius". "forceDefault" means to use the default radius.
    //"scale" means scale on the radius. "bHighlight" is an option to draw the highlight for these atoms.
    //The highlight could be outlines with bHighlight=1 and 3D objects with bHighlight=2.
    createSphereRepresentation(atoms, defaultRadius, forceDefault, scale, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let thisClass = this;

        ic.reprSubCls.createRepresentationSub(atoms, function (atom0) {
            thisClass.createSphere(atom0, defaultRadius, forceDefault, scale, bHighlight);
        });
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Stick {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create sticks for "atoms". "bondR" is the radius of the sticks. "atomR" is the radius of the spheres in the joints.
    //"scale" means scale on the radius. "bHighlight" is an option to draw the highlight for these atoms.
    //The highlight could be outlines with bHighlight=1 and 3D objects with bHighlight=2.
    createStickRepresentation(atoms, atomR, bondR, scale, bHighlight, bSchematic) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let factor = (bSchematic !== undefined && bSchematic) ? atomR / ic.cylinderRadius : 1;
        let doubleBondRadius = ic.cylinderRadius * factor * 0.4; // 0.3
        let triBondRadius = ic.cylinderRadius * factor * 0.3; // 0.2

            ic.reprSubCls.createRepresentationSub(atoms, function (atom0) {
                    ic.sphereCls.createSphere(atom0, atomR, !scale, scale, bHighlight);
            }, function (atom0, atom1) {
                let mp = atom0.coord.clone().add(atom1.coord).multiplyScalar(0.5);
                let pair = atom0.serial + '_' + atom1.serial;

                if(ic.doublebonds.hasOwnProperty(pair)) { // show double bond
                    let a0, a1, a2;

                    let v0;
                    let random = new THREE.Vector3(Math.random(),Math.random(),Math.random());
                    if(atom0.bonds.length == 1 && atom1.bonds.length == 1) {
                        v0 = atom1.coord.clone();
                        v0.sub(atom0.coord);

                        let v = random.clone();
                        v0.cross(v).normalize().multiplyScalar(0.2 * factor);
                    }
                    else {
                        if(atom0.bonds.length >= atom1.bonds.length && atom0.bonds.length > 1) {
                            a0 = atom0.serial;
                            a1 = atom0.bonds[0];
                            a2 = atom0.bonds[1];
                        }
                        //else {
                        else if(atom1.bonds.length >= atom0.bonds.length && atom1.bonds.length > 1) {
                            a0 = atom1.serial;
                            a1 = atom1.bonds[0];
                            a2 = atom1.bonds[1];
                        }
                        else {
                            console.log("Double bond was not drawn due to the undefined cross plane");
                            return;
                        }

                        let v1 = ic.atoms[a0].coord.clone();
                        v1.sub(ic.atoms[a1].coord);
                        let v2 = ic.atoms[a0].coord.clone();
                        v2.sub(ic.atoms[a2].coord);

                        v1.cross(v2);

                        // parallel
                        if(parseInt(v1.length() * 10000) == 0) {
                            //v1 = random.clone();
                            // use a constant so that they are fixed,e.g., in CO2
                            v1 = new THREE.Vector3(0.2, 0.3, 0.5);
                        }

                        v0 = atom1.coord.clone();
                        v0.sub(atom0.coord);

                        v0.cross(v1).normalize().multiplyScalar(0.2 * factor);
                        // parallel
                        if(parseInt(v0.length() * 10000) == 0) {
                            //v1 = random.clone();
                            // use a constant so that they are fixed,e.g., in CO2
                            v1 = new THREE.Vector3(0.5, 0.3, 0.2);
                            v0.cross(v1).normalize().multiplyScalar(0.2 * factor);
                        }
                    }

                    if (atom0.color === atom1.color) {
                        if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                            ic.cylinderCls.createCylinder(atom0.coord.clone().add(v0), atom1.coord.clone().add(v0), doubleBondRadius, atom0.color, bHighlight);
                            ic.cylinderCls.createCylinder(atom0.coord.clone().sub(v0), atom1.coord.clone().sub(v0), doubleBondRadius, atom0.color, bHighlight);
                        }
                    } else {
                        if(ic.bImpo) {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord.clone().add(v0), atom1.coord.clone().add(v0), doubleBondRadius, atom0.color, bHighlight, atom1.color);
                                ic.cylinderCls.createCylinder(atom0.coord.clone().sub(v0), atom1.coord.clone().sub(v0), doubleBondRadius, atom0.color, bHighlight, atom1.color);
                            }
                        }
                        else {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord.clone().add(v0), mp.clone().add(v0), doubleBondRadius, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord.clone().add(v0), mp.clone().add(v0), doubleBondRadius, atom1.color, bHighlight);

                                ic.cylinderCls.createCylinder(atom0.coord.clone().sub(v0), mp.clone().sub(v0), doubleBondRadius, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord.clone().sub(v0), mp.clone().sub(v0), doubleBondRadius, atom1.color, bHighlight);
                            }
                        }
                    }
                }
                else if(ic.aromaticbonds.hasOwnProperty(pair)) { // show aromatic bond
                    let a0, a1, a2;
                    if(atom0.bonds.length > atom1.bonds.length && atom0.bonds.length > 1) {
                        a0 = atom0.serial;
                        a1 = atom0.bonds[0];
                        a2 = atom0.bonds[1];
                    }
                    else if(atom1.bonds.length > 1) {
                        a0 = atom1.serial;
                        a1 = atom1.bonds[0];
                        a2 = atom1.bonds[1];
                    }
                    else {
                        return;
                    }

                    let v1 = ic.atoms[a0].coord.clone();
                    v1.sub(ic.atoms[a1].coord);
                    let v2 = ic.atoms[a0].coord.clone();
                    v2.sub(ic.atoms[a2].coord);

                    v1.cross(v2);

                    let v0 = atom1.coord.clone();
                    v0.sub(atom0.coord);

                    v0.cross(v1).normalize().multiplyScalar(0.2 * factor);

                    // find an aromatic neighbor
                    let aromaticNeighbor = 0;
                    for(let i = 0, il = atom0.bondOrder.length; i < il; ++i) {
                        if(atom0.bondOrder[i] === '1.5' && atom0.bonds[i] !== atom1.serial) {
                            aromaticNeighbor = atom0.bonds[i];
                        }
                    }

                    let dashed = "add";
                    if(aromaticNeighbor === 0 ) { // no neighbor found, atom order does not matter
                        dashed = "add";
                    }
                    else {
                        // calculate the angle between atom1, atom0add, atomNeighbor and the angle atom1, atom0sub, atomNeighbor
                        let atom0add = atom0.coord.clone().add(v0);
                        let atom0sub = atom0.coord.clone().sub(v0);

                        let a = atom1.coord.clone().sub(atom0add).normalize();
                        let b = ic.atoms[aromaticNeighbor].coord.clone().sub(atom0add).normalize();

                        let c = atom1.coord.clone().sub(atom0sub).normalize();
                        let d = ic.atoms[aromaticNeighbor].coord.clone().sub(atom0sub).normalize();

                        let angleadd = Math.acos(a.dot(b));
                        let anglesub = Math.acos(c.dot(d));

                        if(angleadd < anglesub) {
                            dashed = 'sub';
                        }
                        else {
                            dashed = 'add';
                        }
                    }

                    if (atom0.color === atom1.color) {
                        let base, step;
                        if(dashed === 'add') {
                            ic.cylinderCls.createCylinder(atom0.coord.clone().sub(v0), atom1.coord.clone().sub(v0), doubleBondRadius, atom0.color, bHighlight);

                            base = atom0.coord.clone().add(v0);
                            step = atom1.coord.clone().add(v0).sub(base).multiplyScalar(1.0/11);
                        }
                        else {
                            ic.cylinderCls.createCylinder(atom0.coord.clone().add(v0), atom1.coord.clone().add(v0), doubleBondRadius, atom0.color, bHighlight);

                            base = atom0.coord.clone().sub(v0);
                            step = atom1.coord.clone().sub(v0).sub(base).multiplyScalar(1.0/11);
                        }

                        for(let i = 0; i <= 10; ++i) {
                            if(i % 2 == 0) {
                                let pos1 = base.clone().add(step.clone().multiplyScalar(i));
                                let pos2 = base.clone().add(step.clone().multiplyScalar(i + 1));
                                ic.cylinderCls.createCylinder(pos1, pos2, doubleBondRadius, atom0.color, bHighlight);
                            }
                        }

                    } else {
                        let base, step;
                        if(dashed === 'add') {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord.clone().sub(v0), mp.clone().sub(v0), doubleBondRadius, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord.clone().sub(v0), mp.clone().sub(v0), doubleBondRadius, atom1.color, bHighlight);
                            }

                            base = atom0.coord.clone().add(v0);
                            step = atom1.coord.clone().add(v0).sub(base).multiplyScalar(1.0/11);
                        }
                        else {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord.clone().add(v0), mp.clone().add(v0), doubleBondRadius, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord.clone().add(v0), mp.clone().add(v0), doubleBondRadius, atom1.color, bHighlight);
                            }

                            base = atom0.coord.clone().sub(v0);
                            step = atom1.coord.clone().sub(v0).sub(base).multiplyScalar(1.0/11);
                        }

                        for(let i = 0; i <= 10; ++i) {
                            if(i % 2 == 0 && ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                let pos1 = base.clone().add(step.clone().multiplyScalar(i));
                                let pos2 = base.clone().add(step.clone().multiplyScalar(i + 1));
                                if(i < 5) {
                                    ic.cylinderCls.createCylinder(pos1, pos2, doubleBondRadius, atom0.color, bHighlight);
                                }
                                else {
                                    ic.cylinderCls.createCylinder(pos1, pos2, doubleBondRadius, atom1.color, bHighlight);
                                }
                            }
                        }
                    }
                }
                else if(ic.triplebonds.hasOwnProperty(pair)) { // show triple bond
                    let random = new THREE.Vector3(Math.random(),Math.random(),Math.random());
                    let v = atom1.coord.clone();
                    v.sub(atom0.coord);

                    let c = random.clone();
                    c.cross(v).normalize().multiplyScalar(0.3 * factor);

                    if (atom0.color === atom1.color) {
                        if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                            ic.cylinderCls.createCylinder(atom0.coord, atom1.coord, triBondRadius, atom0.color, bHighlight);
                            ic.cylinderCls.createCylinder(atom0.coord.clone().add(c), atom1.coord.clone().add(c), triBondRadius, atom0.color, bHighlight);
                            ic.cylinderCls.createCylinder(atom0.coord.clone().sub(c), atom1.coord.clone().sub(c), ic.triBondRadius, atom0.color, bHighlight);
                        }
                    } else {
                        if(ic.bImpo) {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord, atom1.coord, triBondRadius, atom0.color, bHighlight, atom1.color);
                                ic.cylinderCls.createCylinder(atom0.coord.clone().add(c), atom1.coord.clone().add(c), triBondRadius, atom0.color, bHighlight, atom1.color);
                                ic.cylinderCls.createCylinder(atom0.coord.clone().sub(c), atom1.coord.clone().sub(c), triBondRadius, atom0.color, bHighlight, atom1.color);
                            }
                        }
                        else {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord, mp, triBondRadius, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord, mp, triBondRadius, atom1.color, bHighlight);

                                ic.cylinderCls.createCylinder(atom0.coord.clone().add(c), mp.clone().add(c), triBondRadius, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord.clone().add(c), mp.clone().add(c), triBondRadius, atom1.color, bHighlight);

                                ic.cylinderCls.createCylinder(atom0.coord.clone().sub(c), mp.clone().sub(c), triBondRadius, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord.clone().sub(c), mp.clone().sub(c), triBondRadius, atom1.color, bHighlight);
                            }
                        }
                    }
                }
                else {
                    if (atom0.color === atom1.color) {
                        ic.cylinderCls.createCylinder(atom0.coord, atom1.coord, bondR, atom0.color, bHighlight);
                    } else {
                        if(ic.bImpo) {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord, atom1.coord, bondR, atom0.color, bHighlight, atom1.color);
                            }
                        }
                        else {
                            if(ic.dAtoms.hasOwnProperty(atom0.serial) && ic.dAtoms.hasOwnProperty(atom1.serial)) {
                                ic.cylinderCls.createCylinder(atom0.coord, mp, bondR, atom0.color, bHighlight);
                                ic.cylinderCls.createCylinder(atom1.coord, mp, bondR, atom1.color, bHighlight);
                            }
                        }
                    }
                }
            });
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Strand {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // significantly modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create the style of ribbon or strand for "atoms". "num" means how many lines define the curve.
    //"num" is 2 for ribbon and 6 for strand. "div" means how many pnts are used to smooth the curve.
    //It's typically 5. "coilWidth" is the width of curve for coil. "helixSheetWidth" is the width of curve for helix or sheet.
    //"doNotSmoothen" is a flag to smooth the curve or not. "thickness" is the thickness of the curve.
    //"bHighlight" is an option to draw the highlight for these atoms. The highlight could be outlines
    //with bHighlight=1 and 3D objects with bHighlight=2.
    createStrand(atoms, num, div, fill, coilWidth, helixSheetWidth, doNotSmoothen, thickness, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let bRibbon = fill ? true: false;

        // when highlight, the input atoms may only include part of sheet or helix
        // include the whole sheet or helix when highlighting
        let atomsAdjust = {};

        //if( (bHighlight === 1 || bHighlight === 2) && !ic.bAllAtoms) {
        //if( !ic.bAllAtoms) {
        if( Object.keys(atoms).length < Object.keys(ic.atoms).length) {
            atomsAdjust = this.getSSExpandedAtoms(atoms);
        }
        else {
            atomsAdjust = atoms;
        }

        if(bHighlight === 2) {
            if(fill) {
                fill = false;
                num = null;
                div = null;
                coilWidth = null;
                helixSheetWidth = null;
                thickness = undefined;
            }
            else {
                fill = true;
                num = 2;
                div = undefined;
                coilWidth = undefined;
                helixSheetWidth = undefined;
                thickness = ic.ribbonthickness;
            }
        }

        num = num || ic.strandDIV;
        div = div || ic.axisDIV;
        coilWidth = coilWidth || ic.coilWidth;
        doNotSmoothen = doNotSmoothen || false;
        helixSheetWidth = helixSheetWidth || ic.helixSheetWidth;
        let pnts = {}; for (let k = 0; k < num; ++k) pnts[k] = [];
        let pntsCA = [];
        let prevCOArray = [];
        let bShowArray = [];
        let calphaIdArray = []; // used to store one of the final positions drawn in 3D
        let colors = [];
        let currentChain, currentResi, currentCA = null, currentO = null, currentColor = null, prevCoorCA = null, prevCoorO = null, prevColor = null;
        let prevCO = null, ss = null, ssend = false, atomid = null, prevAtomid = null, prevAtomSelected = null, prevResi = null, calphaid = null, prevCalphaid = null;
        let strandWidth, bSheetSegment = false, bHelixSegment = false;
        let atom, tubeAtoms = {};

        // test the first 30 atoms to see whether only C-alpha is available
        ic.bCalphaOnly = me.utilsCls.isCalphaPhosOnly(atomsAdjust); //, 'CA');

        // when highlight, draw whole beta sheet and use bShowArray to show the highlight part
        let residueHash = {};
        for(let i in atomsAdjust) {
            let atom = atomsAdjust[i];

            let residueid = atom.structure + '_' + atom.chain + '_' + atom.resi;
            residueHash[residueid] = 1;
        }
        let totalResidueCount = Object.keys(residueHash).length;

        let drawnResidueCount = 0;

        let bFullAtom = (Object.keys(ic.hAtoms).length == Object.keys(ic.atoms).length) ? true : false;

        let caArray = []; // record all C-alpha atoms to predict the helix

        let maxDist = 6.0;

        for (let i in atomsAdjust) {
          atom = atomsAdjust[i];
          let chainid = atom.structure + '_' + atom.chain;
          if ((atom.name === 'O' || atom.name === 'CA') && !atom.het) {
            // "CA" has to appear before "O"

            if (atom.name === 'CA') {
                if ( atoms.hasOwnProperty(i) && ((atom.ss !== 'helix' && atom.ss !== 'sheet') || atom.ssend || atom.ssbegin) ) {
                    tubeAtoms[i] = atom;
                }

                currentCA = atom.coord;
                currentColor = atom.color;
                calphaid = atom.serial;

                caArray.push(atom.serial);
            }

            if (atom.name === 'O' || (ic.bCalphaOnly && atom.name === 'CA')) {
                if(currentCA === null || currentCA === undefined) {
                    currentCA = atom.coord;
                    currentColor = atom.color;
                    calphaid = atom.serial;
                }

                if(atom.name === 'O') {
                    currentO = atom.coord;
                }
                // smoothen each coil, helix and sheet separately. The joint residue has to be included both in the previous and next segment
               
                // let bSameChain = true;
                // if (currentChain !== atom.chain) {
                // //if (currentChain !== atom.chain) {
                //     bSameChain = false;
                // }

                if(atom.ssend && atom.ss === 'sheet') {
                    bSheetSegment = true;
                }
                else if(atom.ssend && atom.ss === 'helix') {
                    bHelixSegment = true;
                }

                // assign the previous residue
                if(prevCoorO) {
                    if(bHighlight === 1 || bHighlight === 2) {
                        colors.push(ic.hColor);
                    }
                    else {
                        colors.push(prevColor);
                    }

                    if(ss !== 'coil' && atom.ss === 'coil') {
                        strandWidth = coilWidth;
                    }
                    else if(ssend && atom.ssbegin) { // a transition between two ss
                        strandWidth = coilWidth;
                    }
                    else {
                        strandWidth = (ss === 'coil') ? coilWidth : helixSheetWidth;
                    }

                    let O, oldCA, resSpan = 4;
                    if(atom.name === 'O') {
                        O = prevCoorO.clone();
                        if(prevCoorCA !== null && prevCoorCA !== undefined) {
                            O.sub(prevCoorCA);
                        }
                        else {
                            prevCoorCA = prevCoorO.clone();
                            if(caArray.length > resSpan + 1) { // use the calpha and the previous 4th c-alpha to calculate the helix direction
                                O = prevCoorCA.clone();
                                oldCA = ic.atoms[caArray[caArray.length - 1 - resSpan - 1]].coord.clone();
                                //O.sub(oldCA);
                                oldCA.sub(O);
                            }
                            else {
                                O = new THREE.Vector3(Math.random(),Math.random(),Math.random());
                            }
                        }
                    }
                    else if(ic.bCalphaOnly && atom.name === 'CA') {
                        if(caArray.length > resSpan + 1) { // use the calpha and the previous 4th c-alpha to calculate the helix direction
                            O = prevCoorCA.clone();
                            oldCA = ic.atoms[caArray[caArray.length - 1 - resSpan - 1]].coord.clone();
                            //O.sub(oldCA);
                            oldCA.sub(O);
                        }
                        else {
                            O = new THREE.Vector3(Math.random(),Math.random(),Math.random());
                        }
                    }

                    O.normalize(); // can be omitted for performance
                    O.multiplyScalar(strandWidth);
                    if (prevCO !== null && O.dot(prevCO) < 0) O.negate();
                    prevCO = O;

                    for (let j = 0, numM1Inv2 = 2 / (num - 1); j < num; ++j) {
                        let delta = -1 + numM1Inv2 * j;
                        let v = new THREE.Vector3(prevCoorCA.x + prevCO.x * delta, prevCoorCA.y + prevCO.y * delta, prevCoorCA.z + prevCO.z * delta);
                        if (!doNotSmoothen && ss === 'sheet') v.smoothen = true;
                        pnts[j].push(v);
                    }

                    pntsCA.push(prevCoorCA);
                    prevCOArray.push(prevCO);

                    if(atoms.hasOwnProperty(prevAtomid)) {
                        bShowArray.push(prevResi);
                        calphaIdArray.push(prevCalphaid);
                    }
                    else {
                        bShowArray.push(0);
                        calphaIdArray.push(0);
                    }

                    ++drawnResidueCount;
                }

                //let bBrokenSs =  ic.ParserUtilsCls.getResiNCBI(atom.structure + '_' + currentChain, currentResi) + 1 !== ic.ParserUtilsCls.getResiNCBI(chainid, atom.resi) || (prevCoorCA && Math.abs(currentCA.x - prevCoorCA.x) > maxDist) || (prevCoorCA && Math.abs(currentCA.y - prevCoorCA.y) > maxDist) || (prevCoorCA && Math.abs(currentCA.z - prevCoorCA.z) > maxDist);

                let prevCoor = (prevAtomSelected) ? prevAtomSelected.coord : undefined;
                
                let bBrokenSs =  ic.ParserUtilsCls.getResiNCBI(atom.structure + '_' + currentChain, currentResi) + 1 !== ic.ParserUtilsCls.getResiNCBI(chainid, atom.resi) || (prevCoor && Math.abs(currentCA.x - prevCoor.x) > maxDist) || (prevCoor && Math.abs(currentCA.y - prevCoor.y) > maxDist) || (prevCoor && Math.abs(currentCA.z - prevCoor.z) > maxDist);

                // check whether the atoms are continuous
                // atomsAdjusted has all atoms in the secondary structure
                // atoms has all selected atoms
                // let bBrokenSs = false;
                // if(prevAtomSelected && prevAtomid == prevAtomSelected.serial && !atoms.hasOwnProperty(atom.serial)) {
                //     bBrokenSs = true;                  
                // }


                // The following code didn't work to select one residue
                // let bBrokenSs = !atoms.hasOwnProperty(atom.serial) || (prevCoorCA && Math.abs(currentCA.x - prevCoorCA.x) > maxDist) || (prevCoorCA && Math.abs(currentCA.y - prevCoorCA.y) > maxDist) || (prevCoorCA && Math.abs(currentCA.z - prevCoorCA.z) > maxDist);

                // if(bBrokenSs && atom.ss === 'sheet') {
                //     bSheetSegment = true;
                // }
                // else if(bBrokenSs && atom.ss === 'helix') {
                //     bHelixSegment = true;
                // }

                //if ((atom.ssbegin || atom.ssend || (drawnResidueCount === totalResidueCount - 1) || bBrokenSs) && pnts[0].length > 0 && bSameChain) {
                if ((currentChain !== atom.chain || atom.ssbegin || atom.ssend || (drawnResidueCount === totalResidueCount - 1) || bBrokenSs) && pnts[0].length > 0) {
                    let atomName = 'CA';
                
                    let prevone = [], nexttwo = [];

                    if(isNaN(ic.atoms[prevAtomid].resi)) {
                        prevone = [];
                    }
                    else {
                        let prevoneResid = ic.atoms[prevAtomid].structure + '_' + ic.atoms[prevAtomid].chain + '_' + (parseInt(ic.atoms[prevAtomid].resi) - 1).toString();
                        let prevoneCoord = ic.firstAtomObjCls.getAtomCoordFromResi(prevoneResid, atomName);
                        prevone = (prevoneCoord !== undefined) ? [prevoneCoord] : [];
                    }

                    if(!isNaN(ic.atoms[prevAtomid].resi)) {
                        let nextoneResid = ic.atoms[prevAtomid].structure + '_' + ic.atoms[prevAtomid].chain + '_' + (parseInt(ic.atoms[prevAtomid].resi) + 1).toString();
                        let nextoneCoord = ic.firstAtomObjCls.getAtomCoordFromResi(nextoneResid, atomName);
                        if(nextoneCoord !== undefined) {
                            nexttwo.push(nextoneCoord);
                        }

                        let nexttwoResid = ic.atoms[prevAtomid].structure + '_' + ic.atoms[prevAtomid].chain + '_' + (parseInt(ic.atoms[prevAtomid].resi) + 2).toString();
                        let nexttwoCoord = ic.firstAtomObjCls.getAtomCoordFromResi(nexttwoResid, atomName);
                        if(nexttwoCoord !== undefined) {
                            nexttwo.push(nexttwoCoord);
                        }
                    }

                    // include the current residue
                    if(!bBrokenSs) { 
                        // assign the current joint residue to the previous segment
                        if(bHighlight === 1 || bHighlight === 2) {
                            colors.push(ic.hColor);
                        }
                        else {
                            //colors.push(atom.color);
                            colors.push(prevColor);
                        }

                        if(atom.ssend && atom.ss === 'sheet') { // current residue is the end of ss and is the end of arrow
                            strandWidth = 0; // make the arrow end sharp
                        }
                        else if(ss === 'coil' && atom.ssbegin) {
                            strandWidth = coilWidth;
                        }
                        else if(ssend && atom.ssbegin) { // current residue is the start of ss and  the previous residue is the end of ss, then use coil
                            strandWidth = coilWidth;
                        }
                        else { // use the ss from the previous residue
                            strandWidth = (atom.ss === 'coil') ? coilWidth : helixSheetWidth;
                        }

                        let O, oldCA, resSpan = 4;
                        if(atom.name === 'O') {
                            O = currentO.clone();
                            O.sub(currentCA);
                        }
                        else if(ic.bCalphaOnly && atom.name === 'CA') {
                            if(caArray.length > resSpan) { // use the calpha and the previous 4th c-alpha to calculate the helix direction
                                O = currentCA.clone();
                                oldCA = ic.atoms[caArray[caArray.length - 1 - resSpan]].coord.clone();
                                //O.sub(oldCA);
                                oldCA.sub(O);
                            }
                            else {
                                O = new THREE.Vector3(Math.random(),Math.random(),Math.random());
                            }
                        }

                        O.normalize(); // can be omitted for performance
                        O.multiplyScalar(strandWidth);
                        if (prevCO !== null && O.dot(prevCO) < 0) O.negate();
                        prevCO = O;

                        for (let j = 0, numM1Inv2 = 2 / (num - 1); j < num; ++j) {
                            let delta = -1 + numM1Inv2 * j;
                            let v = new THREE.Vector3(currentCA.x + prevCO.x * delta, currentCA.y + prevCO.y * delta, currentCA.z + prevCO.z * delta);
                            if (!doNotSmoothen && ss === 'sheet') v.smoothen = true;
                            pnts[j].push(v);
                        }

                        atomid = atom.serial;

                        pntsCA.push(currentCA);
                        prevCOArray.push(prevCO);

                        // when a coil connects to a sheet and the last residue of coild is highlighted, the first sheet residue is set as atom.highlightStyle. This residue should not be shown.
                        //if(atoms.hasOwnProperty(atomid) && (bHighlight === 1 && !atom.notshow) ) {
                        if(atoms.hasOwnProperty(atomid)) {
                            bShowArray.push(atom.resi);
                            calphaIdArray.push(calphaid);
                        }
                        else {
                            bShowArray.push(0);
                            calphaIdArray.push(0);
                        }
                    }

                    // draw the current segment
                    for (let j = 0; !fill && j < num; ++j) {
                        if(bSheetSegment) {
                            ic.curveStripArrowCls.createCurveSubArrow(pnts[j], 1, colors, div, bHighlight, bRibbon, num, j, pntsCA, prevCOArray, bShowArray, calphaIdArray, true, prevone, nexttwo);
                        }
                        else if(bHelixSegment) {
                            if(bFullAtom) {
                                ic.curveCls.createCurveSub(pnts[j], 1, colors, div, bHighlight, bRibbon, false, bShowArray, calphaIdArray, undefined, prevone, nexttwo);
                            }
                            else {
                                ic.curveStripArrowCls.createCurveSubArrow(pnts[j], 1, colors, div, bHighlight, bRibbon, num, j, pntsCA, prevCOArray, bShowArray, calphaIdArray, false, prevone, nexttwo);
                            }
                        }
                    }
                    if (fill) {
                        if(bSheetSegment) {
                            let start = 0, end = num - 1;
                            ic.curveStripArrowCls.createStripArrow(pnts[0], pnts[num - 1], colors, div, thickness, bHighlight, num, start, end, pntsCA, prevCOArray, bShowArray, calphaIdArray, true, prevone, nexttwo);
                        }
                        else if(bHelixSegment) {
                            if(bFullAtom) {
                                ic.stripCls.createStrip(pnts[0], pnts[num - 1], colors, div, thickness, bHighlight, false, bShowArray, calphaIdArray, undefined, prevone, nexttwo, pntsCA, prevCOArray);
                            }
                            else {
                                let start = 0, end = num - 1;
                                ic.curveStripArrowCls.createStripArrow(pnts[0], pnts[num - 1], colors, div, thickness, bHighlight, num, start, end, pntsCA, prevCOArray, bShowArray, calphaIdArray, false, prevone, nexttwo);
                            }
                        }
                        else {
                            if(bHighlight === 2) { // draw coils only when highlighted. if not highlighted, coils will be drawn as tubes separately
                                ic.stripCls.createStrip(pnts[0], pnts[num - 1], colors, div, thickness, bHighlight, false, bShowArray, calphaIdArray, undefined, prevone, nexttwo, pntsCA, prevCOArray);
                            }
                        }
                    }
                    for (let k = 0; k < num; ++k) pnts[k] = [];

                    colors = [];
                    pntsCA = [];
                    prevCOArray = [];
                    bShowArray = [];
                    calphaIdArray = [];
                    bSheetSegment = false;
                    bHelixSegment = false;
                } // end if (atom.ssbegin || atom.ssend)

                // end of a chain
                if ((currentChain !== atom.chain || ic.ParserUtilsCls.getResiNCBI(atom.structure + '_' + currentChain, currentResi) + 1 !== ic.ParserUtilsCls.getResiNCBI(chainid, atom.resi)) && pnts[0].length > 0) {
                //if ((currentChain !== atom.chain) && pnts[0].length > 0) {

                    let atomName = 'CA';

                    let prevone = [], nexttwo = [];
                    if(isNaN(ic.atoms[prevAtomid].resi)) {
                        prevone = [];
                    }
                    else {
                        let prevoneResid = ic.atoms[prevAtomid].structure + '_' + ic.atoms[prevAtomid].chain + '_' + (parseInt(ic.atoms[prevAtomid].resi) - 1).toString();
                        ic.firstAtomObjCls.getAtomCoordFromResi(prevoneResid, atomName);
                    }

                    for (let j = 0; !fill && j < num; ++j) {
                        if(bSheetSegment) {
                            ic.curveStripArrowCls.createCurveSubArrow(pnts[j], 1, colors, div, bHighlight, bRibbon, num, j, pntsCA, prevCOArray, bShowArray, calphaIdArray, true, prevone, nexttwo);
                        }
                        else if(bHelixSegment) {
                            if(bFullAtom) {
                                ic.curveCls.createCurveSub(pnts[j], 1, colors, div, bHighlight, bRibbon, false, bShowArray, calphaIdArray, undefined, prevone, nexttwo);
                            }
                            else {
                                ic.curveStripArrowCls.createCurveSubArrow(pnts[j], 1, colors, div, bHighlight, bRibbon, num, j, pntsCA, prevCOArray, bShowArray, calphaIdArray, false, prevone, nexttwo);
                            }
                        }
                    }
                    if (fill) {
                        if(bSheetSegment) {
                            let start = 0, end = num - 1;
                            ic.curveStripArrowCls.createStripArrow(pnts[0], pnts[num - 1], colors, div, thickness, bHighlight, num, start, end, pntsCA, prevCOArray, bShowArray, calphaIdArray, true, prevone, nexttwo);
                        }
                        else if(bHelixSegment) {
                            if(bFullAtom) {
                                ic.stripCls.createStrip(pnts[0], pnts[num - 1], colors, div, thickness, bHighlight, false, bShowArray, calphaIdArray, undefined, prevone, nexttwo, pntsCA, prevCOArray);
                            }
                            else {
                                let start = 0, end = num - 1;
                                ic.curveStripArrowCls.createStripArrow(pnts[0], pnts[num - 1], colors, div, thickness, bHighlight, num, start, end, pntsCA, prevCOArray, bShowArray, calphaIdArray, false, prevone, nexttwo);
                            }
                        }
                    }

                    for (let k = 0; k < num; ++k) pnts[k] = [];
                    colors = [];
                    pntsCA = [];
                    prevCOArray = [];
                    bShowArray = [];
                    calphaIdArray = [];
                    bSheetSegment = false;
                    bHelixSegment = false;
                }

                currentChain = atom.chain;
                currentResi = atom.resi;
                ss = atom.ss;
                ssend = atom.ssend;
                prevAtomid = atom.serial;
                if(atoms.hasOwnProperty(atom.serial)) prevAtomSelected = atom;
                prevResi = atom.resi;

                prevCalphaid = calphaid;

                // only update when atom.name === 'O'
                prevCoorCA = currentCA;
                prevCoorO = atom.coord;
                prevColor = currentColor;
            } // end if (atom.name === 'O' || (ic.bCalphaOnly && atom.name === 'CA') ) {
          } // end if ((atom.name === 'O' || atom.name === 'CA') && !atom.het) {
        } // end for

        caArray = [];

        ic.tubeCls.createTube(tubeAtoms, 'CA', coilWidth, bHighlight);

        tubeAtoms = {};
        pnts = {};
    }

    getSSExpandedAtoms(atoms, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
        let currChain, currResi, currAtom, prevChain, prevResi, prevAtom;
        let firstAtom, lastAtom;
        let index = 0, length = Object.keys(atoms).length;

        let atomsAdjust = me.hashUtilsCls.cloneHash(atoms);
        for(let serial in atoms) {
          currChain = atoms[serial].structure + '_' + atoms[serial].chain;
          currResi = atoms[serial].resi; //parseInt(atoms[serial].resi);
          currAtom = atoms[serial];

          if(prevChain === undefined) firstAtom = atoms[serial];

          if( (currChain !== prevChain && prevChain !== undefined)
           || (currResi !== prevResi && ic.resid2ncbi[currResi] !== ic.resid2ncbi[prevResi] + 1 && prevResi !== undefined) || index === length - 1) {
            if( (currChain !== prevChain && prevChain !== undefined)
              || (currResi !== prevResi && currResi !== ic.resid2ncbi[prevResi] + 1 && prevResi !== undefined) ) {
                lastAtom = prevAtom;
            }
            else if(index === length - 1) {
                lastAtom = currAtom;
            }

            // fill the beginning
            let beginResi = firstAtom.resi;
            if(!isNaN(firstAtom.resi) && firstAtom.ss !== 'coil' && !(firstAtom.ssbegin) ) {
                for(let i = parseInt(firstAtom.resi) - 1; i > 0; --i) {
                    let residueid = firstAtom.structure + '_' + firstAtom.chain + '_' + i;
                    if(!ic.residues.hasOwnProperty(residueid)) break;

                    let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);

                    if(atom.ss === firstAtom.ss && atom.ssbegin) {
                        beginResi = atom.resi;
                        break;
                    }
                }

                for(let i = beginResi; i < firstAtom.resi; ++i) {
                    let residueid = firstAtom.structure + '_' + firstAtom.chain + '_' + i;
                    atomsAdjust = me.hashUtilsCls.unionHash(atomsAdjust, me.hashUtilsCls.hash2Atoms(ic.residues[residueid],
                      ic.atoms));
                }
            }

            // add one extra residue for coils between strands/helix
            if(!isNaN(firstAtom.resi) && ic.pk === 3 && bHighlight === 1 && firstAtom.ss === 'coil') {
                    let residueid = firstAtom.structure + '_' + firstAtom.chain + '_' + (parseInt(firstAtom.resi) - 1).toString();
                    if(ic.residues.hasOwnProperty(residueid)) {
                        atomsAdjust = me.hashUtilsCls.unionHash(atomsAdjust, me.hashUtilsCls.hash2Atoms(ic.residues[residueid],
                          ic.atoms));
                        atoms = me.hashUtilsCls.unionHash(atoms, me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
                    }
            }

            // fill the end
            let endResi = lastAtom.resi;
            // when a coil connects to a sheet and the last residue of coil is highlighted, the first sheet residue is set as atom.notshow. This residue should not be shown.

            if(lastAtom.ss !== undefined && lastAtom.ss !== 'coil' && !(lastAtom.ssend) && !(lastAtom.notshow)) {

                let endChainResi = ic.firstAtomObjCls.getLastAtomObj(ic.chains[lastAtom.structure + '_' + lastAtom.chain]).resi;
                for(let i = parseInt(lastAtom.resi) + 1; i <= parseInt(endChainResi); ++i) {
                    let residueid = lastAtom.structure + '_' + lastAtom.chain + '_' + i;
                    if(!ic.residues.hasOwnProperty(residueid)) break;

                    let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);

                    if(atom.ss === lastAtom.ss && atom.ssend) {
                        endResi = atom.resi;
                        break;
                    }
                }

                for(let i = parseInt(lastAtom.resi) + 1; i <= parseInt(endResi); ++i) {
                    let residueid = lastAtom.structure + '_' + lastAtom.chain + '_' + i;
                    atomsAdjust = me.hashUtilsCls.unionHash(atomsAdjust, me.hashUtilsCls.hash2Atoms(ic.residues[residueid],
                      ic.atoms));
                }
            }

            // add one extra residue for coils between strands/helix
            if(ic.pk === 3 && bHighlight === 1 && lastAtom.ss === 'coil') {
                    let residueid = lastAtom.structure + '_' + lastAtom.chain + '_' + (parseInt(lastAtom.resi) + 1).toString();
                    if(ic.residues.hasOwnProperty(residueid)) {
                        atomsAdjust = me.hashUtilsCls.unionHash(atomsAdjust, me.hashUtilsCls.hash2Atoms(ic.residues[residueid],
                          ic.atoms));
                        atoms = me.hashUtilsCls.unionHash(atoms, me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
                    }
            }

            // reset notshow
            if(lastAtom.notshow) lastAtom.notshow = undefined;

            firstAtom = currAtom;
          }

          prevChain = currChain;
          prevResi = currResi;
          prevAtom = currAtom;

          ++index;
        }

        return atomsAdjust;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Strip {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    createStrip(p0, p1, colors, div, thickness, bHighlight, bNoSmoothen, bShowArray,
      calphaIdArray, positions, prevone, nexttwo, pntsCA, prevCOArray) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        if (p0.length < 2) return;
        div = div || ic.axisDIV;

        if(pntsCA && ic.bDoublecolor && !ic.bCalphaOnly) {
            let bExtendLastRes = false; //true;

            let pnts_clrs = me.subdivideCls.subdivide(pntsCA, colors, div, bShowArray, bHighlight, prevone, nexttwo, bExtendLastRes);
            pntsCA = pnts_clrs[0];

            this.setCalphaDrawnCoord(pntsCA, div, calphaIdArray);

            for(let i = 0, il = prevCOArray.length; i < il; ++i) {
                prevCOArray[i].normalize();
            }

            let pnts_clrs2 = me.subdivideCls.subdivide(prevCOArray, colors, div, bShowArray, bHighlight, prevone, nexttwo, bExtendLastRes);
            prevCOArray = pnts_clrs2[0];

            colors = pnts_clrs[2];
        }
        else {

            if(!bNoSmoothen) {
                //var bExtendLastRes = true;
                let bExtendLastRes = false;
                let pnts_clrs0 = me.subdivideCls.subdivide(p0, colors, div, bShowArray, bHighlight, prevone, nexttwo, bExtendLastRes);
                let pnts_clrs1 = me.subdivideCls.subdivide(p1, colors, div, bShowArray, bHighlight, prevone, nexttwo, bExtendLastRes);
                p0 = pnts_clrs0[0];
                p1 = pnts_clrs1[0];
                colors = pnts_clrs0[2];
            }
            if (p0.length < 2) return;

            this.setCalphaDrawnCoord(p0, div, calphaIdArray);
        }

        if(bHighlight === 1) {
            //mesh = new THREE.Mesh(geo, ic.matShader);

            let radius = ic.coilWidth / 2;
            //var radiusSegments = 8;
            let radiusSegments = 4; // save memory
            let closed = false;

            if(positions !== undefined) {
                let currPos, prevPos;
                let currP0 = [], currP1 = [];

                for(let i = 0, il = p0.length; i < il; ++i) {
                    currPos = positions[i];

                    if((currPos !== prevPos && parseInt(currPos) !== parseInt(prevPos) + 1 && prevPos !== undefined) || (i === il -1) ) {
                        // first tube
                        let geometry0 = new THREE.TubeGeometry(
                            new THREE.CatmullRomCurve3(currP0), // path
                            currP0.length, // segments
                            radius,
                            radiusSegments,
                            closed
                        );

                        let mesh = new THREE.Mesh(geometry0, ic.matShader);
                        mesh.renderOrder = ic.renderOrderPicking;
                        //ic.mdlPicking.add(mesh);
                        ic.mdl.add(mesh);

                        ic.prevHighlightObjects.push(mesh);

                        geometry0 = null;

                        // second tube
                        let geometry1 = new THREE.TubeGeometry(
                            new THREE.CatmullRomCurve3(currP1), // path
                            currP1.length, // segments
                            radius,
                            radiusSegments,
                            closed
                        );

                        mesh = new THREE.Mesh(geometry1, ic.matShader);
                        mesh.renderOrder = ic.renderOrderPicking;
                        //ic.mdlPicking.add(mesh);
                        ic.mdl.add(mesh);

                        ic.prevHighlightObjects.push(mesh);

                        geometry1 = null;

                        currP0 = [];
                        currP1 = [];
                    }

                    currP0.push(p0[i]);
                    currP1.push(p1[i]);

                    prevPos = currPos;
                }

                currP0 = [];
                currP1 = [];
            }
            else {
                // first tube
                let geometry0 = new THREE.TubeGeometry(
                    new THREE.CatmullRomCurve3(p0), // path
                    p0.length, // segments
                    radius,
                    radiusSegments,
                    closed
                );

                let mesh = new THREE.Mesh(geometry0, ic.matShader);
                mesh.renderOrder = ic.renderOrderPicking;
                //ic.mdlPicking.add(mesh);
                ic.mdl.add(mesh);

                ic.prevHighlightObjects.push(mesh);

                geometry0 = null;

                // second tube
                let geometry1 = new THREE.TubeGeometry(
                    new THREE.CatmullRomCurve3(p1), // path
                    p1.length, // segments
                    radius,
                    radiusSegments,
                    closed
                );

                mesh = new THREE.Mesh(geometry1, ic.matShader);
                mesh.renderOrder = ic.renderOrderPicking;
                //ic.mdlPicking.add(mesh);
                ic.mdl.add(mesh);

                ic.prevHighlightObjects.push(mesh);

                geometry1 = null;
            }
        }
        else {
            //https://threejsfundamentals.org/threejs/lessons/threejs-custom-buffergeometry.html

            let geo = new THREE.BufferGeometry();
            //var vs = geo.vertices, fs = geo.faces;
            let vs = [];
            let colorArray = [], indexArray = [];
            let axis, p0v, p1v, a0v, a1v;

            let offset = 0, offset2 = 0, offset3 = 0;
            for (let i = 0, lim = p0.length; i < lim; ++i) {
                p0v = p0[i];
                p1v = p1[i];

                if(!p0v || !p1v) continue;

                //vs = vs.concat((p0v).toArray()); // 0
                //vs = vs.concat((p0v).toArray()); // 1
                //vs = vs.concat((p1v).toArray()); // 2
                //vs = vs.concat((p1v).toArray()); // 3

                for(let j = 0; j < 2; ++j) {
                    vs[offset++] = p0v.x;
                    vs[offset++] = p0v.y;
                    vs[offset++] = p0v.z;
                }
                for(let j = 0; j < 2; ++j) {
                    vs[offset++] = p1v.x;
                    vs[offset++] = p1v.y;
                    vs[offset++] = p1v.z;
                }

                if (i < lim - 1) {
                    axis = p1[i].clone().sub(p0[i]).cross(p0[i + 1].clone().sub(p0[i])).normalize().multiplyScalar(thickness);
                }
                a0v = p0[i].clone().add(axis);
                a1v = p1[i].clone().add(axis);

                //vs = vs.concat((a0v).toArray()); // 4
                //vs = vs.concat((a0v).toArray()); // 5
                //vs = vs.concat((a1v).toArray()); // 6
                //vs = vs.concat((a1v).toArray()); // 7

                for(let j = 0; j < 2; ++j) {
                    vs[offset++] = a0v.x;
                    vs[offset++] = a0v.y;
                    vs[offset++] = a0v.z;
                }
                for(let j = 0; j < 2; ++j) {
                    vs[offset++] = a1v.x;
                    vs[offset++] = a1v.y;
                    vs[offset++] = a1v.z;
                }

                for(let j = 0; j < 8; ++j) {
                    //colorArray = colorArray.concat(colors[i].toArray());
                    let color = (colors[i]) ? colors[i] : (colors[i-1] ? colors[i-1] : {r:0, g:0, b:0});
                    colorArray[offset2++] = color.r;
                    colorArray[offset2++] = color.g;
                    colorArray[offset2++] = color.b;
               }
            }
            let faces = [[0, 2, -6, -8], [-4, -2, 6, 4], [7, 3, -5, -1], [-3, -7, 1, 5]];

            for (let i = 1, lim = p0.length; i < lim; ++i) {
                let offsetTmp = 8 * i;
                //var color = me.parasCls.thr(colors[i - 1]);
                for (let j = 0; j < 4; ++j) {
                    //fs.push(new THREE.Face3(offset + faces[j][0], offset + faces[j][1], offset + faces[j][2], undefined, color));
                    //fs.push(new THREE.Face3(offset + faces[j][3], offset + faces[j][0], offset + faces[j][2], undefined, color));
                    //indexArray = indexArray.concat([offsetTmp + faces[j][0], offsetTmp + faces[j][1], offsetTmp + faces[j][2]]);
                    //indexArray = indexArray.concat([offsetTmp + faces[j][3], offsetTmp + faces[j][0], offsetTmp + faces[j][2]]);
                    indexArray[offset3++] = offsetTmp + faces[j][0];
                    indexArray[offset3++] = offsetTmp + faces[j][1];
                    indexArray[offset3++] = offsetTmp + faces[j][2];

                    indexArray[offset3++] = offsetTmp + faces[j][3];
                    indexArray[offset3++] = offsetTmp + faces[j][0];
                    indexArray[offset3++] = offsetTmp + faces[j][2];
                }
            }
            let nComp = 3;
            let vsize = vs.length / nComp - 8; // Cap
            for (let i = 0; i < 4; ++i) {
                for(let j = 0; j < nComp; ++j) {
                    //vs = vs.concat([vs[i * 2 * nComp + j]]);
                    vs[offset++] = vs[i * 2 * nComp + j];
                }

                for(let j = 0; j < nComp; ++j) {
                    //vs = vs.concat([vs[(vsize + i * 2) * nComp + j]]);
                    vs[offset++] = vs[(vsize + i * 2) * nComp + j];
                }

                //colorArray = colorArray.concat(colors[0].toArray());
                if(colors[0]) {
                    colorArray[offset2++] = colors[0].r;
                    colorArray[offset2++] = colors[0].g;
                    colorArray[offset2++] = colors[0].b;
                    //colorArray = colorArray.concat(colors[p0.length - 1].toArray());
                    let color = (colors[p0.length - 1]) ? colors[p0.length - 1] : (colors[p0.length - 2] ? colors[p0.length - 2] : {r:0, g:0, b:0});
                    colorArray[offset2++] = color.r;
                    colorArray[offset2++] = color.g;
                    colorArray[offset2++] = color.b;
                }
            }            vsize += 8;
            //fs.push(new THREE.Face3(vsize, vsize + 2, vsize + 6, undefined, fs[0].color));
            //fs.push(new THREE.Face3(vsize + 4, vsize, vsize + 6, undefined, fs[0].color));
            //fs.push(new THREE.Face3(vsize + 1, vsize + 5, vsize + 7, undefined, fs[fs.length - 3].color));
            //fs.push(new THREE.Face3(vsize + 3, vsize + 1, vsize + 7, undefined, fs[fs.length - 3].color));

            //indexArray = indexArray.concat([vsize, vsize + 2, vsize + 6]);
            //indexArray = indexArray.concat([vsize + 4, vsize, vsize + 6]);
            //indexArray = indexArray.concat([vsize + 1, vsize + 5, vsize + 7]);
            //indexArray = indexArray.concat([vsize + 3, vsize + 1, vsize + 7]);

            indexArray[offset3++] = vsize;
            indexArray[offset3++] = vsize + 2;
            indexArray[offset3++] = vsize + 6;
            indexArray[offset3++] = vsize + 4;
            indexArray[offset3++] = vsize;
            indexArray[offset3++] = vsize + 6;
            indexArray[offset3++] = vsize + 1;
            indexArray[offset3++] = vsize + 5;
            indexArray[offset3++] = vsize + 7;
            indexArray[offset3++] = vsize + 3;
            indexArray[offset3++] = vsize + 1;
            indexArray[offset3++] = vsize + 7;

            geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vs), nComp));
            geo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), nComp));

            geo.setIndex(new THREE.BufferAttribute(new Uint32Array(indexArray), 1));
            //geo.setIndex(indexArray);

            //geo.computeFaceNormals();
            //geo.computeVertexNormals(false);
            geo.computeVertexNormals();

            let mesh;

            if(bHighlight === 2) {
              mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ transparent: true, opacity: 0.5, specular: ic.frac,
                shininess: ic.shininess, emissive: ic.emissive, vertexColors: THREE.FaceColors, side: THREE.DoubleSide }));

              ic.mdl.add(mesh);
              ic.prevHighlightObjects.push(mesh);
            }
            else {
              mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ specular: ic.frac,
                shininess: ic.shininess, emissive: ic.emissive, vertexColors: THREE.FaceColors, side: THREE.DoubleSide }));

              ic.mdl.add(mesh);
              ic.objects.push(mesh);
            }
        }

        p0 = null;
        p1 = null;
    }

    setCalphaDrawnCoord(pnts, div, calphaIdArray) { let ic = this.icn3d; ic.icn3dui;
        let index = 0;

        if(calphaIdArray !== undefined) {
            for(let i = 0, il = pnts.length; i < il; i += div) { // pnts.length = (calphaIdArray.length - 1) * div + 1
                let serial = calphaIdArray[index];

                if(ic.atoms.hasOwnProperty(serial)) {
                    ic.atoms[serial].coord2 = pnts[i].clone();
                }

                ++index;
            }
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Tube {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create tubes for "atoms" with certain "atomName". "radius" is the radius of the tubes.
    //"bHighlight" is an option to draw the highlight for these atoms. The highlight could be
    //outlines with bHighlight=1 and 3D objects with bHighlight=2.
    createTube(atoms, atomName, radius, bHighlight, bCustom, bRadiusArray) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let pnts = [], colors = [], radii = [], prevone = [], nexttwo = [];
        let currentChain, currentResi;
        let index = 0;
        let maxDist = 6.0;
        let maxDist2 = 3.0; // avoid tube between the residues in 3 residue helix

        let pnts_colors_radii_prevone_nexttwo = [];
        let firstAtom, atom, prevAtom;

        for (let i in atoms) {
            atom = atoms[i];
            if ((atom.name === atomName) && !atom.het) {
                if(index == 0) {
                    firstAtom = atom;
                }

                atom.structure + '_' + atom.chain + '_' + (parseInt(atom.resi) - 1).toString();

                if (index > 0 && (currentChain !== atom.chain || Math.abs(atom.coord.x - prevAtom.coord.x) > maxDist || Math.abs(atom.coord.y - prevAtom.coord.y) > maxDist || Math.abs(atom.coord.z - prevAtom.coord.z) > maxDist
//                  || (parseInt(currentResi) + 1 < parseInt(atom.resi) && (Math.abs(atom.coord.x - prevAtom.coord.x) > maxDist2 || Math.abs(atom.coord.y - prevAtom.coord.y) > maxDist2 || Math.abs(atom.coord.z - prevAtom.coord.z) > maxDist2) && ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[resid]) && ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[resid]).ss == 'helix')
                  || (ic.ParserUtilsCls.getResiNCBI(atom.structure + '_' + currentChain, currentResi) + 1 < ic.ParserUtilsCls.getResiNCBI(atom.structure + '_' + atom.chain, atom.resi) && (Math.abs(atom.coord.x - prevAtom.coord.x) > maxDist2 || Math.abs(atom.coord.y - prevAtom.coord.y) > maxDist2 || Math.abs(atom.coord.z - prevAtom.coord.z) > maxDist2))
                  ) ) {
                    if(bHighlight !== 2) {
                        if(!isNaN(firstAtom.resi) && !isNaN(prevAtom.resi)) {
                            let prevoneResid = firstAtom.structure + '_' + firstAtom.chain + '_' + (parseInt(firstAtom.resi) - 1).toString();
                            let prevoneCoord = ic.firstAtomObjCls.getAtomCoordFromResi(prevoneResid, atomName);
                            prevone = (prevoneCoord !== undefined) ? [prevoneCoord] : [];

                            let nextoneResid = prevAtom.structure + '_' + prevAtom.chain + '_' + (parseInt(prevAtom.resi) + 1).toString();
                            let nexttwoResid = prevAtom.structure + '_' + prevAtom.chain + '_' + (parseInt(prevAtom.resi) + 2).toString();
                            let nextthreeResid = prevAtom.structure + '_' + prevAtom.chain + '_' + (parseInt(prevAtom.resi) + 3).toString();

                            if(ic.residues.hasOwnProperty(nextoneResid)) {
                                let nextAtom = ic.firstAtomObjCls.getAtomFromResi(nextoneResid, atomName);
                                if(nextAtom !== undefined && nextAtom.ssbegin) { // include the residue
                                    nextoneResid = prevAtom.structure + '_' + prevAtom.chain + '_' + (parseInt(prevAtom.resi) + 2).toString();
                                    nexttwoResid = prevAtom.structure + '_' + prevAtom.chain + '_' + (parseInt(prevAtom.resi) + 3).toString();

                                    pnts.push(nextAtom.coord);
                                    if(bCustom) {
                                        radii.push(this.getCustomtubesize(nextoneResid));
                                    }
                                    else {
                                        radii.push(this.getRadius(radius, nextAtom));
                                    }
                                    colors.push(nextAtom.color);
                                }
                            }

                            // add one more residue if only one residue is available
                            if(pnts.length == 1 && ic.residues.hasOwnProperty(nextoneResid)) {
                                let nextAtom = ic.firstAtomObjCls.getAtomFromResi(nextoneResid, atomName);

                                if(nextAtom) {
                                    pnts.push(nextAtom.coord);
                                    colors.push(nextAtom.color);

                                    let radiusFinal = this.getRadius(radius, atom);
                                    radii.push(radiusFinal);

                                    nextoneResid = nexttwoResid;
                                    nexttwoResid = nextthreeResid;
                                }
                            }

                            let nextoneCoord = ic.firstAtomObjCls.getAtomCoordFromResi(nextoneResid, atomName);
                            if(nextoneCoord !== undefined) {
                                nexttwo.push(nextoneCoord);
                            }

                            let nexttwoCoord = ic.firstAtomObjCls.getAtomCoordFromResi(nexttwoResid, atomName);
                            if(nexttwoCoord !== undefined) {
                                nexttwo.push(nexttwoCoord);
                            }
                        }

                        pnts_colors_radii_prevone_nexttwo.push({'pnts':pnts, 'colors':colors, 'radii':radii, 'prevone':prevone, 'nexttwo':nexttwo});
                    }
                    pnts = []; colors = []; radii = []; prevone = []; nexttwo = [];
                    firstAtom = atom;
                    index = 0;
                }

                if(pnts.length == 0 && !isNaN(atom.resi)) {
                    let prevoneResid = atom.structure + '_' + atom.chain + '_' + (parseInt(atom.resi) - 1).toString();
                    if(ic.residues.hasOwnProperty(prevoneResid)) {
                        prevAtom = ic.firstAtomObjCls.getAtomFromResi(prevoneResid, atomName);
                        if(prevAtom !== undefined && prevAtom.ssend) { // include the residue
                            pnts.push(prevAtom.coord);
                            if(bCustom) {
                                radii.push(this.getCustomtubesize(prevoneResid));
                            }
                            else {
                                radii.push(this.getRadius(radius, prevAtom));
                            }
                            colors.push(prevAtom.color);
                        }
                    }
                }

                pnts.push(atom.coord);

                let radiusFinal;
                if(bCustom) {
                    radiusFinal = this.getCustomtubesize(atom.structure + '_' + atom.chain + '_' + atom.resi);
                }
                else {
                    radiusFinal = this.getRadius(radius, atom);
                }

                //radii.push(radius || (atom.b > 0 ? atom.b * 0.01 : ic.coilWidth));
                radii.push(radiusFinal);

                colors.push(atom.color);
                // the starting residue of a coil uses the color from the next residue to avoid using the color of the last helix/sheet residue
                if(index === 1) colors[colors.length - 2] = atom.color;

                currentChain = atom.chain;
                currentResi = atom.resi;

                let scale = 1.2;
                if(bHighlight === 2 && !atom.ssbegin) {
                    ic.boxCls.createBox(atom, undefined, undefined, scale, undefined, bHighlight);
                }

                ++index;

                prevAtom = atom;
            }
        }

        if(bHighlight !== 2) {
            prevone = [];
            if(firstAtom !== undefined && !isNaN(firstAtom.resi)) {
                let prevoneResid = firstAtom.structure + '_' + firstAtom.chain + '_' + (parseInt(firstAtom.resi) - 1).toString();
                let prevoneCoord = ic.firstAtomObjCls.getAtomCoordFromResi(prevoneResid, atomName);
                prevone = (prevoneCoord !== undefined) ? [prevoneCoord] : [];
            }

            nexttwo = [];
            if(atom !== undefined && !isNaN(atom.resi)) {
                let nextoneResid = atom.structure + '_' + atom.chain + '_' + (parseInt(atom.resi) + 1).toString();
                let nexttwoResid = atom.structure + '_' + atom.chain + '_' + (parseInt(atom.resi) + 2).toString();
                let nextthreeResid = atom.structure + '_' + atom.chain + '_' + (parseInt(atom.resi) + 3).toString();

                // add one more residue if only one residue is available
                if(pnts.length == 1 && ic.residues.hasOwnProperty(nextoneResid)) {
                    let nextAtom = ic.firstAtomObjCls.getAtomFromResi(nextoneResid, atomName);

                    if(nextAtom) {
                        pnts.push(nextAtom.coord);
                        colors.push(nextAtom.color);

                        let radiusFinal = this.getRadius(radius, atom);
                        radii.push(radiusFinal);

                        nextoneResid = nexttwoResid;
                        nexttwoResid = nextthreeResid;
                    }
                }

                let nextoneCoord = ic.firstAtomObjCls.getAtomCoordFromResi(nextoneResid, atomName);
                if(nextoneCoord !== undefined) {
                    nexttwo.push(nextoneCoord);
                }

                let nexttwoCoord = ic.firstAtomObjCls.getAtomCoordFromResi(nexttwoResid, atomName);
                if(nexttwoCoord !== undefined) {
                    nexttwo.push(nexttwoCoord);
                }
            }

            pnts_colors_radii_prevone_nexttwo.push({'pnts':pnts, 'colors':colors, 'radii':radii, 'prevone':prevone, 'nexttwo':nexttwo});
        }

        for(let i = 0, il = pnts_colors_radii_prevone_nexttwo.length; i < il; ++i) {
            let pnts = pnts_colors_radii_prevone_nexttwo[i].pnts;
            let colors = pnts_colors_radii_prevone_nexttwo[i].colors;
            let radii = pnts_colors_radii_prevone_nexttwo[i].radii;
            let prevone = pnts_colors_radii_prevone_nexttwo[i].prevone;
            let nexttwo = pnts_colors_radii_prevone_nexttwo[i].nexttwo;

            this.createTubeSub(pnts, colors, radii, bHighlight, prevone, nexttwo, bRadiusArray);
        }

        pnts_colors_radii_prevone_nexttwo = [];    
    }

    getCustomtubesize(resid) { let ic = this.icn3d; ic.icn3dui;
        let pos = resid.lastIndexOf('_');
        let resi = resid.substr(pos + 1);
        let chainid = resid.substr(0, pos);

        let radiusFinal = (ic.queryresi2score[chainid] && ic.queryresi2score[chainid].hasOwnProperty(resi)) ? ic.queryresi2score[chainid][resi] * 0.01 : ic.coilWidth;

        return radiusFinal;
    };

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    createTubeSub(_pnts, colors, radii, bHighlight, prevone, nexttwo, bRadiusArray) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        if (_pnts.length < 2) return;

//        if(bRadiusArray) {
            let circleDiv = ic.tubeDIV, axisDiv = ic.axisDIV;
            let circleDivInv = 1 / circleDiv, axisDivInv = 1 / axisDiv;
            //var geo = new THREE.Geometry();
            let geo = new THREE.BufferGeometry();
            let verticeArray = [], colorArray = [],indexArray = [], color;
            let offset = 0, offset2 = 0, offset3 = 0;

            let pnts_clrs = me.subdivideCls.subdivide(_pnts, colors, axisDiv, undefined, undefined, prevone, nexttwo);

            let pnts = pnts_clrs[0];
            colors = pnts_clrs[2];

            let prevAxis1 = new THREE.Vector3(), prevAxis2;
            for (let i = 0, lim = pnts.length; i < lim; ++i) {
                let r, idx = (i - 1) * axisDivInv;
                if (i === 0) r = radii[0];
                else {
                    if (idx % 1 === 0) r = radii[idx];
                    else {
                        let floored = Math.floor(idx);
                        let tmp = idx - floored;
                        r = radii[floored] * tmp + radii[floored + 1] * (1 - tmp);
                    }
                }
                let delta, axis1, axis2;
                if (i < lim - 1) {
                    delta = pnts[i].clone().sub(pnts[i + 1]);
                    axis1 = new THREE.Vector3(0, -delta.z, delta.y).normalize().multiplyScalar(r);
                    axis2 = delta.clone().cross(axis1).normalize().multiplyScalar(r);
                    //      let dir = 1, offset = 0;
                    if (prevAxis1.dot(axis1) < 0) {
                        axis1.negate(); axis2.negate();  //dir = -1;//offset = 2 * Math.PI / axisDiv;
                    }
                    prevAxis1 = axis1; prevAxis2 = axis2;
                } else {
                    axis1 = prevAxis1; axis2 = prevAxis2;
                }
                for (let j = 0; j < circleDiv; ++j) {
                    let angle = 2 * Math.PI * circleDivInv * j; //* dir  + offset;
                    let point = pnts[i].clone().add(axis1.clone().multiplyScalar(Math.cos(angle))).add(axis2.clone().multiplyScalar(Math.sin(angle)));
                    verticeArray[offset++] = point.x;
                    verticeArray[offset++] = point.y;
                    verticeArray[offset++] = point.z;

                    color = (i == colors.length - 1 && colors.length > 1) ? me.parasCls.thr(colors[colors.length - 2]) : me.parasCls.thr(colors[i]);
                    colorArray[offset2++] = color.r;
                    colorArray[offset2++] = color.g;
                    colorArray[offset2++] = color.b;
                }
            }
            let offsetTmp = 0, nComp = 3;
            for (let i = 0, lim = pnts.length - 1; i < lim; ++i) {
                let reg = 0;
                //var r1 = geo.vertices[offset].clone().sub(geo.vertices[offset + circleDiv]).lengthSq();
                //var r2 = geo.vertices[offset].clone().sub(geo.vertices[offset + circleDiv + 1]).lengthSq();
                let pos = offsetTmp * nComp;
                let point1 = new THREE.Vector3(verticeArray[pos], verticeArray[pos + 1], verticeArray[pos + 2]);
                pos = (offsetTmp + circleDiv) * nComp;
                let point2 = new THREE.Vector3(verticeArray[pos], verticeArray[pos + 1], verticeArray[pos + 2]);
                pos = (offsetTmp + circleDiv + 1) * nComp;
                let point3 = new THREE.Vector3(verticeArray[pos], verticeArray[pos + 1], verticeArray[pos + 2]);

                let r1 = point1.clone().sub(point2).lengthSq();
                let r2 = point1.clone().sub(point3).lengthSq();
                if (r1 > r2) { r1 = r2; reg = 1; }                for (let j = 0; j < circleDiv; ++j) {
                    //geo.faces.push(new THREE.Face3(offset + j, offset + (j + reg) % circleDiv + circleDiv, offset + (j + 1) % circleDiv, undefined, c));
                    //geo.faces.push(new THREE.Face3(offset + (j + 1) % circleDiv, offset + (j + reg) % circleDiv + circleDiv, offset + (j + reg + 1) % circleDiv + circleDiv, undefined, c));
                    //indexArray = indexArray.concat([offset + j, offset + (j + reg) % circleDiv + circleDiv, offset + (j + 1) % circleDiv]);
                    indexArray[offset3++] = offsetTmp + j;
                    indexArray[offset3++] = offsetTmp + (j + reg) % circleDiv + circleDiv;
                    indexArray[offset3++] = offsetTmp + (j + 1) % circleDiv;

                    //indexArray = indexArray.concat([offset + (j + 1) % circleDiv, offset + (j + reg) % circleDiv + circleDiv, offset + (j + reg + 1) % circleDiv + circleDiv]);
                    indexArray[offset3++] = offsetTmp + (j + 1) % circleDiv;
                    indexArray[offset3++] = offsetTmp + (j + reg) % circleDiv + circleDiv;
                    indexArray[offset3++] = offsetTmp + (j + reg + 1) % circleDiv + circleDiv;
                }
                offsetTmp += circleDiv;
            }

            geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(verticeArray), nComp));
            geo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), nComp));

            geo.setIndex(new THREE.BufferAttribute(new Uint32Array(indexArray), 1));
            //geo.setIndex(indexArray);

            //geo.computeFaceNormals();
            //geo.computeVertexNormals(false);
            geo.computeVertexNormals();
/*
        }
        else {
            let axisDiv = ic.axisDIV;

            let pnts_clrs = me.subdivideCls.subdivide(_pnts, colors, axisDiv, undefined, undefined, prevone, nexttwo);
            // extend one residue
            //var pnts_clrs = me.subdivideCls.subdivide(_pnts, colors, axisDiv, undefined, undefined, prevone, nexttwo, true);

            _pnts = pnts_clrs[0];
            colors = pnts_clrs[2];

            let radius = ic.coilWidth;
            segments = _pnts.length;
            //radiusSegments = 8;
            radiusSegments = 4; // save memory
            let closed = false;

            // when using radiusArray with modified three.js, the tube didn't work in picking
            let geo = new THREE.TubeGeometry(
                new THREE.CatmullRomCurve3(_pnts), // path
                segments,
                radius, //radiusArray, //radius,
                radiusSegments,
                closed
            );

            //https://stemkoski.github.io/Three.js/Graphulus-Curve.html
            let color, face, numberOfSides, vertexIndex;
            // faces are indexed using characters
            let faceIndices = [ 'a', 'b', 'c', 'd' ];

            // first, assign colors to vertices as desired
            let prevColor,  geoColors = {};
            for ( let s = 0; s <= segments; s++ ) {
                for ( let r = 0; r < radiusSegments; r++ )
                {
                    vertexIndex = r + s * radiusSegments;
                    color = colors[s];
                    if(!color) color = prevColor;

                    geoColors[vertexIndex] = color; // use this array for convenience

                    prevColor = color;
                }
            }

            // copy the colors as necessary to the face's vertexColors array.
            // after version r125, geo.faces is undefined for TubeGeometry
            for ( let i = 0; geo.faces && i < geo.faces.length; i++ )
            {
                face = geo.faces[ i ];

                numberOfSides = ( face instanceof THREE.Face3 ) ? 3 : 4;
                for( let j = 0; j < numberOfSides; j++ )
                {
                    vertexIndex = face[ faceIndices[ j ] ];
                    face.vertexColors[ j ] = geoColors[ vertexIndex ];
                }
            }

            geo.computeFaceNormals();
            geo.computeVertexNormals(false);
        }
*/
        let mesh;
        if(bHighlight === 2) {
          mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ transparent: true, opacity: 0.5, specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, vertexColors: THREE.FaceColors, side: THREE.DoubleSide }));

          if(ic.mdl) ic.mdl.add(mesh);
        }
        else if(bHighlight === 1) {
          mesh = new THREE.Mesh(geo, ic.matShader);
          mesh.renderOrder = ic.renderOrderPicking;
          //ic.mdlPicking.add(mesh);
          if(ic.mdl) ic.mdl.add(mesh);
        }
        else {
          mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, vertexColors: THREE.FaceColors, side: THREE.DoubleSide }));
          //mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: 0xFFFFFF, side: THREE.DoubleSide }));

          if(ic.mdl) ic.mdl.add(mesh);
        }

        if(bHighlight === 1 || bHighlight === 2) {
            ic.prevHighlightObjects.push(mesh);
        }
        else {
            ic.objects.push(mesh);
        }
    }

    getRadius(radius, atom) { let ic = this.icn3d; ic.icn3dui;
        let radiusFinal = radius;
        if(radius) {
            radiusFinal = radius;
        }
        else {
            if(atom.b > 0 && atom.b <= 100) {
                radiusFinal = atom.b * 0.01;
            }
            else if(atom.b > 100) {
                radiusFinal = 100 * 0.01;
            }
            else {
                radiusFinal = ic.coilWidth;
            }
        }

        return radiusFinal;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class CartoonNucl {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from GLmol (http://webglmol.osdn.jp/index-en.html)
    //Create curves for nucleotide "atoms". "div" means how many pnts are used to smooth the curve. It's typically 5.
    //"thickness" is the thickness of the curve. "bHighlight" is an option to draw the highlight for these atoms.
    //The highlight could be outlines with bHighlight=1 and 3D objects with bHighlight=2.
    drawCartoonNucleicAcid(atomlist, div, thickness, bHighlight) {
       this.drawStrandNucleicAcid(atomlist, 2, div, true, undefined, thickness, bHighlight);
    }

    // modified from GLmol (http://webglmol.osdn.jp/index-en.html)
    drawStrandNucleicAcid(atomlist, num, div, fill, nucleicAcidWidth, thickness, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
       if(me.bNode) return;

       if(bHighlight === 2) {
           num = undefined;
           thickness = undefined;
       }

       nucleicAcidWidth = nucleicAcidWidth || ic.nucleicAcidWidth;
       div = div || ic.axisDIV;
       num = num || ic.nucleicAcidStrandDIV;
       let i, j, k;
       let pnts = []; for (k = 0; k < num; k++) pnts[k] = [];
       let colors = [];
       let currentChain, currentResi, currentO3;
       let prevOO = null;

       for (i in atomlist) {
          let atom = atomlist[i];
          if (atom === undefined) continue;

          let chainid = atom.structure + '_' + atom.chain;
          let currentChainid = atom.structure + '_' + currentChain;

          if ((atom.name === 'O3\'' || atom.name === 'OP2' || atom.name === 'O3*' || atom.name === 'O2P') && !atom.het) {
             if (atom.name === 'O3\'' || atom.name === 'O3*') { // to connect 3' end. FIXME: better way to do?
                if (currentChain !== atom.chain 
                  || ic.ParserUtilsCls.getResiNCBI(currentChainid, currentResi) + 1 !== ic.ParserUtilsCls.getResiNCBI(chainid, atom.resi)) {
    //            if (currentChain !== atom.chain) {
                   if (currentO3 && prevOO) {
                      for (j = 0; j < num; j++) {
                         let delta = -1 + 2 / (num - 1) * j;
                         pnts[j].push(new THREE.Vector3(currentO3.x + prevOO.x * delta,
                          currentO3.y + prevOO.y * delta, currentO3.z + prevOO.z * delta));
                      }
                   }
                   if (fill) ic.stripCls.createStrip(pnts[0], pnts[1], colors, div, thickness, bHighlight);
                   for (j = 0; !thickness && j < num; j++)
                      ic.curveCls.createCurveSub(pnts[j], 1 ,colors, div, bHighlight);
                   pnts = []; for (k = 0; k < num; k++) pnts[k] = [];
                   colors = [];
                   prevOO = null;
                }
                currentO3 = new THREE.Vector3(atom.coord.x, atom.coord.y, atom.coord.z);
                currentChain = atom.chain;
                currentResi = atom.resi;
                if(bHighlight === 1 || bHighlight === 2) {
                    colors.push(ic.hColor);
                }
                else {
                    colors.push(atom.color);
                }

             }
             else if (atom.name === 'OP2' || atom.name === 'O2P') {
                if (!currentO3) {prevOO = null; continue;} // for 5' phosphate (e.g. 3QX3)
                let O = new THREE.Vector3(atom.coord.x, atom.coord.y, atom.coord.z);
                O.sub(currentO3);
                O.normalize().multiplyScalar(nucleicAcidWidth);  // TODO: refactor
                //if (prevOO !== undefined && O.dot(prevOO) < 0) {
                if (prevOO !== null && O.dot(prevOO) < 0) {
                   O.negate();
                }
                prevOO = O;
                for (j = 0; j < num; j++) {
                   let delta = -1 + 2 / (num - 1) * j;
                   pnts[j].push(new THREE.Vector3(currentO3.x + prevOO.x * delta,
                     currentO3.y + prevOO.y * delta, currentO3.z + prevOO.z * delta));
                }
                currentO3 = null;
             }
          }
       }

       if (currentO3 && prevOO) {
          for (j = 0; j < num; j++) {
             let delta = -1 + 2 / (num - 1) * j;
             pnts[j].push(new THREE.Vector3(currentO3.x + prevOO.x * delta,
               currentO3.y + prevOO.y * delta, currentO3.z + prevOO.z * delta));
          }
       }
       if (fill) ic.stripCls.createStrip(pnts[0], pnts[1], colors, div, thickness, bHighlight);
       for (j = 0; !thickness && j < num; j++)
          ic.curveCls.createCurveSub(pnts[j], 1 ,colors, div, bHighlight);
    }

    // modified from GLmol (http://webglmol.osdn.jp/index-en.html)
    //Create sticks between two nucleotide curves for nucleotide "atoms". "bHighlight" is an option to
    //draw the highlight for these atoms. The highlight could be outlines with bHighlight=1 and 3D objects with bHighlight=2.
    drawNucleicAcidStick(atomlist, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;
       if(me.bNode) return;

       let currentChain, currentResi, start = null, end = null;
       let i;

       for (i in atomlist) {
          let atom = atomlist[i];
          if (atom === undefined || atom.het) continue;

          if (atom.resi !== currentResi || atom.chain !== currentChain) {
             if (start !== null && end !== null) {
                ic.cylinderCls.createCylinder(new THREE.Vector3(start.coord.x, start.coord.y, start.coord.z),
                                  new THREE.Vector3(end.coord.x, end.coord.y, end.coord.z), ic.cylinderRadius, start.color, bHighlight);
             }
             start = null; end = null;
          }
          if (atom.name === 'O3\'' || atom.name === 'O3*') start = atom;

          if (atom.resn.trim() === 'A' || atom.resn.trim() === 'G' || atom.resn.trim() === 'DA' || atom.resn.trim() === 'DG') {
             //if (atom.name === 'N1')  end = atom; //  N1(AG), N3(CTU)
             if (atom.name === 'N9')  end = atom; //  N1(AG), N3(CTU)
          //} else if (atom.name === 'N3') {
          } else if (atom.name === 'N1') {
             end = atom;
          }

          currentResi = atom.resi; currentChain = atom.chain;
       }
       if (start !== null && end !== null)
          ic.cylinderCls.createCylinder(new THREE.Vector3(start.coord.x, start.coord.y, start.coord.z),
                            new THREE.Vector3(end.coord.x, end.coord.y, end.coord.z), ic.cylinderRadius, start.color, bHighlight);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class TextSprite {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from 3Dmol (http://3dmol.csb.pitt.edu/)
    // new: http://stackoverflow.com/questions/23514274/three-js-2d-text-sprite-labels
    // old: http://stemkoski.github.io/Three.js/Sprite-Text-Labels.html
    makeTextSprite( message, parameters ) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        if ( parameters === undefined ) parameters = {};
        let fontface = parameters.hasOwnProperty("fontface") ? parameters["fontface"] : "Arial";
        let fontsize = parameters.hasOwnProperty("fontsize") ? parameters["fontsize"] : 18;
        let factor = parameters.hasOwnProperty("factor") ? parameters["factor"] : 1;

        let a = parameters.hasOwnProperty("alpha") ? parameters["alpha"] : 1.0;

        let bBkgd = false; //true;
        let bSchematic = false;
        if(parameters.hasOwnProperty("bSchematic") &&  parameters["bSchematic"]) {
            bSchematic = true;
            bBkgd = true;

            fontsize = 40;
        }

        let backgroundColor, borderColor, borderThickness;
        if(parameters.hasOwnProperty("backgroundColor") &&  parameters["backgroundColor"] !== undefined) {
            backgroundColor = me.utilsCls.hexToRgb(parameters["backgroundColor"], a);

            borderColor = parameters.hasOwnProperty("borderColor") ? me.utilsCls.hexToRgb(parameters["borderColor"], a) : { r:0, g:0, b:0, a:1.0 };
            borderThickness = parameters.hasOwnProperty("borderThickness") ? parameters["borderThickness"] : 4;
        }
        else {
            bBkgd = false;
            backgroundColor = undefined;
            borderColor = undefined;
            borderThickness = 0;
        }

        let textAlpha = 1.0;
        // default yellow
        //let textColor = parameters.hasOwnProperty("textColor") &&  parameters["textColor"] !== undefined ? me.utilsCls.hexToRgb(parameters["textColor"], textAlpha) : { r:255, g:255, b:0, a:1.0 };
        // default black or white
        let defaultColor = (ic.opts.background != 'black') ? { r:0, g:0, b:0, a:1.0 } : { r:255, g:255, b:0, a:1.0 };
        let textColor = parameters.hasOwnProperty("textColor") &&  parameters["textColor"] !== undefined ? me.utilsCls.hexToRgb(parameters["textColor"], textAlpha) 
            : defaultColor;
        if(!textColor) textColor = defaultColor;

        let canvas = document.createElement('canvas');

        let context = canvas.getContext('2d');

        context.font = "Bold " + fontsize + "px " + fontface;

        let metrics = context.measureText( message );

        let textWidth = metrics.width;

        let width = textWidth + 2*borderThickness;
        let height = fontsize + 2*borderThickness;

        if(bSchematic) {
            if(width > height) {
                height = width;
            }
            else {
                width = height;
            }
        }

        let expandWidthFactor = 0.8 * textWidth / height;

        canvas.width = width;
        canvas.height = height;

        context.clearRect(0, 0, width, height);

        //var radius = context.measureText( "M" ).width;

        if(bBkgd) {
            // background color
            context.fillStyle   = "rgba(" + backgroundColor.r + "," + backgroundColor.g + "," + backgroundColor.b + "," + backgroundColor.a + ")";
            // border color
            context.strokeStyle = "rgba(" + borderColor.r + "," + borderColor.g + "," + borderColor.b + "," + borderColor.a + ")";

            context.lineWidth = borderThickness;

            if(bSchematic) {
                let r = width * 0.4; //width * 0.35;
                this.circle(context, 0, 0, width, height, r);
            }
            else {
                //var r = (message.length <= textLengthThreshold) ? height * 0.5 : 0;
                //var r = height * 0.8;
                let r = 0;
                this.roundRect(context, 0, 0, width, height, r);
            }
        }

        // need to redefine again
        context.font = "Bold " + fontsize + "px " + fontface;

        context.textAlign = "center";
        context.textBaseline = "middle";

        context.fillStyle = "rgba("+textColor.r+", "+textColor.g+", "+textColor.b+", 1.0)";
        context.strokeStyle = "rgba("+textColor.r+", "+textColor.g+", "+textColor.b+", 1.0)";

        context.fillText( message, width * 0.5, height * 0.5);

        // canvas contents will be used for a texture
        let texture = new THREE.Texture(canvas);
        texture.needsUpdate = true;

        let frontOfTarget = true;
        //var spriteMaterial = new THREE.SpriteMaterial( { map: texture, useScreenCoordinates: false } );
        let spriteMaterial = new THREE.SpriteMaterial( {
            map: texture,
            //useScreenCoordinates: false,
            depthTest: !frontOfTarget,
            depthWrite: !frontOfTarget
        } );

        //https://stackoverflow.com/questions/29421702/threejs-texture
        spriteMaterial.map.minFilter = THREE.LinearFilter;

        let sprite = new THREE.Sprite( spriteMaterial );

        if(bSchematic) {
            //sprite.scale.set(factor, factor, 1.0);
            sprite.scale.set(0.3*factor, 0.3*factor, 1.0);
        }
        else {
            sprite.scale.set(expandWidthFactor * factor, factor, 1.0);
        }

        return sprite;
    }

    // function for drawing rounded rectangles
    roundRect(ctx, x, y, w, h, r) {
        ctx.beginPath();
        ctx.moveTo(x+r, y);
        ctx.lineTo(x+w-r, y);
        ctx.quadraticCurveTo(x+w, y, x+w, y+r);
        ctx.lineTo(x+w, y+h-r);
        ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
        ctx.lineTo(x+r, y+h);
        ctx.quadraticCurveTo(x, y+h, x, y+h-r);
        ctx.lineTo(x, y+r);
        ctx.quadraticCurveTo(x, y, x+r, y);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
    }

    circle(ctx, x, y, w, h, r) {
        ctx.beginPath();
        ctx.arc(x+w/2, (y+h/2) * 0.9, r, 0, 2*Math.PI, true); // adjust the y by 0.9
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
    }
}

class Label {
    constructor(icn3d) {
        this.icn3d = icn3d;

        this.textSpriteCls = new TextSprite(icn3d);
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create labels for a list of "labels", each of which has the properties 'position',
    //'text', 'size', 'color', and 'background'.
    createLabelRepresentation(labels) { let ic = this.icn3d; ic.icn3dui;
        let dimFactor = ic.oriMaxD / 100;
        if(dimFactor < 0.4) dimFactor = 0.4;

        let oriFactor = 3 * dimFactor * ic.labelScale;

        for(let name in labels) {
            let labelArray = (labels[name] !== undefined) ? labels[name] : [];
            let defaultColor = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd;

            for (let i = 0, il = labelArray.length; i < il; ++i) {
                let label = labelArray[i];
                // make sure fontsize is a number

                if(label.size == 0) label.size = undefined;
                if(label.color == 0) label.color = undefined;
                if(label.background == 0) label.background = undefined;

                let labelsize = (label.size !== undefined) ? label.size : ic.LABELSIZE;
                let labelcolor = (label.color !== undefined) ? label.color : defaultColor;
                if(ic.labelcolor) labelcolor = ic.labelcolor;
                
                let labelbackground = (label.background !== undefined) ? label.background : '#cccccc';
                let labelalpha = (label.alpha !== undefined) ? label.alpha : 1.0;

                // if label.background is undefined, no background will be drawn
                labelbackground = label.background;

                if(labelcolor !== undefined && labelbackground !== undefined && labelcolor.toLowerCase() === labelbackground.toString().toLowerCase()) {
                    labelcolor = "#888888";
                }

                let bb;
                if(label.bSchematic !== undefined && label.bSchematic) {
                    bb = this.textSpriteCls.makeTextSprite(label.text, {fontsize: parseInt(labelsize), textColor: labelcolor, borderColor: labelbackground, backgroundColor: labelbackground, alpha: labelalpha, bSchematic: 1, factor: oriFactor});
                }
                else {
                    if(label.text.length === 1) {
                        bb = this.textSpriteCls.makeTextSprite(label.text, {fontsize: parseInt(labelsize), textColor: labelcolor, borderColor: labelbackground, backgroundColor: labelbackground, alpha: labelalpha, bSchematic: 1, factor: oriFactor});
                    }
                    else {
                        let factor = (label.factor) ? oriFactor * label.factor : oriFactor;
                        bb = this.textSpriteCls.makeTextSprite(label.text, {fontsize: parseInt(labelsize), textColor: labelcolor, borderColor: labelbackground, backgroundColor: labelbackground, alpha: labelalpha, bSchematic: 0, factor: factor});
                    }
                }

                let labelOffset = (name == 'schematic' || name == 'residue') ? 0 : ic.coilWidth; // 0.3
                bb.position.set(parseFloat(label.position.x) + labelOffset, parseFloat(label.position.y) + labelOffset, parseFloat(label.position.z) + labelOffset);
                ic.mdl.add(bb);          
                // do not add labels to objects for pk
            }
        }
    }

    hideLabels() { let ic = this.icn3d; ic.icn3dui;
        // remove previous labels
        if(ic.mdl !== undefined) {
            for(let i = 0, il = ic.mdl.children.length; i < il; ++i) {
                 let mesh = ic.mdl.children[i];
                 if(mesh !== undefined && mesh.type === 'Sprite') {
                     ic.mdl.remove(mesh); // somehow didn't work
                 }
            }
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Axes {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // http://soledadpenades.com/articles/three-js-tutorials/drawing-the-coordinate-axes/
    //Build the xyz-axes from the center of atoms. The maximum axes length is equal to "radius" in angstrom.
    buildAxes(radius, center, positionX, positionY, positionZ, bSelection) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        new THREE.Object3D();

        let x = 0, y = 0, z = 0;

        if(bSelection) {
            x = center.x;
            y = center.y;
            z = center.z;
        }
        else {
            x -= radius * 0.3; //0.707; // move to the left
            y -= radius * 0.3; //0.707; // move to the botom
        }
        let origin = new THREE.Vector3( x, y, z );

        let axisLen = radius / 10;
        let r = radius / 100;

        let axisVecX, axisVecY, axisVecZ;
        let axisLenX, axisLenY, axisLenZ;
        axisLenX = axisLenY = axisLenZ = axisLen;
        if(bSelection) {
            axisVecX = positionX.clone().sub(center);
            axisVecY = positionY.clone().sub(center);
            axisVecZ = positionZ.clone().sub(center);

            axisLenX = axisVecX.length();
            axisLenY = axisVecY.length();
            axisLenZ = axisVecZ.length();

            r = axisLenX / 100;

            if(r < 0.4) r = 0.4;
        }

        let meshX, meshY, meshZ;
        if(bSelection) {
            meshX = ic.cylinderCls.createCylinder_base( center, positionX, r, me.parasCls.thr(0xFF0000)); // +X
            meshY = ic.cylinderCls.createCylinder_base( center, positionY, r, me.parasCls.thr(0x00FF00)); // +Y
            meshZ = ic.cylinderCls.createCylinder_base( center, positionZ, r, me.parasCls.thr(0x0000FF)); // +Z
        }
        else {
            meshX = ic.cylinderCls.createCylinder_base( new THREE.Vector3( x, y, z ), new THREE.Vector3( x + axisLenX, y, z ), r, me.parasCls.thr(0xFF0000)); // +X
            meshY = ic.cylinderCls.createCylinder_base( new THREE.Vector3( x, y, z ), new THREE.Vector3( x, y + axisLenY, z ), r, me.parasCls.thr(0x00FF00)); // +Y
            meshZ = ic.cylinderCls.createCylinder_base( new THREE.Vector3( x, y, z ), new THREE.Vector3( x, y, z + axisLenZ ), r, me.parasCls.thr(0x0000FF)); // +Z
        }

        ic.mdl.add( meshX );
        ic.mdl.add( meshY );
        ic.mdl.add( meshZ );

        let dirX = (bSelection) ? axisVecX.normalize() : new THREE.Vector3( 1, 0, 0 );
        let colorX = 0xff0000;
        let posX = (bSelection) ? positionX : new THREE.Vector3(origin.x + axisLen, origin.y, origin.z);
        let arrowX = this.createArrow( dirX, posX, axisLenX, colorX, 4*r, 4*r);
        ic.mdl.add( arrowX );

        let dirY = (bSelection) ? axisVecY.normalize() : new THREE.Vector3( 0, 1, 0 );
        let colorY = 0x00ff00;
        let posY = (bSelection) ? positionY : new THREE.Vector3(origin.x, origin.y + axisLen, origin.z);
        let arrowY = this.createArrow( dirY, posY, axisLenY, colorY, 4*r, 4*r);
        ic.mdl.add( arrowY );

        let dirZ = (bSelection) ? axisVecZ.normalize() : new THREE.Vector3( 0, 0, 1 );
        let colorZ = 0x0000ff;
        let posZ = (bSelection) ? positionZ : new THREE.Vector3(origin.x, origin.y, origin.z + axisLen);
        let arrowZ = this.createArrow( dirZ, posZ, axisLenZ, colorZ, 4*r, 4*r);
        ic.mdl.add( arrowZ );
    }

    buildAllAxes(radius, bSelection) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        if(ic.pc1) {
            for(let i = 0, il = ic.axes.length; i < il; ++i) {
               let center = ic.axes[i][0];
               let positionX = ic.axes[i][1];
               let positionY = ic.axes[i][2];
               let positionZ = ic.axes[i][3];

               this.buildAxes(radius, center, positionX, positionY, positionZ, bSelection);
            }
        }
    }

    createArrow(dir, origin, axisLen, color, headLength, headWidth, bGlycan) {  let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let coneGeometry = new THREE.CylinderBufferGeometry( 0, 0.5, 1, 32, 1 );
        //coneGeometry.translate( 0, - 0.5, 0 );
        coneGeometry.translate( 0, 0.5, 0 );
        let material;
        if(bGlycan) {
            material = new THREE.MeshPhongMaterial({ transparent: true, opacity: 0.5, specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, color: color });

        }
        else {
            material = new THREE.MeshPhongMaterial({ specular: ic.frac, shininess: ic.shininess, emissive: ic.emissive, side: THREE.DoubleSide, color: color});
        }

        let cone = new THREE.Mesh( coneGeometry, material);
    //    cone.matrixAutoUpdate = false;

        let quaternion = new THREE.Quaternion();
        // dir is assumed to be normalized
        if ( dir.y > 0.99999 ) {
            quaternion.set( 0, 0, 0, 1 );
        } else if ( dir.y < - 0.99999 ) {
            quaternion.set( 1, 0, 0, 0 );
        } else {
            let axis = new THREE.Vector3();
            axis.set( dir.z, 0, - dir.x ).normalize();
            let radians = Math.acos( dir.y );
            quaternion.setFromAxisAngle( axis, radians );
        }

        cone.applyQuaternion(quaternion);
        cone.scale.set( headWidth, headLength, headWidth );
        //origin.add(new THREE.Vector3(0, axisLen, 0));
        cone.position.copy( origin );

        return cone;
    }

    setPc1Axes() { let ic = this.icn3d, me = ic.icn3dui;
       if(me.bNode) return;

       let atomHash = me.hashUtilsCls.intHash(ic.hAtoms, ic.dAtoms);

       // do PCA, get first eigen vector
       let coordArray = [];
       let prevResid = '';
       let bSmall = (Object.keys(atomHash).length < 100) ? true : false;
       for(let serial in atomHash) {
           let atom = ic.atoms[serial];
           let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
           if(!bSmall && resid == prevResid) continue; // speed up
           coordArray.push(atom.coord.clone());
       }

       let eigenRet = me.rmsdSuprCls.getEigenForSelection(coordArray, coordArray.length);
       let vecX = new THREE.Vector3(eigenRet.h1[0], eigenRet.h1[1], eigenRet.h1[2]);

       if(eigenRet.k == 0 && ic.bRender) {
           alert("Can't determine the first principal component. Please select a subset and try it again.");
           return;
       }

       let result = ic.applyCenterCls.centerAtoms(atomHash);
       let maxD = result.maxD;
       let center = result.center;

    /*
       let positionXTmp = center.clone().add(vecX.normalize().multiplyScalar(maxD * 0.5));
       let positionXMinusTmp = center.clone().multiplyScalar(2).sub(positionXTmp);

       let linex = new THREE.Line3( positionXMinusTmp, positionXTmp );

       let maxLenY = 0, maxLenX = 0, coordY, coordYInLine;
       prevResid = '';
       for(let serial in atomHash) {
           let atom = ic.atoms[serial];
           let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
           if(!bSmall && resid == prevResid) continue; // speed up

           let posInLine = new THREE.Vector3();
           linex.closestPointToPoint ( atom.coord, false, posInLine);

           let lenY = posInLine.distanceTo(atom.coord);
           if(lenY > maxLenY) {
               coordY = atom.coord;
               coordYInLine = posInLine;

               maxLenY = lenY;
           }

           let lenX = posInLine.distanceTo(center);
           if(lenX > maxLenX) {
               maxLenX = lenX;
           }
       }

       let positionX = center.clone().add(vecX.normalize().multiplyScalar(maxLenX));

       // translate
       centerTrans = center.clone().sub(coordYInLine);
       let positionY = coordY.clone().add(centerTrans);

       let vecZ = new THREE.Vector3();
       let vecY = positionY.clone().sub(center);
       vecZ.crossVectors( positionX.clone().sub(center), vecY ).normalize();
       vecZ.multiplyScalar(vecY.length());

       positionZ = center.clone().add(vecZ);

       this.buildAxes(undefined, center, positionX, positionY, positionZ, true);

       let axisPos = [center, positionX, positionY, positionZ];
       ic.axes.push(axisPos);

       ic.drawCls.draw();
    */

       let positionX = center.clone().add(vecX.normalize().multiplyScalar(maxD * 0.4));

       let vecY = new THREE.Vector3(eigenRet.h2[0], eigenRet.h2[1], eigenRet.h2[2]);
       let positionY = center.clone().add(vecY.normalize().multiplyScalar(maxD * 0.3));

       let vecZ = new THREE.Vector3(eigenRet.h3[0], eigenRet.h3[1], eigenRet.h3[2]);
       let positionZ = center.clone().add(vecZ.normalize().multiplyScalar(maxD * 0.3));

       this.buildAxes(undefined, center, positionX, positionY, positionZ, true);

       let axisPos = [center, positionX, positionY, positionZ];
       ic.axes.push(axisPos);

       ic.drawCls.draw();

       return axisPos;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Glycan {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    showGlycans() { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let glycan2resids = {};
        //var atomHash = me.hashUtilsCls.intHash(ic.hAtoms, ic.dAtoms);
        let atomHash = ic.dAtoms;

        for(let i in atomHash) {
            let atom = ic.atoms[i];
            if(atom.het && me.parasCls.glycanHash.hasOwnProperty(atom.resn) != -1) {
                if(glycan2resids[atom.resn] === undefined) glycan2resids[atom.resn] = {};
                if(atom.chain != 'Misc') {
                    glycan2resids[atom.resn][atom.structure + '_' + atom.chain + '_' + atom.resi] = 1;
                }
            }
        }

        // two types of shape: cube,sphere
        // four types of color: ic.glycanColors
        let glycanNames = Object.keys(glycan2resids);
        for(let i = 0, il = glycanNames.length; i < il; ++i) {
            let glycanName = glycanNames[i];
            if(!me.parasCls.glycanHash.hasOwnProperty(glycanName)) continue;

            let shape = me.parasCls.glycanHash[glycanName].s;
            let color = new THREE.Color('#' + me.parasCls.glycanHash[glycanName].c);

            let resiArray = Object.keys(glycan2resids[glycanName]);
            for(let j = 0, jl = resiArray.length; j < jl; ++j) {
                let result = ic.applyCenterCls.centerAtoms(ic.residues[resiArray[j]]);
                let center = result.center;
                let radius = result.maxD * 0.5 * 0.6;

                if(shape == 'cube') {
                    ic.boxCls.createBox_base(center, radius, color, false, false, true);
                }
                else if(shape == 'sphere') {
                    ic.sphereCls.createSphereBase(center, color, radius, 1, false, true);
                }
                else if(shape == 'cone') {
                    let dirZ = new THREE.Vector3( 0, 0, 1 );

                    let arrowZ = ic.axesCls.createArrow( dirZ, new THREE.Vector3(0, 0, -1*radius).add(center), 0, color, 2*radius, 2*radius, true);
                    ic.mdl.add( arrowZ );
                    ic.objects.push(arrowZ);
                }
                else if(shape == 'cylinder') {
                    let p0 = new THREE.Vector3(0, 0, radius).add(center);
                    let p1 = new THREE.Vector3(0, 0, -1*radius).add(center);
                    ic.cylinderCls.createCylinder(p0, p1, radius, color, false, color, false, true);
                }
            }
        }
    }

}

/* marchingcube.js
 * @author David Koes  / https://github.com/3dmol/3Dmol.js/tree/master/3Dmol
 * Modified by Jiyao Wang / https://github.com/ncbi/icn3d
 */

//var $3Dmol = $3Dmol || {};

class MarchingCube {
    constructor(icn3d) {
        this.icn3d = icn3d;

//Encapsulate marching cube algorithm for isosurface generation
//(currently used by protein surface rendering and generic volumetric data reading)
//$3Dmol.MarchingCubeInitializer = function() { let me = this, ic = me.icn3d; "use strict";

    //Marching cube algorithm - assume data has been pre-treated so isovalue is 0
    //(i.e. select points greater than 0)
    //origin -  vector of origin of volumetric data(default is(0,0,0))
    // nX, nY, nZ - specifies number of voxels in each dimension
    // scale - cube diagonal unit vector scale(3Dmol vector)(specifying distance between data points); diagonal of cube
    // - default is 1 - assumes unit cube(1,1,1) diag)
    // fulltable - if true, use full marching cubes and tritables - else use trimmed table(e.g. surf render)
    // voxel - if true, draws with a blocky voxel style(default false)
    // verts, faces - vertex and face arrays to fill up

        //to match with protein surface...
        this.ISDONE = 2;
        //var my = {};

        /*
         * These tables are based off those by Paul Bourke and Geoffrey Heller:
         * http://paulbourke.net/geometry/polygonise/
         * http://paulbourke.net/geometry/polygonise/table2.txt
         *
         * However, they have been substantially modified to reflect a more
         * sensible corner numbering scheme and the discrete nature of our voxel data
         *(resulting in fewer faces).
         */
        let edgeTableOri = [ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
                0xb00, 0x0, 0x0, 0x0, 0x700, 0x0, 0xd00, 0xe00, 0xf00, 0x0, 0x0, 0x0,
                0x8a, 0x0, 0x15, 0x0, 0x86, 0x0, 0x0, 0x0, 0x28c, 0x0, 0x813, 0xf19,
                0xe10, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x0, 0x0, 0x126, 0x0, 0x0, 0x15, 0x1c,
                0x0, 0xf23, 0x419, 0xd20, 0x0, 0xa8, 0xa2, 0xaa, 0x0, 0x285, 0x9ab,
                0x8a2, 0x0, 0x2af, 0x125, 0xac, 0xfaa, 0xea3, 0xda9, 0xca0, 0x0, 0x0,
                0x0, 0x0, 0x0, 0x45, 0x0, 0x384, 0x0, 0x0, 0x0, 0x700, 0x8a, 0x83,
                0x648, 0x780, 0x0, 0x51, 0x0, 0x81a, 0x54, 0x55, 0x54, 0x56, 0x0, 0x51,
                0x0, 0xe5c, 0x14a, 0x451, 0x759, 0x650, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x45,
                0x0, 0x1f6, 0x0, 0x0, 0x15, 0xdfc, 0x8a, 0x7f3, 0x4f9, 0x5f0, 0xb00,
                0x68, 0x921, 0x6a, 0x348, 0x245, 0x16f, 0x66, 0xb00, 0xe6f, 0xd65,
                0xc6c, 0x76a, 0x663, 0x569, 0x460, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
                0xf46, 0x0, 0x0, 0x45, 0x24c, 0x2a, 0x823, 0x29, 0xb40, 0x0, 0x0, 0x0,
                0x6ba, 0x0, 0x8f5, 0xfff, 0xef6, 0x0, 0xff, 0x2f5, 0x2fc, 0x9ea, 0x8f3,
                0xbf9, 0xaf0, 0x0, 0x0, 0x51, 0x152, 0x0, 0xf55, 0x45f, 0xd56, 0x54,
                0x357, 0x55, 0x154, 0x852, 0xb53, 0x59, 0x950, 0x700, 0x2c8, 0xc2,
                0x48a, 0xfc4, 0xec5, 0xdcf, 0xcc6, 0x2c4, 0x2cf, 0xc5, 0xcc, 0xbca,
                0xac3, 0x9c9, 0x8c0, 0x0, 0x0, 0x0, 0x0, 0xa8, 0x1a4, 0xa8, 0x7a6,
                0xa2, 0xa2, 0x2a4, 0xbac, 0xaa, 0xa3, 0x2a8, 0x3a0, 0xd00, 0xc18,
                0xd00, 0xe3a, 0x34, 0x35, 0x73f, 0x636, 0x924, 0x83f, 0xb35, 0xa3c,
                0x12a, 0x33, 0x339, 0x230, 0xe00, 0xe00, 0xc12, 0xd9a, 0x684, 0x795,
                0x49f, 0x596, 0x92, 0xb9f, 0x815, 0x99c, 0x9a, 0x393, 0x99, 0x190,
                0xf00, 0xe08, 0xd01, 0xc0a, 0x704, 0x605, 0x50f, 0x406, 0xb02, 0xa0f,
                0x905, 0x80c, 0x30a, 0x203, 0x109, 0x0 ];

        this.edgeTable = new Uint32Array(edgeTableOri);

        this.triTable = [ [], [], [], [], [], [], [], [ 11, 9, 8 ], [], [], [],
                [ 8, 10, 9 ], [], [ 10, 8, 11 ], [ 9, 11, 10 ],
                [ 8, 10, 9, 8, 11, 10 ], [], [], [], [ 1, 7, 3 ], [], [ 4, 2, 0 ], [],
                [ 2, 1, 7 ], [], [], [], [ 2, 7, 3, 2, 9, 7 ], [],
                [ 1, 4, 11, 1, 0, 4 ], [ 3, 8, 0, 11, 9, 4, 11, 10, 9 ],
                [ 4, 11, 9, 11, 10, 9 ], [], [], [], [ 5, 3, 1 ], [], [], [],
                [ 2, 5, 8, 2, 1, 5 ], [], [], [ 2, 4, 0 ], [ 3, 2, 4 ], [],
                [ 0, 9, 1, 8, 10, 5, 8, 11, 10 ], [ 3, 4, 0, 3, 10, 4 ],
                [ 5, 8, 10, 8, 11, 10 ], [], [ 3, 5, 7 ], [ 7, 1, 5 ],
                [ 1, 7, 3, 1, 5, 7 ], [], [ 9, 2, 0, 9, 7, 2 ],
                [ 0, 3, 8, 1, 7, 11, 1, 5, 7 ], [ 11, 1, 7, 1, 5, 7 ], [],
                [ 9, 1, 0, 5, 3, 2, 5, 7, 3 ], [ 8, 2, 5, 8, 0, 2 ],
                [ 2, 5, 3, 5, 7, 3 ], [ 3, 9, 1, 3, 8, 9, 7, 11, 10, 7, 10, 5 ],
                [ 9, 1, 0, 10, 7, 11, 10, 5, 7 ], [ 3, 8, 0, 7, 10, 5, 7, 11, 10 ],
                [ 11, 5, 7, 11, 10, 5 ], [], [], [], [], [], [ 0, 6, 2 ], [],
                [ 7, 2, 9, 7, 9, 8 ], [], [], [], [ 8, 10, 9 ], [ 7, 1, 3 ],
                [ 7, 1, 0 ], [ 6, 9, 3, 6, 10, 9 ], [ 7, 10, 8, 10, 9, 8 ], [],
                [ 6, 0, 4 ], [], [ 11, 1, 4, 11, 3, 1 ], [ 2, 4, 6 ],
                [ 2, 0, 4, 2, 4, 6 ], [ 2, 4, 6 ], [ 1, 4, 2, 4, 6, 2 ], [],
                [ 6, 0, 4 ], [], [ 2, 11, 3, 6, 9, 4, 6, 10, 9 ], [ 8, 6, 1, 8, 1, 3 ],
                [ 10, 0, 6, 0, 4, 6 ], [ 8, 0, 3, 9, 6, 10, 9, 4, 6 ],
                [ 10, 4, 6, 10, 9, 4 ], [], [], [], [ 5, 3, 1 ], [], [ 0, 6, 2 ], [],
                [ 7, 4, 8, 5, 2, 1, 5, 6, 2 ], [], [], [ 2, 4, 0 ],
                [ 7, 4, 8, 2, 11, 3, 10, 5, 6 ], [ 7, 1, 3 ],
                [ 5, 6, 10, 0, 9, 1, 8, 7, 4 ], [ 5, 6, 10, 7, 0, 3, 7, 4, 0 ],
                [ 10, 5, 6, 4, 8, 7 ], [ 9, 11, 8 ], [ 3, 5, 6 ],
                [ 0, 5, 11, 0, 11, 8 ], [ 6, 3, 5, 3, 1, 5 ], [ 3, 9, 6, 3, 8, 9 ],
                [ 9, 6, 0, 6, 2, 0 ], [ 0, 3, 8, 2, 5, 6, 2, 1, 5 ],
                [ 1, 6, 2, 1, 5, 6 ], [ 9, 11, 8 ], [ 1, 0, 9, 6, 10, 5, 11, 3, 2 ],
                [ 6, 10, 5, 2, 8, 0, 2, 11, 8 ], [ 3, 2, 11, 10, 5, 6 ],
                [ 10, 5, 6, 9, 3, 8, 9, 1, 3 ], [ 0, 9, 1, 5, 6, 10 ],
                [ 8, 0, 3, 10, 5, 6 ], [ 10, 5, 6 ], [], [], [], [], [], [], [],
                [ 1, 10, 2, 9, 11, 6, 9, 8, 11 ], [], [], [ 6, 0, 2 ],
                [ 3, 6, 9, 3, 2, 6 ], [ 3, 5, 1 ], [ 0, 5, 1, 0, 11, 5 ], [ 0, 3, 5 ],
                [ 6, 9, 11, 9, 8, 11 ], [], [], [], [ 4, 5, 9, 7, 1, 10, 7, 3, 1 ], [],
                [ 11, 6, 7, 2, 4, 5, 2, 0, 4 ],
                [ 11, 6, 7, 8, 0, 3, 1, 10, 2, 9, 4, 5 ],
                [ 6, 7, 11, 1, 10, 2, 9, 4, 5 ], [],
                [ 4, 1, 0, 4, 5, 1, 6, 7, 3, 6, 3, 2 ], [ 9, 4, 5, 0, 6, 7, 0, 2, 6 ],
                [ 4, 5, 9, 6, 3, 2, 6, 7, 3 ], [ 6, 7, 11, 5, 3, 8, 5, 1, 3 ],
                [ 6, 7, 11, 4, 1, 0, 4, 5, 1 ], [ 4, 5, 9, 3, 8, 0, 11, 6, 7 ],
                [ 9, 4, 5, 7, 11, 6 ], [], [], [ 0, 6, 4 ], [ 8, 6, 4, 8, 1, 6 ], [],
                [ 0, 10, 2, 0, 9, 10, 4, 8, 11, 4, 11, 6 ],
                [ 10, 2, 1, 6, 0, 3, 6, 4, 0 ], [ 10, 2, 1, 11, 4, 8, 11, 6, 4 ],
                [ 4, 2, 6 ], [ 1, 0, 9, 2, 4, 8, 2, 6, 4 ], [ 2, 4, 0, 2, 6, 4 ],
                [ 8, 2, 4, 2, 6, 4 ], [ 11, 4, 1, 11, 6, 4 ],
                [ 0, 9, 1, 4, 11, 6, 4, 8, 11 ], [ 3, 6, 0, 6, 4, 0 ],
                [ 8, 6, 4, 8, 11, 6 ], [ 10, 8, 9 ], [ 6, 3, 9, 6, 7, 3 ], [ 6, 7, 1 ],
                [ 10, 7, 1, 7, 3, 1 ], [ 7, 11, 6, 8, 10, 2, 8, 9, 10 ],
                [ 11, 6, 7, 10, 0, 9, 10, 2, 0 ], [ 2, 1, 10, 7, 11, 6, 8, 0, 3 ],
                [ 1, 10, 2, 6, 7, 11 ], [ 7, 2, 6, 7, 9, 2 ],
                [ 1, 0, 9, 3, 6, 7, 3, 2, 6 ], [ 7, 0, 6, 0, 2, 6 ],
                [ 2, 7, 3, 2, 6, 7 ], [ 7, 11, 6, 3, 9, 1, 3, 8, 9 ],
                [ 9, 1, 0, 11, 6, 7 ], [ 0, 3, 8, 11, 6, 7 ], [ 11, 6, 7 ], [], [], [],
                [], [ 5, 3, 7 ], [ 8, 5, 2, 8, 7, 5 ], [ 5, 3, 7 ],
                [ 1, 10, 2, 5, 8, 7, 5, 9, 8 ], [ 1, 7, 5 ], [ 1, 7, 5 ],
                [ 9, 2, 7, 9, 7, 5 ], [ 11, 3, 2, 8, 5, 9, 8, 7, 5 ],
                [ 1, 3, 7, 1, 7, 5 ], [ 0, 7, 1, 7, 5, 1 ], [ 9, 3, 5, 3, 7, 5 ],
                [ 9, 7, 5, 9, 8, 7 ], [ 8, 10, 11 ], [ 3, 4, 10, 3, 10, 11 ],
                [ 8, 10, 11 ], [ 5, 9, 4, 1, 11, 3, 1, 10, 11 ], [ 2, 4, 5 ],
                [ 5, 2, 4, 2, 0, 4 ], [ 0, 3, 8, 5, 9, 4, 10, 2, 1 ],
                [ 2, 1, 10, 9, 4, 5 ], [ 2, 8, 5, 2, 11, 8 ],
                [ 3, 2, 11, 1, 4, 5, 1, 0, 4 ], [ 9, 4, 5, 8, 2, 11, 8, 0, 2 ],
                [ 11, 3, 2, 9, 4, 5 ], [ 8, 5, 3, 5, 1, 3 ], [ 5, 0, 4, 5, 1, 0 ],
                [ 3, 8, 0, 4, 5, 9 ], [ 9, 4, 5 ], [ 11, 9, 10 ], [ 11, 9, 10 ],
                [ 1, 11, 4, 1, 10, 11 ], [ 8, 7, 4, 11, 1, 10, 11, 3, 1 ],
                [ 2, 7, 9, 2, 9, 10 ], [ 4, 8, 7, 0, 10, 2, 0, 9, 10 ],
                [ 2, 1, 10, 0, 7, 4, 0, 3, 7 ], [ 10, 2, 1, 8, 7, 4 ], [ 1, 7, 4 ],
                [ 3, 2, 11, 4, 8, 7, 9, 1, 0 ], [ 11, 4, 2, 4, 0, 2 ],
                [ 2, 11, 3, 7, 4, 8 ], [ 4, 1, 7, 1, 3, 7 ], [ 1, 0, 9, 8, 7, 4 ],
                [ 3, 4, 0, 3, 7, 4 ], [ 8, 7, 4 ], [ 8, 9, 10, 8, 10, 11 ],
                [ 3, 9, 11, 9, 10, 11 ], [ 0, 10, 8, 10, 11, 8 ],
                [ 10, 3, 1, 10, 11, 3 ], [ 2, 8, 10, 8, 9, 10 ], [ 9, 2, 0, 9, 10, 2 ],
                [ 8, 0, 3, 1, 10, 2 ], [ 10, 2, 1 ], [ 1, 11, 9, 11, 8, 9 ],
                [ 11, 3, 2, 0, 9, 1 ], [ 11, 0, 2, 11, 8, 0 ], [ 11, 3, 2 ],
                [ 8, 1, 3, 8, 9, 1 ], [ 9, 1, 0 ], [ 8, 0, 3 ], [] ];

        this.edgeTable2 = [ 0x0, 0x109, 0x203, 0x30a, 0x80c, 0x905, 0xa0f,
                0xb06, 0x406, 0x50f, 0x605, 0x70c, 0xc0a, 0xd03, 0xe09, 0xf00, 0x190,
                0x99, 0x393, 0x29a, 0x99c, 0x895, 0xb9f, 0xa96, 0x596, 0x49f, 0x795,
                0x69c, 0xd9a, 0xc93, 0xf99, 0xe90, 0x230, 0x339, 0x33, 0x13a, 0xa3c,
                0xb35, 0x83f, 0x936, 0x636, 0x73f, 0x435, 0x53c, 0xe3a, 0xf33, 0xc39,
                0xd30, 0x3a0, 0x2a9, 0x1a3, 0xaa, 0xbac, 0xaa5, 0x9af, 0x8a6, 0x7a6,
                0x6af, 0x5a5, 0x4ac, 0xfaa, 0xea3, 0xda9, 0xca0, 0x8c0, 0x9c9, 0xac3,
                0xbca, 0xcc, 0x1c5, 0x2cf, 0x3c6, 0xcc6, 0xdcf, 0xec5, 0xfcc, 0x4ca,
                0x5c3, 0x6c9, 0x7c0, 0x950, 0x859, 0xb53, 0xa5a, 0x15c, 0x55, 0x35f,
                0x256, 0xd56, 0xc5f, 0xf55, 0xe5c, 0x55a, 0x453, 0x759, 0x650, 0xaf0,
                0xbf9, 0x8f3, 0x9fa, 0x2fc, 0x3f5, 0xff, 0x1f6, 0xef6, 0xfff, 0xcf5,
                0xdfc, 0x6fa, 0x7f3, 0x4f9, 0x5f0, 0xb60, 0xa69, 0x963, 0x86a, 0x36c,
                0x265, 0x16f, 0x66, 0xf66, 0xe6f, 0xd65, 0xc6c, 0x76a, 0x663, 0x569,
                0x460, 0x460, 0x569, 0x663, 0x76a, 0xc6c, 0xd65, 0xe6f, 0xf66, 0x66,
                0x16f, 0x265, 0x36c, 0x86a, 0x963, 0xa69, 0xb60, 0x5f0, 0x4f9, 0x7f3,
                0x6fa, 0xdfc, 0xcf5, 0xfff, 0xef6, 0x1f6, 0xff, 0x3f5, 0x2fc, 0x9fa,
                0x8f3, 0xbf9, 0xaf0, 0x650, 0x759, 0x453, 0x55a, 0xe5c, 0xf55, 0xc5f,
                0xd56, 0x256, 0x35f, 0x55, 0x15c, 0xa5a, 0xb53, 0x859, 0x950, 0x7c0,
                0x6c9, 0x5c3, 0x4ca, 0xfcc, 0xec5, 0xdcf, 0xcc6, 0x3c6, 0x2cf, 0x1c5,
                0xcc, 0xbca, 0xac3, 0x9c9, 0x8c0, 0xca0, 0xda9, 0xea3, 0xfaa, 0x4ac,
                0x5a5, 0x6af, 0x7a6, 0x8a6, 0x9af, 0xaa5, 0xbac, 0xaa, 0x1a3, 0x2a9,
                0x3a0, 0xd30, 0xc39, 0xf33, 0xe3a, 0x53c, 0x435, 0x73f, 0x636, 0x936,
                0x83f, 0xb35, 0xa3c, 0x13a, 0x33, 0x339, 0x230, 0xe90, 0xf99, 0xc93,
                0xd9a, 0x69c, 0x795, 0x49f, 0x596, 0xa96, 0xb9f, 0x895, 0x99c, 0x29a,
                0x393, 0x99, 0x190, 0xf00, 0xe09, 0xd03, 0xc0a, 0x70c, 0x605, 0x50f,
                0x406, 0xb06, 0xa0f, 0x905, 0x80c, 0x30a, 0x203, 0x109, 0x0 ];

        this.triTable2 = [ [], [ 8, 3, 0 ], [ 9, 0, 1 ], [ 8, 3, 1, 8, 1, 9 ],
                [ 11, 2, 3 ], [ 11, 2, 0, 11, 0, 8 ], [ 11, 2, 3, 0, 1, 9 ],
                [ 2, 1, 11, 1, 9, 11, 11, 9, 8 ], [ 10, 1, 2 ], [ 8, 3, 0, 1, 2, 10 ],
                [ 9, 0, 2, 9, 2, 10 ], [ 3, 2, 8, 2, 10, 8, 8, 10, 9 ],
                [ 10, 1, 3, 10, 3, 11 ], [ 1, 0, 10, 0, 8, 10, 10, 8, 11 ],
                [ 0, 3, 9, 3, 11, 9, 9, 11, 10 ], [ 8, 10, 9, 8, 11, 10 ], [ 8, 4, 7 ],
                [ 3, 0, 4, 3, 4, 7 ], [ 1, 9, 0, 8, 4, 7 ],
                [ 9, 4, 1, 4, 7, 1, 1, 7, 3 ], [ 2, 3, 11, 7, 8, 4 ],
                [ 7, 11, 4, 11, 2, 4, 4, 2, 0 ], [ 3, 11, 2, 4, 7, 8, 9, 0, 1 ],
                [ 2, 7, 11, 2, 1, 7, 1, 4, 7, 1, 9, 4 ], [ 10, 1, 2, 8, 4, 7 ],
                [ 2, 10, 1, 0, 4, 7, 0, 7, 3 ], [ 4, 7, 8, 0, 2, 10, 0, 10, 9 ],
                [ 2, 7, 3, 2, 9, 7, 7, 9, 4, 2, 10, 9 ],
                [ 8, 4, 7, 11, 10, 1, 11, 1, 3 ],
                [ 11, 4, 7, 1, 4, 11, 1, 11, 10, 1, 0, 4 ],
                [ 3, 8, 0, 7, 11, 4, 11, 9, 4, 11, 10, 9 ],
                [ 7, 11, 4, 4, 11, 9, 11, 10, 9 ], [ 9, 5, 4 ], [ 3, 0, 8, 4, 9, 5 ],
                [ 5, 4, 0, 5, 0, 1 ], [ 4, 8, 5, 8, 3, 5, 5, 3, 1 ],
                [ 11, 2, 3, 9, 5, 4 ], [ 9, 5, 4, 8, 11, 2, 8, 2, 0 ],
                [ 3, 11, 2, 1, 5, 4, 1, 4, 0 ],
                [ 8, 5, 4, 2, 5, 8, 2, 8, 11, 2, 1, 5 ], [ 2, 10, 1, 9, 5, 4 ],
                [ 0, 8, 3, 5, 4, 9, 10, 1, 2 ], [ 10, 5, 2, 5, 4, 2, 2, 4, 0 ],
                [ 3, 4, 8, 3, 2, 4, 2, 5, 4, 2, 10, 5 ],
                [ 5, 4, 9, 1, 3, 11, 1, 11, 10 ],
                [ 0, 9, 1, 4, 8, 5, 8, 10, 5, 8, 11, 10 ],
                [ 3, 4, 0, 3, 10, 4, 4, 10, 5, 3, 11, 10 ],
                [ 4, 8, 5, 5, 8, 10, 8, 11, 10 ], [ 9, 5, 7, 9, 7, 8 ],
                [ 0, 9, 3, 9, 5, 3, 3, 5, 7 ], [ 8, 0, 7, 0, 1, 7, 7, 1, 5 ],
                [ 1, 7, 3, 1, 5, 7 ], [ 11, 2, 3, 8, 9, 5, 8, 5, 7 ],
                [ 9, 2, 0, 9, 7, 2, 2, 7, 11, 9, 5, 7 ],
                [ 0, 3, 8, 2, 1, 11, 1, 7, 11, 1, 5, 7 ],
                [ 2, 1, 11, 11, 1, 7, 1, 5, 7 ], [ 1, 2, 10, 5, 7, 8, 5, 8, 9 ],
                [ 9, 1, 0, 10, 5, 2, 5, 3, 2, 5, 7, 3 ],
                [ 5, 2, 10, 8, 2, 5, 8, 5, 7, 8, 0, 2 ],
                [ 10, 5, 2, 2, 5, 3, 5, 7, 3 ],
                [ 3, 9, 1, 3, 8, 9, 7, 11, 10, 7, 10, 5 ],
                [ 9, 1, 0, 10, 7, 11, 10, 5, 7 ], [ 3, 8, 0, 7, 10, 5, 7, 11, 10 ],
                [ 11, 5, 7, 11, 10, 5 ], [ 11, 7, 6 ], [ 0, 8, 3, 11, 7, 6 ],
                [ 9, 0, 1, 11, 7, 6 ], [ 7, 6, 11, 3, 1, 9, 3, 9, 8 ],
                [ 2, 3, 7, 2, 7, 6 ], [ 8, 7, 0, 7, 6, 0, 0, 6, 2 ],
                [ 1, 9, 0, 3, 7, 6, 3, 6, 2 ], [ 7, 6, 2, 7, 2, 9, 2, 1, 9, 7, 9, 8 ],
                [ 1, 2, 10, 6, 11, 7 ], [ 2, 10, 1, 7, 6, 11, 8, 3, 0 ],
                [ 11, 7, 6, 10, 9, 0, 10, 0, 2 ],
                [ 7, 6, 11, 3, 2, 8, 8, 2, 10, 8, 10, 9 ],
                [ 6, 10, 7, 10, 1, 7, 7, 1, 3 ],
                [ 6, 10, 1, 6, 1, 7, 7, 1, 0, 7, 0, 8 ],
                [ 9, 0, 3, 6, 9, 3, 6, 10, 9, 6, 3, 7 ],
                [ 6, 10, 7, 7, 10, 8, 10, 9, 8 ], [ 8, 4, 6, 8, 6, 11 ],
                [ 11, 3, 6, 3, 0, 6, 6, 0, 4 ], [ 0, 1, 9, 4, 6, 11, 4, 11, 8 ],
                [ 1, 9, 4, 11, 1, 4, 11, 3, 1, 11, 4, 6 ],
                [ 3, 8, 2, 8, 4, 2, 2, 4, 6 ], [ 2, 0, 4, 2, 4, 6 ],
                [ 1, 9, 0, 3, 8, 2, 2, 8, 4, 2, 4, 6 ], [ 9, 4, 1, 1, 4, 2, 4, 6, 2 ],
                [ 10, 1, 2, 11, 8, 4, 11, 4, 6 ],
                [ 10, 1, 2, 11, 3, 6, 6, 3, 0, 6, 0, 4 ],
                [ 0, 2, 10, 0, 10, 9, 4, 11, 8, 4, 6, 11 ],
                [ 2, 11, 3, 6, 9, 4, 6, 10, 9 ],
                [ 8, 4, 6, 8, 6, 1, 6, 10, 1, 8, 1, 3 ],
                [ 1, 0, 10, 10, 0, 6, 0, 4, 6 ], [ 8, 0, 3, 9, 6, 10, 9, 4, 6 ],
                [ 10, 4, 6, 10, 9, 4 ], [ 9, 5, 4, 7, 6, 11 ],
                [ 4, 9, 5, 3, 0, 8, 11, 7, 6 ], [ 6, 11, 7, 4, 0, 1, 4, 1, 5 ],
                [ 6, 11, 7, 4, 8, 5, 5, 8, 3, 5, 3, 1 ], [ 4, 9, 5, 6, 2, 3, 6, 3, 7 ],
                [ 9, 5, 4, 8, 7, 0, 0, 7, 6, 0, 6, 2 ],
                [ 4, 0, 1, 4, 1, 5, 6, 3, 7, 6, 2, 3 ], [ 7, 4, 8, 5, 2, 1, 5, 6, 2 ],
                [ 6, 11, 7, 1, 2, 10, 9, 5, 4 ],
                [ 11, 7, 6, 8, 3, 0, 1, 2, 10, 9, 5, 4 ],
                [ 11, 7, 6, 10, 5, 2, 2, 5, 4, 2, 4, 0 ],
                [ 7, 4, 8, 2, 11, 3, 10, 5, 6 ],
                [ 4, 9, 5, 6, 10, 7, 7, 10, 1, 7, 1, 3 ],
                [ 5, 6, 10, 0, 9, 1, 8, 7, 4 ], [ 5, 6, 10, 7, 0, 3, 7, 4, 0 ],
                [ 10, 5, 6, 4, 8, 7 ], [ 5, 6, 9, 6, 11, 9, 9, 11, 8 ],
                [ 0, 9, 5, 0, 5, 3, 3, 5, 6, 3, 6, 11 ],
                [ 0, 1, 5, 0, 5, 11, 5, 6, 11, 0, 11, 8 ],
                [ 11, 3, 6, 6, 3, 5, 3, 1, 5 ], [ 9, 5, 6, 3, 9, 6, 3, 8, 9, 3, 6, 2 ],
                [ 5, 6, 9, 9, 6, 0, 6, 2, 0 ], [ 0, 3, 8, 2, 5, 6, 2, 1, 5 ],
                [ 1, 6, 2, 1, 5, 6 ], [ 1, 2, 10, 5, 6, 9, 9, 6, 11, 9, 11, 8 ],
                [ 1, 0, 9, 6, 10, 5, 11, 3, 2 ], [ 6, 10, 5, 2, 8, 0, 2, 11, 8 ],
                [ 3, 2, 11, 10, 5, 6 ], [ 10, 5, 6, 9, 3, 8, 9, 1, 3 ],
                [ 0, 9, 1, 5, 6, 10 ], [ 8, 0, 3, 10, 5, 6 ], [ 10, 5, 6 ],
                [ 10, 6, 5 ], [ 8, 3, 0, 10, 6, 5 ], [ 0, 1, 9, 5, 10, 6 ],
                [ 10, 6, 5, 9, 8, 3, 9, 3, 1 ], [ 3, 11, 2, 10, 6, 5 ],
                [ 6, 5, 10, 2, 0, 8, 2, 8, 11 ], [ 1, 9, 0, 6, 5, 10, 11, 2, 3 ],
                [ 1, 10, 2, 5, 9, 6, 9, 11, 6, 9, 8, 11 ], [ 1, 2, 6, 1, 6, 5 ],
                [ 0, 8, 3, 2, 6, 5, 2, 5, 1 ], [ 5, 9, 6, 9, 0, 6, 6, 0, 2 ],
                [ 9, 6, 5, 3, 6, 9, 3, 9, 8, 3, 2, 6 ], [ 11, 6, 3, 6, 5, 3, 3, 5, 1 ],
                [ 0, 5, 1, 0, 11, 5, 5, 11, 6, 0, 8, 11 ],
                [ 0, 5, 9, 0, 3, 5, 3, 6, 5, 3, 11, 6 ],
                [ 5, 9, 6, 6, 9, 11, 9, 8, 11 ], [ 10, 6, 5, 4, 7, 8 ],
                [ 5, 10, 6, 7, 3, 0, 7, 0, 4 ], [ 5, 10, 6, 0, 1, 9, 8, 4, 7 ],
                [ 4, 5, 9, 6, 7, 10, 7, 1, 10, 7, 3, 1 ],
                [ 7, 8, 4, 2, 3, 11, 10, 6, 5 ],
                [ 11, 6, 7, 10, 2, 5, 2, 4, 5, 2, 0, 4 ],
                [ 11, 6, 7, 8, 0, 3, 1, 10, 2, 9, 4, 5 ],
                [ 6, 7, 11, 1, 10, 2, 9, 4, 5 ], [ 7, 8, 4, 5, 1, 2, 5, 2, 6 ],
                [ 4, 1, 0, 4, 5, 1, 6, 7, 3, 6, 3, 2 ],
                [ 9, 4, 5, 8, 0, 7, 0, 6, 7, 0, 2, 6 ], [ 4, 5, 9, 6, 3, 2, 6, 7, 3 ],
                [ 6, 7, 11, 4, 5, 8, 5, 3, 8, 5, 1, 3 ],
                [ 6, 7, 11, 4, 1, 0, 4, 5, 1 ], [ 4, 5, 9, 3, 8, 0, 11, 6, 7 ],
                [ 9, 4, 5, 7, 11, 6 ], [ 10, 6, 4, 10, 4, 9 ],
                [ 8, 3, 0, 9, 10, 6, 9, 6, 4 ], [ 1, 10, 0, 10, 6, 0, 0, 6, 4 ],
                [ 8, 6, 4, 8, 1, 6, 6, 1, 10, 8, 3, 1 ],
                [ 2, 3, 11, 6, 4, 9, 6, 9, 10 ],
                [ 0, 10, 2, 0, 9, 10, 4, 8, 11, 4, 11, 6 ],
                [ 10, 2, 1, 11, 6, 3, 6, 0, 3, 6, 4, 0 ],
                [ 10, 2, 1, 11, 4, 8, 11, 6, 4 ], [ 9, 1, 4, 1, 2, 4, 4, 2, 6 ],
                [ 1, 0, 9, 3, 2, 8, 2, 4, 8, 2, 6, 4 ], [ 2, 4, 0, 2, 6, 4 ],
                [ 3, 2, 8, 8, 2, 4, 2, 6, 4 ],
                [ 1, 4, 9, 11, 4, 1, 11, 1, 3, 11, 6, 4 ],
                [ 0, 9, 1, 4, 11, 6, 4, 8, 11 ], [ 11, 6, 3, 3, 6, 0, 6, 4, 0 ],
                [ 8, 6, 4, 8, 11, 6 ], [ 6, 7, 10, 7, 8, 10, 10, 8, 9 ],
                [ 9, 3, 0, 6, 3, 9, 6, 9, 10, 6, 7, 3 ],
                [ 6, 1, 10, 6, 7, 1, 7, 0, 1, 7, 8, 0 ],
                [ 6, 7, 10, 10, 7, 1, 7, 3, 1 ],
                [ 7, 11, 6, 3, 8, 2, 8, 10, 2, 8, 9, 10 ],
                [ 11, 6, 7, 10, 0, 9, 10, 2, 0 ], [ 2, 1, 10, 7, 11, 6, 8, 0, 3 ],
                [ 1, 10, 2, 6, 7, 11 ], [ 7, 2, 6, 7, 9, 2, 2, 9, 1, 7, 8, 9 ],
                [ 1, 0, 9, 3, 6, 7, 3, 2, 6 ], [ 8, 0, 7, 7, 0, 6, 0, 2, 6 ],
                [ 2, 7, 3, 2, 6, 7 ], [ 7, 11, 6, 3, 9, 1, 3, 8, 9 ],
                [ 9, 1, 0, 11, 6, 7 ], [ 0, 3, 8, 11, 6, 7 ], [ 11, 6, 7 ],
                [ 11, 7, 5, 11, 5, 10 ], [ 3, 0, 8, 7, 5, 10, 7, 10, 11 ],
                [ 9, 0, 1, 10, 11, 7, 10, 7, 5 ],
                [ 3, 1, 9, 3, 9, 8, 7, 10, 11, 7, 5, 10 ],
                [ 10, 2, 5, 2, 3, 5, 5, 3, 7 ],
                [ 5, 10, 2, 8, 5, 2, 8, 7, 5, 8, 2, 0 ],
                [ 9, 0, 1, 10, 2, 5, 5, 2, 3, 5, 3, 7 ],
                [ 1, 10, 2, 5, 8, 7, 5, 9, 8 ], [ 2, 11, 1, 11, 7, 1, 1, 7, 5 ],
                [ 0, 8, 3, 2, 11, 1, 1, 11, 7, 1, 7, 5 ],
                [ 9, 0, 2, 9, 2, 7, 2, 11, 7, 9, 7, 5 ],
                [ 11, 3, 2, 8, 5, 9, 8, 7, 5 ], [ 1, 3, 7, 1, 7, 5 ],
                [ 8, 7, 0, 0, 7, 1, 7, 5, 1 ], [ 0, 3, 9, 9, 3, 5, 3, 7, 5 ],
                [ 9, 7, 5, 9, 8, 7 ], [ 4, 5, 8, 5, 10, 8, 8, 10, 11 ],
                [ 3, 0, 4, 3, 4, 10, 4, 5, 10, 3, 10, 11 ],
                [ 0, 1, 9, 4, 5, 8, 8, 5, 10, 8, 10, 11 ],
                [ 5, 9, 4, 1, 11, 3, 1, 10, 11 ],
                [ 3, 8, 4, 3, 4, 2, 2, 4, 5, 2, 5, 10 ],
                [ 10, 2, 5, 5, 2, 4, 2, 0, 4 ], [ 0, 3, 8, 5, 9, 4, 10, 2, 1 ],
                [ 2, 1, 10, 9, 4, 5 ], [ 8, 4, 5, 2, 8, 5, 2, 11, 8, 2, 5, 1 ],
                [ 3, 2, 11, 1, 4, 5, 1, 0, 4 ], [ 9, 4, 5, 8, 2, 11, 8, 0, 2 ],
                [ 11, 3, 2, 9, 4, 5 ], [ 4, 5, 8, 8, 5, 3, 5, 1, 3 ],
                [ 5, 0, 4, 5, 1, 0 ], [ 3, 8, 0, 4, 5, 9 ], [ 9, 4, 5 ],
                [ 7, 4, 11, 4, 9, 11, 11, 9, 10 ],
                [ 3, 0, 8, 7, 4, 11, 11, 4, 9, 11, 9, 10 ],
                [ 11, 7, 4, 1, 11, 4, 1, 10, 11, 1, 4, 0 ],
                [ 8, 7, 4, 11, 1, 10, 11, 3, 1 ],
                [ 2, 3, 7, 2, 7, 9, 7, 4, 9, 2, 9, 10 ],
                [ 4, 8, 7, 0, 10, 2, 0, 9, 10 ], [ 2, 1, 10, 0, 7, 4, 0, 3, 7 ],
                [ 10, 2, 1, 8, 7, 4 ], [ 2, 11, 7, 2, 7, 1, 1, 7, 4, 1, 4, 9 ],
                [ 3, 2, 11, 4, 8, 7, 9, 1, 0 ], [ 7, 4, 11, 11, 4, 2, 4, 0, 2 ],
                [ 2, 11, 3, 7, 4, 8 ], [ 9, 1, 4, 4, 1, 7, 1, 3, 7 ],
                [ 1, 0, 9, 8, 7, 4 ], [ 3, 4, 0, 3, 7, 4 ], [ 8, 7, 4 ],
                [ 8, 9, 10, 8, 10, 11 ], [ 0, 9, 3, 3, 9, 11, 9, 10, 11 ],
                [ 1, 10, 0, 0, 10, 8, 10, 11, 8 ], [ 10, 3, 1, 10, 11, 3 ],
                [ 3, 8, 2, 2, 8, 10, 8, 9, 10 ], [ 9, 2, 0, 9, 10, 2 ],
                [ 8, 0, 3, 1, 10, 2 ], [ 10, 2, 1 ], [ 2, 11, 1, 1, 11, 9, 11, 8, 9 ],
                [ 11, 3, 2, 0, 9, 1 ], [ 11, 0, 2, 11, 8, 0 ], [ 11, 3, 2 ],
                [ 8, 1, 3, 8, 9, 1 ], [ 9, 1, 0 ], [ 8, 0, 3 ], [] ];
    }
}

MarchingCube.prototype.march = function(data, verts, faces, spec) {

    let fulltable = !!(spec.fulltable);
    let origin =(spec.hasOwnProperty('origin') && spec.origin.hasOwnProperty('x')) ? spec.origin : {x:0, y:0, z:0};
    let voxel = !!(spec.voxel);
    let transform = spec.matrix; //if this is set, it overrides origin and unitCube

    let nX = spec.nX || 0;
    let nY = spec.nY || 0;
    let nZ = spec.nZ || 0;

    let scale = spec.scale || 1.0;
    let unitCube = null;
    if(spec.unitCube) {
        unitCube = spec.unitCube;
    } else {
        unitCube = {x:scale,y:scale,z:scale};
    }

    //keep track of calculated vertices to avoid repeats
    let vertnums = new Int32Array(nX*nY*nZ);

    let i, il;

    for(i = 0, il = vertnums.length; i < il; ++i)
        vertnums[i] = -1;

    // create(or retrieve) a vertex at the appropriate point for
    // the edge(p1,p2)

    let getVertex = function(i, j, k, code, p1, p2) {
        let pt = {x:0,y:0,z:0};
        let val1 = !!(code &(1 << p1));
        let val2 = !!(code &(1 << p2));

        // p1 if they are the same or if !val1
        let p = p1;
        if(!val1 && val2)
            p = p2;

        // adjust i,j,k by p
        if(p & 1)
            k++;
        if(p & 2)
            j++;
        if(p & 4)
            i++;

        if(transform) {
            pt = new THREE.Vector3(i,j,k);
            pt = pt.applyMatrix4(transform);
            pt = {x: pt.x, y: pt.y, z: pt.z}; //remove vector gunk
        } else {
            pt.x = origin.x+unitCube.x*i;
            pt.y = origin.y+unitCube.y*j;
            pt.z = origin.z+unitCube.z*k;
        }

        let index =((nY * i) + j) * nZ + k;

        //Have to add option to do voxels
        if(!voxel) {

            if(vertnums[index] < 0) // not created yet
            {
                vertnums[index] = verts.length;
                verts.push( pt );
            }
            return vertnums[index];

        }

        else {
            verts.push(pt);
            return verts.length - 1;
        }

    };

    let intersects = new Int32Array(12);

    let etable =(fulltable) ? this.edgeTable2 : this.edgeTable;
    let tritable =(fulltable) ? this.triTable2 : this.triTable;

    //Run marching cubes algorithm
    for(i = 0; i < nX-1; ++i) {

        for(let j = 0; j < nY-1; ++j){

            for(let k = 0; k < nZ-1; ++k){

                let code = 0;

                for(let p = 0; p < 8; ++p) {
                    let index =((nY *(i +((p & 4) >> 2))) + j +((p & 2) >> 1)) *
                                    nZ + k +(p & 1);

                    //TODO: Need to fix vpBits in protein surface for this to work
                    let val = !!(data[index] & this.ISDONE);
                    //var val = !!(data[index] > 0);

                    code |= val << p;
                }

                if(code === 0 || code === 255)
                    continue;

                let ecode = etable[code];

                if(ecode === 0)
                    continue;

                let ttable = tritable[code];

                if(ecode & 1)
                    intersects[0] = getVertex(i, j, k, code, 0, 1);
                if(ecode & 2)
                    intersects[1] = getVertex(i, j, k, code, 1, 3);
                if(ecode & 4)
                    intersects[2] = getVertex(i, j, k, code, 3, 2);
                if(ecode & 8)
                    intersects[3] = getVertex(i, j, k, code, 2, 0);
                if(ecode & 16)
                    intersects[4] = getVertex(i, j, k, code, 4, 5);
                if(ecode & 32)
                    intersects[5] = getVertex(i, j, k, code, 5, 7);
                if(ecode & 64)
                    intersects[6] = getVertex(i, j, k, code, 7, 6);
                if(ecode & 128)
                    intersects[7] = getVertex(i, j, k, code, 6, 4);
                if(ecode & 256)
                    intersects[8] = getVertex(i, j, k, code, 0, 4);
                if(ecode & 512)
                    intersects[9] = getVertex(i, j, k, code, 1, 5);
                if(ecode & 1024)
                    intersects[10] = getVertex(i, j, k, code, 3, 7);
                if(ecode & 2048)
                    intersects[11] = getVertex(i, j, k, code, 2, 6);

                for(let t = 0; t < ttable.length; t += 3) {

                    let a = intersects[ttable[t]],
                        b = intersects[ttable[t+1]],
                        c = intersects[ttable[t+2]];

                    if(voxel && t >= 3) {
                        verts.push(verts[a]); a = verts.length - 1;
                        verts.push(verts[b]); b = verts.length - 1;
                        verts.push(verts[c]); c = verts.length - 1;
                    }


                    faces.push(a); faces.push(b); faces.push(c);
                }

            }

        }

    }
};

MarchingCube.prototype.laplacianSmooth = function(numiter, verts, faces) {
    let tps = new Array(verts.length);
    let i, il, j, jl, k;
    for(i = 0, il = verts.length; i < il; i++)
            tps[i] = {
                x : 0,
                y : 0,
                z : 0
            };
    let vertdeg = new Array(20);
    let flagvert;
    for(i = 0; i < 20; i++)
            vertdeg[i] = new Array(verts.length);
    for(i = 0, il = verts.length; i < il; i++)
            vertdeg[0][i] = 0;
    for(i = 0, il = faces.length / 3; i < il; i++) {
        let aoffset = i*3, boffset = i*3 + 1, coffset = i*3 + 2;
        flagvert = true;
        for(j = 0, jl = vertdeg[0][faces[aoffset]]; j < jl; j++) {
            if(faces[boffset] == vertdeg[j + 1][faces[aoffset]]) {
                flagvert = false;
                break;
            }
        }
        if(flagvert) {
            vertdeg[0][faces[aoffset]]++;
            vertdeg[vertdeg[0][faces[aoffset]]][faces[aoffset]] = faces[boffset];
        }
        flagvert = true;
        for(j = 0, jl = vertdeg[0][faces[aoffset]]; j < jl; j++) {
            if(faces[coffset] == vertdeg[j + 1][faces[aoffset]]) {
                flagvert = false;
                break;
            }
        }
        if(flagvert) {
            vertdeg[0][faces[aoffset]]++;
            vertdeg[vertdeg[0][faces[aoffset]]][faces[aoffset]] = faces[coffset];
        }
        // b
        flagvert = true;
        for(j = 0, jl = vertdeg[0][faces[boffset]]; j < jl; j++) {
            if(faces[aoffset] == vertdeg[j + 1][faces[boffset]]) {
                flagvert = false;
                break;
            }
        }
        if(flagvert) {
            vertdeg[0][faces[boffset]]++;
            vertdeg[vertdeg[0][faces[boffset]]][faces[boffset]] = faces[aoffset];
        }
        flagvert = true;
        for(j = 0, jl = vertdeg[0][faces[boffset]]; j < jl; j++) {
            if(faces[coffset] == vertdeg[j + 1][faces[boffset]]) {
                flagvert = false;
                break;
            }
        }
        if(flagvert) {
            vertdeg[0][faces[boffset]]++;
            vertdeg[vertdeg[0][faces[boffset]]][faces[boffset]] = faces[coffset];
        }
        // c
        flagvert = true;
        for(j = 0; j < vertdeg[0][faces[coffset]]; j++) {
            if(faces[aoffset] == vertdeg[j + 1][faces[coffset]]) {
                flagvert = false;
                break;
            }
        }
        if(flagvert) {
            vertdeg[0][faces[coffset]]++;
            vertdeg[vertdeg[0][faces[coffset]]][faces[coffset]] = faces[aoffset];
        }
        flagvert = true;
        for(j = 0, jl = vertdeg[0][faces[coffset]]; j < jl; j++) {
            if(faces[boffset] == vertdeg[j + 1][faces[coffset]]) {
                flagvert = false;
                break;
            }
        }
        if(flagvert) {
            vertdeg[0][faces[coffset]]++;
            vertdeg[vertdeg[0][faces[coffset]]][faces[coffset]] = faces[boffset];
        }
    }

    let wt = 1.00;
    let wt2 = 0.50;
    for(k = 0; k < numiter; k++) {
            for(i = 0, il = verts.length; i < il; i++) {
                    if(vertdeg[0][i] < 3) {
                            tps[i].x = verts[i].x;
                            tps[i].y = verts[i].y;
                            tps[i].z = verts[i].z;
                    } else if(vertdeg[0][i] == 3 || vertdeg[0][i] == 4) {
                            tps[i].x = 0;
                            tps[i].y = 0;
                            tps[i].z = 0;
                            for(j = 0, jl = vertdeg[0][i]; j < jl; j++) {
                                    tps[i].x += verts[vertdeg[j + 1][i]].x;
                                    tps[i].y += verts[vertdeg[j + 1][i]].y;
                                    tps[i].z += verts[vertdeg[j + 1][i]].z;
                            }
                            tps[i].x += wt2 * verts[i].x;
                            tps[i].y += wt2 * verts[i].y;
                            tps[i].z += wt2 * verts[i].z;
                            tps[i].x /= wt2 + vertdeg[0][i];
                            tps[i].y /= wt2 + vertdeg[0][i];
                            tps[i].z /= wt2 + vertdeg[0][i];
                    } else {
                            tps[i].x = 0;
                            tps[i].y = 0;
                            tps[i].z = 0;
                            for(j = 0, jl = vertdeg[0][i]; j < jl; j++) {
                                    tps[i].x += verts[vertdeg[j + 1][i]].x;
                                    tps[i].y += verts[vertdeg[j + 1][i]].y;
                                    tps[i].z += verts[vertdeg[j + 1][i]].z;
                            }
                            tps[i].x += wt * verts[i].x;
                            tps[i].y += wt * verts[i].y;
                            tps[i].z += wt * verts[i].z;
                            tps[i].x /= wt + vertdeg[0][i];
                            tps[i].y /= wt + vertdeg[0][i];
                            tps[i].z /= wt + vertdeg[0][i];
                    }
            }
            for(i = 0, il = verts.length; i < il; i++) {
                    verts[i].x = tps[i].x;
                    verts[i].y = tps[i].y;
                    verts[i].z = tps[i].z;
            }
            /*
             * computenorm(); for(let i = 0; i < vertnumber; i++) { if
             *(verts[i].inout) ssign = 1; else ssign = -1; verts[i].x += ssign *
             * outwt * verts[i].pn.x; verts[i].y += ssign * outwt *
             * verts[i].pn.y; verts[i].z += ssign * outwt * verts[i].pn.z; }
             */
    }
};

/* ProteinSurface4.js
 * @author David Koes  / https://github.com/3dmol/3Dmol.js/tree/master/3Dmol
 * Modified by Jiyao Wang / https://github.com/ncbi/icn3d
 */

// dkoes
// Surface calculations.  This must be safe to use within a web worker.
class ProteinSurface {
    constructor(icn3d, threshbox) {
        this.icn3d = icn3d;
        this.threshbox = threshbox;

    //$3Dmol.ProteinSurface = function(threshbox) {
        //"use strict";

        // for delphi
        this.dataArray = {};
        this.header;
        this.data = undefined;
        this.matrix = undefined;
        this.isovalue = undefined;
        this.loadPhiFrom = undefined;
        this.vpColor = null; // intarray
        this.vpPot = null; // floatarray

        // constants for vpbits bitmasks
        /** @this.*/
        this.INOUT = 1;
        /** @this.*/
        this.ISDONE = 2;
        /** @this.*/
        this.ISBOUND = 4;

        this.ptranx = 0;
        this.ptrany = 0;
        this.ptranz = 0;
        this.probeRadius = 1.4;
        this.defaultScaleFactor = 2;
        this.scaleFactor = this.defaultScaleFactor; // 2 is .5A grid; if this is made user configurable,
                                // also have to adjust offset used to find non-shown
                                // atoms
        this.finalScaleFactor = {};

        this.pHeight = 0;
        this.pWidth = 0;
        this.pLength = 0;
        this.cutRadius = 0;
        this.vpBits = null; // uint8 array of bitmasks
        this.vpDistance = null; // floatarray of _squared_ distances
        this.vpAtomID = null; // intarray
        this.vertnumber = 0;
        this.facenumber = 0;
        this.pminx = 0;
        this.pminy = 0;
        this.pminz = 0;
        this.pmaxx = 0;
        this.pmaxy = 0;
        this.pmaxz = 0;

        this.bCalcArea = false;
        this.atomsToShow = {};

        this.vdwRadii = {
                "H" : 1.2,
                "LI" : 1.82,
                "Na" : 2.27,
                "K" : 2.75,
                "C" : 1.7,
                "N" : 1.55,
                "O" : 1.52,
                "F" : 1.47,
                "P" : 1.80,
                "S" : 1.80,
                "CL" : 1.75,
                "BR" : 1.85,
                "SE" : 1.90,
                "ZN" : 1.39,
                "CU" : 1.4,
                "NI" : 1.63,
                "X" : 2
            };

        this.depty = {};
        this.widxz = {};
        this.faces = undefined;
        this.verts = undefined;
        this.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.origextent = undefined;

        this.marchingCube = new MarchingCube();
    }
}

/** @param {AtomSpec} atom */
ProteinSurface.prototype.getVDWIndex = function(atom) {
    if(!atom.elem || typeof(this.vdwRadii[atom.elem.toUpperCase()]) == "undefined") {
        return "X";
    }
    return atom.elem;
};

ProteinSurface.prototype.inOrigExtent = function(x, y, z) {
    if(x < this.origextent[0][0] || x > this.origextent[1][0])
        return false;
    if(y < this.origextent[0][1] || y > this.origextent[1][1])
        return false;
    if(z < this.origextent[0][2] || z > this.origextent[1][2])
        return false;
    return true;
};

ProteinSurface.prototype.getFacesAndVertices = function() {
    let i, il;
    let vertices = this.verts;
    for(i = 0, il = vertices.length; i < il; i++) {
        vertices[i].x = vertices[i].x / this.scaleFactor - this.ptranx;
        vertices[i].y = vertices[i].y / this.scaleFactor - this.ptrany;
        vertices[i].z = vertices[i].z / this.scaleFactor - this.ptranz;
    }

    let finalfaces = [];
    for(i = 0, il = this.faces.length; i < il; i += 3) {
        //var f = faces[i];
        let fa = this.faces[i], fb = this.faces[i+1], fc = this.faces[i+2];
        let a = vertices[fa]['atomid'], b = vertices[fb]['atomid'], c = vertices[fc]['atomid'];

        // must be a unique face for each atom
        if(!this.atomsToShow[a] || !this.atomsToShow[b] || !this.atomsToShow[c]) {
            continue;
        }

        if(fa !== fb && fb !== fc && fa !== fc){
            // !!! different between 3Dmol and iCn3D
            finalfaces.push({"a":fa, "b":fb, "c":fc});
        }

    }

    //try to help the garbage collector
    this.vpBits = null; // uint8 array of bitmasks
    this.vpDistance = null; // floatarray
    this.vpAtomID = null; // intarray

    this.vpColor = null; // intarray
    this.vpPot = null; // floatarray

    return {
        'vertices' : vertices,
        'faces' : finalfaces
    };
};


ProteinSurface.prototype.initparm = function(extent, btype, in_bCalcArea, atomlist
  , inHeader, inData, inMatrix, inIsovalue, inLoadPhiFrom) {
    // for delphi
    this.header = inHeader;
    this.dataArray = inData;
    this.matrix = inMatrix;
    this.isovalue = inIsovalue;
    this.loadPhiFrom = inLoadPhiFrom;

    this.bCalcArea = in_bCalcArea;

    for(let i = 0, il = atomlist.length; i < il; i++)
        this.atomsToShow[atomlist[i]] = 1;

    // !!! different between 3Dmol and iCn3D
    //if(volume > 1000000) //heuristical decrease resolution to avoid large memory consumption
    //    this.scaleFactor = this.defaultScaleFactor/2;

    let margin =(1 / this.scaleFactor) * 5.5; // need margin to avoid
                                            // boundary/round off effects
    this.origextent = extent;
    this.pminx = extent[0][0]; this.pmaxx = extent[1][0];
    this.pminy = extent[0][1]; this.pmaxy = extent[1][1];
    this.pminz = extent[0][2]; this.pmaxz = extent[1][2];

    if(!btype) {
        this.pminx -= margin;
        this.pminy -= margin;
        this.pminz -= margin;
        this.pmaxx += margin;
        this.pmaxy += margin;
        this.pmaxz += margin;
    } else {
        this.pminx -= this.probeRadius + margin;
        this.pminy -= this.probeRadius + margin;
        this.pminz -= this.probeRadius + margin;
        this.pmaxx += this.probeRadius + margin;
        this.pmaxy += this.probeRadius + margin;
        this.pmaxz += this.probeRadius + margin;
    }

    this.pminx = Math.floor(this.pminx * this.scaleFactor) / this.scaleFactor;
    this.pminy = Math.floor(this.pminy * this.scaleFactor) / this.scaleFactor;
    this.pminz = Math.floor(this.pminz * this.scaleFactor) / this.scaleFactor;
    this.pmaxx = Math.ceil(this.pmaxx * this.scaleFactor) / this.scaleFactor;
    this.pmaxy = Math.ceil(this.pmaxy * this.scaleFactor) / this.scaleFactor;
    this.pmaxz = Math.ceil(this.pmaxz * this.scaleFactor) / this.scaleFactor;

    this.ptranx = -this.pminx;
    this.ptrany = -this.pminy;
    this.ptranz = -this.pminz;

    // !!! different between 3Dmol and iCn3D
    // copied from surface.js from iview
    let boxLength = 129;
    //maxLen = this.pmaxx - this.pminx + 2*(this.probeRadius + 5.5/2)
    let maxLen = this.pmaxx - this.pminx;
    if((this.pmaxy - this.pminy) > maxLen) maxLen = this.pmaxy - this.pminy;
    if((this.pmaxz - this.pminz) > maxLen) maxLen = this.pmaxz - this.pminz;
    this.scaleFactor =(boxLength - 1.0) / maxLen;

    // 1. typically(size < 90) use the default scale factor 2
    this.scaleFactor = this.defaultScaleFactor;

    // 2. If size > 90, change scale
    //var threshbox = 180; // maximum possible boxsize
    if(this.bCalcArea || this.defaultScaleFactor * maxLen > this.threshbox) {
        boxLength = Math.floor(this.threshbox);
        this.scaleFactor =(this.threshbox - 1.0) / maxLen;
    }
    // end of surface.js part

    this.pLength = Math.ceil(this.scaleFactor *(this.pmaxx - this.pminx)) + 1;
    this.pWidth = Math.ceil(this.scaleFactor *(this.pmaxy - this.pminy)) + 1;
    this.pHeight = Math.ceil(this.scaleFactor *(this.pmaxz - this.pminz)) + 1;

    this.finalScaleFactor.x =(this.pLength - 1.0) /(this.pmaxx - this.pminx);
    this.finalScaleFactor.y =(this.pWidth - 1.0) /(this.pmaxy - this.pminy);
    this.finalScaleFactor.z =(this.pHeight - 1.0) /(this.pmaxz - this.pminz);

    this.boundingatom(btype);
    this.cutRadius = this.probeRadius * this.scaleFactor;

    this.vpBits = new Uint8Array(this.pLength * this.pWidth * this.pHeight);
    this.vpDistance = new Float64Array(this.pLength * this.pWidth * this.pHeight); // float 32
    this.vpAtomID = new Int32Array(this.pLength * this.pWidth * this.pHeight);

    this.vpColor = [];
    this.vpPot = [];
};

ProteinSurface.prototype.boundingatom = function(btype) {
    let tradius = [];
    let txz, tdept, sradius, indx;
    //flagradius = btype;

    for(let i in this.vdwRadii) {
        if(!this.vdwRadii.hasOwnProperty(i))
            continue;
        let r = this.vdwRadii[i];
        if(!btype)
            tradius[i] = r * this.scaleFactor + 0.5;
        else
            tradius[i] =(r + this.probeRadius) * this.scaleFactor + 0.5;

        sradius = tradius[i] * tradius[i];
        this.widxz[i] = Math.floor(tradius[i]) + 1;
        this.depty[i] = new Int32Array(this.widxz[i] * this.widxz[i]);
        indx = 0;
        for(let j = 0; j < this.widxz[i]; j++) {
            for(let k = 0; k < this.widxz[i]; k++) {
                txz = j * j + k * k;
                if(txz > sradius)
                    this.depty[i][indx] = -1; // outside
                else {
                    tdept = Math.sqrt(sradius - txz);
                    this.depty[i][indx] = Math.floor(tdept);
                }
                indx++;
            }
        }
    }
};

ProteinSurface.prototype.fillvoxels = function(atoms, atomlist) { //(int seqinit,int
    // seqterm,bool
    // atomtype,atom*
    // proseq,bool bcolor)
    let i, j, k, il;
    for(i = 0, il = this.vpBits.length; i < il; i++) {
        this.vpBits[i] = 0;
        this.vpDistance[i] = -1.0;
        this.vpAtomID[i] = -1;

        this.vpColor[i] = new THREE.Color();
        this.vpPot[i] = 0;
    }

    for(i in atomlist) {
        let atom = atoms[atomlist[i]];
        if(atom === undefined || atom.resn === 'DUM')
            continue;
        this.fillAtom(atom, atoms);
    }

    // show delphi potential on surface
    if(this.dataArray) {
        let pminx2 = 0, pmaxx2 = this.header.xExtent - 1;
        let pminy2 = 0, pmaxy2 = this.header.yExtent - 1;
        let pminz2 = 0, pmaxz2 = this.header.zExtent - 1;

        let scaleFactor2 = 1; // angstrom / grid

        let pLength2 = Math.floor(0.5 + scaleFactor2 *(pmaxx2 - pminx2)) + 1;
        let pWidth2 = Math.floor(0.5 + scaleFactor2 *(pmaxy2 - pminy2)) + 1;
        let pHeight2 = Math.floor(0.5 + scaleFactor2 *(pmaxz2 - pminz2)) + 1;

        // fill the color
        let widthHeight2 = pWidth2 * pHeight2;
        let height2 = pHeight2;

        // generate the correctly ordered this.dataArray
        let vData = new Float32Array(pLength2 * pWidth2 * pHeight2);

        // loop through the delphi box
        for(i = 0; i < pLength2; ++i) {
            for(j = 0; j < pWidth2; ++j) {
                for(k = 0; k < pHeight2; ++k) {
                    let index = i * widthHeight2 + j * height2 + k;

                    let index2;
                    if(this.header.filetype == 'phi') { // loop z, y, x
                        index2 = k * widthHeight2 + j * height2 + i;
                    }
                    else if(this.header.filetype == 'cube') { // loop x, y, z
                        index2 = i * widthHeight2 + j * height2 + k;
                    }

                    if(index2 < this.dataArray.length) {
                        vData[index] = this.dataArray[index2];
                    }
                }
            }
        }

        let widthHeight = this.pWidth * this.pHeight;
        let height = this.pHeight;

        // loop through the surface box
        for(i = 0; i < this.pLength; ++i) {
            for(j = 0; j < this.pWidth; ++j) {
                for(k = 0; k < this.pHeight; ++k) {
                    let x = i / this.finalScaleFactor.x - this.ptranx;
                    let y = j / this.finalScaleFactor.y - this.ptrany;
                    let z = k / this.finalScaleFactor.z - this.ptranz;

                    let r = new THREE.Vector3(x, y, z);

                    // scale to the grid
                    r.sub(this.header.ori).multiplyScalar(this.header.scale);

                    // determine the neighboring grid coordinate
                    let nx0 = Math.floor(r.x), nx1 = Math.ceil(r.x);
                    let ny0 = Math.floor(r.y), ny1 = Math.ceil(r.y);
                    let nz0 = Math.floor(r.z), nz1 = Math.ceil(r.z);
                    if(nx1 == nx0) nx1 = nx0 + 1;
                    if(ny1 == ny0) ny1 = ny0 + 1;
                    if(nz1 == nz0) nz1 = nz0 + 1;

                    if(nx1 > pLength2) nx1 = pLength2;
                    if(ny1 > pWidth2) ny1 = pWidth2;
                    if(nz1 > pHeight2) nz1 = pHeight2;

                    //https://en.wikipedia.org/wiki/Trilinear_interpolation
                    let c000 = vData[nx0 * widthHeight2 + ny0 * height2 + nz0];
                    let c100 = vData[nx1 * widthHeight2 + ny0 * height2 + nz0];
                    let c010 = vData[nx0 * widthHeight2 + ny1 * height2 + nz0];
                    let c001 = vData[nx0 * widthHeight2 + ny0 * height2 + nz1];
                    let c110 = vData[nx1 * widthHeight2 + ny1 * height2 + nz0];
                    let c011 = vData[nx0 * widthHeight2 + ny1 * height2 + nz1];
                    let c101 = vData[nx1 * widthHeight2 + ny0 * height2 + nz1];
                    let c111 = vData[nx1 * widthHeight2 + ny1 * height2 + nz1];

                    let xd = r.x - nx0;
                    let yd = r.y - ny0;
                    let zd = r.z - nz0;

                    let c00 = c000 *(1 - xd) + c100 * xd;
                    let c01 = c001 *(1 - xd) + c101 * xd;
                    let c10 = c010 *(1 - xd) + c110 * xd;
                    let c11 = c011 *(1 - xd) + c111 * xd;

                    let c0 = c00 *(1 - yd) + c10 * yd;
                    let c1 = c01 *(1 - yd) + c11 * yd;

                    let c = c0 *(1 - zd) + c1 * zd;

                    let index = i * widthHeight + j * height + k;

                    this.vpPot[index] = c;

                    // determine the color based on the potential value
                    if(c > this.isovalue) c = this.isovalue;
                    if(c < -this.isovalue) c = -this.isovalue;

                    let color;
                    if(c > 0) {
                        c /= 1.0 * this.isovalue;
                        color = new THREE.Color(1-c, 1-c, 1);
                    }
                    else {
                        c /= -1.0 * this.isovalue;
                        color = new THREE.Color(1, 1-c, 1-c);
                    }

                    this.vpColor[index] = color;
                } // for k
            } // for j
        } // for i
    }

    for(i = 0, il = this.vpBits.length; i < il; i++)
        if(this.vpBits[i] & this.INOUT)
            this.vpBits[i] |= this.ISDONE;

};


ProteinSurface.prototype.fillAtom = function(atom, atoms) {
    let cx, cy, cz, ox, oy, oz, mi, mj, mk, i, j, k, si, sj, sk;
    let ii, jj, kk, n;

    // !!! different between 3Dmol and iCn3D
    cx = Math.floor(0.5 + this.scaleFactor *(atom.coord.x + this.ptranx));
    cy = Math.floor(0.5 + this.scaleFactor *(atom.coord.y + this.ptrany));
    cz = Math.floor(0.5 + this.scaleFactor *(atom.coord.z + this.ptranz));

    let at = this.getVDWIndex(atom);
    let nind = 0;
    let pWH = this.pWidth*this.pHeight;

    for(i = 0, n = this.widxz[at]; i < n; i++) {
        for(j = 0; j < n; j++) {
            if(this.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 <= this.depty[at][nind]; k++) {
                    mj = k * jj;
                    si = cx + mi;
                    sj = cy + mj;
                    sk = cz + mk;
                    if(si < 0 || sj < 0 ||
                            sk < 0 ||
                            si >= this.pLength ||
                            sj >= this.pWidth ||
                            sk >= this.pHeight)
                        continue;
                    let index = si * pWH + sj * this.pHeight + sk;

                    if(!(this.vpBits[index] & this.INOUT)) {
                        this.vpBits[index] |= this.INOUT;
                        this.vpAtomID[index] = atom.serial;
                    } else {
                        let atom2 = atoms[this.vpAtomID[index]];
                        if(atom2.serial != atom.serial) {
                            ox = cx + mi - Math.floor(0.5 + this.scaleFactor *
                                   (atom2.x + this.ptranx));
                            oy = cy + mj - Math.floor(0.5 + this.scaleFactor *
                                   (atom2.y + this.ptrany));
                            oz = cz + mk - Math.floor(0.5 + this.scaleFactor *
                                   (atom2.z + this.ptranz));
                            if(mi * mi + mj * mj + mk * mk < ox *
                                    ox + oy * oy + oz * oz)
                                this.vpAtomID[index] = atom.serial;
                        }
                    }

                }// k
            }// if
        }// kk
    }// jj
                }// ii
            }// if
            nind++;
        }// j
    }// i
};

ProteinSurface.prototype.fillvoxelswaals = function(atoms, atomlist) {
    let i, il;
    for(i = 0, il = this.vpBits.length; i < il; i++)
        this.vpBits[i] &= ~this.ISDONE; // not isdone

    for(i in atomlist) {
        let atom = atoms[atomlist[i]];
        if(atom === undefined)
            continue;

        this.fillAtomWaals(atom, atoms);
    }
};

ProteinSurface.prototype.fillAtomWaals = function(atom, atoms) {
    let cx, cy, cz, ox, oy, oz, nind = 0;
    let mi, mj, mk, si, sj, sk, i, j, k, ii, jj, kk, n;

    // !!! different between 3Dmol and iCn3D
    cx = Math.floor(0.5 + this.scaleFactor *(atom.coord.x + this.ptranx));
    cy = Math.floor(0.5 + this.scaleFactor *(atom.coord.y + this.ptrany));
    cz = Math.floor(0.5 + this.scaleFactor *(atom.coord.z + this.ptranz));

    let at = this.getVDWIndex(atom);
    let pWH = this.pWidth*this.pHeight;
    for(i = 0, n = this.widxz[at]; i < n; i++) {
        for(j = 0; j < n; j++) {
            if(this.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 <= this.depty[at][nind]; k++) {
                    mj = k * jj;
                    si = cx + mi;
                    sj = cy + mj;
                    sk = cz + mk;
                    if(si < 0 || sj < 0 ||
                            sk < 0 ||
                            si >= this.pLength ||
                            sj >= this.pWidth ||
                            sk >= this.pHeight)
                        continue;
                    let index = si * pWH + sj * this.pHeight + sk;
                    if(!(this.vpBits[index] & this.ISDONE)) {
                        this.vpBits[index] |= this.ISDONE;
                        this.vpAtomID[index] = atom.serial;
                    }  else {
                        let atom2 = atoms[this.vpAtomID[index]];
                        if(atom2.serial != atom.serial) {
                            ox = cx + mi - Math.floor(0.5 + this.scaleFactor *
                                   (atom2.x + this.ptranx));
                            oy = cy + mj - Math.floor(0.5 + this.scaleFactor *
                                   (atom2.y + this.ptrany));
                            oz = cz + mk - Math.floor(0.5 + this.scaleFactor *
                                   (atom2.z + this.ptranz));
                            if(mi * mi + mj * mj + mk * mk < ox *
                                    ox + oy * oy + oz * oz)
                                this.vpAtomID[index] = atom.serial;
                        }
                    }
                }// k
            }// if
        }// kk
    }// jj
                }// ii
            }// if
            nind++;
        }// j
    }// i
};

ProteinSurface.prototype.buildboundary = function() {
    let pWH = this.pWidth*this.pHeight;
    for(let i = 0; i < this.pLength; i++) {
        for(let j = 0; j < this.pHeight; j++) {
            for(let k = 0; k < this.pWidth; k++) {
                let index = i * pWH + k * this.pHeight + j;
                if(this.vpBits[index] & this.INOUT) {
                    let ii = 0;
                    while(ii < 26) {
                        let ti = i + this.nb[ii][0], tj = j + this.nb[ii][2], tk = k +
                                this.nb[ii][1];
                        if(ti > -1 &&
                            ti < this.pLength &&
                            tk > -1 &&
                            tk < this.pWidth &&
                            tj > -1 &&
                            tj < this.pHeight &&
                            !(this.vpBits[ti * pWH + tk * this.pHeight + tj] & this.INOUT)) {
                            this.vpBits[index] |= this.ISBOUND;
                            break;
                        } else
                            ii++;
                    }
                }
            }
        }
    }
};

ProteinSurface.prototype.fastdistancemap = function() {
    let i, j, k, n;

    // a little class for 3d array, should really generalize this and
    // use throughout...
    let PointGrid = function(length, width, height) {
        // the standard says this is zero initialized
        let data = new Int32Array(length * width * height * 3);

        // set position x,y,z to pt, which has ix,iy,and iz
        this.set = function(x, y, z, pt) {
            let index =((((x * width) + y) * height) + z) * 3;
            data[index] = pt.ix;
            data[index + 1] = pt.iy;
            data[index + 2] = pt.iz;
        };

        // return point at x,y,z
        this.get = function(x, y, z) {
            let index =((((x * width) + y) * height) + z) * 3;
            return {
                ix : data[index],
                iy : data[index + 1],
                iz : data[index + 2]
            };
        };
    };

    let boundPoint = new PointGrid(this.pLength, this.pWidth, this.pHeight);
    let pWH = this.pWidth*this.pHeight;
    let cutRSq = this.cutRadius*this.cutRadius;

    let inarray = [];
    let outarray = [];

    let index;

    for(i = 0; i < this.pLength; i++) {
        for(j = 0; j < this.pWidth; j++) {
            for(k = 0; k < this.pHeight; k++) {
                index = i * pWH + j * this.pHeight + k;
                this.vpBits[index] &= ~this.ISDONE; // isdone = false
                if(this.vpBits[index] & this.INOUT) {
                    if(this.vpBits[index] & this.ISBOUND) {
                        let triple = {
                            ix : i,
                            iy : j,
                            iz : k
                        };
                        boundPoint.set(i, j, k, triple);
                        inarray.push(triple);
                        this.vpDistance[index] = 0;
                        this.vpBits[index] |= this.ISDONE;
                        this.vpBits[index] &= ~this.ISBOUND;
                    }
                }
            }
        }
    }

    do {
        outarray = this.fastoneshell(inarray, boundPoint);
        inarray = [];
        for(i = 0, n = outarray.length; i < n; i++) {
            index = pWH * outarray[i].ix + this.pHeight *
                outarray[i].iy + outarray[i].iz;
            this.vpBits[index] &= ~this.ISBOUND;
            if(this.vpDistance[index] <= 1.0404 * cutRSq) {
                inarray.push({
                    ix : outarray[i].ix,
                    iy : outarray[i].iy,
                    iz : outarray[i].iz
                });
            }
        }
    } while(inarray.length !== 0);

    inarray = [];
    outarray = [];
    boundPoint = null;

    let cutsf = this.scaleFactor - 0.5;
    if(cutsf < 0)
        cutsf = 0;
    let cutoff = cutRSq - 0.50 /(0.1 + cutsf);
    for(i = 0; i < this.pLength; i++) {
        for(j = 0; j < this.pWidth; j++) {
            for(k = 0; k < this.pHeight; k++) {
                index = i * pWH + j * this.pHeight + k;
                this.vpBits[index] &= ~this.ISBOUND;
                // ses solid
                if(this.vpBits[index] & this.INOUT) {
                    if(!(this.vpBits[index] & this.ISDONE) ||
                           ((this.vpBits[index] & this.ISDONE) && this.vpDistance[index] >= cutoff)) {
                        this.vpBits[index] |= this.ISBOUND;
                    }
                }
            }
        }
    }

};

ProteinSurface.prototype.fastoneshell = function(inarray, boundPoint) { //(int* innum,int
    // *allocout,voxel2
    // ***boundPoint, int*
    // outnum, int *elimi)
    let tx, ty, tz;
    let dx, dy, dz;
    let i, j, n;
    let square;
    let bp, index;
    let outarray = [];
    if(inarray.length === 0)
        return outarray;

    let tnv = {
        ix : -1,
        iy : -1,
        iz : -1
    };
    let pWH = this.pWidth*this.pHeight;
    for( i = 0, n = inarray.length; i < n; i++) {
        tx = inarray[i].ix;
        ty = inarray[i].iy;
        tz = inarray[i].iz;
        bp = boundPoint.get(tx, ty, tz);

        for(j = 0; j < 6; j++) {
            tnv.ix = tx + this.nb[j][0];
            tnv.iy = ty + this.nb[j][1];
            tnv.iz = tz + this.nb[j][2];

            if(tnv.ix < this.pLength && tnv.ix > -1 && tnv.iy < this.pWidth &&
                    tnv.iy > -1 && tnv.iz < this.pHeight && tnv.iz > -1) {
                index = tnv.ix * pWH + this.pHeight * tnv.iy + tnv.iz;

                if((this.vpBits[index] & this.INOUT) && !(this.vpBits[index] & this.ISDONE)) {

                    boundPoint.set(tnv.ix, tnv.iy, tz + this.nb[j][2], bp);
                    dx = tnv.ix - bp.ix;
                    dy = tnv.iy - bp.iy;
                    dz = tnv.iz - bp.iz;
                    square = dx * dx + dy * dy + dz * dz;
                    this.vpDistance[index] = square;
                    this.vpBits[index] |= this.ISDONE;
                    this.vpBits[index] |= this.ISBOUND;

                    outarray.push({
                        ix : tnv.ix,
                        iy : tnv.iy,
                        iz : tnv.iz
                    });
                } else if((this.vpBits[index] & this.INOUT) &&(this.vpBits[index] & this.ISDONE)) {

                    dx = tnv.ix - bp.ix;
                    dy = tnv.iy - bp.iy;
                    dz = tnv.iz - bp.iz;
                    square = dx * dx + dy * dy + dz * dz;
                    if(square < this.vpDistance[index]) {
                        boundPoint.set(tnv.ix, tnv.iy, tnv.iz, bp);

                        this.vpDistance[index] = square;
                        if(!(this.vpBits[index] & this.ISBOUND)) {
                            this.vpBits[index] |= this.ISBOUND;
                            outarray.push({
                                ix : tnv.ix,
                                iy : tnv.iy,
                                iz : tnv.iz
                            });
                        }
                    }
                }
            }
        }
    }

    for(i = 0, n = inarray.length; i < n; i++) {
        tx = inarray[i].ix;
        ty = inarray[i].iy;
        tz = inarray[i].iz;
        bp = boundPoint.get(tx, ty, tz);

        for(j = 6; j < 18; j++) {
            tnv.ix = tx + this.nb[j][0];
            tnv.iy = ty + this.nb[j][1];
            tnv.iz = tz + this.nb[j][2];

            if(tnv.ix < this.pLength && tnv.ix > -1 && tnv.iy < this.pWidth &&
                    tnv.iy > -1 && tnv.iz < this.pHeight && tnv.iz > -1) {
                index = tnv.ix * pWH + this.pHeight * tnv.iy + tnv.iz;

                if((this.vpBits[index] & this.INOUT) && !(this.vpBits[index] & this.ISDONE)) {
                    boundPoint.set(tnv.ix, tnv.iy, tz + this.nb[j][2], bp);

                    dx = tnv.ix - bp.ix;
                    dy = tnv.iy - bp.iy;
                    dz = tnv.iz - bp.iz;
                    square = dx * dx + dy * dy + dz * dz;
                    this.vpDistance[index] = square;
                    this.vpBits[index] |= this.ISDONE;
                    this.vpBits[index] |= this.ISBOUND;

                    outarray.push({
                        ix : tnv.ix,
                        iy : tnv.iy,
                        iz : tnv.iz
                    });
                } else if((this.vpBits[index] & this.INOUT) &&(this.vpBits[index] & this.ISDONE)) {
                    dx = tnv.ix - bp.ix;
                    dy = tnv.iy - bp.iy;
                    dz = tnv.iz - bp.iz;
                    square = dx * dx + dy * dy + dz * dz;
                    if(square < this.vpDistance[index]) {
                        boundPoint.set(tnv.ix, tnv.iy, tnv.iz, bp);
                        this.vpDistance[index] = square;
                        if(!(this.vpBits[index] & this.ISBOUND)) {
                            this.vpBits[index] |= this.ISBOUND;
                            outarray.push({
                                ix : tnv.ix,
                                iy : tnv.iy,
                                iz : tnv.iz
                            });
                        }
                    }
                }
            }
        }
    }

    for(i = 0, n = inarray.length; i < n; i++) {
        tx = inarray[i].ix;
        ty = inarray[i].iy;
        tz = inarray[i].iz;
        bp = boundPoint.get(tx, ty, tz);

        for(j = 18; j < 26; j++) {
            tnv.ix = tx + this.nb[j][0];
            tnv.iy = ty + this.nb[j][1];
            tnv.iz = tz + this.nb[j][2];

            if(tnv.ix < this.pLength && tnv.ix > -1 && tnv.iy < this.pWidth &&
                    tnv.iy > -1 && tnv.iz < this.pHeight && tnv.iz > -1) {
                index = tnv.ix * pWH + this.pHeight * tnv.iy + tnv.iz;

                if((this.vpBits[index] & this.INOUT) && !(this.vpBits[index] & this.ISDONE)) {
                    boundPoint.set(tnv.ix, tnv.iy, tz + this.nb[j][2], bp);

                    dx = tnv.ix - bp.ix;
                    dy = tnv.iy - bp.iy;
                    dz = tnv.iz - bp.iz;
                    square = dx * dx + dy * dy + dz * dz;
                    this.vpDistance[index] = square;
                    this.vpBits[index] |= this.ISDONE;
                    this.vpBits[index] |= this.ISBOUND;

                    outarray.push({
                        ix : tnv.ix,
                        iy : tnv.iy,
                        iz : tnv.iz
                    });
                } else if((this.vpBits[index] & this.INOUT)  &&(this.vpBits[index] & this.ISDONE)) {
                    dx = tnv.ix - bp.ix;
                    dy = tnv.iy - bp.iy;
                    dz = tnv.iz - bp.iz;
                    square = dx * dx + dy * dy + dz * dz;
                    if(square < this.vpDistance[index]) {
                        boundPoint.set(tnv.ix, tnv.iy, tnv.iz, bp);

                        this.vpDistance[index] = square;
                        if(!(this.vpBits[index] & this.ISBOUND)) {
                            this.vpBits[index] |= this.ISBOUND;
                            outarray.push({
                                ix : tnv.ix,
                                iy : tnv.iy,
                                iz : tnv.iz
                            });
                        }
                    }
                }
            }
        }
    }

    return outarray;
};

ProteinSurface.prototype.marchingcubeinit = function(stype) {
    for( let i = 0, lim = this.vpBits.length; i < lim; i++) {
        if(stype == 1) {// vdw
            this.vpBits[i] &= ~this.ISBOUND;
        } else if(stype == 4) { // ses
            this.vpBits[i] &= ~this.ISDONE;
            if(this.vpBits[i] & this.ISBOUND)
                this.vpBits[i] |= this.ISDONE;
            this.vpBits[i] &= ~this.ISBOUND;
        } else if(stype == 2) {// after vdw
            if((this.vpBits[i] & this.ISBOUND) &&(this.vpBits[i] & this.ISDONE))
                this.vpBits[i] &= ~this.ISBOUND;
            else if((this.vpBits[i] & this.ISBOUND) && !(this.vpBits[i] & this.ISDONE))
                this.vpBits[i] |= this.ISDONE;
        } else if(stype == 3) { // sas
            this.vpBits[i] &= ~this.ISBOUND;
        }
    }
};

// this code allows me to empirically prune the marching cubes code tables
// to more efficiently handle discrete data
ProteinSurface.prototype.counter = function() {
    let data = Array(256);
    for( let i = 0; i < 256; i++)
        data[i] = [];

    this.incrementUsed = function(i, j) {
        if(typeof data[i][j] === 'undefined')
            data[i][j] = {
                used : 0,
                unused : 0
            };
        data[i][j].used++;
    };

    this.incrementUnused = function(i, j) {
        if(typeof data[i][j] === 'undefined')
            data[i][j] = {
                used : 0,
                unused : 0
            };
        data[i][j].unused++;

    };

    let redoTable = function(triTable) {
        let str = "[";
        for( let i = 0; i < triTable.length; i++) {
            let code = 0;
            let table = triTable[i];
            for( let j = 0; j < table.length; j++) {
                code |=(1 <<(table[j]));
            }
            str += "0x" + code.toString(16) + ", ";
        }
        str += "]";
    };

    this.print = function() {

        let table = this.marchingCube.triTable;
        let newtable = [];
        for( let i = 0; i < table.length; i++) {
            let newarr = [];
            for( let j = 0; j < table[i].length; j += 3) {
                let k = j / 3;
                if(typeof data[i][k] === 'undefined' || !data[i][k].unused) {
                    newarr.push(table[i][j]);
                    newarr.push(table[i][j + 1]);
                    newarr.push(table[i][j + 2]);
                }
                if(typeof data[i][k] === 'undefined')
                    console.log("undef " + i + "," + k);
            }
            newtable.push(newarr);
        }
        redoTable(newtable);
    };
};

ProteinSurface.prototype.marchingcube = function(stype) {
    this.marchingcubeinit(stype);
    this.verts = []; this.faces = [];
    this.marchingCube.march(this.vpBits, this.verts, this.faces, {
        smooth : 1,
        nX : this.pLength,
        nY : this.pWidth,
        nZ : this.pHeight
    });

    let pWH = this.pWidth*this.pHeight;
    for(let i = 0, vlen = this.verts.length; i < vlen; i++) {
        this.verts[i]['atomid'] = this.vpAtomID[this.verts[i].x * pWH + this.pHeight *
                this.verts[i].y + this.verts[i].z];
        if(this.dataArray) this.verts[i]['color'] = this.vpColor[this.verts[i].x * pWH + this.pHeight *
                this.verts[i].y + this.verts[i].z];
        if(this.dataArray) this.verts[i]['pot'] = this.vpPot[this.verts[i].x * pWH + this.pHeight *
                this.verts[i].y + this.verts[i].z];
    }

    // calculate surface area
    let serial2area, maxScaleFactor, area = 0;
    if(this.bCalcArea) {
        let faceHash = {};
        serial2area = {};
        for(let i = 0, il = this.faces.length; i < il; i += 3) {
            let fa = this.faces[i], fb = this.faces[i+1], fc = this.faces[i+2];

            if(fa == fb || fb == fc || fa == fc) continue;

            let fmin = Math.min(fa, fb, fc);
            let fmax = Math.max(fa, fb, fc);
            let fmid = fa + fb + fc - fmin - fmax;
            let fmin_fmid_fmax = fmin + '_' + fmid + '_' + fmax;

            if(faceHash.hasOwnProperty(fmin_fmid_fmax)) {
                continue;
            }

            faceHash[fmin_fmid_fmax] = 1;

            let ai = this.verts[fa]['atomid'], bi = this.verts[fb]['atomid'], ci = this.verts[fc]['atomid'];

            if(!this.atomsToShow[ai] || !this.atomsToShow[bi] || !this.atomsToShow[ci]) {
                continue;
            }

            //if(fa !== fb && fb !== fc && fa !== fc){
                let a = this.verts[fa];
                let b = this.verts[fb];
                let c = this.verts[fc];

                let ab2 =(a.x - b.x) *(a.x - b.x) +(a.y - b.y) *(a.y - b.y) +(a.z - b.z) *(a.z - b.z);
                let ac2 =(a.x - c.x) *(a.x - c.x) +(a.y - c.y) *(a.y - c.y) +(a.z - c.z) *(a.z - c.z);
                let cb2 =(c.x - b.x) *(c.x - b.x) +(c.y - b.y) *(c.y - b.y) +(c.z - b.z) *(c.z - b.z);

                let min = Math.min(ab2, ac2, cb2);
                let max = Math.max(ab2, ac2, cb2);
                let mid = ab2 + ac2 + cb2 - min - max;

                // there are only three kinds of triangles as shown at
                // https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0008140
                // case 1: 1, 1, sqrt(2)     area: 0.5 * a * a;
                // case 2: sqrt(2), sqrt(2), sqrt(2)    area: 0.5 * a * a * sqrt(3) * 0.5;
                // case 3: 1, sqrt(2), sqrt(3)      area: 0.5 * a * b
                let currArea = 0;
                if(parseInt((max - min)*100) == 0) { // case 2
                    currArea = 0.433 * min;
                }
                else if(parseInt((mid - min)*100) == 0) { // case 1
                    currArea = 0.5 * min;
                }
                else { // case 3
                    currArea = 0.707 * min;
                }

                let partArea = currArea / 3;

                if(serial2area[ai] === undefined) serial2area[ai] = partArea;
                else serial2area[ai] += partArea;

                if(serial2area[bi] === undefined) serial2area[bi] = partArea;
                else serial2area[bi] += partArea;

                if(serial2area[ci] === undefined) serial2area[ci] = partArea;
                else serial2area[ci] += partArea;

                area += currArea;
            //}
        } // for loop

        maxScaleFactor = Math.max(this.finalScaleFactor.x, this.finalScaleFactor.y, this.finalScaleFactor.z);
        area = area / maxScaleFactor / maxScaleFactor;
        //area = area / this.scaleFactor / this.scaleFactor;
    }

    if(!this.bCalcArea) this.marchingCube.laplacianSmooth(1, this.verts, this.faces);

    return {"area": area, "serial2area": serial2area, "scaleFactor": maxScaleFactor};
};

/* ProteinSurface4.js
 * @author David Koes  / https://github.com/3dmol/3Dmol.js/tree/master/3Dmol
 * Modified by Jiyao Wang / https://github.com/ncbi/icn3d
 */

// dkoes
// Surface calculations.  This must be safe to use within a web worker.
class ElectronMap {
    constructor(icn3d) {
        this.icn3d = icn3d;

//$3Dmol.ElectronMap = function(threshbox) {
    //"use strict";

        // constants for vpbits bitmasks
        /** @this.*/
        this.INOUT = 1;
        /** @this.*/
        this.ISDONE = 2;
        /** @this.*/
        this.ISBOUND = 4;

        this.isovalue = 1.5;
        this.dataArray = {};
        this.matrix = undefined;
        this.center = undefined;
        this.maxdist = undefined;
        this.pmin = undefined;
        this.pmax = undefined;
        this.water = undefined;
        this.header = undefined;
        this.type = undefined;
        this.rmsd_supr = undefined;
        this.loadPhiFrom = undefined;

        this.ptranx = 0;
        this.ptrany = 0;
        this.ptranz = 0;
        this.probeRadius = 1.4;
        this.defaultScaleFactor = 2;
        this.scaleFactor = this.defaultScaleFactor; // 2 is .5A grid; if this is made user configurable,
                                // also have to adjust offset used to find non-shown
                                // atoms
        this.pHeight = 0;
        this.pWidth = 0;
        this.pLength = 0;
        this.cutRadius = 0;
        this.vpBits = null; // uint8 array of bitmasks
        this.vpDistance = null; // floatarray of _squared_ distances
        this.vpAtomID = null; // intarray
        this.vertnumber = 0;
        this.facenumber = 0;
        this.pminx = 0;
        this.pminy = 0;
        this.pminz = 0;
        this.pmaxx = 0;
        this.pmaxy = 0;
        this.pmaxz = 0;

        this.depty = {};
        this.widxz = {};
        this.faces = undefined;
        this.verts = undefined;
        this.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.marchingCube = new MarchingCube();
    }
}

ElectronMap.prototype.getFacesAndVertices = function(allatoms, atomlist) {
    let atomsToShow = {};
    let i, il;
    for(i = 0, il = atomlist.length; i < il; i++)
        atomsToShow[atomlist[i]] = 1;
    let vertices = this.verts;

    for(i = 0, il = vertices.length; i < il; i++) {
        let r;
        if(this.type == 'phi') {
            r = new THREE.Vector3(vertices[i].x, vertices[i].y, vertices[i].z).multiplyScalar(1.0/this.header.scale).applyMatrix4(this.matrix);
        }
        else {
            r = new THREE.Vector3(vertices[i].x, vertices[i].y, vertices[i].z).applyMatrix4(this.matrix);
        }
//            vertices[i].x = r.x / this.scaleFactor - this.ptranx;
//            vertices[i].y = r.y / this.scaleFactor - this.ptrany;
//            vertices[i].z = r.z / this.scaleFactor - this.ptranz;

        vertices[i].x = r.x;
        vertices[i].y = r.y;
        vertices[i].z = r.z;
    }

    let finalfaces = [];

    for(i = 0, il = this.faces.length; i < il; i += 3) {
        //var f = this.faces[i];
        let fa = this.faces[i], fb = this.faces[i+1], fc = this.faces[i+2];

        if(fa !== fb && fb !== fc && fa !== fc){
            finalfaces.push({"a":fa, "b":fb, "c":fc});
        }
    }

    //try to help the garbage collector
    this.vpBits = null; // uint8 array of bitmasks
    this.vpDistance = null; // floatarray
    this.vpAtomID = null; // intarray

    return {
        'vertices' : vertices, //shownVertices,
        'faces' : finalfaces
    };
};


ElectronMap.prototype.initparm = function(inHeader, inData, inMatrix, inIsovalue, inCenter, inMaxdist,
  inPmin, inPmax, inWater, inType, inRmsd_supr, inLoadPhiFrom, inIcn3d) {
    this.header = inHeader;
    this.loadPhiFrom = inLoadPhiFrom;
    //icn3d = inIcn3d;

    if(this.header && this.header.max !== undefined) { // EM density map from EBI
        this.isovalue = this.header.min +(this.header.max - this.header.min) * inIsovalue / 100.0;
    }
    else if(this.header && this.header.mean !== undefined) { // density map from EBI
        this.isovalue = this.header.mean + this.header.sigma * inIsovalue; // electron density map from EBI
    }
    else {
        this.isovalue = inIsovalue;
    }

    this.dataArray = inData;
    this.matrix = inMatrix;
    this.center = inCenter;
    this.maxdist = inMaxdist;
    this.pmin = inPmin;
    this.pmax = inPmax;
    this.water = inWater;
    this.type = inType;

    this.rmsd_supr = inRmsd_supr;

    this.pminx = 0; this.pmaxx = this.header.xExtent - 1;
    this.pminy = 0; this.pmaxy = this.header.yExtent - 1;
    this.pminz = 0; this.pmaxz = this.header.zExtent - 1;

    this.ptranx = -this.pminx;
    this.ptrany = -this.pminy;
    this.ptranz = -this.pminz;

    let maxLen = this.pmaxx - this.pminx;
    if((this.pmaxy - this.pminy) > maxLen) maxLen = this.pmaxy - this.pminy;
    if((this.pmaxz - this.pminz) > maxLen) maxLen = this.pmaxz - this.pminz;

    this.scaleFactor = 1; // angstrom / grid

    this.pLength = Math.floor(0.5 + this.scaleFactor *(this.pmaxx - this.pminx)) + 1;
    this.pWidth = Math.floor(0.5 + this.scaleFactor *(this.pmaxy - this.pminy)) + 1;
    this.pHeight = Math.floor(0.5 + this.scaleFactor *(this.pmaxz - this.pminz)) + 1;

    //this.boundingatom();
    this.cutRadius = this.probeRadius * this.scaleFactor;

    this.vpBits = new Uint8Array(this.pLength * this.pWidth * this.pHeight);
    this.vpAtomID = new Uint8Array(this.pLength * this.pWidth * this.pHeight);
};

ElectronMap.prototype.transformMemPro = function(inCoord, rot, centerFrom, centerTo) {
    let coord = inCoord.clone();
    coord.sub(centerFrom);

    let x = coord.x*rot[0] + coord.y*rot[1] + coord.z*rot[2] + centerTo.x;
    let y = coord.x*rot[3] + coord.y*rot[4] + coord.z*rot[5] + centerTo.y;
    let z = coord.x*rot[6] + coord.y*rot[7] + coord.z*rot[8] + centerTo.z;

    coord.x = x;
    coord.y = y;
    coord.z = z;

    return coord;
};

ElectronMap.prototype.fillvoxels = function(atoms, atomlist) { //(int seqinit,int
    // seqterm,bool
    // atomthis.type,atom*
    // proseq,bool bcolor)
    let i, j, k, il, jl, kl;
    for(i = 0, il = this.vpBits.length; i < il; i++) {
        this.vpBits[i] = 0;
        //this.vpDistance[i] = -1.0;
        this.vpAtomID[i] = 0;
    }

    let widthHeight = this.pWidth * this.pHeight;
    let height = this.pHeight;

    if(this.type == 'phi' && !this.header.bSurface) { // equipotential map
        // Do NOT exclude map far away from the atoms
        //var index = 0;
        for(i = 0; i < this.pLength; ++i) {
            for(j = 0; j < this.pWidth; ++j) {
                for(k = 0; k < this.pHeight; ++k) {
                    let index = i * widthHeight + j * height + k;

                    let index2;
                    if(this.header.filetype == 'phi') { // loop z, y, x
                        index2 = k * widthHeight + j * height + i;
                    }
                    else if(this.header.filetype == 'cube') { // loop x, y, z
                        index2 = i * widthHeight + j * height + k;
                    }

                    if(index2 < this.dataArray.length) {
                        this.vpBits[index] =(this.dataArray[index2] >= this.isovalue || this.dataArray[index2] <= -this.isovalue) ? 1 : 0;
                        this.vpAtomID[index] =(this.dataArray[index2] >= 0) ? 1 : 0; // determine whether it's positive
                    }
                    //++index;
                }
            }
        }
    }
    else {
        //var inverseMatrix = new THREE.Matrix4().getInverse(this.matrix);
        let inverseMatrix = new THREE.Matrix4().copy( this.matrix ).invert();

        let indexArray = [];
        this.maxdist = parseInt(this.maxdist); // has to be integer

        let rot, inverseRot = new Array(9), centerFrom, centerTo;
        if(this.rmsd_supr !== undefined && this.rmsd_supr.rot !== undefined) {
          rot = this.rmsd_supr.rot;
          centerFrom = this.rmsd_supr.trans1;
          centerTo = this.rmsd_supr.trans2;

          let m = new THREE.Matrix3(), inverseM = new THREE.Matrix3();
          m.set(rot[0], rot[1], rot[2], rot[3], rot[4], rot[5], rot[6], rot[7], rot[8]);
          //inverseM.getInverse(m);
          inverseM.copy(m).invert();

          inverseRot[0] = inverseM.elements[0];
          inverseRot[1] = inverseM.elements[3];
          inverseRot[2] = inverseM.elements[6];
          inverseRot[3] = inverseM.elements[1];
          inverseRot[4] = inverseM.elements[4];
          inverseRot[5] = inverseM.elements[7];
          inverseRot[6] = inverseM.elements[2];
          inverseRot[7] = inverseM.elements[5];
          inverseRot[8] = inverseM.elements[8];
        }

        if(this.type == 'phi' && this.header.bSurface) { // surface with potential
            // Do NOT exclude map far away from the atoms

            // generate the correctly ordered this.dataArray
            let vData = new Float32Array(this.pLength * this.pWidth * this.pHeight);

            for(i = 0; i < this.pLength; ++i) {
                for(j = 0; j < this.pWidth; ++j) {
                    for(k = 0; k < this.pHeight; ++k) {
                        let index = i * widthHeight + j * height + k;

                        let index2;
                        if(this.header.filetype == 'phi') { // loop z, y, x
                            index2 = k * widthHeight + j * height + i;
                        }
                        else if(this.header.filetype == 'cube') { // loop x, y, z
                            index2 = i * widthHeight + j * height + k;
                        }

                        if(index2 < this.dataArray.length) {
                            vData[index] = this.dataArray[index2];
                        }
                    }
                }
            }

            for(let serial in atomlist) {
                let atom = atoms[atomlist[serial]];

                if(atom.resn === 'DUM') continue;

                let r = atom.coord.clone();
                if(this.loadPhiFrom != 'delphi') { // transform to the original position if the potential file is imported
                    if(this.rmsd_supr !== undefined && this.rmsd_supr.rot !== undefined) {
                        // revert to the orginal coord
                        let coord = this.transformMemPro(atom.coord, inverseRot, centerTo, centerFrom);
                        r = coord.applyMatrix4(inverseMatrix);
                    }
                    else {
                        r = atom.coord.clone().applyMatrix4(inverseMatrix);
                    }
                }

                // scale to the grid
                r.sub(this.header.ori).multiplyScalar(this.header.scale);

                // determine the neighboring grid coordinate
                let nx0 = Math.floor(r.x), nx1 = Math.ceil(r.x);
                let ny0 = Math.floor(r.y), ny1 = Math.ceil(r.y);
                let nz0 = Math.floor(r.z), nz1 = Math.ceil(r.z);
                if(nx1 == nx0) nx1 = nx0 + 1;
                if(ny1 == ny0) ny1 = ny0 + 1;
                if(nz1 == nz0) nz1 = nz0 + 1;

                if(nx1 > this.pLength) nx1 = this.pLength;
                if(ny1 > this.pWidth) ny1 = this.pWidth;
                if(nz1 > this.pHeight) nz1 = this.pHeight;

                //https://en.wikipedia.org/wiki/Trilinear_interpolation
                let c000 = vData[nx0 * widthHeight + ny0 * height + nz0];
                let c100 = vData[nx1 * widthHeight + ny0 * height + nz0];
                let c010 = vData[nx0 * widthHeight + ny1 * height + nz0];
                let c001 = vData[nx0 * widthHeight + ny0 * height + nz1];
                let c110 = vData[nx1 * widthHeight + ny1 * height + nz0];
                let c011 = vData[nx0 * widthHeight + ny1 * height + nz1];
                let c101 = vData[nx1 * widthHeight + ny0 * height + nz1];
                let c111 = vData[nx1 * widthHeight + ny1 * height + nz1];

                let xd = r.x - nx0;
                let yd = r.y - ny0;
                let zd = r.z - nz0;

                let c00 = c000 *(1 - xd) + c100 * xd;
                let c01 = c001 *(1 - xd) + c101 * xd;
                let c10 = c010 *(1 - xd) + c110 * xd;
                let c11 = c011 *(1 - xd) + c111 * xd;

                let c0 = c00 *(1 - yd) + c10 * yd;
                let c1 = c01 *(1 - yd) + c11 * yd;

                let c = c0 *(1 - zd) + c1 * zd;

                // determine the color based on the potential value
                if(c > this.isovalue) c = this.isovalue;
                if(c < -this.isovalue) c = -this.isovalue;

                let color;
                if(c > 0) {
                    c /= 1.0 * this.isovalue;
                    color = new THREE.Color(1-c, 1-c, 1);
                }
                else {
                    c /= -1.0 * this.isovalue;
                    color = new THREE.Color(1, 1-c, 1-c);
                }

                this.icn3d.atoms[atomlist[serial]].color = color;
                this.icn3d.atomPrevColors[atomlist[serial]] = color;
            }
        }
        else {
            for(let serial in atomlist) {
                let atom = atoms[atomlist[serial]];

                if(atom.resn === 'DUM') continue;

                let r;
                if(this.rmsd_supr !== undefined && this.rmsd_supr.rot !== undefined) {
                    // revert to the orginal coord
                    let coord = this.transformMemPro(atom.coord, inverseRot, centerTo, centerFrom);
                    r = coord.applyMatrix4(inverseMatrix);
                }
                else {
                    r = atom.coord.clone().applyMatrix4(inverseMatrix);
                }

                for(i = Math.floor(r.x) - this.maxdist, il = Math.ceil(r.x) + this.maxdist; i <= il; ++i) {
                    if(i < 0 || i > this.header.xExtent*this.scaleFactor - 1) continue;
                    for(j = Math.floor(r.y) - this.maxdist, jl = Math.ceil(r.y) + this.maxdist; j<= jl; ++j) {
                        if(j < 0 || j > this.header.yExtent*this.scaleFactor - 1) continue;
                        for(k = Math.floor(r.z) - this.maxdist, kl = Math.ceil(r.z) + this.maxdist; k<= kl; ++k) {
                            if(k < 0 || k > this.header.zExtent*this.scaleFactor - 1) continue;
                            let index = i * widthHeight + j * height + k;
                            indexArray.push(index);
                        }
                    }
                }
            }

            for(i = 0, il = indexArray.length; i < il; ++i) {
                let index = indexArray[i];
                if(this.type == '2fofc') {
                    this.vpBits[index] =(this.dataArray[index] >= this.isovalue) ? 1 : 0;
                    //this.vpAtomID[index] =(this.dataArray[index] >= 0) ? 1 : 0; // determine whether it's positive
                }
                else if(this.type == 'fofc') {
                    this.vpBits[index] =(this.dataArray[index] >= this.isovalue || this.dataArray[index] <= -this.isovalue) ? 1 : 0;
                    this.vpAtomID[index] =(this.dataArray[index] >= 0) ? 1 : 0; // determine whether it's positive
                }
                else if(this.type == 'em') {
                    this.vpBits[index] =(this.dataArray[index] >= this.isovalue) ? 1 : 0;
                    //this.vpAtomID[index] =(this.dataArray[index] >= 0) ? 1 : 0; // determine whether it's positive
                }
            }
        }
    }

    for(i = 0, il = this.vpBits.length; i < il; i++)
        if(this.vpBits[i] & this.INOUT)
            this.vpBits[i] |= this.ISDONE;

};

ElectronMap.prototype.buildboundary = function() {
    let pWH = this.pWidth*this.pHeight;
    let i, j, k;

    for(i = 0; i < this.pLength; i++) {
        for(j = 0; j < this.pHeight; j++) {
            for(k = 0; k < this.pWidth; k++) {
                let index = i * pWH + k * this.pHeight + j;
                if(this.vpBits[index] & this.INOUT) {
                    let ii = 0;
                    while(ii < 26) {
                        let ti = i + this.nb[ii][0], tj = j + this.nb[ii][2], tk = k +
                                this.nb[ii][1];
                        if(ti > -1 &&
                            ti < this.pLength &&
                            tk > -1 &&
                            tk < this.pWidth &&
                            tj > -1 &&
                            tj < this.pHeight &&
                            !(this.vpBits[ti * pWH + tk * this.pHeight + tj] & this.INOUT)) {
                            this.vpBits[index] |= this.ISBOUND;
                            break;
                        } else
                            ii++;
                    }
                }
            }
        }
    }
};

ElectronMap.prototype.marchingcubeinit = function(stype) {
    for( let i = 0, lim = this.vpBits.length; i < lim; i++) {
        if(stype == 1) {// vdw
            this.vpBits[i] &= ~this.ISBOUND;
        } else if(stype == 4) { // ses
            this.vpBits[i] &= ~this.ISDONE;
            if(this.vpBits[i] & this.ISBOUND)
                this.vpBits[i] |= this.ISDONE;
            this.vpBits[i] &= ~this.ISBOUND;
        } else if(stype == 2) {// after vdw
            if((this.vpBits[i] & this.ISBOUND) &&(this.vpBits[i] & this.ISDONE))
                this.vpBits[i] &= ~this.ISBOUND;
            else if((this.vpBits[i] & this.ISBOUND) && !(this.vpBits[i] & this.ISDONE))
                this.vpBits[i] |= this.ISDONE;
        } else if(stype == 3) { // sas
            this.vpBits[i] &= ~this.ISBOUND;
        }
        else {
            this.vpBits[i] &= ~this.ISBOUND;
        }
    }
};

// this code allows me to empirically prune the marching cubes code tables
// to more efficiently handle discrete data
ElectronMap.prototype.counter = function() {
    let data = Array(256);
    for( let i = 0; i < 256; i++)
        data[i] = [];

    this.incrementUsed = function(i, j) {
        if(typeof data[i][j] === 'undefined')
            data[i][j] = {
                used : 0,
                unused : 0
            };
        data[i][j].used++;
    };

    this.incrementUnused = function(i, j) {
        if(typeof data[i][j] === 'undefined')
            data[i][j] = {
                used : 0,
                unused : 0
            };
        data[i][j].unused++;

    };

    let redoTable = function(triTable) {
        let str = "[";
        for( let i = 0; i < triTable.length; i++) {
            let code = 0;
            let table = triTable[i];
            for( let j = 0; j < table.length; j++) {
                code |=(1 <<(table[j]));
            }
            str += "0x" + code.toString(16) + ", ";
        }
        str += "]";
    };

    this.print = function() {

        let table = this.marchingCube.triTable;
        let newtable = [];
        for( let i = 0; i < table.length; i++) {
            let newarr = [];
            for( let j = 0; j < table[i].length; j += 3) {
                let k = j / 3;
                if(typeof data[i][k] === 'undefined' || !data[i][k].unused) {
                    newarr.push(table[i][j]);
                    newarr.push(table[i][j + 1]);
                    newarr.push(table[i][j + 2]);
                }
                if(typeof data[i][k] === 'undefined')
                    console.log("undef " + i + "," + k);
            }
            newtable.push(newarr);
        }
        redoTable(newtable);
    };
};

ElectronMap.prototype.marchingcube = function(stype) {
    this.marchingcubeinit(stype);
    this.verts = []; this.faces = [];

    this.marchingCube.march(this.vpBits, this.verts, this.faces, {
        smooth : 1,
        nX : this.pLength,
        nY : this.pWidth,
        nZ : this.pHeight
    });

    let pWH = this.pWidth*this.pHeight;
    for(let i = 0, vlen = this.verts.length; i < vlen; i++) {
        // positive values
        this.verts[i]['atomid'] = this.vpAtomID[this.verts[i].x * pWH + this.pHeight *
                this.verts[i].y + this.verts[i].z];
    }

    this.marchingCube.laplacianSmooth(1, this.verts, this.faces);

};

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Surface {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Create surface for "atoms". "type" can be 1 (Van der Waals surface), 2 (molecular surface),
    //and 3 (solvent accessible surface). "wireframe" is a boolean to determine whether to show
    //the surface as a mesh. "opacity" is a value between 0 and 1. "1" means not transparent at all.
    //"0" means 100% transparent.
    createSurfaceRepresentation(atoms, type, wireframe, opacity) { let ic = this.icn3d, me = ic.icn3dui;
        //if(me.bNode) return;

        let thisClass = this;

        if(Object.keys(atoms).length == 0) return;

        if(opacity == undefined) opacity = 1.0;

        ic.opacity = opacity;

        let geo;

        let extent = ic.contactCls.getExtent(atoms);

        // surface from 3Dmol
        let distance = 5; // consider atom 5 angstrom from the selected atoms

        let extendedAtoms = [];

        if(ic.bConsiderNeighbors) {
            let unionAtoms;
            unionAtoms = me.hashUtilsCls.unionHash(unionAtoms, atoms);
            unionAtoms = me.hashUtilsCls.unionHash(unionAtoms, ic.contactCls.getAtomsWithinAtom(ic.atoms, atoms, distance));

            extendedAtoms = Object.keys(unionAtoms);
        }
        else {
            extendedAtoms = Object.keys(atoms);
        }

        //var sigma2fofc = 1.5;
        //var sigmafofc = 3.0;
        let maxdist = 1; // maximum distance to show electron density map, set it between 1 AND 2

        (parseInt(10*opacity) != 10 && !wireframe && !(ic.bInstanced && Object.keys(ic.atoms).length * ic.biomtMatrices.length > ic.maxatomcnt) ) ? true : false;

        let ps;

        let cfg = {
                allatoms: ic.atoms,
                atomsToShow: Object.keys(atoms),
                extendedAtoms: extendedAtoms,
                water: ic.water,
                //header: ic.mapData.header2,
                //data: ic.mapData.data2,
                //matrix: ic.mapData.matrix2,
                //isovalue: ic.mapData.sigma2,
                center: ic.center,
                maxdist: maxdist,
                pmin: ic.pmin,
                pmax: ic.pmax,
                //type: '2fofc',
                rmsd_supr: ic.rmsd_supr
            };

        if(type == 11) { // 2fofc
            cfg.header = ic.mapData.header2;
            cfg.data = ic.mapData.data2;
            cfg.matrix = ic.mapData.matrix2;
            cfg.isovalue = ic.mapData.sigma2;
            cfg.type = '2fofc';

            ps = this.SetupMap(cfg);
        }
        else if(type == 12) { // fofc
            cfg.header = ic.mapData.header;
            cfg.data = ic.mapData.data;
            cfg.matrix = ic.mapData.matrix;
            cfg.isovalue = ic.mapData.sigma;
            cfg.type = 'fofc';

            ps = this.SetupMap(cfg);
        }
        else if(type == 13) { // em
            cfg.maxdist = 3; // EM map has no unit cell. It could include more gird space.

            cfg.header = ic.mapData.headerEm;
            cfg.data = ic.mapData.dataEm;
            cfg.matrix = ic.mapData.matrixEm;
            cfg.isovalue = ic.mapData.sigmaEm;
            cfg.type = 'em';

            ps = this.SetupMap(cfg);
        }
        else if(type == 14) { // phimap, equipotential
            cfg.header = ic.mapData.headerPhi;
            cfg.data = ic.mapData.dataPhi;
            cfg.matrix = ic.mapData.matrixPhi;
            cfg.isovalue = ic.mapData.contourPhi;
            cfg.type = 'phi';
            cfg.loadPhiFrom = ic.loadPhiFrom;

            ps = this.SetupMap(cfg);
        }
        else {
             //1: van der waals surface, 2: molecular surface, 3: solvent accessible surface

            //exclude water
            let atomsToShow = me.hashUtilsCls.exclHash(atoms, ic.water);
            //extendedAtoms = Object.keys(atomsToShow);
            extendedAtoms = me.hashUtilsCls.exclHash(extendedAtoms, ic.water);

            let realType = type;
            if(realType == 21) realType = 1;
            else if(realType == 22) realType = 2;
            else if(realType == 23) realType = 3;

            cfg = {
                extent: extent,
                allatoms: ic.atoms,
                atomsToShow: Object.keys(atomsToShow),
                extendedAtoms: extendedAtoms,
                type: realType,
                threshbox: (ic.transparentRenderOrder) ? 60 : ic.threshbox,
                bCalcArea: ic.bCalcArea
            };

            cfg.header = ic.mapData.headerPhi; // header.bSurface is true
            cfg.data = ic.mapData.dataPhi;
            cfg.matrix = ic.mapData.matrixPhi;
            cfg.isovalue = ic.mapData.contourPhi;
            //cfg.type = 'phi';
            cfg.loadPhiFrom = ic.loadPhiFrom;
            //cfg.icn3d = me;

            //cfg.rmsd_supr: ic.rmsd_supr

            ps = this.SetupSurface(cfg);
        }

        if(ic.bCalcArea) {
            ic.areavalue = ps.area.toFixed(2);
            let serial2area = ps.serial2area;
            let scaleFactorSq = ps.scaleFactor * ps.scaleFactor;

            ic.resid2area = {};
            let structureHash = {}, chainHash = {};
            for(let i in serial2area) {
                let atom = ic.atoms[i];
                let resid = atom.structure + '_' + atom.chain + '_' + atom.resi + '_' + atom.resn;
                structureHash[atom.structure] = 1;
                chainHash[atom.structure + '_' + atom.chain] = 1;

                if(ic.resid2area[resid] === undefined) ic.resid2area[resid] = serial2area[i];
                else ic.resid2area[resid] += serial2area[i];
            }

            let html = '<table border="1" cellpadding="10" cellspacing="0">';
            let structureStr = (Object.keys(structureHash).length > 1) ? '<th>Structure</th>' : '';
            let chainStr = (Object.keys(chainHash).length > 1) ? '<th>Chain</th>' : '';
            html += '<tr>' + structureStr + chainStr + '<th>Residue</th><th>Number</th><th>SASA (&#8491;<sup>2</sup>)</th><th>Percent Out</th><th>In/Out</th></tr>';
            for(let resid in ic.resid2area) {
                //var idArray = resid.split('_');
                let pos = resid.lastIndexOf('_');
                let resn = resid.substr(pos + 1);

                let idArray = me.utilsCls.getIdArray(resid.substr(0, pos));

                structureStr = (Object.keys(structureHash).length > 1) ? '<td>' + idArray[0] + '</td>' : '';
                chainStr = (Object.keys(chainHash).length > 1) ? '<td>' + idArray[1] + '</td>' : '';
                // outside: >= 50%; Inside: < 20%; middle: 35
                let inoutStr = '', percent = '';
                ic.resid2area[resid] = (ic.resid2area[resid] / scaleFactorSq).toFixed(2);
                if(me.parasCls.residueArea.hasOwnProperty(resn)) {
                    percent = parseInt(ic.resid2area[resid] / me.parasCls.residueArea[resn] * 100);
                    if(percent > 100) percent = 100;

                    if(percent >= 50) inoutStr = 'out';
                    if(percent < 20) inoutStr = 'in';
                }

                html += '<tr align="center">' + structureStr + chainStr + '<td>' + resn + '</td><td align="right">' + idArray[2] + '</td><td align="right">'
                    + ic.resid2area[resid] + '</td><td align="right">' + percent + '%</td><td>' + inoutStr + '</td></tr>';
            }

            html += '</table>';

            ic.areahtml = html;

            return;
        }

        let verts = ps.vertices;
        let faces = ps.faces;

        let colorFor2fofc = me.parasCls.thr('#00FFFF');
        let colorForfofcPos = me.parasCls.thr('#00FF00');
        //var colorForfofcNeg = me.parasCls.thr('#ff3300');
        let colorForfofcNeg = me.parasCls.thr('#ff0000');
        let colorForEm = me.parasCls.thr('#00FFFF');

        let colorForPhiPos = me.parasCls.thr('#0000FF');
        let colorForPhiNeg = me.parasCls.thr('#FF0000');

        let rot, centerFrom, centerTo;
        if((type == 11 || type == 12 || type == 13 || type == 14 ) && ic.rmsd_supr !== undefined && ic.rmsd_supr.rot !== undefined) {
          rot = ic.rmsd_supr.rot;
          centerFrom = ic.rmsd_supr.trans1;
          centerTo = ic.rmsd_supr.trans2;
        }

        // Direct "delphi" calculation uses the transformed PDB file, not the original PDB
        let bTrans = (type == 11 || type == 12 || type == 13 || (type == 14 && ic.loadPhiFrom != 'delphi') )
          && ic.rmsd_supr !== undefined && ic.rmsd_supr.rot !== undefined;

        //geo = new THREE.Geometry();
        geo = new THREE.BufferGeometry();
        let verticeArray = [], colorArray = [], indexArray = [], color;

        //var geoVertices = verts.map(function (v) {
        let offset = 0;
        for(let i = 0, il = verts.length; i < il; ++i, offset += 3) {
            let v = verts[i];

            let r = new THREE.Vector3(v.x, v.y, v.z);
            if(bTrans) {
               r = thisClass.transformMemPro(r, rot, centerFrom, centerTo);
            }

            //verticeArray = verticeArray.concat(r.toArray());
            verticeArray[offset] = r.x;
            verticeArray[offset + 1] = r.y;
            verticeArray[offset + 2] = r.z;

            if(type == 11) { // 2fofc
                color = colorFor2fofc;
            }
            else if(type == 12) { // fofc
                color = (v.atomid) ? colorForfofcPos : colorForfofcNeg;
            }
            else if(type == 13) { // em
                color = colorForEm;
            }
            else if(type == 14) { // phi
                color = (v.atomid) ? colorForPhiPos : colorForPhiNeg;
            }
            else if(type == 21 || type == 22 || type == 23) { // potential on surface
                color = v.color;

                let atomid = v.atomid;
                ic.atoms[atomid].pot = v.pot; // unit kt/e (25.6 mV)
            }
            else {
                let atomid = v.atomid;
                color = ic.atoms[atomid].color;
            }

            //colorArray = colorArray.concat(color.toArray());
            colorArray[offset] = color.r;
            colorArray[offset + 1] = color.g;
            colorArray[offset + 2] = color.b;

            //r.atomid = v.atomid;
            //r.color = v.color;
            //return r;
        }
        //});

        if(me.bNode) return;

        offset = 0;
        for(let i = 0, il = faces.length; i < il; ++i, offset += 3) {
            let f = faces[i];

            //indexArray = indexArray.concat(f.a, f.b, f.c);
            indexArray[offset] = f.a;
            indexArray[offset + 1] = f.b;
            indexArray[offset + 2] = f.c;
        }

        let nComp = 3;
        geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(verticeArray), nComp));
        geo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), nComp));

        geo.setIndex(new THREE.BufferAttribute(new Uint32Array(indexArray), 1));
        //geo.setIndex(indexArray);

        //http://analyticphysics.com/Coding%20Methods/Special%20Topics%20in%20Three.js.htm
        //geo.computeVertexNormals(true);
        //geo.colorsNeedUpdate = true;
        //geo.normalsNeedUpdate = true;

        geo.computeVertexNormals();

        geo.type = 'Surface'; // to be recognized in vrml.js for 3D printing

        // use the regular way to show transparency for type == 15 (surface with potential)
    //    if(ic.transparentRenderOrder && (type == 1 || type == 2 || type == 3)) { // WebGL has some ordering problem when dealing with transparency
        if(ic.transparentRenderOrder) { // WebGL has some ordering problem when dealing with transparency
          //var normalArrayIn = JSON.parse(JSON.stringify(geo)).data.normals;
          //var normalArrayIn = geo.getAttribute('normal').array;

          // the following method minimize the number of objects by a factor of 3
          let va2faces = {};

          for(let i = 0, il = faces.length; i < il; ++i) {
            let va = faces[i].a;
            let vb = faces[i].b;
            let vc = faces[i].c;

            // It produces less objects using va as the key
            if(va2faces[va] === undefined) va2faces[va] = [];
            //va2faces[va].push(va);
            va2faces[va].push(vb);
            va2faces[va].push(vc);
          }

          for(let va in va2faces) {
            //this.geometry = new THREE.Geometry();
            this.geometry = new THREE.BufferGeometry();
            //this.geometry.vertices = [];
            //this.geometry.faces = [];
            let verticeArray = [], colorArray = [], indexArray = [];
            let offset = 0, offset2 = 0, offset3 = 0;

            let faceVertices = va2faces[va];
            let sum = new THREE.Vector3(0,0,0);
            let nComp = 3;

            let verticesLen = 0;
            for(let i = 0, il = faceVertices.length; i < il; i += 2) {
                let vb = faceVertices[i];
                let vc = faceVertices[i + 1];

                verticeArray[offset++] = verts[va].x;
                verticeArray[offset++] = verts[va].y;
                verticeArray[offset++] = verts[va].z;

                verticeArray[offset++] = verts[vb].x;
                verticeArray[offset++] = verts[vb].y;
                verticeArray[offset++] = verts[vb].z;

                verticeArray[offset++] = verts[vc].x;
                verticeArray[offset++] = verts[vc].y;
                verticeArray[offset++] = verts[vc].z;

                if(type == 21 || type == 22 || type == 23) { // potential on surface
                    colorArray[offset2++] = verts[va].color.r;
                    colorArray[offset2++] = verts[va].color.g;
                    colorArray[offset2++] = verts[va].color.b;

                    colorArray[offset2++] = verts[vb].color.r;
                    colorArray[offset2++] = verts[vb].color.g;
                    colorArray[offset2++] = verts[vb].color.b;

                    colorArray[offset2++] = verts[vc].color.r;
                    colorArray[offset2++] = verts[vc].color.g;
                    colorArray[offset2++] = verts[vc].color.b;
                }
                else {
                    colorArray[offset2++] = ic.atoms[verts[va].atomid].color.r;
                    colorArray[offset2++] = ic.atoms[verts[va].atomid].color.g;
                    colorArray[offset2++] = ic.atoms[verts[va].atomid].color.b;
    
                    colorArray[offset2++] = ic.atoms[verts[vb].atomid].color.r;
                    colorArray[offset2++] = ic.atoms[verts[vb].atomid].color.g;
                    colorArray[offset2++] = ic.atoms[verts[vb].atomid].color.b;
    
                    colorArray[offset2++] = ic.atoms[verts[vc].atomid].color.r;
                    colorArray[offset2++] = ic.atoms[verts[vc].atomid].color.g;
                    colorArray[offset2++] = ic.atoms[verts[vc].atomid].color.b;
                }

                let initPos = i / 2 * 3;
                //this.geometry.faces.push(new THREE.Face3(initPos, initPos + 1, initPos + 2, normals, vertexColors));

                indexArray[offset3++] = initPos;
                indexArray[offset3++] = initPos + 1;
                indexArray[offset3++] = initPos + 2;

                sum = sum.add(new THREE.Vector3(verts[initPos].x, verts[initPos].y, verts[initPos].z));
                sum = sum.add(new THREE.Vector3(verts[initPos + 1].x, verts[initPos + 1].y, verts[initPos + 1].z));
                sum = sum.add(new THREE.Vector3(verts[initPos + 2].x, verts[initPos + 2].y, verts[initPos + 2].z));

                verticesLen += 3;
            }

            this.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(verticeArray), nComp));
            this.geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), nComp));
//            this.geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normalArray), nComp));

            this.geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indexArray), 1));
            //geo.setIndex(indexArray);

            //this.geometry.colorsNeedUpdate = true;
            this.geometry.computeVertexNormals();

            this.geometry.type = 'Surface'; // to be recognized in vrml.js for 3D printing

            let mesh = new THREE.Mesh(this.geometry, new THREE.MeshBasicMaterial({ //new THREE.MeshPhongMaterial({
                specular: ic.frac,
                shininess: 0, //10, //30,
                emissive: ic.emissive,
                vertexColors: THREE.VertexColors,
                wireframe: wireframe,
                opacity: opacity,
                transparent: true,
                side: THREE.DoubleSide
            }));

            //http://www.html5gamedevs.com/topic/7288-threejs-transparency-bug-or-limitation-or-what/
            //mesh.renderOrder = 0; // default 0
            //var sum = new THREE.Vector3(0,0,0);
            //for(let i = 0, il = mesh.geometry.vertices.length; i < il; ++i) {
            //    sum = sum.add(mesh.geometry.vertices[i]);
            //}

            let realPos;
            if(ic.bControlGl && !me.bNode) {
                //realPos = sum.multiplyScalar(1.0 / mesh.geometry.vertices.length).sub(ic.oriCenter).applyMatrix4(window.cam.matrixWorldInverse);
                realPos = sum.multiplyScalar(1.0 / verticesLen).sub(ic.oriCenter).applyMatrix4(window.cam.matrixWorldInverse);
            }
            else {
                //realPos = sum.multiplyScalar(1.0 / mesh.geometry.vertices.length).sub(ic.oriCenter).applyMatrix4(ic.cam.matrixWorldInverse);
                realPos = sum.multiplyScalar(1.0 / verticesLen).sub(ic.oriCenter).applyMatrix4(ic.cam.matrixWorldInverse);
            }
            mesh.renderOrder = (ic.cam_z > 0) ? -parseInt(realPos.z) : parseInt(realPos.z);

            mesh.onBeforeRender = function(renderer, scene, camera, geometry, material, group) {
                //https://juejin.im/post/5a0872d4f265da43062a4156
                let sum = new THREE.Vector3(0,0,0);
                let vertices = geometry.getAttribute('position').array;
                for(let i = 0, il = vertices.length; i < il; i += 3) {
                    sum = sum.add(new THREE.Vector3(vertices[i], vertices[i+1], vertices[i+2]));
                }

                let realPos;
                if(ic.bControlGl && !me.bNode) {
                    //realPos = sum.multiplyScalar(1.0 / this.geometry.vertices.length).sub(ic.oriCenter).applyMatrix4(window.cam.matrixWorldInverse);
                    realPos = sum.multiplyScalar(3.0 / vertices.length).sub(ic.oriCenter).applyMatrix4(window.cam.matrixWorldInverse);
                }
                else {
                    //realPos = sum.multiplyScalar(1.0 / this.geometry.vertices.length).sub(ic.oriCenter).applyMatrix4(ic.cam.matrixWorldInverse);
                    //realPos = thisClass.sum.multiplyScalar(1.0 / thisClass.verticesLen).sub(ic.oriCenter).applyMatrix4(ic.cam.matrixWorldInverse);
                    realPos = sum.multiplyScalar(3.0 / vertices.length).sub(ic.oriCenter).applyMatrix4(ic.cam.matrixWorldInverse);
                }
                this.renderOrder = (ic.cam_z > 0) ? -parseInt(realPos.z) : parseInt(realPos.z);
            };

            ic.mdl.add(mesh);

            if(type == 11 || type == 12) {
                ic.prevMaps.push(mesh);
            }
            else if(type == 13) {
                ic.prevEmmaps.push(mesh);
            }
            else if(type == 14) {
                ic.prevPhimaps.push(mesh);
            }
            else {
                ic.prevSurfaces.push(mesh);
            }
          } // for(let va
        }
        else {         
            let mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({
                specular: ic.frac,
                shininess: 20, //10, //30,
                emissive: ic.emissive,
                vertexColors: THREE.VertexColors,
                wireframe: wireframe,
                opacity: opacity,
                transparent: true,
                depthWrite: (parseInt(10*opacity) != 10) ? false : true, // important to make the transparency work
                side: THREE.DoubleSide
                //depthTest: (ic.ic.transparentRenderOrder) ? false : true
            }));

            //http://www.html5gamedevs.com/topic/7288-threejs-transparency-bug-or-limitation-or-what/
            mesh.renderOrder = -2; // default: 0, picking: -1

            ic.mdl.add(mesh);

            if(type == 11 || type == 12) {
                ic.prevMaps.push(mesh);
            }
            else if(type == 13) {
                ic.prevEmmaps.push(mesh);
            }
            else if(type == 14) {
                ic.prevPhimaps.push(mesh);
            }
            else {
                ic.prevSurfaces.push(mesh);
            }
        }
        
        // remove the reference
        ps = null;
        verts = null;
        faces = null;

        // remove the reference
        geo = null;

        // do not add surface to raycasting objects for pk
    }

    transformMemPro(inCoord, rot, centerFrom, centerTo, bOut) { let ic = this.icn3d; ic.icn3dui;
        let coord = inCoord.clone();

        coord.sub(centerFrom);
    if(bOut) console.log("sub coord: " + JSON.stringify(coord));

        let x = coord.x*rot[0] + coord.y*rot[1] + coord.z*rot[2] + centerTo.x;
        let y = coord.x*rot[3] + coord.y*rot[4] + coord.z*rot[5] + centerTo.y;
        let z = coord.x*rot[6] + coord.y*rot[7] + coord.z*rot[8] + centerTo.z;

        coord.x = x;
        coord.y = y;
        coord.z = z;
    if(bOut) console.log("out coord: " + JSON.stringify(coord));

        return coord;
    }

    SetupSurface(data) { let ic = this.icn3d; ic.icn3dui;

        let threshbox = data.threshbox; // maximum possible boxsize, default 180

        let ps = new ProteinSurface(ic, threshbox);
        ps.initparm(data.extent,(data.type === 1) ? false : true, data.bCalcArea, data.atomsToShow
          , data.header, data.data, data.matrix, data.isovalue, data.loadPhiFrom);

        ps.fillvoxels(data.allatoms, data.extendedAtoms);

        ps.buildboundary();

        //if(data.type === 4 || data.type === 2) {
        if(data.type === 2) {
            ps.fastdistancemap();
            ps.boundingatom(false);
            ps.fillvoxelswaals(data.allatoms, data.extendedAtoms);
        }

        //ps.marchingcube(data.type);
        let area_serial2area = ps.marchingcube();

        ps.vpBits = null; // uint8 array of bitmasks
        ps.vpDistance = null; // floatarray of _squared_ distances
        ps.vpAtomID = null; // intarray

        let result = ps.getFacesAndVertices(data.atomsToShow);
        result.area = area_serial2area.area;
        result.serial2area = area_serial2area.serial2area;
        result.scaleFactor = area_serial2area.scaleFactor;

        ps.faces = null;
        ps.verts = null;

        return result;
    }

    SetupMap(data) { let ic = this.icn3d; ic.icn3dui;
        let ps = new ElectronMap(ic);

        ps.initparm(data.header, data.data, data.matrix, data.isovalue, data.center, data.maxdist,
          data.pmin, data.pmax, data.water, data.type, data.rmsd_supr, data.loadPhiFrom, data.icn3d);

        ps.fillvoxels(data.allatoms, data.extendedAtoms);

        if(!data.header.bSurface) ps.buildboundary();

        if(!data.header.bSurface) ps.marchingcube();

        ps.vpBits = null; // uint8 array of bitmasks
        //ps.vpDistance = null; // floatarray of _squared_ distances
        ps.vpAtomID = null; // intarray

        let result;

        if(!data.header.bSurface) result = ps.getFacesAndVertices(data.allatoms, data.atomsToShow);

        ps.faces = null;
        ps.verts = null;

        return result;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplyCenter {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    applyCenterOptions(options) { let ic = this.icn3d; ic.icn3dui;
        if(options === undefined) options = ic.opts;

        let center;
        switch (options.rotationcenter.toLowerCase()) {
            case 'molecule center':
                // move the molecule to the origin
                if(ic.center !== undefined) {
                    this.setRotationCenter(ic.center);
                }
                break;
            case 'pick center':
                if(ic.pAtom !== undefined) {
                  this.setRotationCenter(ic.pAtom.coord);
                }
                break;
            case 'display center':
                center = this.centerAtoms(ic.dAtoms).center;
                this.setRotationCenter(center);
                break;
            case 'highlight center':
                center = this.centerAtoms(ic.hAtoms).center;
                this.setRotationCenter(center);
                break;
        }
    }

    //Set the center at the position with coordinated "coord".
    setRotationCenter(coord) { let ic = this.icn3d; ic.icn3dui;
       this.setCenter(coord);
    }

    setCenter(center) { let ic = this.icn3d; ic.icn3dui;
       //if(!ic.bChainAlign) {
           ic.mdl.position.set(0,0,0);
           ic.mdlImpostor.position.set(0,0,0);
           ic.mdl_ghost.position.set(0,0,0);

           ic.mdl.position.sub(center);
           //ic.mdlPicking.position.sub(center);
           ic.mdlImpostor.position.sub(center);
           ic.mdl_ghost.position.sub(center);
       //}
    }

    //Center on the selected atoms.
    centerSelection(atoms, bNoOrientation) { let ic = this.icn3d, me = ic.icn3dui;
       //ic.transformCls.resetOrientation();

       ic.opts['rotationcenter'] = 'highlight center';

       if(atoms === undefined) {
           atoms = me.hashUtilsCls.hash2Atoms(ic.hAtoms, ic.atoms);
       }

       if(!bNoOrientation) {
            // reset parameters
            ic._zoomFactor = 1.0;
            ic.mouseChange = new THREE.Vector2(0,0);
            ic.quaternion = new THREE.Quaternion(0,0,0,1);
       }

       // center on the hAtoms if more than one residue is selected
       if(Object.keys(atoms).length > 1) {
               let centerAtomsResults = this.centerAtoms(atoms);

               ic.center = centerAtomsResults.center;
               this.setCenter(ic.center);

               // reset cameara
               ic.cameraCls.setCamera();
       }
    }

    //Return an object {"center": center, "maxD": maxD}, where "center" is the center of
    //a set of "atoms" with a value of THREE.Vector3(), and "maxD" is the maximum distance
    //between any two atoms in the set.
    centerAtoms(atoms) { let ic = this.icn3d; ic.icn3dui;
        let pmin = new THREE.Vector3( 9999, 9999, 9999);
        let pmax = new THREE.Vector3(-9999,-9999,-9999);
        let psum = new THREE.Vector3();

        for (let i in atoms) {
            let atom = ic.atoms[i];
            let coord = atom.coord;
            psum.add(coord);
            pmin.min(coord);
            pmax.max(coord);
        }

        //let maxD = pmax.distanceTo(pmin);

        //let center = psum.multiplyScalar(1.0 / cnt);
        let center = ic.ParserUtilsCls.getGeoCenter(pmin, pmax);
        let maxD = ic.ParserUtilsCls.getStructureSize(atoms, pmin, pmax, center);

        return {"center": center, "maxD": maxD, "pmin": pmin, "pmax": pmax};
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //Set the width and height of the canvas.
    setWidthHeight(width, height) { let ic = this.icn3d; ic.icn3dui;
        //ic.renderer.setSize(width, height);
        if(ic.scaleFactor === undefined) ic.scaleFactor = 1.0;

        //antialiasing by render twice large:
        //https://stackoverflow.com/questions/17224795/antialiasing-not-working-in-three-js
        ic.renderer.setSize(width*ic.scaleFactor, height*ic.scaleFactor);
        ic.renderer.domElement.style.width = width*ic.scaleFactor + "px";
        ic.renderer.domElement.style.height = height*ic.scaleFactor + "px";
        ic.renderer.domElement.width = width*ic.scaleFactor;
        ic.renderer.domElement.height = height*ic.scaleFactor;

        //ic.container.widthInv  = 1 / (ic.scaleFactor*width);
        //ic.container.heightInv = 1 / (ic.scaleFactor*height);

        ic.container.whratio = width / height;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplyClbonds {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    applyClbondsOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
       if(options === undefined) options = ic.opts;

       if(!ic.bCalcCrossLink) {
         // find all bonds to chemicals
         ic.clbondpnts = {};
         ic.clbondResid2serial = {};

         // chemical to chemical first
         this.applyClbondsOptions_base('chemical');

         // chemical to protein/nucleotide
         this.applyClbondsOptions_base('all');

         ic.bCalcCrossLink = true;
       }

       if (options.clbonds.toLowerCase() === 'yes' && options.chemicals !== 'nothing') {
         let color = '#006400';
         let colorObj = me.parasCls.thr(0x006400);

         ic.lines['clbond'] = [];
         ic.residuesHashClbonds = {};

         if(ic.structures) {
             let strucArray = Object.keys(ic.structures);
             for(let i = 0, il = strucArray.length; i < il; ++i) {
                 let struc = strucArray[i];
                 if(!ic.clbondpnts[struc]) continue;

                 for(let j = 0, jl = ic.clbondpnts[struc].length; j < jl; j += 2) {
                    let resid0 = ic.clbondpnts[struc][j];
                    let resid1 = ic.clbondpnts[struc][j+1];

                    let line = {};
                    line.color = color;
                    line.dashed = false;

                    line.serial1 = ic.clbondResid2serial[resid0 + ',' + resid1];
                    line.serial2 = ic.clbondResid2serial[resid1 + ',' + resid0];

                    if(!ic.dAtoms.hasOwnProperty(line.serial1) || !ic.dAtoms.hasOwnProperty(line.serial2)) continue;

                    line.position1 = ic.atoms[line.serial1].coord;
                    line.position2 = ic.atoms[line.serial2].coord;

                    ic.lines['clbond'].push(line);
                    ic.cylinderCls.createCylinder(line.position1, line.position2, ic.crosslinkRadius, colorObj);

                    // show stick for these two residues
                    let residueAtoms = {};
                    residueAtoms = me.hashUtilsCls.unionHash(residueAtoms, ic.residues[resid0]);
                    residueAtoms = me.hashUtilsCls.unionHash(residueAtoms, ic.residues[resid1]);

                    // show side chains for the selected atoms
                    let atoms = me.hashUtilsCls.intHash(residueAtoms, ic.sidec);

                    // draw sidec separatedly
                    for(let k in atoms) {
                      ic.atoms[k].style2 = 'stick';
                    }

                    // return the residues
                    ic.residuesHashClbonds[resid0] = 1;
                    ic.residuesHashClbonds[resid1] = 1;
                } // for j
            } // for i
        } // if
      } // if

      return ic.residuesHashClbonds;
    }

    applyClbondsOptions_base(type) { let ic = this.icn3d; ic.icn3dui;
         // chemical to chemical first
         for (let i in ic.chemicals) {
            let atom0 = ic.atoms[i];

            let chain0 = atom0.structure + '_' + atom0.chain;
            let resid0 = chain0 + '_' + atom0.resi;

            for (let j in atom0.bonds) {
                let atom1 = ic.atoms[atom0.bonds[j]];

                if (atom1 === undefined) continue;
                if (atom1.chain !== atom0.chain || atom1.resi !== atom0.resi) {
                    let chain1 = atom1.structure + '_' + atom1.chain;
                    let resid1 = chain1 + '_' + atom1.resi;

                    let bType = (type == 'chemical') ? atom1.het : true; //(ic.proteins.hasOwnProperty(atom1.serial) || ic.nucleotides.hasOwnProperty(atom1.serial));

                    if(bType ) {
                        if(type == 'chemical') continue; // just connect checmicals together

                        if(ic.clbondpnts[atom0.structure] === undefined) ic.clbondpnts[atom0.structure] = [];
                        ic.clbondpnts[atom0.structure].push(resid0);
                        ic.clbondpnts[atom1.structure].push(resid1);

                        // one residue may have different atom for different clbond
                        ic.clbondResid2serial[resid0 + ',' + resid1] = atom0.serial;
                        ic.clbondResid2serial[resid1 + ',' + resid0] = atom1.serial;
                    }
                }
            } // for j
        } // for i
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplyDisplay {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Apply style and label options to a certain set of atoms.
    applyDisplayOptions(options, atoms, bHighlight) { let ic = this.icn3d, me = ic.icn3dui;

        // get parameters from cookies
        if(!me.bNode && me.htmlCls.setHtmlCls.getCookie('lineRadius') != '') {
            let lineRadius = parseFloat(me.htmlCls.setHtmlCls.getCookie('lineRadius'));
            let coilWidth = parseFloat(me.htmlCls.setHtmlCls.getCookie('coilWidth'));
            let cylinderRadius = parseFloat(me.htmlCls.setHtmlCls.getCookie('cylinderRadius'));
            let clRad = me.htmlCls.setHtmlCls.getCookie('crosslinkRadius');
            let crosslinkRadius = (clRad && !isNaN(clRad)) ? parseFloat(clRad) : ic.crosslinkRadius;
            let traceRadius = parseFloat(me.htmlCls.setHtmlCls.getCookie('traceRadius'));
            let dotSphereScale = parseFloat(me.htmlCls.setHtmlCls.getCookie('dotSphereScale'));
            let ribbonthickness = parseFloat(me.htmlCls.setHtmlCls.getCookie('ribbonthickness'));
            let helixSheetWidth = parseFloat(me.htmlCls.setHtmlCls.getCookie('helixSheetWidth'));
            let nucleicAcidWidth = parseFloat(me.htmlCls.setHtmlCls.getCookie('nucleicAcidWidth'));

            if(!ic.bSetThicknessOnce && (ic.lineRadius != lineRadius || ic.coilWidth != coilWidth || ic.cylinderRadius != cylinderRadius || ic.crosslinkRadius != crosslinkRadius || ic.traceRadius != traceRadius || ic.dotSphereScale != dotSphereScale || ic.ribbonthickness != ribbonthickness || ic.helixSheetWidth != helixSheetWidth || ic.nucleicAcidWidth != nucleicAcidWidth) ) {
                ic.bSetThicknessOnce = true;

                me.htmlCls.clickMenuCls.setLogCmd('set thickness | linerad ' + lineRadius + ' | coilrad ' + coilWidth + ' | stickrad ' + cylinderRadius + ' | crosslinkrad ' + crosslinkRadius + ' | tracerad ' + traceRadius + ' | ribbonthick ' + ribbonthickness + ' | proteinwidth ' + helixSheetWidth + ' | nucleotidewidth ' + nucleicAcidWidth  + ' | ballscale ' + dotSphereScale, true);
            }

            ic.lineRadius = lineRadius;
            ic.coilWidth = coilWidth;
            ic.cylinderRadius = cylinderRadius;
            ic.crosslinkRadius = crosslinkRadius;
            ic.traceRadius = traceRadius;
            ic.dotSphereScale = dotSphereScale;
            ic.ribbonthickness = ribbonthickness;
            ic.helixSheetWidth = helixSheetWidth;
            ic.nucleicAcidWidth = nucleicAcidWidth;
        }

        let residueHash = {};
        let singletonResidueHash = {};
        let atomsObj = {};
        let residueid;

        if(bHighlight === 1 && Object.keys(atoms).length < Object.keys(ic.atoms).length) {
            atomsObj = me.hashUtilsCls.hash2Atoms(atoms, ic.atoms);

            residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(atoms, ic.atoms);

            // find singleton residues
            for(let i in residueHash) {
                residueid = i;

                let last = i.lastIndexOf('_');
                let base = i.substr(0, last + 1);
                let lastResiStr = i.substr(last + 1);
                if(isNaN(lastResiStr)) continue;

                let lastResi = parseInt(lastResiStr);

                let prevResidueid = base + (lastResi - 1).toString();
                base + (lastResi + 1).toString();

                if(!residueHash.hasOwnProperty(prevResidueid) && !residueHash.hasOwnProperty(prevResidueid)) {
                    singletonResidueHash[i] = 1;
                }
            }

            // show the only atom in a transparent box
            if(Object.keys(atomsObj).length === 1 && Object.keys(ic.residues[residueid]).length > 1
                  && atomsObj[Object.keys(atomsObj)[0]].style !== 'sphere' && atomsObj[Object.keys(atomsObj)[0]].style !== 'dot') {
                if(ic.bCid === undefined || !ic.bCid) {
                    for(let i in atomsObj) {
                        let atom = atomsObj[i];
                        let scale = 1.0;
                        ic.boxCls.createBox(atom, undefined, undefined, scale, undefined, bHighlight);
                    }
                }
            }
            else {
                // if only one residue, add the next residue in order to show highlight
                for(let residueid in singletonResidueHash) {
                    // get calpha
                    let calpha = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
                    let atom = calpha;

                    let prevResidueid = atom.structure + '_' + atom.chain + '_' + (parseInt(atom.resi) - 1).toString();
                    let nextResidueid = atom.structure + '_' + atom.chain + '_' + (parseInt(atom.resi) + 1).toString();

                    //ribbon, strand, cylinder and plate, nucleotide cartoon, o3 trace, schematic, c alpha trace, b factor tube, lines, stick, ball and stick, sphere, dot

                    if(atom.style === 'cylinder and plate' && atom.ss === 'helix') { // no way to highlight part of cylinder
                        for(let i in ic.residues[residueid]) {
                            let atom = ic.atoms[i];
                            let scale = 1.0;
                            ic.boxCls.createBox(atom, undefined, undefined, scale, undefined, bHighlight);
                        }
                    }
                    else if( (atom.style === 'ribbon' && atom.ss === 'coil') || (atom.style === 'strand' && atom.ss === 'coil') || atom.style === 'o3 trace' || atom.style === 'schematic' || atom.style === 'c alpha trace' || atom.style === 'b factor tube' || (atom.style === 'cylinder and plate' && atom.ss !== 'helix') ) {
                        // do not add extra residue if the side chain is shown
                        if(calpha !== undefined && calpha.style2 !== undefined && calpha.style2 !== 'nothing') continue;

                        let bAddResidue = false;
                        // add the next residue with same style
                        if(!isNaN(atom.resi) && !bAddResidue && ic.residues.hasOwnProperty(nextResidueid)) {
                            let index2 = Object.keys(ic.residues[nextResidueid])[0];
                            let atom2 = me.hashUtilsCls.hash2Atoms(ic.residues[nextResidueid], ic.atoms)[index2];
                            if( (atom.style === atom2.style && !atom2.ssbegin) || atom2.ssbegin) {
                                let residueAtoms = ic.residues[nextResidueid];
                                atoms = me.hashUtilsCls.unionHash(atoms, residueAtoms);

                                bAddResidue = true;

                                // record the highlight style for the artificial residue
                                if(atom2.ssbegin) {
                                    for(let i in residueAtoms) {
                                        ic.atoms[i].notshow = true;
                                    }
                                }
                            }
                        }

                        // add the previous residue with same style
                        if(!isNaN(atom.resi) && !bAddResidue && ic.residues.hasOwnProperty(prevResidueid)) {
                            let index2 = Object.keys(ic.residues[prevResidueid])[0];
                            let atom2 = me.hashUtilsCls.hash2Atoms(ic.residues[prevResidueid], ic.atoms)[index2];
                            if(atom.style === atom2.style) {
                                atoms = me.hashUtilsCls.unionHash(atoms, ic.residues[prevResidueid]);

                                bAddResidue = true;
                            }
                        }
                    }
                    else if( (atom.style === 'ribbon' && atom.ss !== 'coil' && atom.ssend) || (atom.style === 'strand' && atom.ss !== 'coil' && atom.ssend)) {
                        // do not add extra residue if the side chain is shown
                        if(calpha !== undefined && calpha.style2 !== undefined && calpha.style2 !== 'nothing') continue;

                        let bAddResidue = false;
                        // add the next residue with same style
                        if(!isNaN(atom.resi) && !bAddResidue && ic.residues.hasOwnProperty(nextResidueid)) {
                            let index2 = Object.keys(ic.residues[nextResidueid])[0];
                            me.hashUtilsCls.hash2Atoms(ic.residues[nextResidueid], ic.atoms)[index2];
                            //if(atom.style === atom2.style && !atom2.ssbegin) {
                                atoms = me.hashUtilsCls.unionHash(atoms, ic.residues[nextResidueid]);

                                bAddResidue = true;
                            //}
                        }
                    }
                } // end for
            } // end else {

            atomsObj = {};
        } // end if(bHighlight === 1)

        if(ic.bInitial) {
            if(me.htmlCls.setHtmlCls.getCookie('membrane') != '') {
                let bMembrane = parseInt(me.htmlCls.setHtmlCls.getCookie('membrane'));

                if(ic.bMembrane != bMembrane) {
                    me.htmlCls.clickMenuCls.setLogCmd('set membrane ' + bMembrane, true);
                }

                ic.bMembrane = (!isNaN(bMembrane)) ? parseInt(bMembrane) : 0;
            }

            // show membrane
            if(ic.bMembrane) {
                ic.selectionCls.toggleMembrane(true);
            }
            else {
                ic.selectionCls.toggleMembrane(false);
            }
        }

        ic.setStyleCls.setStyle2Atoms(atoms);

        //ic.bAllAtoms = false;
        //if(atoms && atoms !== undefined ) {
        //    ic.bAllAtoms = (Object.keys(atoms).length === Object.keys(ic.atoms).length);
        //}

        let chemicalSchematicRadius = ic.cylinderRadius * 0.5;

        // remove schematic labels
        //if(ic.labels !== undefined) ic.labels['schematic'] = undefined;
        if(ic.labels !== undefined) delete ic.labels['schematic'];
/*
        if(bHighlight) {
            //let residueHashCalpha = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(ic.hAtoms);

            let proteinAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.proteins);

            let residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(proteinAtoms);
            let residueHashCalpha = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(proteinAtoms);

            if(Object.keys(residueHash).length > Object.keys(residueHashCalpha).length) { // some residues have only side chains
                bOnlySideChains = true;
            }
        }
*/
        for(let style in ic.style2atoms) {
          // 14 styles: ribbon, strand, cylinder and plate, nucleotide cartoon, o3 trace, schematic, c alpha trace, b factor tube, lines, stick, ball and stick, sphere, dot, nothing
          let atomHash = ic.style2atoms[style];
          //var bPhosphorusOnly = me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(atomHash), "O3'", "O3*") || me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(atomHash), "P");
          //let bPhosphorusOnly = me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms));
          let nucleotidesAtoms = me.hashUtilsCls.intHash(atomHash, ic.nucleotides);
          let bPhosphorusOnly = me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(nucleotidesAtoms, ic.atoms));

          if(style === 'ribbon') {
          //if(style === 'ribbon' && (!bHighlight || (bHighlight && !bOnlySideChains))) {
              ic.strandCls.createStrand(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), 2, undefined, true, undefined, undefined, false, ic.ribbonthickness, bHighlight);
          }
          else if(style === 'strand') {
          //else if(style === 'strand' && (!bHighlight || (bHighlight && !bOnlySideChains))) {
              ic.strandCls.createStrand(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), null, null, null, null, null, false, undefined, bHighlight);
          }
          else if(style === 'cylinder and plate') {
          //else if(style === 'cylinder and plate' && (!bHighlight || (bHighlight && !bOnlySideChains))) {
            ic.cylinderCls.createCylinderHelix(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ic.cylinderHelixRadius, bHighlight);
          }
          else if(style === 'nucleotide cartoon') {
            if(bPhosphorusOnly) {
                ic.cylinderCls.createCylinderCurve(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ["P"], ic.traceRadius, false, bHighlight);
            }
            else {
                ic.cartoonNuclCls.drawCartoonNucleicAcid(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), null, ic.ribbonthickness, bHighlight);

                if(bHighlight !== 2) ic.cartoonNuclCls.drawNucleicAcidStick(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), bHighlight);
            }
          }
          else if(style === 'o3 trace') {
            if(bPhosphorusOnly) {
                ic.cylinderCls.createCylinderCurve(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ["P"], ic.traceRadius, false, bHighlight);
            }
            else {
                ic.cylinderCls.createCylinderCurve(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ["O3'", "O3*"], ic.traceRadius, false, bHighlight);
            }
          }
          else if(style === 'schematic') {
            // either proteins, nucleotides, or chemicals
            let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(atomHash);

            //if(firstAtom.het) { // chemicals
            if(ic.chemicals.hasOwnProperty(firstAtom.serial)) { // chemicals
                ic.residueLabelsCls.addNonCarbonAtomLabels(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms));

                let bSchematic = true;
                ic.stickCls.createStickRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), chemicalSchematicRadius, chemicalSchematicRadius, undefined, bHighlight, bSchematic);
            }
            else { // nucleotides or proteins
                ic.residueLabelsCls.addResidueLabels(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), true);

                if(bPhosphorusOnly) {
                    ic.cylinderCls.createCylinderCurve(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ["P"], ic.traceRadius, false, bHighlight);
                }
                else {
                    ic.cylinderCls.createCylinderCurve(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ["O3'", "O3*"], ic.traceRadius, false, bHighlight);
                }
                ic.cylinderCls.createCylinderCurve(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ['CA'], ic.traceRadius, false, bHighlight);
            }
          }
          else if(style === 'c alpha trace') {
            ic.cylinderCls.createCylinderCurve(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ['CA'], ic.traceRadius, false, bHighlight);
          }
          else if(style === 'b factor tube') {
            ic.tubeCls.createTube(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), 'CA', null, bHighlight, false, true);
          }
          else if(style === 'custom tube') {
            ic.tubeCls.createTube(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), 'CA', null, bHighlight, true, true);
          }
          else if(style === 'lines' || style === 'lines2') {
            if(bHighlight === 1) {
                ic.stickCls.createStickRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ic.hlLineRadius, ic.hlLineRadius, undefined, bHighlight);
            }
            else {
                ic.lineCls.createLineRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), bHighlight);
            }

            ic.lineCls.createConnCalphSidechain(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), style);
          }
          else if(style === 'stick' || style === 'stick2') {
            ic.stickCls.createStickRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ic.cylinderRadius, ic.cylinderRadius, undefined, bHighlight, undefined);
            ic.lineCls.createConnCalphSidechain(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), style);
          }
          else if(style === 'backbone') {
            atomHash = this.selectMainChainSubset(atomHash);
            ic.stickCls.createStickRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ic.cylinderRadius, ic.cylinderRadius, undefined, bHighlight, undefined);
          }
          else if(style === 'ball and stick' || style === 'ball and stick2') {
            ic.stickCls.createStickRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ic.cylinderRadius, ic.cylinderRadius * 0.5, ic.dotSphereScale, bHighlight, undefined);
            ic.lineCls.createConnCalphSidechain(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), style);
          }
          else if(style === 'sphere' || style === 'sphere2') {
            ic.sphereCls.createSphereRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ic.sphereRadius, undefined, undefined, bHighlight);
          }
          else if(style === 'dot') {
            ic.sphereCls.createSphereRepresentation(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms), ic.sphereRadius, false, ic.dotSphereScale, bHighlight);
          }
        } // end for loop

        if(ic.cnt > ic.maxmaxatomcnt) { // release memory
            ic.init_base();
        }

        // hide the previous labels
        if(ic.labels !== undefined && Object.keys(ic.labels).length > 0) {
            ic.labelCls.hideLabels();

            // change label color
            for(let labeltype in ic.labels) {
                if(labeltype != 'schematic') this.changeLabelColor(ic.labels[labeltype]);
            }

            // labels
            ic.labelCls.createLabelRepresentation(ic.labels);
        }
    }

    changeLabelColor(labelArray) { let ic = this.icn3d; ic.icn3dui;
        if(labelArray) {
            for(let i = 0, il = labelArray.length; i < il; ++i) {
                let label = labelArray[i];
                if((ic.opts.background != 'black') && label.color == ic.colorBlackbkgd) {
                    label.color = ic.colorWhitebkgd;
                }
                else if((ic.opts.background == 'black') && label.color == ic.colorWhitebkgd) {
                    label.color = ic.colorBlackbkgd;
                }
            }
        }
    }

    selectMainChainSubset(atoms) { let ic = this.icn3d; ic.icn3dui;
        let nuclMainArray = ["C1'", "C1*", "C2'", "C2*", "C3'", "C3*", "C4'", "C4*", "C5'", "C5*", "O3'", "O3*", "O4'", "O4*", "O5'", "O5*", "P", "OP1", "O1P", "OP2", "O2P"];

        let atomHash = {};
        for(let i in atoms) {
            if( (ic.proteins.hasOwnProperty(i) && (ic.atoms[i].name === "N" || ic.atoms[i].name === "C" || ic.atoms[i].name === "O"
              || (ic.atoms[i].name === "CA" && ic.atoms[i].elem === "C") ) )
              || (ic.nucleotides.hasOwnProperty(i) && nuclMainArray.indexOf(ic.atoms[i].name) !== -1) ) {
                atomHash[i] = 1;
            }
        }

        return atomHash;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplyOther {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Apply the rest options (e.g., hydrogen bonds, center, etc).
    applyOtherOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
            if(options === undefined) options = ic.opts;

    //    if(ic.lines !== undefined) {
            // contact lines
            ic.hBondCls.setHbondsContacts(options, 'contact');

            // halogen lines
            ic.hBondCls.setHbondsContacts(options, 'halogen');
            // pi-cation lines
            ic.hBondCls.setHbondsContacts(options, 'pi-cation');
            // pi-stacking lines
            ic.hBondCls.setHbondsContacts(options, 'pi-stacking');

            // hbond lines
            ic.hBondCls.setHbondsContacts(options, 'hbond');
            // salt bridge lines
            ic.hBondCls.setHbondsContacts(options, 'saltbridge');
            if (ic.pairArray !== undefined && ic.pairArray.length > 0) {
                this.updateStabilizer(); // to update ic.stabilizerpnts

                let color = '#FFFFFF';
                let pnts = ic.stabilizerpnts;
                ic.lines['stabilizer'] = []; // reset
                for (let i = 0, lim = Math.floor(pnts.length / 2); i < lim; i++) {
                    let line = {};
                    line.position1 = pnts[2 * i];
                    line.position2 = pnts[2 * i + 1];
                    line.color = color;
                    line.dashed = false; // if true, there will be too many cylinders in the dashed lines

                    ic.lines['stabilizer'].push(line);
                }
            }

            ic.lineCls.createLines(ic.lines);
    //    }

        // distance sets
        if(ic.distPnts && ic.distPnts.length > 0) {
            for(let i = 0, il = ic.distPnts.length; i < il; ++i) {
               ic.boxCls.createBox_base(ic.distPnts[i], ic.originSize, ic.hColor, false);
            }
        }

        // maps
        if(ic.prevMaps !== undefined) {
            for(let i = 0, il = ic.prevMaps.length; i < il; ++i) {
                ic.mdl.add(ic.prevMaps[i]);
            }
        }

        // EM map
        if(ic.prevEmmaps !== undefined) {
            for(let i = 0, il = ic.prevEmmaps.length; i < il; ++i) {
                ic.mdl.add(ic.prevEmmaps[i]);
            }
        }

        if(ic.prevPhimaps !== undefined) {
            for(let i = 0, il = ic.prevPhimaps.length; i < il; ++i) {
                ic.mdl.add(ic.prevPhimaps[i]);
            }
        }

        // surfaces
        if(ic.prevSurfaces !== undefined) {
            for(let i = 0, il = ic.prevSurfaces.length; i < il; ++i) {
                ic.mdl.add(ic.prevSurfaces[i]);
            }
        }

        // symmetry axes and polygon
        if(ic.symmetryHash !== undefined && ic.symmetrytitle !== undefined) {
            ic.applySymdCls.applySymmetry(ic.symmetrytitle);
        }

        if(ic.symdArray !== undefined && ic.symdArray.length > 0) {
            //var bSymd = true;
            //ic.applySymmetry(ic.symdtitle, bSymd);
            ic.applySymdCls.applySymd();
        }

        // other meshes
        if(ic.prevOtherMesh !== undefined) {
            for(let i = 0, il = ic.prevOtherMesh.length; i < il; ++i) {
                ic.mdl.add(ic.prevOtherMesh[i]);
            }
        }

        if(ic.bInitial) {
            if(me.htmlCls.setHtmlCls.getCookie('glycan') != '') {
                let bGlycansCartoon = parseInt(me.htmlCls.setHtmlCls.getCookie('glycan'));

                if(ic.bGlycansCartoon != bGlycansCartoon) {
                    me.htmlCls.clickMenuCls.setLogCmd('set glycan ' + bGlycansCartoon, true);
                }

                ic.bGlycansCartoon = bGlycansCartoon;
            }
        }

        // add cartoon for glycans
        if(ic.bGlycansCartoon && !ic.bAlternate) {
            ic.glycanCls.showGlycans();
        }

        // add extra spheres or cubes
        for(let command in ic.shapeCmdHash) {
            if(command.substr(0, 8) == 'add cube') {
                ic.applyCommandCls.addShape(command, 'cube');
            }
            else { // 'add sphere'
                ic.applyCommandCls.addShape(command, 'sphere');
            }
        }

        ic.applyCenterCls.applyCenterOptions(options);

        ic.axesCls.buildAllAxes(undefined, true);

        switch (options.axis.toLowerCase()) {
            case 'yes':
                ic.axis = true;
                ic.axesCls.buildAxes(ic.maxD/2);

                break;
            case 'no':
                ic.axis = false;
                break;
        }
        switch (options.pk.toLowerCase()) {
            case 'atom':
                ic.pk = 1;
                break;
            case 'no':
                ic.pk = 0;
                break;
            case 'residue':
                ic.pk = 2;
                break;
            case 'strand':
                ic.pk = 3;
                break;
        }
    }

    applyChemicalbindingOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        // display mode
        if (options.chemicalbinding === 'show') {
            let startAtoms;
            if(ic.chemicals !== undefined && Object.keys(ic.chemicals).length > 0) { // show chemical-protein interaction
                startAtoms = me.hashUtilsCls.hash2Atoms(ic.chemicals, ic.atoms);
            }

            // find atoms in chainid1, which interact with chainid2
            let radius = 4;

            if(startAtoms !== undefined) {
                let targetAtoms = ic.contactCls.getAtomsWithinAtom(ic.atoms, startAtoms, radius);

                // show hydrogens
                let threshold = 3.5;
                ic.opts["hbonds"] = "yes";

                if(Object.keys(targetAtoms).length > 0) {
                    ic.hBondCls.calculateChemicalHbonds(startAtoms, targetAtoms, parseFloat(threshold) );
                }

                // zoom in on the atoms
                if(!ic.bSetFog) ic.transformCls.zoominSelection( me.hashUtilsCls.unionHash(startAtoms, targetAtoms) );
            }
        }
        else if (options.chemicalbinding === 'hide') {
            // truen off hdonds
            ic.hBondCls.hideHbonds();

            // center on the atoms
            if(!ic.bSetFog) ic.transformCls.zoominSelection(ic.atoms);
        }
    }

    updateStabilizer() { let ic = this.icn3d; ic.icn3dui;
        ic.stabilizerpnts = [];

        if(ic.pairArray !== undefined) {
            for(let i = 0, il = ic.pairArray.length; i < il; i += 2) {
                let coordI = this.getResidueRepPos(ic.pairArray[i]);
                let coordJ = this.getResidueRepPos(ic.pairArray[i + 1]);

                ic.stabilizerpnts.push(coordI);
                ic.stabilizerpnts.push(coordJ);
            }
        }
    }

    getResidueRepPos(serial) { let ic = this.icn3d; ic.icn3dui;
        let atomIn = ic.atoms[serial];
        let residueid = atomIn.structure + "_" + atomIn.chain + "_" + atomIn.resi;

        let pos;
        if(!ic.proteins.hasOwnProperty(serial) && !ic.nucleotides.hasOwnProperty(serial)) { // chemicals or ions
            pos = atomIn.coord;
        }
        else {
            for(let i in ic.residues[residueid]) {
                let atom = ic.atoms[i];
                if(atom.name === 'N3') { // nucleotide: N3
                    pos = ic.atoms[i].coord;
                    break;
                }
                else if(atom.name === 'CA' && atom.ss == 'coil') { // protein coil: CA
                    pos = ic.atoms[i].coord;
                    break;
                }
                else if(atom.name === 'CA' && (atom.ss == 'helix' || atom.ss == 'sheet')) { // protein secondary: CA
                    pos = (ic.atoms[i].coord2 !== undefined) ? ic.atoms[i].coord2 : ic.atoms[i].coord;
                    break;
                }
            }
        }

        if(pos === undefined) pos = atomIn.coord;

        return pos;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplySsbonds {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Apply the disulfide bond options.
    applySsbondsOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        if (options.ssbonds.toLowerCase() === 'yes' && ic.ssbondpnts !== undefined) {
          let color = '#FFFF00';
          let colorObj = me.parasCls.thr(0xFFFF00);

          let structureArray = Object.keys(ic.structures);
          let start, end;

          if(ic.bAlternate) {
              let nStructures = structureArray.length;
              start = ic.ALTERNATE_STRUCTURE % nStructures;
              end = ic.ALTERNATE_STRUCTURE % nStructures + 1;
          }
          else {
              start = 0;
              end = structureArray.length;
          }

          ic.lines['ssbond'] = [];

          for(let s = start, sl = end; s < sl; ++s) {
              let structure = structureArray[s];

              if(ic.ssbondpnts[structure] === undefined) continue;

              //for(let i = 0, lim = Math.floor(ic.ssbondpnts[structure].length / 2); i < lim; i++) {
              for(let i = Math.floor(ic.ssbondpnts[structure].length / 2) - 1; i >= 0; i--) {
                let res1 = ic.ssbondpnts[structure][2 * i], res2 = ic.ssbondpnts[structure][2 * i + 1];

                let line = {};
                line.color = color;
                line.dashed = true;

                // each Cys has two S atoms
                let serial1Array = [], serial2Array = [];
                let position1Array = [], position2Array = [];

                let bFound = false, bCalpha = false;
                for(let j in ic.residues[res1]) {
                    if(ic.atoms[j].name === 'SG') {
                        position1Array.push(ic.atoms[j].coord);
                        serial1Array.push(ic.atoms[j].serial);
                        bFound = true;
                    }
                }

                if(!bFound) {
                    for(let j in ic.residues[res1]) {
                        if(ic.atoms[j].name === 'CA') {
                            position1Array.push(ic.atoms[j].coord);
                            serial1Array.push(ic.atoms[j].serial);
                            bFound = true;
                            bCalpha = true;
                            break;
                        }
                    }
                }

                bFound = false;
                for(let j in ic.residues[res2]) {
                    if(ic.atoms[j].name === 'SG') {
                        position2Array.push(ic.atoms[j].coord);
                        serial2Array.push(ic.atoms[j].serial);
                        bFound = true;
                    }
                }

                if(!bFound) {
                    for(let j in ic.residues[res2]) {
                        if(ic.atoms[j].name === 'CA') {
                            position2Array.push(ic.atoms[j].coord);
                            serial2Array.push(ic.atoms[j].serial);
                            bFound = true;
                            bCalpha = true;
                            break;
                        }
                    }
                }

                // determine whether it's true disulfide bonds
                // disulfide bond is about 2.05 angstrom
                let distMax = (bCalpha) ? 7.0 : 3.0;

                let bSsbond = false;
                for(let m = 0, ml = position1Array.length; m < ml; ++m) {
                    for(let n = 0, nl = position2Array.length; n < nl; ++n) {
                        if(position1Array[m].distanceTo(position2Array[n]) < distMax) {
                            bSsbond = true;

                            line.serial1 = serial1Array[m];
                            line.position1 = position1Array[m];

                            line.serial2 = serial2Array[n];
                            line.position2 = position2Array[n];

                            break;
                        }
                    }
                }

                // only draw bonds connected with currently displayed atoms
                if(line.serial1 !== undefined && line.serial2 !== undefined && !ic.dAtoms.hasOwnProperty(line.serial1) && !ic.dAtoms.hasOwnProperty(line.serial2)) continue;

                //if(line.position1 === undefined || line.position2 === undefined || line.position1.distanceTo(line.position2) > distMax) {
                if(!bSsbond) {
                    ic.ssbondpnts[structure].splice(2 * i, 2);
                    continue;
                }

                //if(ic.atoms[serial1].ids !== undefined) { // mmdb id as input
                    // remove the original disulfide bonds
                    let pos = ic.atoms[line.serial1].bonds.indexOf(line.serial2);
                    let array1, array2;
                    if(pos != -1) {
                        array1 = ic.atoms[line.serial1].bonds.slice(0, pos);
                        array2 = ic.atoms[line.serial1].bonds.slice(pos + 1);

                        ic.atoms[line.serial1].bonds = array1.concat(array2);
                    }

                    pos = ic.atoms[line.serial2].bonds.indexOf(line.serial1);
                    if(pos != -1) {
                        array1 = ic.atoms[line.serial2].bonds.slice(0, pos);
                        array2 = ic.atoms[line.serial2].bonds.slice(pos + 1);

                        ic.atoms[line.serial2].bonds = array1.concat(array2);
                    }
                //}

                //if(ic.lines['ssbond'] === undefined) ic.lines['ssbond'] = [];
                ic.lines['ssbond'].push(line);

                // create bonds for disulfide bonds
                ic.cylinderCls.createCylinder(line.position1, line.position2, ic.cylinderRadius, colorObj);

                // show ball and stick for these two residues
                let residueAtoms;
                residueAtoms = me.hashUtilsCls.unionHash(residueAtoms, ic.residues[res1]);
                residueAtoms = me.hashUtilsCls.unionHash(residueAtoms, ic.residues[res2]);

                // show side chains for the selected atoms
                let atoms = me.hashUtilsCls.intHash(residueAtoms, ic.sidec);
    //            let calpha_atoms = me.hashUtilsCls.intHash(residueAtoms, ic.calphas);
                // include calphas
    //            atoms = me.hashUtilsCls.unionHash(atoms, calpha_atoms);

                // draw sidec separatedly
                for(let j in atoms) {
                  ic.atoms[j].style2 = 'stick';
                }
              } // for(let i = 0,
          } // for(let s = 0,
        } // if (options.ssbonds.toLowerCase() === 'yes'
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplySymd {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    applySymd() { let ic = this.icn3d; ic.icn3dui;
        for(let i = 0, il = ic.symdArray.length; i < il; ++i) {
            let symdHash = ic.symdArray[i];
            let title = Object.keys(symdHash)[0];
            this.applySymmetry(title, true, symdHash[title]);
        }
    }

    applySymmetry(title, bSymd, inDataArray) { let ic = this.icn3d, me = ic.icn3dui;
        //var dataArray = (bSymd) ? ic.symdHash[title] : ic.symmetryHash[title]; // start_end_colorAxis_colorPolygon_order_chain
        let dataArray = (bSymd) ? inDataArray : ic.symmetryHash[title]; // start_end_colorAxis_colorPolygon_order_chain
        if(!dataArray) dataArray = [];

        let symmetryType = title.substr(0, 1);
        let nSide = parseInt(title.substring(1, title.indexOf(' ')));

        //var axisRadius = 2 * ic.cylinderRadius * ic.oriMaxD / 150;
        //var polygonRadius = 1 * ic.cylinderRadius * ic.oriMaxD / 150;

        let axisRadius = 1.5 * ic.cylinderRadius;
        let polygonRadius = 1 * ic.cylinderRadius;

        let pointArray = [];
        for(let i = 0, il = dataArray.length; i < il; ++i) {
            let start = dataArray[i][0];
            let end = dataArray[i][1];
            let colorAxis = dataArray[i][2];
            let colorPolygon = dataArray[i][3];
            let order = dataArray[i][4];
            let chain = dataArray[i][5];

            ic.cylinderCls.createCylinder(start, end, axisRadius, colorAxis, 0);

            if(ic.bAxisOnly) continue;

            if(symmetryType == 'C' || (symmetryType == 'D' && order == nSide) ) {
                // find the center and size of the selected protein chain

                let selection = {};
                // check the number of chains
                Object.keys(ic.chains).length;
                let bMultiChain = false;
                let chainHashTmp = {};

                if(bSymd && Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) {
                    for(let serial in ic.hAtoms) {
                        let atom = ic.atoms[serial];
                        let chainid = atom.structure + '_' + atom.chain;
                        chainHashTmp[chainid] = 1;
                    }

                    if(Object.keys(chainHashTmp).length > 1) {
                        bMultiChain = true;
                    }
                }

                //if(!bSymd || bMultiChain || Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) {
                if(!bSymd) {
                    let selectedChain = Object.keys(ic.structures)[0] + '_' + chain;

                    if(!ic.chains.hasOwnProperty(selectedChain)) {
                        selectedChain = Object.keys(ic.structures)[0] + '_' + chain.toLowerCase();
                    }

                    if(!ic.chains.hasOwnProperty(selectedChain)) {
                        selectedChain = Object.keys(ic.chains)[0];
                        for(let chainid in ic.chains) {
                            let firstSerial = Object.keys(ic.chains[chainid])[0];
                            if(ic.proteins.hasOwnProperty(firstSerial)) {
                                selectedChain = chainid;
                                break;
                            }
                        }
                    }
                    selection = ic.chains[selectedChain];
                }
                else if(bMultiChain) {
                    let selectedChain = Object.keys(chainHashTmp)[0];
                    selection = ic.chains[selectedChain];
                }
                else { // bSymd, subset, and one chain
                    if(Object.keys(ic.hAtoms).length == 0) {
                        //ic.hAtoms = me.hashUtilsCls.cloneHash(ic.atoms);
                        ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);
                    }

                    // pick the first 1/order of selection
                    let cnt = parseInt(Object.keys(ic.hAtoms).length / order);
                    let j = 0, lastSerial;

                    for(let serial in ic.hAtoms) {
                        selection[serial] = 1;
                        lastSerial = serial;
                        ++j;
                        if(j > cnt) break;
                    }

                    // add the whole residue for the last serial
                    let resid = ic.atoms[lastSerial].structure + '_' + ic.atoms[lastSerial].chain + '_' + ic.atoms[lastSerial].resi;
                    selection = me.hashUtilsCls.unionHash(selection, ic.residues[resid]);
                }


                let middle = start.clone().add(end).multiplyScalar(0.5);

                let psum = new THREE.Vector3();
                let cnt = 0;

                // apply the transformation to make the axis in the z-axis
                let axis = end.clone().sub(start).normalize();
                let vTo = new THREE.Vector3(0, 0, 1);

                let quaternion = new THREE.Quaternion();
                quaternion.setFromUnitVectors (axis, vTo);

                let distSqMax = -9999;
                for (let serial in selection) {
                    let atom = ic.atoms[serial];
                    let coord = atom.coord.clone();
                    psum.add(coord);

                    coord.sub(middle).applyQuaternion(quaternion);

                    let distSq = coord.x*coord.x + coord.y*coord.y;

                    if(distSq > distSqMax) distSqMax = distSq;

                    ++cnt;
                }

                //let center = psum.multiplyScalar(1.0 / cnt);
                let center = ic.ParserUtilsCls.getMassCenter(psum, cnt);

                let line = new THREE.Line3(start, end);

                // project center on line
                let proj = new THREE.Vector3();
                line.closestPointToPoint(center, true, proj);

                let rLen = Math.sqrt(distSqMax);

                let rDir = center.clone().sub(proj).normalize().multiplyScalar(rLen);

                //var start2 = start.clone().add(rDir);
                //var end2 = end.clone().add(rDir);

                let start2 = middle.clone().add(start.clone().sub(middle).multiplyScalar(0.83)).add(rDir);
                let end2 = middle.clone().add(end.clone().sub(middle).multiplyScalar(0.83)).add(rDir);

                //var axis = end.clone().sub(start).normalize();
                let anglePerSide = 2*Math.PI / nSide;

                let startInit, endInit, startPrev, endPrev;
                for(let j = 0; j < nSide; ++j) {
                    let angle = (0.5 + j) * anglePerSide;

                    let startCurr = start2.clone().sub(start);
                    startCurr.applyAxisAngle(axis, angle).add(start);

                    let endCurr = end2.clone().sub(start);
                    endCurr.applyAxisAngle(axis, angle).add(start);

                    ic.cylinderCls.createCylinder(startCurr, endCurr, polygonRadius, colorPolygon, 0);

                    ic.sphereCls.createSphereBase(startCurr, colorPolygon, polygonRadius, 1.0, 0);
                    ic.sphereCls.createSphereBase(endCurr, colorPolygon, polygonRadius, 1.0, 0);

                    if(j == 0) {
                        startInit = startCurr;
                        endInit = endCurr;
                    }
                    else {
                        ic.cylinderCls.createCylinder(startCurr, startPrev, polygonRadius, colorPolygon, 0);
                        ic.cylinderCls.createCylinder(endCurr, endPrev, polygonRadius, colorPolygon, 0);
                    }

                    startPrev = startCurr;
                    endPrev = endCurr;
                }

                if(startInit && startPrev) ic.cylinderCls.createCylinder(startInit, startPrev, polygonRadius, colorPolygon, 0);
                if(endInit && endPrev) ic.cylinderCls.createCylinder(endInit, endPrev, polygonRadius, colorPolygon, 0);
            }
            else if( (symmetryType == 'T' && order == 3)
              || (symmetryType == 'O' && order == 4)
              || (symmetryType == 'I' && order == 5) ) {
                pointArray.push(start);
                pointArray.push(end);
            }
            else ;
        }

        if(symmetryType == 'T') {
            let pos1 = pointArray[0]; // pointArray: start, end, start, end, ...
            ic.sphereCls.createSphereBase(pos1, colorPolygon, polygonRadius, 1.0, 0);

            let dist2 = pos1.distanceTo(pointArray[2]);
            let dist3 = pos1.distanceTo(pointArray[3]);

            let distSmall, posSel;
            if(dist2 < dist3) {
                distSmall = dist2;
                posSel = pointArray[3];
            }
            else {
                distSmall = dist3;
                posSel = pointArray[2];
            }

            ic.sphereCls.createSphereBase(posSel, colorPolygon, polygonRadius, 1.0, 0);
            ic.cylinderCls.createCylinder(pos1, posSel, polygonRadius, colorPolygon, 0);

            let iPrev;
            for(let i = 4, il = pointArray.length; i < il; ++i) {
                let pos2 = pointArray[i];

                let dist = pos1.distanceTo(pos2);
                if(dist > distSmall) {
                    ic.sphereCls.createSphereBase(pos2, colorPolygon, polygonRadius, 1.0, 0);
                    ic.cylinderCls.createCylinder(pos1, pos2, polygonRadius, colorPolygon, 0);

                    ic.cylinderCls.createCylinder(posSel, pos2, polygonRadius, colorPolygon, 0);
                    if(iPrev !== undefined) {
                        ic.cylinderCls.createCylinder(pointArray[iPrev], pos2, polygonRadius, colorPolygon, 0);
                    }

                    iPrev = i;
                }
            }
        }
        else if(symmetryType == 'O') {
            for(let i = 0, il = pointArray.length; i < il; i += 2) {
                let pos1 = pointArray[i];
                let pos2 = pointArray[i+1];
                ic.sphereCls.createSphereBase(pos1, colorPolygon, polygonRadius, 1.0, 0);
                ic.sphereCls.createSphereBase(pos2, colorPolygon, polygonRadius, 1.0, 0);
                for(let j = i + 2, jl = pointArray.length; j < jl; ++j) {
                    let pos3 = pointArray[j];
                    ic.sphereCls.createSphereBase(pos3, colorPolygon, polygonRadius, 1.0, 0);
                    ic.cylinderCls.createCylinder(pos1, pos3, polygonRadius, colorPolygon, 0);
                    ic.cylinderCls.createCylinder(pos2, pos3, polygonRadius, colorPolygon, 0);
                }
            }
        }
        else if(symmetryType == 'I') {
            for(let i = 0, il = pointArray.length; i < il; i += 2) {
                let pos1 = pointArray[i];
                let pos2 = pointArray[i+1];
                ic.sphereCls.createSphereBase(pos1, colorPolygon, polygonRadius, 1.0, 0);
                ic.sphereCls.createSphereBase(pos2, colorPolygon, polygonRadius, 1.0, 0);
                for(let j = i + 2, jl = pointArray.length; j < jl; j += 2) {
                    let pos3 = pointArray[j];
                    let pos4 = pointArray[j+1];

                    let dist3 = pos1.distanceTo(pos3);
                    let dist4 = pos1.distanceTo(pos4);

                    let pos1Sel, pos2Sel;
                    if(dist3 < dist4) {
                        pos1Sel = pos3;
                        pos2Sel = pos4;
                    }
                    else {
                        pos1Sel = pos4;
                        pos2Sel = pos3;
                    }

                    ic.sphereCls.createSphereBase(pos1Sel, colorPolygon, polygonRadius, 1.0, 0);
                    ic.sphereCls.createSphereBase(pos2Sel, colorPolygon, polygonRadius, 1.0, 0);
                    ic.cylinderCls.createCylinder(pos1, pos1Sel, polygonRadius, colorPolygon, 0);
                    ic.cylinderCls.createCylinder(pos2, pos2Sel, polygonRadius, colorPolygon, 0);
                }
            }
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplyMap {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Apply the surface options.
    applySurfaceOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        //switch (options.wirefraic.toLowerCase()) {
        switch (options.wireframe) {
            case 'yes':
                options.wireframe = true;
                break;
            case 'no':
                options.wireframe = false;
                break;
        }

        options.opacity = parseFloat(options.opacity);

        let atoms, currAtoms;

        // only show the surface for atoms which are displaying
        atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);
        // exclude water molecules
        if(options['water'] === 'nothing') atoms = me.hashUtilsCls.exclHash(atoms, ic.water);

        currAtoms = me.hashUtilsCls.hash2Atoms(atoms, ic.atoms);

        switch (options.surface.toLowerCase()) {
            case 'van der waals surface':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 1, options.wireframe, options.opacity);
                break;
    //            case 'solvent excluded surface':
    //                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 2, options.wireframe, options.opacity);
    //                break;
            case 'solvent accessible surface':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 3, options.wireframe, options.opacity);
                break;
            case 'molecular surface':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 2, options.wireframe, options.opacity);
                break;
            case 'van der waals surface with context':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 1, options.wireframe, options.opacity);
                break;
            case 'solvent accessible surface with context':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 3, options.wireframe, options.opacity);
                break;
            case 'molecular surface with context':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 2, options.wireframe, options.opacity);
                break;
            case 'nothing':
                // remove surfaces
                this.removeSurfaces();
                break;
        }
    }

    //Apply options for electron density map.
    applyMapOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        switch (options.mapwireframe) {
            case 'yes':
                options.mapwireframe = true;
                break;
            case 'no':
                options.mapwireframe = false;
                break;
        }

        let atoms, currAtoms;

        // only show the surface for atoms which are displaying
        atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);

        currAtoms = me.hashUtilsCls.hash2Atoms(atoms, ic.atoms);

        switch (options.map.toLowerCase()) {
            case '2fofc':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 11, options.mapwireframe);
                break;
            case 'fofc':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 12, options.mapwireframe);
                break;
            case 'nothing':
                // remove surfaces
                this.removeMaps();
                break;
        }
    }

    //Apply options for EM density map.
    applyEmmapOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        switch (options.emmapwireframe) {
            case 'yes':
                options.emmapwireframe = true;
                break;
            case 'no':
                options.emmapwireframe = false;
                break;
        }

        let atoms, currAtoms;

        // only show the surface for atoms which are displaying
        atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);

        currAtoms = me.hashUtilsCls.hash2Atoms(atoms, ic.atoms);

        switch (options.emmap.toLowerCase()) {
            case 'em':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 13, options.emmapwireframe);
                break;
            case 'nothing':
                // remove surfaces
                this.removeEmmaps();
                break;
        }
    }

    applyPhimapOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        switch (options.phimapwireframe) {
            case 'yes':
                options.phimapwireframe = true;
                break;
            case 'no':
                options.phimapwireframe = false;
                break;
        }

        let atoms, currAtoms;

        // only show the surface for atoms which are displaying
        atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);

        currAtoms = me.hashUtilsCls.hash2Atoms(atoms, ic.atoms);

        switch (options.phimap.toLowerCase()) {
            case 'phi':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, 14, options.phimapwireframe);
                break;
            case 'nothing':
                // remove surfaces
                this.removePhimaps();
                break;
        }
    }

    applyphisurfaceOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        //switch (options.wirefraic.toLowerCase()) {
        switch (ic.phisurfwf) {
            case 'yes':
                options.phisurfwf = true;
                break;
            case 'no':
                options.phisurfwf = false;
                break;
        }

        options.phisurfop = parseFloat(ic.phisurfop);

        let atoms, currAtoms;

        // only show the surface for atoms which are displaying
        atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);
        // exclude water molecules
        if(options['water'] === 'nothing') atoms = me.hashUtilsCls.exclHash(atoms, ic.water);

        currAtoms = me.hashUtilsCls.hash2Atoms(atoms, ic.atoms);

        switch (options.phisurface.toLowerCase()) {
            case 'phi':
                ic.surfaceCls.createSurfaceRepresentation(currAtoms, parseInt(ic.phisurftype), options.phisurfwf, options.phisurfop);
                break;
            case 'nothing':
                // remove surfaces
                this.removeSurfaces();
                break;
        }
    }

    //Remove previously drawn surfaces.
    removeSurfaces() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       for(let i = 0, il = ic.prevSurfaces.length; i < il; ++i) {
           ic.mdl.remove(ic.prevSurfaces[i]);
       }

       ic.prevSurfaces = [];
    }

    removeLastSurface() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       if(ic.prevSurfaces.length > 0) {
           ic.mdl.remove(ic.prevSurfaces[ic.prevSurfaces.length - 1]);
           ic.prevSurfaces.slice(ic.prevSurfaces.length - 1, 1);
       }
    }

    removeMaps() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       for(let i = 0, il = ic.prevMaps.length; i < il; ++i) {
           ic.mdl.remove(ic.prevMaps[i]);
       }

       ic.prevMaps = [];
    }

    removeEmmaps() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       for(let i = 0, il = ic.prevEmmaps.length; i < il; ++i) {
           ic.mdl.remove(ic.prevEmmaps[i]);
       }

       ic.prevEmmaps = [];
    }

    removePhimaps() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight

       for(let i = 0, il = ic.prevPhimaps.length; i < il; ++i) {
           ic.mdl.remove(ic.prevPhimaps[i]);
       }

       ic.prevPhimaps = [];
    }

    removeLastMap() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       if(ic.prevMaps.length > 0) {
           ic.mdl.remove(ic.prevMaps[ic.prevMaps.length - 1]);
           ic.prevMaps.slice(ic.prevMaps.length - 1, 1);
       }
    }

    removeLastEmmap() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       if(ic.prevEmmaps.length > 0) {
           ic.mdl.remove(ic.prevEmmaps[ic.prevEmmaps.length - 1]);
           ic.prevEmmaps.slice(ic.prevEmmaps.length - 1, 1);
       }
    }

    removeLastPhimap() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       if(ic.prevPhimaps.length > 0) {
           ic.mdl.remove(ic.prevPhimaps[ic.prevPhimaps.length - 1]);
           ic.prevPhimaps.slice(ic.prevPhimaps.length - 1, 1);
       }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ResidueLabels {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Add labels for all residues containing the input "atoms". The labels are one-letter residue abbreviations.
    //If "bSchematic" is true, the labels are in circles. Otherwise, they are in round-corner rectangles.
    addResidueLabels(atoms, bSchematic, alpha, bNumber) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let size = 18;
        let background = "#FFFFFF"; //"#CCCCCC";

        let atomsHash = me.hashUtilsCls.intHash(ic.hAtoms, atoms);

        if(bSchematic) {
            if(ic.labels['schematic'] === undefined) ic.labels['schematic'] = [];
        }
        else {
            if(ic.labels['residue'] === undefined) ic.labels['residue'] = [];
        }

        let prevReidueID = '';
        for(let i in atomsHash) {
            let atom = ic.atoms[i];

            // allow chemicals
            //if(atom.het) continue;

            let label = {}; // Each label contains 'position', 'text', 'color', 'background'

            let currReidueID = atom.structure + '_' + atom.chain + '_' + atom.resi;

            if( (!atom.het && (atom.name === 'CA' || atom.name === "O3'" || atom.name === "O3*") )
              || ic.water.hasOwnProperty(atom.serial)
              || ic.ions.hasOwnProperty(atom.serial)
              || (ic.chemicals.hasOwnProperty(atom.serial) && currReidueID !== prevReidueID) ) {
                label.position = atom.coord;

                label.bSchematic = 0;
                if(bSchematic) label.bSchematic = 1;

                label.text = me.utilsCls.residueName2Abbr(atom.resn);
                if(bNumber) {
                    label.text += atom.resi;
                    //label.factor = 0.3;
                }
                label.size = size;
                label.factor = 0.3;

                let atomColorStr = atom.color.getHexString().toUpperCase();
                //label.color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd; //(atomColorStr === "CCCCCC" || atomColorStr === "C8C8C8") ? "#888888" : "#" + atomColorStr;
                //if(bSchematic) label.color = (atomColorStr === "CCCCCC" || atomColorStr === "C8C8C8") ? "#888888" : "#" + atomColorStr;
                // don't change residue labels
                if(bNumber) {
                    label.color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd;
                }
                else {
                    label.color = (atomColorStr === "CCCCCC" || atomColorStr === "C8C8C8") ? "#888888" : "#" + atomColorStr;
                }
                label.background = background;
                //label.alpha = alpha; // ic.labelCls.hideLabels() didn't work. Remove this line for now

                if(bSchematic) {
                    ic.labels['schematic'].push(label);
                }
                else {
                    ic.labels['residue'].push(label);
                }
            }

            prevReidueID = currReidueID;
        }

        ic.hlObjectsCls.removeHlObjects();
    }

    addNonCarbonAtomLabels(atoms) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let size = 18;
        let background = "#FFFFFF";

        let atomsHash = me.hashUtilsCls.intHash(ic.hAtoms, atoms);

        if(ic.labels['schematic'] === undefined) ic.labels['schematic'] = [];

        for(let i in atomsHash) {
            let atom = ic.atoms[i];

            //if(!atom.het) continue;
            if(!ic.residues.hasOwnProperty(atom.structure + '_' + atom.chain + '_' + atom.resi)) continue;
            if(atom.elem === 'C') continue;

            let label = {}; // Each label contains 'position', 'text', 'color', 'background'

            label.position = atom.coord;

            label.bSchematic = 1;

            label.text = atom.elem;
            label.size = size;

            label.color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : atom.color.getHexString();
            label.background = background;

            ic.labels['schematic'].push(label);
        }

        ic.hlObjectsCls.removeHlObjects();
    };

    addAtomLabels(atoms, bElement) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let size = 18;
        //let background = (bElement) ? "#FFFFFF" : "#CCCCCC";
        let background = "#FFFFFF";

        let atomsHash = me.hashUtilsCls.intHash(ic.hAtoms, atoms);
        atomsHash = me.hashUtilsCls.intHash(ic.dAtoms, atomsHash);

        if(ic.labels['residue'] === undefined) ic.labels['residue'] = [];

        for(let i in atomsHash) {
            let atom = ic.atoms[i];

            let label = {}; // Each label contains 'position', 'text', 'color', 'background'

            label.position = atom.coord;

            label.bSchematic = 0;

            label.text = (bElement) ? atom.elem : atom.name.padEnd(2, ' ');
            label.size = size;

            if(bElement) {
                label.bSchematic = true;
            }

            let atomColorStr = atom.color.getHexString().toUpperCase();
            label.color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd; //(atomColorStr === "CCCCCC" || atomColorStr === "C8C8C8") ? "#888888" : "#" + atomColorStr; 
            if(bElement) label.color = (atomColorStr === "CCCCCC" || atomColorStr === "C8C8C8") ? "#888888" : "#" + atomColorStr;
            label.background = background;

            ic.labels['residue'].push(label);
        }

        ic.hlObjectsCls.removeHlObjects();
    };

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Impostor {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    onBeforeRender(renderer, scene, camera, geometry, material, group) {
      let u = material.uniforms;
      let updateList = [];

      if (u.objectId) {
        u.objectId.value = SupportsReadPixelsFloat ? this.id : this.id / 255;
        updateList.push('objectId');
      }

      if (u.modelViewMatrixInverse || u.modelViewMatrixInverseTranspose ||
          u.modelViewProjectionMatrix || u.modelViewProjectionMatrixInverse
      ) {
        this.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, this.matrixWorld);
      }

      if (u.modelViewMatrixInverse) {
        //u.modelViewMatrixInverse.value.getInverse(this.modelViewMatrix);
        u.modelViewMatrixInverse.value.copy( this.modelViewMatrix ).invert();
        updateList.push('modelViewMatrixInverse');
      }

      if (u.modelViewMatrixInverseTranspose) {
        if (u.modelViewMatrixInverse) {
          u.modelViewMatrixInverseTranspose.value.copy(
            u.modelViewMatrixInverse.value
          ).transpose();
        } else {
          //u.modelViewMatrixInverseTranspose.value
          //  .getInverse(this.modelViewMatrix)
          //  .transpose();
          u.modelViewMatrixInverseTranspose.value
            .copy( this.modelViewMatrix )
            .invert()
            .transpose();
        }
        updateList.push('modelViewMatrixInverseTranspose');
      }

      if (u.modelViewProjectionMatrix) {
        camera.updateProjectionMatrix();
        u.modelViewProjectionMatrix.value.multiplyMatrices(
          camera.projectionMatrix, this.modelViewMatrix
        );
        updateList.push('modelViewProjectionMatrix');
      }

      if (u.modelViewProjectionMatrixInverse) {
        let tmpMatrix = new THREE.Matrix4();
        if (u.modelViewProjectionMatrix) {
          tmpMatrix.copy(
            u.modelViewProjectionMatrix.value
          );
          //u.modelViewProjectionMatrixInverse.value.getInverse(
          //  tmpMatrix
          //);
          u.modelViewProjectionMatrixInverse.value.copy( tmpMatrix ).invert();
        } else {
          camera.updateProjectionMatrix();
          tmpMatrix.multiplyMatrices(
            camera.projectionMatrix, this.modelViewMatrix
          );
          //u.modelViewProjectionMatrixInverse.value.getInverse(
          //  tmpMatrix
          //);
          u.modelViewProjectionMatrixInverse.value.copy( tmpMatrix ).invert();
        }
        updateList.push('modelViewProjectionMatrixInverse');
      }

      if (u.projectionMatrix) {
        camera.updateProjectionMatrix();
        u.projectionMatrix.value.copy( camera.projectionMatrix );
        updateList.push('projectionMatrix');
      }

      if (u.projectionMatrixInverse) {
        camera.updateProjectionMatrix();
        //u.projectionMatrixInverse.value.getInverse(camera.projectionMatrix);
        u.projectionMatrixInverse.value.copy( camera.projectionMatrix ).invert();
        updateList.push('projectionMatrixInverse');
      }

      if (updateList.length) {
        let materialProperties = renderer.properties.get(material);

        if (materialProperties.program) {
          let gl = renderer.getContext();
          let p = materialProperties.program;
          gl.useProgram(p.program);
          let pu = p.getUniforms();

          updateList.forEach(function (name) {
            pu.setValue(gl, name, u[ name ].value);
          });
        }
      }
    }

    setParametersForShader (opacity) { let ic = this.icn3d, me = ic.icn3dui;
        let background = me.parasCls.backgroundColors[ic.opts.background.toLowerCase()];

        let near = 2.5*ic.maxD;
        let far = 4*ic.maxD;

        let bInstance = (ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > ic.maxatomcnt) ? true : false;

        let nearClip;
        if(ic.opts['slab'] === 'yes') {
            if(bInstance) {
                nearClip = 0.1;
            }
            else if(ic.camMaxDFactorFog !== undefined) {
                nearClip = ic.maxD * ic.camMaxDFactorFog - 10; // keep some surrounding residues
                near = (2.5*ic.maxD - nearClip < 0) ? 0 : 2.5*ic.maxD - nearClip;
                far = 4*ic.maxD - nearClip;
            }
            else {
                nearClip = ic.maxD * ic.camMaxDFactor;
            }
        }
        else {
            nearClip = 0.1;
        }

        let opacityValue = (opacity !== undefined) ? opacity : 1.0;

        let shiness = ic.shininess / 100.0 * 0.5;

        ic.uniforms = THREE.UniformsUtils.merge([
          THREE.UniformsLib.common,
          {
            modelViewMatrix: { value: new THREE.Matrix4() },
            modelViewMatrixInverse: { value: new THREE.Matrix4() },
            modelViewMatrixInverseTranspose: { value: new THREE.Matrix4() },
            modelViewProjectionMatrix: { value: new THREE.Matrix4() },
            modelViewProjectionMatrixInverse: { value: new THREE.Matrix4() },
            projectionMatrix: { value: new THREE.Matrix4() },
            projectionMatrixInverse: { value: new THREE.Matrix4() },

            //ambientLightColor: { type: "v3", value: [0.25, 0.25, 0.25] },
            diffuse: { type: "v3", value: [1.0, 1.0, 1.0] },
            emissive: { type: "v3", value: [0.06,0.06,0.06] }, //[0.0,0.0,0.0] },
            roughness: { type: "f", value: 0.5 },
            metalness: { type: "f", value: shiness } , //0.3 },
            opacity: { type: "f", value: opacityValue },
            nearClip: { type: "f", value: nearClip },
            ortho: { type: "f", value: 0.0 },
            shrink: { type: "f", value: 0.13 },
            fogColor: { type: "v3", value: [background.r, background.g, background.b] },
            fogNear: { type: "f", value: near },
            fogFar: { type: "f", value: far },
            fogDensity: { type: "f", value: 2.0 }
          },
            THREE.UniformsLib.ambient,
            THREE.UniformsLib.lights
        ]);

        ic.defines = {
            USE_COLOR: 1,
            //PICKING: 1,
            NEAR_CLIP: 1,
            CAP: 1
        };

        if(ic.opts['fog'] === 'yes' && !bInstance) {
            ic.defines['USE_FOG'] = 1;

            if(ic.opts['camera'] === 'orthographic') {
                ic.defines['FOG_EXP2'] = 1;
            }
        }

        if(ic.bExtFragDepth) {
            ic.defines['USE_LOGDEPTHBUF_EXT'] = 1;
        }
    }

    drawImpostorShader () { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        this.setParametersForShader();

        this.createImpostorShaderSphere("SphereImpostor");
        this.createImpostorShaderCylinder("CylinderImpostor");
        //this.createImpostorShaderCylinder("HyperballStickImpostor");
    }

    getShader (name) { let ic = this.icn3d; ic.icn3dui;
      let shaderText = $NGL_shaderTextHash[name];
      let reInclude = /#include\s+(\S+)/gmi;

      shaderText = shaderText.replace( reInclude, function( match, p1 ){

            let chunk;
            if(THREE.ShaderChunk.hasOwnProperty(p1)) {
                chunk = THREE.ShaderChunk[ p1 ];
            }

            return chunk ? chunk : "";

      } );

      return shaderText;
    }

    createImpostorShaderBase(shaderName, mapping, mappingIndices, data, attributeData, count, mappingSize, mappingIndicesSize, mappingItemSize) { let ic = this.icn3d; ic.icn3dui;
      let shaderMaterial =
        new THREE.ShaderMaterial({
          defines: ic.defines,
          uniforms:  ic.uniforms,
          vertexShader:   this.getShader(shaderName + ".vert"),
          fragmentShader: this.getShader(shaderName + ".frag"),
          depthTest: true,
          depthWrite: true,
          //needsUpdate: true,
          lights: true
      });

      shaderMaterial.extensions.fragDepth = true;

      if(shaderName == 'CylinderImpostor') {
          ic.CylinderImpostorMaterial = shaderMaterial;
      }
      else if(shaderName == 'SphereImpostor') {
          ic.SphereImpostorMaterial = shaderMaterial;
      }

        //MappedBuffer
        let attributeSize = count * mappingSize;

        let n = count * mappingIndicesSize;
        let TypedArray = attributeSize > 65535 ? Uint32Array : Uint16Array;
        let index = new TypedArray( n );

            //makeIndex();
        let ix, it;

        for( let v = 0; v < count; v++ ) {
            ix = v * mappingIndicesSize;
            it = v * mappingSize;

            index.set( mappingIndices, ix );

            for( let s = 0; s < mappingIndicesSize; ++s ){
                index[ ix + s ] += it;
            }
        }


        let geometry = new THREE.BufferGeometry();

        if( index ){
            geometry.setIndex(
                new THREE.BufferAttribute( index, 1 )
            );
            //https://discourse.threejs.org/t/what-is-setusage-on-bufferattribute/12441
            geometry.getIndex().setUsage(THREE.DynamicDrawUsage); //.setDynamic( dynamic );
        }

        // add attributes from buffer.js
        let itemSize = {
            "f": 1, "v2": 2, "v3": 3, "c": 3
        };

        for( let name in attributeData ){

            let buf;
            let a = attributeData[ name ];

                buf = new Float32Array(
                    attributeSize * itemSize[ a.type ]
                );

            geometry.setAttribute(
                name,
                new THREE.BufferAttribute( buf, itemSize[ a.type ] )
                    .setUsage(THREE.DynamicDrawUsage) //.setDynamic( dynamic )
            );

        }

        // set attributes from mapped-buffer.js
        let attributes = geometry.attributes;

        let a, d, itemSize2, array, i, j;

        for( let name in data ){

            d = data[ name ];
            a = attributes[ name ];
            itemSize2 = a.itemSize;
            array = a.array;

            for( let k = 0; k < count; ++k ) {

                n = k * itemSize2;
                i = n * mappingSize;

                for( let l = 0; l < mappingSize; ++l ) {

                    j = i + ( itemSize2 * l );

                    for( let m = 0; m < itemSize2; ++m ) {

                        array[ j + m ] = d[ n + m ];

                    }

                }

            }

            a.needsUpdate = true;

        }

        // makemapping
        let aMapping = geometry.attributes.mapping.array;

        for( let v = 0; v < count; v++ ) {
            aMapping.set( mapping, v * mappingItemSize * mappingSize );
        }

        let mesh = new THREE.Mesh(geometry, shaderMaterial);

        // important: https://stackoverflow.com/questions/21184061/mesh-suddenly-disappears-in-three-js-clipping
        // You are moving the camera in the CPU. You are moving the vertices of the plane in the GPU
        mesh.frustumCulled = false;

        mesh.scale.x = mesh.scale.y = mesh.scale.z = 1.0;

        if(shaderName == 'CylinderImpostor') {
          mesh.type = 'Cylinder';
        }
        else if(shaderName == 'SphereImpostor') {
          mesh.type = 'Sphere';
        }

        //mesh.onBeforeRender = this.onBeforeRender(ic.renderer, ic.scene, ic.cam, geometry, shaderMaterial);
        mesh.onBeforeRender = this.onBeforeRender;

        ic.mdlImpostor.add(mesh);

        //ic.objects.push(mesh);
    }

    createImpostorShaderCylinder(shaderName) { let ic = this.icn3d; ic.icn3dui;
        let positions = new Float32Array( ic.posArray );
        let colors = new Float32Array( ic.colorArray );
        let positions2 = new Float32Array( ic.pos2Array );
        let colors2 = new Float32Array( ic.color2Array );
        let radii = new Float32Array( ic.radiusArray );

        // cylinder
        let 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
        ]);

        let mappingIndices = new Uint16Array([
            0, 1, 2,
            1, 4, 2,
            2, 4, 3,
            4, 5, 3
        ]);

        let mappingIndicesSize = 12;
        let mappingType = "v3";
        let mappingSize = 6;
        let mappingItemSize = 3;


        let count = positions.length / 3;

        let data = {
            "position1": positions,
            "color": colors,
            "position2": positions2,
            "color2": colors2,
            "radius": radii
        };

        let attributeData = {
            "position1": { type: "v3", value: null },
            "color": { type: "v3", value: null },
            "position2": { type: "v3", value: null },
            "color2": { type: "v3", value: null },
            "radius": { type: "f", value: null },
            "mapping": { type: mappingType, value: null }
        };

        this.createImpostorShaderBase(shaderName, mapping, mappingIndices, data, attributeData, count, mappingSize, mappingIndicesSize, mappingItemSize);

        data = null;
        positions = null;
        colors = null;
        positions2 = null;
        colors2 = null;
        radii = null;

      ic.posArray = [];
      ic.colorArray = [];
      ic.pos2Array = [];
      ic.color2Array = [];
      ic.radiusArray = [];
    }

    createImpostorShaderSphere(shaderName) { let ic = this.icn3d; ic.icn3dui;
        let positions = new Float32Array( ic.posArraySphere );
        let colors = new Float32Array( ic.colorArraySphere );
        let radii = new Float32Array( ic.radiusArraySphere );

        // sphere
        let mapping = new Float32Array([
            -1.0,  1.0,
            -1.0, -1.0,
             1.0,  1.0,
             1.0, -1.0
        ]);

        let mappingIndices = new Uint16Array([
            0, 1, 2,
            1, 3, 2
        ]);

        let mappingIndicesSize = 6;
        let mappingType = "v2";
        let mappingSize = 4;
        let mappingItemSize = 2;

        let count = positions.length / 3;

        let data = {
            "position": positions,
            "color": colors,
            "radius": radii
        };

        let attributeData = {
            "position": { type: "v3", value: null },
            "color": { type: "v3", value: null },
            "radius": { type: "f", value: null },
            "mapping": { type: mappingType, value: null }
        };

        this.createImpostorShaderBase(shaderName, mapping, mappingIndices, data, attributeData, count, mappingSize, mappingIndicesSize, mappingItemSize);

        data = null;
        positions = null;
        colors = null;
        radii = null;

      ic.posArraySphere = [];
      ic.colorArraySphere = [];
      ic.radiusArraySphere = [];
    }

    clearImpostors() { let ic = this.icn3d; ic.icn3dui;
        ic.posArray = [];
        ic.colorArray = [];
        ic.pos2Array = [];
        ic.color2Array = [];
        ic.radiusArray = [];

        ic.posArraySphere = [];
        ic.colorArraySphere = [];
        ic.radiusArraySphere = [];
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Instancing {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    positionFromGeometry( mesh ){ let ic = this.icn3d; ic.icn3dui;
        let geometry = mesh.geometry;

        let vertices = geometry.vertices;

        let meshPosition = mesh.position;
        let scale = mesh.scale;
        let matrix = mesh.matrix;

        let j, v3;
        let n = vertices.length;
        //var position = new Float32Array( n * 3 );
        let position = [];

        for( let v = 0; v < n; v++ ){

            j = v * 3;

            if(geometry.type == 'SphereGeometry' || geometry.type == 'BoxGeometry') {
                v3 = vertices[v].clone().multiply(scale).add(meshPosition);
            }
            else if(geometry.type == 'CylinderGeometry') {
                v3 = vertices[v].clone().applyMatrix4(matrix);
            }
            else {
                v3 = vertices[v];
            }

            position[ j + 0 ] = v3.x;
            position[ j + 1 ] = v3.y;
            position[ j + 2 ] = v3.z;
        }

        return position;
    }

    colorFromGeometry( mesh ){ let ic = this.icn3d, me = ic.icn3dui;
        let geometry = mesh.geometry;

        let meshColor = me.parasCls.thr(1, 1, 1);
        if(geometry.type == 'SphereGeometry' || geometry.type == 'BoxGeometry' || geometry.type == 'CylinderGeometry') {
             if(mesh.material !== undefined) meshColor = mesh.material.color;
        }

        let faces = geometry.faces;
        geometry.vertices.length;

        (geometry.type == 'Surface') ? true : false;

        let j, f, c1, c2, c3;
        let n = faces.length;
        //var color = new Float32Array( vn * 3 );
        let color = [];

        for( let v = 0; v < n; v++ ){

            f = faces[ v ];

            if(geometry.type == 'Surface') {
                c1 = f.vertexColors[0];
                c2 = f.vertexColors[1];
                c3 = f.vertexColors[2];
            }
            else if(geometry.type == 'SphereGeometry' || geometry.type == 'BoxGeometry' || geometry.type == 'CylinderGeometry') {
                c1 = meshColor;
                c2 = meshColor;
                c3 = meshColor;
            }
            else {
                c1 = f.color;
                c2 = f.color;
                c3 = f.color;
            }

            j = f.a * 3;
            color[ j + 0 ] = c1.r;
            color[ j + 1 ] = c1.g;
            color[ j + 2 ] = c1.b;

            j = f.b * 3;
            color[ j + 0 ] = c2.r;
            color[ j + 1 ] = c2.g;
            color[ j + 2 ] = c2.b;

            j = f.c * 3;
            color[ j + 0 ] = c3.r;
            color[ j + 1 ] = c3.g;
            color[ j + 2 ] = c3.b;

        }

        return color;
    }

    indexFromGeometry( mesh ){  let ic = this.icn3d; ic.icn3dui;
        let geometry = mesh.geometry;

        let faces = geometry.faces;

        let j, f;
        let n = faces.length;
        //var TypedArray = n * 3 > 65535 ? Uint32Array : Uint16Array;
        //var index = new TypedArray( n * 3 );
        let index = [];

        for( let 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( mesh ){  let ic = this.icn3d; ic.icn3dui;
        let geometry = mesh.geometry;

        let faces = geometry.faces;
        geometry.vertices.length;

        let j, f, nn, n1, n2, n3;
        let n = faces.length;
        //var normal = new Float32Array( vn * 3 );
        let normal = [];

        for( let 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;
    }

    //Draw the biological unit assembly using the matrix.
    drawSymmetryMates() {  let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

//        if(ic.bInstanced && Object.keys(ic.atoms).length * ic.biomtMatrices.length > ic.maxatomcnt) {
        if(ic.bInstanced) {
            this.drawSymmetryMatesInstancing();
        }
        else {
            this.drawSymmetryMatesNoInstancing();
        }
    }

    applyMat(obj, mat, bVector3) {  let ic = this.icn3d; ic.icn3dui;
        // applyMatrix was renamed to applyMatrix4
        if(ic.rmsd_supr === undefined) {
/*
          if(bVector3 === undefined) {
              obj.applyMatrix(mat);
          }
          else if(bVector3) {
              obj.applyMatrix4(mat);
          }
*/
          obj.applyMatrix4(mat);
        }
        else {
          let rot = ic.rmsd_supr.rot;
          let centerFrom = ic.rmsd_supr.trans1;
          let centerTo = ic.rmsd_supr.trans2;

          let rotationM4 = new THREE.Matrix4();
          rotationM4.set(rot[0], rot[1], rot[2], 0, rot[3], rot[4], rot[5], 0, rot[6], rot[7], rot[8], 0, 0, 0, 0, 1);

          let rotationM4Inv = new THREE.Matrix4();
          //rotationM4Inv.getInverse(rotationM4);
          rotationM4Inv.copy( rotationM4 ).invert();

          //modifiedMat.makeTranslation(-centerTo.x, -centerTo.y, -centerTo.z).multiply(rotationM4Inv).makeTranslation(centerFrom.x, centerFrom.y, centerFrom.z).multiply(mat).makeTranslation(-centerFrom.x, -centerFrom.y, -centerFrom.z).multiply(rotationM4).makeTranslation(centerTo.x, centerTo.y, centerTo.z);

          let tmpMat = new THREE.Matrix4();

/*
          if(bVector3 === undefined) {
              tmpMat.makeTranslation(-centerTo.x, -centerTo.y, -centerTo.z);
              obj.applyMatrix(tmpMat);

              obj.applyMatrix(rotationM4Inv);

              tmpMat.makeTranslation(centerFrom.x, centerFrom.y, centerFrom.z);
              obj.applyMatrix(tmpMat);

              obj.applyMatrix(mat);

              tmpMat.makeTranslation(-centerFrom.x, -centerFrom.y, -centerFrom.z);
              obj.applyMatrix(tmpMat);

              obj.applyMatrix(rotationM4);

              tmpMat.makeTranslation(centerTo.x, centerTo.y, centerTo.z);
              obj.applyMatrix(tmpMat);
          }
          else if(bVector3) {
*/
              tmpMat.makeTranslation(-centerTo.x, -centerTo.y, -centerTo.z);
              obj.applyMatrix4(tmpMat);

              obj.applyMatrix4(rotationM4Inv);

              tmpMat.makeTranslation(centerFrom.x, centerFrom.y, centerFrom.z);
              obj.applyMatrix4(tmpMat);

              obj.applyMatrix4(mat);

              tmpMat.makeTranslation(-centerFrom.x, -centerFrom.y, -centerFrom.z);
              obj.applyMatrix4(tmpMat);

              obj.applyMatrix4(rotationM4);

              tmpMat.makeTranslation(centerTo.x, centerTo.y, centerTo.z);
              obj.applyMatrix4(tmpMat);
//          }
        }
    }

    drawSymmetryMatesNoInstancing() {  let ic = this.icn3d; ic.icn3dui;
       if (ic.biomtMatrices === undefined || ic.biomtMatrices.length == 0) return;
       let cnt = 1; // itself
       let centerSum = ic.center.clone();

       let identity = new THREE.Matrix4();
       identity.identity();

       let mdlTmp = new THREE.Object3D();
       let mdlImpostorTmp = new THREE.Object3D();
       let mdl_ghostTmp = new THREE.Object3D();

//       for (let i = 0; i < ic.biomtMatrices.length; i++) {  // skip itself
       for (let i = 0; i < ic.biomtMatrices.length && Object.keys(ic.structures).length == 1; i++) {  // skip itself
          let mat = ic.biomtMatrices[i];
          if (mat === undefined) continue;

          // skip itself
          if(mat.equals(identity)) continue;

          let symmetryMate;

          if(ic.mdl !== undefined) {
              symmetryMate = ic.mdl.clone();
              //symmetryMate.applyMatrix(mat);
              this.applyMat(symmetryMate, mat);

              mdlTmp.add(symmetryMate);
          }

          if(ic.mdlImpostor !== undefined) {
              // after three.js version 128, the cylinder impostor seemed to have a problem in cloning!!!
              symmetryMate = ic.mdlImpostor.clone();
              //symmetryMate.applyMatrix(mat);
              this.applyMat(symmetryMate, mat);

              //symmetryMate.onBeforeRender = ic.impostorCls.onBeforeRender;
              for(let j = symmetryMate.children.length - 1; j >= 0; j--) {
                   let mesh = symmetryMate.children[j];
                   mesh.onBeforeRender = ic.impostorCls.onBeforeRender;
                   //mesh.onBeforeRender = this.onBeforeRender;

                   mesh.frustumCulled = false;
              }

              mdlImpostorTmp.add(symmetryMate);
          }

          if(ic.mdl_ghost !== undefined) {
              symmetryMate = ic.mdl_ghost.clone();
              //symmetryMate.applyMatrix(mat);
              this.applyMat(symmetryMate, mat);

              mdl_ghostTmp.add(symmetryMate);
          }

          let center = ic.center.clone();
          //center.applyMatrix4(mat);
          this.applyMat(center, mat, true);

          centerSum.add(center);

          ++cnt;
       }

       ic.mdl.add(mdlTmp);
       ic.mdlImpostor.add(mdlImpostorTmp);
       ic.mdl_ghost.add(mdl_ghostTmp);

       if(ic.bSetInstancing === undefined || !ic.bSetInstancing) {
           ic.maxD *= Math.sqrt(cnt);

           //ic.center = centerSum.multiplyScalar(1.0 / cnt);
           ic.center = ic.ParserUtilsCls.getMassCenter(centerSum, cnt);

           ic.maxDAssembly = ic.maxD;

           ic.centerAssembly = ic.center.clone();

           ic.applyCenterCls.setCenter(ic.center);

           // reset cameara
           ic.cameraCls.setCamera();
       }
       else {
           ic.maxD = ic.maxDAssembly;

           ic.center = ic.centerAssembly.clone();

           ic.applyCenterCls.setCenter(ic.center);

           // reset cameara
           ic.cameraCls.setCamera();
       }

       ic.bSetInstancing = true;
    }

    createInstancedGeometry(mesh) {  let ic = this.icn3d, me = ic.icn3dui;
       let baseGeometry = mesh.geometry;

       let geometry = new THREE.InstancedBufferGeometry();

       let positionArray = [];
       let normalArray = [];
       let colorArray = [];
       let indexArray = [];

       let radiusArray = [];
       let mappingArray = [];
       let position2Array = [];
       let color2Array = [];

       //else if(ic.bImpo && baseGeometry.attributes.color2 !== undefined) { // cylinder
       if(ic.bImpo && (mesh.type == 'Cylinder')) { // cylinder
           ic.instancedMaterial = this.getInstancedMaterial('CylinderInstancing');

           let positionArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.position1.array);
           let colorArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.color.array);

           let positionArray2b = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.position2.array);
           let colorArray2b = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.color2.array);

           let indexArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.index.array);
           let radiusArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.radius.array);
           let mappingArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.mapping.array);

           positionArray = positionArray.concat(positionArray2);
           colorArray = colorArray.concat(colorArray2);

           position2Array = position2Array.concat(positionArray2b);
           color2Array = color2Array.concat(colorArray2b);

           indexArray = indexArray.concat(indexArray2);
           radiusArray = radiusArray.concat(radiusArray2);
           mappingArray = mappingArray.concat(mappingArray2);

           geometry.setAttribute('position1', new THREE.BufferAttribute(new Float32Array(positionArray), 3));
           geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), 3) );

           geometry.setAttribute('position2', new THREE.BufferAttribute(new Float32Array(position2Array), 3));
           geometry.setAttribute('color2', new THREE.BufferAttribute(new Float32Array(color2Array), 3) );

           geometry.setAttribute('radius', new THREE.BufferAttribute(new Float32Array(radiusArray), 1) );
           geometry.setAttribute('mapping', new THREE.BufferAttribute(new Float32Array(mappingArray), 3) );
           geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indexArray), 1));

           positionArray2 = null;
           colorArray2 = null;
           positionArray2b = null;
           colorArray2b = null;
           indexArray2 = null;
           radiusArray2 = null;
           mappingArray2 = null;
       }
       //else if(ic.bImpo && baseGeometry.attributes.color !== undefined) { // sphere
       else if(ic.bImpo && (mesh.type == 'Sphere')) { // sphere
           ic.instancedMaterial = this.getInstancedMaterial('SphereInstancing');

           let positionArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.position.array);
           let colorArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.color.array);
           let indexArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.index.array);
           let radiusArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.radius.array);
           let mappingArray2 = me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.mapping.array);

           positionArray = positionArray.concat(positionArray2);
           colorArray = colorArray.concat(colorArray2);
           indexArray = indexArray.concat(indexArray2);
           radiusArray = radiusArray.concat(radiusArray2);
           mappingArray = mappingArray.concat(mappingArray2);

           geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positionArray), 3));
           geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), 3) );
           geometry.setAttribute('radius', new THREE.BufferAttribute(new Float32Array(radiusArray), 1) );
           geometry.setAttribute('mapping', new THREE.BufferAttribute(new Float32Array(mappingArray), 2) );
           geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indexArray), 1));

           positionArray2 = null;
           colorArray2 = null;
           indexArray2 = null;
           radiusArray2 = null;
           mappingArray2 = null;
       }
       //if( baseGeometry.vertices && baseGeometry.faces ){
       else { // now BufferGeometry
           ic.instancedMaterial = this.getInstancedMaterial('Instancing');

           //var positionArray2 = this.positionFromGeometry( mesh );
           //var normalArray2 = this.normalFromGeometry( mesh );
           //var colorArray2 = this.colorFromGeometry( mesh );
           //var indexArray2 = this.indexFromGeometry( mesh );

           let positionArray2 = (baseGeometry.attributes.position) ? me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.position.array) : [];
           let normalArray2 = (baseGeometry.attributes.normal) ? me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.normal.array) : [];
           let colorArray2 = (baseGeometry.attributes.color) ? me.hashUtilsCls.hashvalue2array(baseGeometry.attributes.color.array) : [];
           let indexArray2 = (baseGeometry.index) ? me.hashUtilsCls.hashvalue2array(baseGeometry.index.array) : [];

           positionArray = positionArray.concat(positionArray2);
           normalArray = normalArray.concat(normalArray2);
           colorArray = colorArray.concat(colorArray2);
           indexArray = indexArray.concat(indexArray2);

           let bCylinderArray = [];
           let bCylinder = (baseGeometry.type == 'CylinderGeometry') ? 1.0 : 0.0;
           for(let i = 0, il = positionArray.length / 3; i < il; ++i) {
               bCylinderArray.push(bCylinder);
           }

           geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positionArray), 3));
           geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normalArray), 3) );
           geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colorArray), 3) );

           geometry.setAttribute('cylinder', new THREE.BufferAttribute(new Float32Array(bCylinderArray), 1) );
           geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indexArray), 1));

           positionArray2 = null;
           normalArray2 = null;
           colorArray2 = null;
           indexArray2 = null;

       }

       positionArray = null;
       normalArray = null;
       colorArray = null;
       indexArray = null;

       radiusArray = null;
       mappingArray = null;
       position2Array = null;
       color2Array = null;

       let matricesAttribute1 = new THREE.InstancedBufferAttribute( new Float32Array( ic.matricesElements1 ), 4 );
       let matricesAttribute2 = new THREE.InstancedBufferAttribute( new Float32Array( ic.matricesElements2 ), 4 );
       let matricesAttribute3 = new THREE.InstancedBufferAttribute( new Float32Array( ic.matricesElements3 ), 4 );
       let matricesAttribute4 = new THREE.InstancedBufferAttribute( new Float32Array( ic.matricesElements4 ), 4 );

       geometry.setAttribute( 'matrix1', matricesAttribute1 );
       geometry.setAttribute( 'matrix2', matricesAttribute2 );
       geometry.setAttribute( 'matrix3', matricesAttribute3 );
       geometry.setAttribute( 'matrix4', matricesAttribute4 );

       return geometry;
    }

    getInstancedMaterial(name) {  let ic = this.icn3d; ic.icn3dui;
       //var material = new THREE.RawShaderMaterial({
       let material = new THREE.ShaderMaterial({
          defines: ic.defines,
          uniforms:  ic.uniforms,
          vertexShader:   ic.impostorCls.getShader(name + ".vert"),
          fragmentShader: ic.impostorCls.getShader(name + ".frag"),
          depthTest: true,
          depthWrite: true,
          //needsUpdate: true,
          lights: true
       });

       material.extensions.fragDepth = true;
       //https://stackoverflow.com/questions/33094496/three-js-shadermaterial-flatshading
       material.extensions.derivatives = '#extension GL_OES_standard_derivatives : enable';

       return material;
    }

    createInstancedMesh(mdl) { let ic = this.icn3d; ic.icn3dui;
       for(let i = 0, il = mdl.children.length; i < il; ++i) {
           let mesh = mdl.children[i];

           if(mesh.type === 'Sprite') continue;

           let geometry = this.createInstancedGeometry(mesh);

           let mesh2 = new THREE.Mesh(geometry, ic.instancedMaterial);

           mesh2.onBeforeRender = ic.impostorCls.onBeforeRender;
           //mesh2.onBeforeRender = this.onBeforeRender;

           // important: https://stackoverflow.com/questions/21184061/mesh-suddenly-disappears-in-three-js-clipping
           // You are moving the camera in the CPU. You are moving the vertices of the plane in the GPU
           mesh2.frustumCulled = false;

           mesh2.scale.x = mesh2.scale.y = mesh2.scale.z = 1.0;
           mesh2.type = mesh.type;

           geometry = null;

           mdl.add(mesh2);
       }
    }

    drawSymmetryMatesInstancing() { let ic = this.icn3d; ic.icn3dui;
       if (ic.biomtMatrices === undefined || ic.biomtMatrices.length == 0) return;
       let cnt = 1; // itself
       let centerSum = ic.center.clone();

       ic.impostorCls.setParametersForShader();

       if(ic.bSetInstancing === undefined || !ic.bSetInstancing) {
           //ic.offsets = [];
           //ic.orientations = [];
           ic.matricesElements1 = [];
           ic.matricesElements2 = [];
           ic.matricesElements3 = [];
           ic.matricesElements4 = [];

           let identity = new THREE.Matrix4();
           identity.identity();

           for (let i = 0; i < ic.biomtMatrices.length && Object.keys(ic.structures).length == 1; i++) {  // skip itself
              let mat = ic.biomtMatrices[i];
              if (mat === undefined) continue;

              let matArray = mat.toArray();

              // skip itself
              if(mat.equals(identity)) continue;

              ic.matricesElements1.push(matArray[0], matArray[1], matArray[2], matArray[3]);
              ic.matricesElements2.push(matArray[4], matArray[5], matArray[6], matArray[7]);
              ic.matricesElements3.push(matArray[8], matArray[9], matArray[10], matArray[11]);
              ic.matricesElements4.push(matArray[12], matArray[13], matArray[14], matArray[15]);

              let center = ic.center.clone();
              center.applyMatrix4(mat);
              centerSum.add(center);

              ++cnt;
           }
       }

       this.createInstancedMesh(ic.mdl);
       this.createInstancedMesh(ic.mdlImpostor);

       if(ic.bSetInstancing === undefined || !ic.bSetInstancing) {
           ic.maxD *= Math.sqrt(cnt);

           //ic.center = centerSum.multiplyScalar(1.0 / cnt);
           ic.center = ic.ParserUtilsCls.getMassCenter(centerSum, cnt);

           ic.maxDAssembly = ic.maxD;

           ic.centerAssembly = ic.center.clone();

           ic.applyCenterCls.setCenter(ic.center);

           // reset cameara
           ic.cameraCls.setCamera();
       }
       else {
           ic.maxD = ic.maxDAssembly;

           ic.center = ic.centerAssembly.clone();

           ic.applyCenterCls.setCenter(ic.center);

           // reset cameara
           ic.cameraCls.setCamera();
       }

       ic.bSetInstancing = true;
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Alternate {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // change the display atom when alternating
    //Show structures one by one.
    alternateStructures() { let ic = this.icn3d, me = ic.icn3dui;
        // default ic.ALTERNATE_STRUCTURE = -1
        if(ic.ALTERNATE_STRUCTURE == -1) {
            ic.viewSelectionAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);
        }

        let viewSelectionAtomsCount = Object.keys(ic.viewSelectionAtoms).length;
        let allAtomsCount = Object.keys(ic.atoms).length;

        //ic.dAtoms = {};

        // 1. alternate all structures
        //let moleculeArray = Object.keys(ic.structures);

        // 2. only alternate displayed structures
        let structureHash = {};
        for(let i in ic.viewSelectionAtoms) {
            let structure = ic.atoms[i].structure;
            structureHash[structure] = 1;
        }
        let moleculeArray = Object.keys(structureHash);

        ic.dAtoms = {};

        for(let i = 0, il = moleculeArray.length; i < il; ++i) {
            let structure = moleculeArray[i];
            //if(i > ic.ALTERNATE_STRUCTURE || (ic.ALTERNATE_STRUCTURE === il - 1 && i === 0) ) {
            let bChoose;
            if(ic.bShift) {
                // default ic.ALTERNATE_STRUCTURE = -1
                if(ic.ALTERNATE_STRUCTURE < 0) ic.ALTERNATE_STRUCTURE = 1;

                bChoose = (i == ic.ALTERNATE_STRUCTURE % il - 1) 
                  || (ic.ALTERNATE_STRUCTURE % il === 0 && i === il - 1);
            } 
            else {
                bChoose = (i == ic.ALTERNATE_STRUCTURE % il + 1) 
                  || (ic.ALTERNATE_STRUCTURE % il === il - 1 && i === 0);
            }

            if(bChoose) {
                for(let k in ic.structures[structure]) {
                    let chain = ic.structures[structure][k];
                    ic.dAtoms = me.hashUtilsCls.unionHash(ic.dAtoms, ic.chains[chain]);
                }

                //ic.ALTERNATE_STRUCTURE = i;
                if(ic.bShift) {
                    --ic.ALTERNATE_STRUCTURE;
                }
                else {
                    ++ic.ALTERNATE_STRUCTURE;
                }

                if(ic.ALTERNATE_STRUCTURE < 0) ic.ALTERNATE_STRUCTURE += il;

                $("#" + ic.pre + "title").html(structure);

                break;
            }
        } 

        if(viewSelectionAtomsCount < allAtomsCount) {
            let tmpAtoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.viewSelectionAtoms);
            if(Object.keys(tmpAtoms).length > 0) {
                ic.dAtoms = me.hashUtilsCls.cloneHash(tmpAtoms);
            }
            
            ic.bShowHighlight = false;
//            ic.opts['rotationcenter'] = 'highlight center';
        }

        // also alternating the surfaces
        ic.applyMapCls.removeSurfaces();
        ic.applyMapCls.applySurfaceOptions();

        ic.applyMapCls.removeMaps();
        ic.applyMapCls.applyMapOptions();

        ic.applyMapCls.removeEmmaps();
        ic.applyMapCls.applyEmmapOptions();

    // disallow the alternation of DelPhi map
        ic.applyMapCls.removePhimaps();
    //    ic.applyMapCls.applyPhimapOptions();
        // should recalculate the potential
        //ic.loadDelphiFileBase('delphi');

    //    ic.applyMapCls.removeSurfaces();
    //    ic.applyMapCls.applyphisurfaceOptions();
        // should recalculate the potential
        //ic.loadDelphiFileBase('delphi2');

        // alternate the PCA axes
        ic.axes = [];
        if(ic.pc1) {
           ic.axesCls.setPc1Axes();
        }

        //ic.glycanCls.showGlycans();

        ic.drawCls.draw();

        ic.bShowHighlight = true;
        ic.opts['rotationcenter'] = 'molecule center';
    }

    alternateWrapper() { let ic = this.icn3d; ic.icn3dui;
       ic.bAlternate = true;
       this.alternateStructures();
       ic.bAlternate = false;
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Draw {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Draw the 3D structure. It rebuilds scene, applies previous color, applies the transformation, and renders the image.
    draw(bVrAr) { let ic = this.icn3d, me = ic.icn3dui;
        if(ic.bRender && (!ic.hAtoms || Object.keys(ic.hAtoms) == 0)) ic.hAtoms = me.hashUtilsCls.cloneHash(ic.atoms);

        ic.sceneCls.rebuildScene();

        // Impostor display using the saved arrays
        if(ic.bImpo) {
            ic.impostorCls.drawImpostorShader(); // target
        }

        ic.setColorCls.applyPrevColor();

        if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1) {        
            if(ic.bAssembly && Object.keys(ic.structures).length == 1 && ((me.cfg.mmdbid === undefined && me.cfg.bu == 1)
              || (me.cfg.mmdbid !== undefined && me.cfg.bu == 1 && Object.keys(ic.atoms).length * ic.biomtMatrices.length > ic.maxatomcnt)) ) {
                ic.instancingCls.drawSymmetryMates();
            }
            else {
                let bNoOrientation = true;
                ic.applyCenterCls.centerSelection(undefined, bNoOrientation);
            }
        }

        // show the hAtoms
        let hAtomsLen = (ic.hAtoms !== undefined) ? Object.keys(ic.hAtoms).length : 0;

        if(hAtomsLen > 0 && hAtomsLen < Object.keys(ic.dAtoms).length) {
            ic.hlObjectsCls.removeHlObjects();
            if(ic.bShowHighlight === undefined || ic.bShowHighlight) ic.hlObjectsCls.addHlObjects();
        }

        if(ic.bRender === true) {
          if(ic.bInitial || $("#" + ic.pre + "wait").is(":visible")) {
              if($("#" + ic.pre + "wait")) $("#" + ic.pre + "wait").hide();
              if($("#" + ic.pre + "canvas")) $("#" + ic.pre + "canvas").show();
              if($("#" + ic.pre + "cmdlog")) $("#" + ic.pre + "cmdlog").show();
          }

          this.applyTransformation(ic._zoomFactor, ic.mouseChange, ic.quaternion);
          this.render(bVrAr);
        }

        ic.impostorCls.clearImpostors();

        // show membranes
        if(ic.bOpm) {
            //if(window.dialog) window.dialog.dialog( "close" );
            
            let html = me.utilsCls.getMemDesc();
            $("#" + ic.pre + "dl_rmsd").html(html);
            if(!me.cfg.bSidebyside) me.htmlCls.dialogCls.openDlg('dl_rmsd', 'Membranes');
        }
    }

    //Update the rotation, translation, and zooming before rendering. Typically used before the function render().
    applyTransformation(_zoomFactor, mouseChange, quaternion) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let para = {};
        para.update = false;

        // zoom
        para._zoomFactor = _zoomFactor;

        // translate
        para.mouseChange = new THREE.Vector2();
        para.mouseChange.copy(mouseChange);

        // rotation
        para.quaternion = new THREE.Quaternion();
        para.quaternion.copy(quaternion);

        if(ic.bControlGl && !me.bNode) {
            window.controls.update(para);
        }
        else {
            ic.controls.update(para);
        }      
    }

    //Render the scene and objects into pixels.
    render(bVrAr) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        // setAnimationLoop is required for VR
        if(bVrAr) {
            ic.renderer.setAnimationLoop( function() {
                thisClass.render_base();
            });
        }
        else {
            thisClass.render_base();
        }
    }

    handleController( controller, dt, selectPressed) { let ic = this.icn3d; ic.icn3dui;
        // modified from https://github.com/NikLever/Learn-WebXR/blob/master/complete/lecture3_7/app.js
        if ( selectPressed ){
/*            
            ic.workingMatrix.identity().extractRotation( controller.matrixWorld );

            ic.raycasterVR.ray.origin.setFromMatrixPosition( controller.matrixWorld );
            ic.raycasterVR.ray.direction.set( 0, 0, - 1 ).applyMatrix4( ic.workingMatrix );

            const intersects = ic.raycasterVR.intersectObjects( ic.objects );

            if (intersects.length>0){
                intersects[0].object.add(ic.highlightVR);
                ic.highlightVR.visible = true;
            }else{
                ic.highlightVR.visible = false;
            }
*/            
            const speed = 5; //2;
            const quaternion = ic.dolly.quaternion.clone();
            //ic.dolly.quaternion.copy(ic.dummyCam.getWorldQuaternion());
            ic.dummyCam.getWorldQuaternion(ic.dolly.quaternion);
            ic.dolly.translateZ(-dt * speed);
            //ic.dolly.position.y = 0; // limit to a plane
            ic.dolly.quaternion.copy(quaternion); 
                    
        }
    }

    //Render the scene and objects into pixels.
    render_base() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        if(me.bNode) return;

        let cam = (ic.bControlGl && !me.bNode) ? window.cam : ic.cam;

        if(ic.directionalLight) {
            let quaternion = new THREE.Quaternion();
            quaternion.setFromUnitVectors( new THREE.Vector3(0, 0, ic.cam_z).normalize(), cam.position.clone().normalize() );

            ic.directionalLight.position.copy(ic.lightPos.clone().applyQuaternion( quaternion ).normalize());
            ic.directionalLight2.position.copy(ic.lightPos2.clone().applyQuaternion( quaternion ).normalize());
            ic.directionalLight3.position.copy(ic.lightPos3.clone().applyQuaternion( quaternion ).normalize());
        }

        ic.renderer.setPixelRatio( window.devicePixelRatio ); // r71

        if(ic.bVr) {
            let dt = 0.04; // ic.clock.getDelta();

            if (ic.controllers){
                // let result = this.getThumbStickMove();
                // let y = result.y * -1;
                // let pressed = result.pressed;

                for(let i = 0, il = ic.controllers.length; i < il; ++i) {
                    let controller = ic.controllers[i];
                    dt = (i % 2 == 0) ? dt : -dt; // dt * y; 
                    thisClass.handleController( controller, dt, controller.userData.selectPressed );
                    //thisClass.handleController( controller, dt, pressed );
                }
            }
        }
        else if(ic.bAr) {
            if ( ic.renderer.xr.isPresenting ){    
                ic.gestures.update();
            }
        }

        if(ic.scene) {
            ic.renderer.render(ic.scene, cam);
        }
    }

    getThumbStickMove() { let ic = this.icn3d; ic.icn3dui;
        let y = 0;
        let btnPressed = false;

        if ( ic.renderer.xr.isPresenting ){
            const session = ic.renderer.xr.getSession();
            const inputSources = session.inputSources;
          
            if ( ic.getInputSources ){    
                //const info = [];
                
                inputSources.forEach( inputSource => {
                    const gp = inputSource.gamepad;
                    gp.axes;
                    gp.buttons;
                    const mapping = gp.mapping;
                    ic.useStandard = (mapping == 'xr-standard');
                    inputSource.handedness;
                    const profiles = inputSource.profiles;
                    ic.gamepadType = "";
                    profiles.forEach( profile => {
                        if (profile.indexOf('touchpad')!=-1) ic.gamepadType = 'touchpad';
                        if (profile.indexOf('thumbstick')!=-1) ic.gamepadType = 'thumbstick';
                    });
                    inputSource.targetRayMode;
                    //info.push({ gamepad, handedness, profiles, targetRayMode });
                });
                 
                ic.getInputSources = false;
            }else if (ic.useStandard && ic.gamepadType != ""){
                inputSources.forEach( inputSource => {
                    const gp = inputSource.gamepad;
                    const thumbstick = (ic.gamepadType=='thumbstick');
                    //{"trigger":{"button":0},"touchpad":{"button":2,"xAxis":0,"yAxis":1}},
                    //"squeeze":{"button":1},"thumbstick":{"button":3,"xAxis":2,"yAxis":3},"button":{"button":6}}}
                    const xaxisOffset = (thumbstick) ? 2 : 0;
                    const btnIndex = (thumbstick) ? 3 : 2;
                    btnPressed = gp.buttons[btnIndex].pressed;
                    // if ( inputSource.handedness == 'right') {
                    // } else if ( inputSource.handedness == 'left') {
                    // }

                    //https://beej.us/blog/data/javascript-gamepad/
                    // x,y-axis values are between -1 and 1
                    gp.axes[xaxisOffset];
                    y = gp.axes[xaxisOffset + 1]; 
                });
            }
        }

        return {'y': y, 'pressed': btnPressed};
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Contact {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

     // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
     //This function returns atoms within a certain "distance" (in angstrom) from the "targetAtoms".
     //The returned atoms are stored in a hash with atom indices as keys and 1 as values.
     //Only those atoms in "allAtoms" are considered.
     getAtomsWithinAtom(atomlist, atomlistTarget, distance, bGetPairs, bInteraction, bInternal, bIncludeTarget) { let ic = this.icn3d; ic.icn3dui;
        let neighbors = this.getNeighboringAtoms(atomlist, atomlistTarget, distance, bIncludeTarget);
        if(bGetPairs) ic.resid2Residhash = {};

        let ret = {};
        for(let i in atomlistTarget) {
            //var oriAtom = atomlistTarget[i];
            let oriAtom = ic.atoms[i];

            let oriCalpha = undefined, oriResidName = undefined;
            let oriResid = oriAtom.structure + '_' + oriAtom.chain + '_' + oriAtom.resi;
            for(let serial in ic.residues[oriResid]) {
                if(!ic.atoms[serial]) continue;

                if((ic.atoms[serial].name === 'CA' && ic.atoms[serial].elem === 'C') || ic.atoms[serial].name === "O3'" || ic.atoms[serial].name === "O3*") {
                    oriCalpha = ic.atoms[serial];
                    break;
                }
            }

            if(oriCalpha === undefined) oriCalpha = oriAtom;

            if(bGetPairs) {
                oriResidName = oriAtom.resn + ' $' + oriAtom.structure + '.' + oriAtom.chain + ':' + oriAtom.resi;
                if(ic.resid2Residhash[oriResidName] === undefined) ic.resid2Residhash[oriResidName] = {};
            }

            let chain_resi = oriAtom.structure + '_' + oriAtom.chain + '_' + oriAtom.resi;

            for (let j in neighbors) {
               let atom = neighbors[j];

               if(!ic.crossstrucinter && oriAtom.structure != atom.structure) continue;

               // exclude the target atoms
               if(!bIncludeTarget && atom.serial in atomlistTarget) continue;
               if(ic.bOpm && atom.resn === 'DUM') continue;

               //var atomDistSq = (atom.coord.x - oriAtom.coord.x) * (atom.coord.x - oriAtom.coord.x) + (atom.coord.y - oriAtom.coord.y) * (atom.coord.y - oriAtom.coord.y) + (atom.coord.z - oriAtom.coord.z) * (atom.coord.z - oriAtom.coord.z);
               let atomDist = atom.coord.distanceTo(oriAtom.coord);

               //if(atomDistSq < maxDistSq) {
               if(atomDist < distance) {
                    ret[atom.serial] = atom;
                    let calpha = undefined, residName = undefined;
                    if(bInteraction) {
                        ret[oriAtom.serial] = oriAtom;
                    }

                    let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
                    for(let serial in ic.residues[resid]) {
                        if( (ic.atoms[serial].name === 'CA' && ic.atoms[serial].elem === 'C') || ic.atoms[serial].name === "O3'" || ic.atoms[serial].name === "O3*") {
                            calpha = ic.atoms[serial];
                            break;
                        }
                    }

                    if(calpha === undefined) calpha = atom;

                        // output contact lines
                    if(bInteraction) {
                        ic.contactpnts.push({'serial': calpha.serial, 'coord': calpha.coord});
                        ic.contactpnts.push({'serial': oriCalpha.serial, 'coord': oriCalpha.coord});
                    }

                    if(bGetPairs) {
        let chain_resi2 = atom.structure + '_' + atom.chain + '_' + atom.resi;

        residName = atom.resn + ' $' + atom.structure + '.' + atom.chain + ':' + atom.resi;
        //var dist = Math.sqrt(atomDistSq).toFixed(1);
        let dist1 = atomDist.toFixed(1);
        let dist2 = calpha.coord.distanceTo(oriCalpha.coord).toFixed(1);

        let resids = chain_resi + '_' + oriAtom.resn + ',' + chain_resi2 + '_' + atom.resn;
        let residNames = oriResidName + ',' + residName;
        if(ic.resids2interAll[resids] === undefined
            || ic.resids2interAll[resids]['contact'] === undefined
            || !ic.resids2interAll[resids]['contact'].hasOwnProperty(residNames)
            || (ic.resids2interAll[resids]['hbond'] !== undefined && !ic.resids2interAll[resids]['hbond'].hasOwnProperty(residNames))
            || (ic.resids2interAll[resids]['ionic'] !== undefined && !ic.resids2interAll[resids]['ionic'].hasOwnProperty(residNames))
            || (ic.resids2interAll[resids]['halogen'] !== undefined && !ic.resids2interAll[resids]['halogen'].hasOwnProperty(residNames))
            || (ic.resids2interAll[resids]['pi-cation'] !== undefined && !ic.resids2interAll[resids]['pi-cation'].hasOwnProperty(residNames))
            || (ic.resids2interAll[resids]['pi-stacking'] !== undefined && !ic.resids2interAll[resids]['pi-stacking'].hasOwnProperty(residNames))
            ) {
              if(ic.resid2Residhash[oriResidName][residName] === undefined || dist1 < ic.resid2Residhash[oriResidName][residName].split('_')[0]) {
                  let cnt = (ic.resid2Residhash[oriResidName][residName] === undefined) ? 1 : parseInt(ic.resid2Residhash[oriResidName][residName].split('_')[4]) + 1;
                  ic.resid2Residhash[oriResidName][residName] = dist1 + '_' + dist2 + '_' + oriAtom.name + '_' + atom.name + '_' + cnt;

                  if(!bInternal) {
                      if(ic.resids2inter[resids] === undefined) ic.resids2inter[resids] = {};
                      if(ic.resids2inter[resids]['contact'] === undefined) ic.resids2inter[resids]['contact'] = {};
                      ic.resids2inter[resids]['contact'][oriResidName + ',' + residName] = dist1 + '_' + dist2 + '_' + oriAtom.name + '_' + atom.name + '_' + cnt;
                  }

                  if(ic.resids2interAll[resids] === undefined) ic.resids2interAll[resids] = {};
                  if(ic.resids2interAll[resids]['contact'] === undefined) ic.resids2interAll[resids]['contact'] = {};
                  ic.resids2interAll[resids]['contact'][oriResidName + ',' + residName] = dist1 + '_' + dist2 + '_' + oriAtom.name + '_' + atom.name + '_' + cnt;
              }
        }
                    } // if(bGetPairs) {
               }
            } // inner for
        } // outer for

        return ret;
     }

     getNeighboringAtoms(atomlist, atomlistTarget, distance, bIncludeTarget) { let ic = this.icn3d; ic.icn3dui;
        let extent = this.getExtent(atomlistTarget);

        let targetRadiusSq1 = (extent[2][0] - extent[0][0]) * (extent[2][0] - extent[0][0]) + (extent[2][1] - extent[0][1]) * (extent[2][1] - extent[0][1]) + (extent[2][2] - extent[0][2]) * (extent[2][2] - extent[0][2]);
        let targetRadiusSq2 = (extent[2][0] - extent[1][0]) * (extent[2][0] - extent[1][0]) + (extent[2][1] - extent[1][1]) * (extent[2][1] - extent[1][1]) + (extent[2][2] - extent[1][2]) * (extent[2][2] - extent[1][2]);
        let targetRadiusSq = (targetRadiusSq1 > targetRadiusSq2) ? targetRadiusSq1 : targetRadiusSq2;
        let targetRadius = Math.sqrt(targetRadiusSq);

        let maxDistSq = (targetRadius + distance) * (targetRadius + distance);

        let neighbors = {};
        for (let i in atomlist) {
           //var atom = atomlist[i];
           let atom = ic.atoms[i];

           // exclude the target atoms
           if(!bIncludeTarget && atomlistTarget.hasOwnProperty(atom.serial)) continue;

           if(this.bOpm && atom.resn === 'DUM') continue;

           if (atom.coord.x < extent[0][0] - distance || atom.coord.x > extent[1][0] + distance) continue;
           if (atom.coord.y < extent[0][1] - distance || atom.coord.y > extent[1][1] + distance) continue;
           if (atom.coord.z < extent[0][2] - distance || atom.coord.z > extent[1][2] + distance) continue;

           // only show protein or DNA/RNA
           //if(atom.serial in this.proteins || atom.serial in this.nucleotides) {
               let atomDistSq = (atom.coord.x - extent[2][0]) * (atom.coord.x - extent[2][0]) + (atom.coord.y - extent[2][1]) * (atom.coord.y - extent[2][1]) + (atom.coord.z - extent[2][2]) * (atom.coord.z - extent[2][2]);

               if(atomDistSq < maxDistSq) {
                   neighbors[atom.serial] = atom;
               }
           //}
        }

        return neighbors;
     }

     // from iview (http://istar.cse.cuhk.edu.hk/iview/)
     //For a list of atoms, return an array containing three coordinates: minimum x- y- z- values,
     //maximum x- y- z- values, and average x- y- z- values.
     getExtent(atomlist) { let ic = this.icn3d; ic.icn3dui;
        let xmin, ymin, zmin;
        let xmax, ymax, zmax;
        let xsum, ysum, zsum, cnt;

        xmin = ymin = zmin = 9999;
        xmax = ymax = zmax = -9999;
        xsum = ysum = zsum = cnt = 0;
        let i;
        for (i in atomlist) {
           //var atom = atomlist[i];
           let atom = ic.atoms[i];
           cnt++;
           xsum += atom.coord.x; ysum += atom.coord.y; zsum += atom.coord.z;


           xmin = (xmin < atom.coord.x) ? xmin : atom.coord.x;

           ymin = (ymin < atom.coord.y) ? ymin : atom.coord.y;
           zmin = (zmin < atom.coord.z) ? zmin : atom.coord.z;
           xmax = (xmax > atom.coord.x) ? xmax : atom.coord.x;
           ymax = (ymax > atom.coord.y) ? ymax : atom.coord.y;
           zmax = (zmax > atom.coord.z) ? zmax : atom.coord.z;
        }

        return [[xmin, ymin, zmin], [xmax, ymax, zmax], [xsum / cnt, ysum / cnt, zsum / cnt]];
     }

    hideContact() { let ic = this.icn3d; ic.icn3dui;
        ic.opts["contact"] = "no";
        if(ic.lines === undefined) ic.lines = { };
        ic.lines['contact'] = [];
        ic.contactpnts = [];

        for(let i in ic.atoms) {
            ic.atoms[i].style2 = 'nothing';
        }

        for(let i in ic.sidec) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style2 = ic.opts["sidec"];
            }
        }

        for(let i in ic.water) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style = ic.opts["water"];
            }
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class HBond {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //http://www.imgt.org/IMGTeducation/Aide-memoire/_UK/aminoacids/charge/#hydrogen
    // return: 'donor', 'acceptor', 'both', 'ring', 'none'
    isHbondDonorAcceptor(atom) { let ic = this.icn3d; ic.icn3dui;
      if( (atom.name == 'N' && !atom.het ) // backbone
        || (atom.elem == 'N' && atom.resn == 'Arg')
        || (atom.elem == 'N' && atom.resn == 'Asn')
        || (atom.elem == 'N' && atom.resn == 'Gln')
        || (atom.elem == 'N' && atom.resn == 'Lys')
        || (atom.elem == 'N' && atom.resn == 'Trp')
        ) {
          return 'donor';
      }
      else if( (atom.name == 'O' && !atom.het ) // backbone
        || (atom.elem == 'S' && atom.resn == 'Met')
        || (atom.elem == 'O' && atom.resn == 'Asn')
        || (atom.elem == 'O' && atom.resn == 'Asp')
        || (atom.elem == 'O' && atom.resn == 'Gln')
        || (atom.elem == 'O' && atom.resn == 'Glu')
        ) {
          return 'acceptor';
      }
      else if((atom.elem == 'S' && atom.resn == 'Cys')
        || (atom.elem == 'N' && atom.resn == 'His')
        || (atom.elem == 'O' && atom.resn == 'Ser')
        || (atom.elem == 'O' && atom.resn == 'Thr')
        || (atom.elem == 'O' && atom.resn == 'Tyr')
        ) {
          return 'both';
      }
      else if(atom.resn == 'Pro') {
          return 'none';
      }
      // if the Nitrogen has one or two non-hydrogen bonded atom, the nitrogen is a donor
      else if(atom.elem == 'N') {
          // X-ray can not differentiate N and O
          if(atom.resn == 'Asn' || atom.resn == 'Gln') return 'both';

          let cnt = 0, cntN = 0;
          for(let k = 0, kl = atom.bonds.length; k < kl; ++k) {
              if(ic.atoms[atom.bonds[k]].elem == 'H') {
                  ++cnt;
              }
          }

          if(cnt == 2) return 'donor';

          cnt = 0;
          for(let i = 0, il = atom.bonds.length; i < il; ++i) {
              let nbAtom = ic.atoms[atom.bonds[i]];
              if(nbAtom.elem != 'H') {
                  ++cnt;

                  for(let j = 0, jl = nbAtom.bonds.length; j < jl; ++j) {
                      if(ic.atoms[nbAtom.bonds[j]].elem == 'N') {
                          ++cntN;
                      }
                  }
              }
          }

          if(cnt == 1) { // donor
              return 'donor';
          }
          else if(cnt == 2) {
              if(cntN > 1) {
                  return 'ring'; //'both'; // possible
              }
              else {
                return 'donor';
              }
          }
          else {
              return 'none';
          }
      }
      // if the neighboring C of Oxygen has two or more bonds with O or N, the oxygen is an acceptor
      else if(atom.elem == 'O' && atom.bonds.length == 1) {
          // X-ray can not differentiate N and O
          if(atom.resn == 'Asn' || atom.resn == 'Gln') return 'both';

          for(let k = 0, kl = atom.bonds.length; k < kl; ++k) {
              if(ic.atoms[atom.bonds[k]].elem == 'H') {
                  return 'donor';
              }
          }

          let cAtom = ic.atoms[atom.bonds[0]];
          let cnt = 0;
          for(let k = 0, kl = cAtom.bonds.length; k < kl; ++k) {
              if(ic.atoms[cAtom.bonds[k]].elem == 'O' || ic.atoms[cAtom.bonds[k]].elem == 'N' || ic.atoms[cAtom.bonds[k]].elem == 'S') {
                  ++cnt;
              }
          }

          if(cnt >= 2) { // acceptor
              return 'acceptor';
          }
          else {
              return 'both'; // possible
          }
      }
      // if Oxygen has two bonds, the oxygen is an acceptor
      else if(atom.elem == 'O' && atom.bonds.length == 2) {
          for(let k = 0, kl = atom.bonds.length; k < kl; ++k) {
              if(ic.atoms[atom.bonds[k]].elem == 'H') {
                  return 'donor';
              }
          }
          return 'acceptor';
      }
      else {
          return 'both'; // possible
      }
    }

    /**
     * From ngl https://github.com/arose/ngl
     * Calculate the angles x-1-2 for all x where x is a heavy atom bonded to ap1.
     * @param  {AtomProxy} ap1 First atom (angle centre)
     * @param  {AtomProxy} ap2 Second atom
     * @return {number[]}        Angles in radians
     */
    calcAngles(ap1, ap2) { let ic = this.icn3d; ic.icn3dui;
      let angles = [];
      let d1 = new THREE.Vector3();
      let d2 = new THREE.Vector3();
      d1.subVectors(ap2.coord, ap1.coord);

      for(let k = 0, kl = ap1.bonds.length; k < kl; ++k) {
          if(ic.atoms[ap1.bonds[k]].elem != 'H') {
              d2.subVectors(ic.atoms[ap1.bonds[k]].coord, ap1.coord);
              angles.push(d1.angleTo(d2));
          }
      }

      return angles;
    }

    /**
     * From ngl https://github.com/arose/ngl
     * Find two neighbours of ap1 to define a plane (if possible) and
     * measure angle out of plane to ap2
     * @param  {AtomProxy} ap1 First atom (angle centre)
     * @param  {AtomProxy} ap2 Second atom (out-of-plane)
     * @return {number}        Angle from plane to second atom
     */
    calcPlaneAngle(ap1, ap2) { let ic = this.icn3d; ic.icn3dui;
      let x1 = ap1;

      let v12 = new THREE.Vector3();
      v12.subVectors(ap2.coord, ap1.coord);

      let neighbours = [new THREE.Vector3(), new THREE.Vector3()];

      let ni = 0;
      for(let k = 0, kl = ap1.bonds.length; k < kl; ++k) {
          if (ni > 1) { break; }
          if(ic.atoms[ap1.bonds[k]].elem != 'H') {
              x1 = ic.atoms[ap1.bonds[k]];
              neighbours[ni++].subVectors(ic.atoms[ap1.bonds[k]].coord, ap1.coord);
          }
      }

      if (ni === 1) {
          for(let k = 0, kl = x1.bonds.length; k < kl; ++k) {
              if (ni > 1) { break; }
              if(ic.atoms[x1.bonds[k]].elem != 'H' && ic.atoms[x1.bonds[k]].serial != ap1.serial) {
                  neighbours[ni++].subVectors(ic.atoms[x1.bonds[k]].coord, ap1.coord);
              }
          }
      }

      if (ni !== 2) {
        return;
      }

      let cp = neighbours[0].cross(neighbours[1]);
      return Math.abs((Math.PI / 2) - cp.angleTo(v12));
    }

    // https://www.rcsb.org/pages/help/3dview#ligand-view
    // exclude pairs accordingto angles
    isValidHbond(atom, atomHbond, threshold) { let ic = this.icn3d; ic.icn3dui;
          // return: 'donor', 'acceptor', 'both', 'ring', 'none'
          let atomType = this.isHbondDonorAcceptor(atom);
          let atomHbondType = this.isHbondDonorAcceptor(atomHbond);

          let tolerance = 5;
          let maxHbondAccAngle = (45 + tolerance) * Math.PI / 180;
          let maxHbondDonAngle = (45 + tolerance) * Math.PI / 180;
          let maxHbondAccPlaneAngle = 90 * Math.PI / 180;
          let maxHbondDonPlaneAngle = 30 * Math.PI / 180;

          let donorAtom, acceptorAtom;

          if( (atomType == 'donor' &&  (atomHbondType == 'acceptor' || atomHbondType == 'both' || atomHbondType == 'ring'))
            || (atomHbondType == 'acceptor' && (atomType == 'donor' || atomType == 'both' || atomType == 'ring'))
            ) {
              donorAtom = atom;
              acceptorAtom = atomHbond;
          }
          else if( (atomType == 'acceptor' &&  (atomHbondType == 'donor' || atomHbondType == 'both' || atomHbondType == 'ring'))
            || (atomHbondType == 'donor' && (atomType == 'acceptor' || atomType == 'both' || atomType == 'ring'))
            ) {
              acceptorAtom = atom;
              donorAtom = atomHbond;
          }
          else if( (atomType == 'both' || atomType == 'ring') &&  (atomHbondType == 'both'  || atomHbondType == 'ring') ) {
              donorAtom = atom;
              acceptorAtom = atomHbond;
              // or
              //donorAtom = atomHbond;
              //acceptorAtom = atom;

              if( (ic.nucleotides.hasOwnProperty(atom.serial) && ic.nucleotides.hasOwnProperty(atomHbond.serial) && (atomType == 'ring' || atomHbondType == 'ring') ) // 1TUP
                  || ( (atom.het || atomHbond.het) && atomType == 'ring' && atomHbondType == 'ring')  // 3GVU
                  ) ;
              else {
                  maxHbondDonPlaneAngle = 90 * Math.PI / 180;
              }
          }
          else if(atomType == 'none' ||  atomHbondType == 'none') {
              return false;
          }
          else {
              return false;
          }

          let donorAngles = this.calcAngles(donorAtom, acceptorAtom);
          let idealDonorAngle = 90 * Math.PI / 180; // 90 for sp2, 60 for sp3
          for(let i = 0, il = donorAngles.length; i < il; ++i) {
              if(Math.abs(idealDonorAngle - donorAngles[i]) > maxHbondDonAngle) {
// commented out on Nov 19, 2021
// uncommented on Sep 8, 2022 since these conditions should be used for nucleotides
                  return false;
              }
          }

          //if (idealGeometry[donor.index] === AtomGeometry.Trigonal){ // 120
            let outOfPlane1 = this.calcPlaneAngle(donorAtom, acceptorAtom);

            if (outOfPlane1 !== undefined && outOfPlane1 > maxHbondDonPlaneAngle) {
                return false;
            }
          //}

          let acceptorAngles = this.calcAngles(acceptorAtom, donorAtom);
          let idealAcceptorAngle = 90 * Math.PI / 180;
          for(let i = 0, il = acceptorAngles.length; i < il; ++i) {
              if(Math.abs(idealAcceptorAngle - acceptorAngles[i]) > maxHbondAccAngle) {
// commented out on Nov 19, 2021, but keep it for nucleotides
// uncommented on Sep 8, 2022 since these conditions should be used for nucleotides
                  return false;
              }
          }

          //if (idealGeometry[acceptor.index] === AtomGeometry.Trigonal){ // 120
            let outOfPlane2 = this.calcPlaneAngle(acceptorAtom, donorAtom);
            if (outOfPlane2 !== undefined && outOfPlane2 > maxHbondAccPlaneAngle) return false;
          //}

          return true;
    }

    //Set up hydrogen bonds between chemical and protein/nucleotide in the same structure.
    //"protein" and "chemicals" are hashes with atom indices as keys and 1 as values.
    //"threshold" is the maximum distance of hydrogen bonds and has the unit of angstrom.
    calculateChemicalHbonds(startAtoms, targetAtoms, threshold, bSaltbridge, type, bInternal) { let ic = this.icn3d, me = ic.icn3dui;
        if(Object.keys(startAtoms).length === 0 || Object.keys(targetAtoms).length === 0) return;

        ic.resid2Residhash = {};

        let atomHbond = {};
        let chain_resi, chain_resi_atom;

        let maxlengthSq = threshold * threshold;

        for (let i in startAtoms) {
          let atom = startAtoms[i];

          // salt bridge: calculate hydrogen bond between Lys/Arg and Glu/Asp
          // hbonds: calculate hydrogen bond
          let bAtomCond = (bSaltbridge) ? ( atom.resn === 'LYS' && atom.elem === "N" && atom.name !== "N")
            || ( atom.resn === 'ARG' && (atom.name === "NH1" || atom.name === "NH2"))
            || ( (atom.resn === 'GLU' || atom.resn === 'ASP') && atom.elem === "O" && atom.name !== "O")
            || (atom.het && (atom.elem === "N" || atom.elem === "O" || atom.elem === "S"))
            : atom.elem === "N" || atom.elem === "O" || (atom.elem === "S" && (atom.het || atom.resn === "Cys" || atom.resn === "Met"));

          bAtomCond = (ic.bOpm) ? bAtomCond && atom.resn !== 'DUM' : bAtomCond;

          if(bAtomCond) {
            chain_resi = atom.structure + "_" + atom.chain + "_" + atom.resi;
            chain_resi_atom = chain_resi + "_" + atom.name;

            atomHbond[chain_resi_atom] = atom;
          }
        } // end of for (let i in startAtoms) {

        let hbondsAtoms = {};
        let residueHash = {};

        // from DSSP C++ code
        //var kSSBridgeDistance = 3.0;
        let kMinimalDistance = 0.5;
        //var kMinimalCADistance = 9.0;
        let kMinHBondEnergy = -9.9;
        let kMaxHBondEnergy = -0.5;
        let kCouplingConstant = -27.888;    //  = -332 * 0.42 * 0.2
        //var kMaxPeptideBondLength = 2.5;

        let hbondCnt = {};

        for (let i in targetAtoms) {
          let atom = targetAtoms[i];

          // salt bridge: calculate hydrogen bond between Lys/Arg and Glu/Asp
          // hbonds: calculate hydrogen bond
          let bAtomCond = (bSaltbridge) ? ( atom.resn === 'LYS' && atom.elem === "N" && atom.name !== "N")
            || ( atom.resn === 'ARG' && (atom.name === "NH1" || atom.name === "NH2"))
            || ( (atom.resn === 'GLU' || atom.resn === 'ASP') && atom.elem === "O" && atom.name !== "O")
            || (atom.het && (atom.elem === "N" || atom.elem === "O" || atom.elem === "S") )
            : atom.elem === "N" || atom.elem === "O" || (atom.elem === "S" && (atom.het || atom.resn === "Cys" || atom.resn === "Met"));

          bAtomCond = (ic.bOpm) ? bAtomCond && atom.resn !== 'DUM' : bAtomCond;
          if(bAtomCond) {
            chain_resi = atom.structure + "_" + atom.chain + "_" + atom.resi;
            chain_resi_atom = chain_resi + "_" + atom.name;

            //var oriResidName = atom.resn + ' ' + chain_resi_atom;
            let oriResidName = atom.resn + ' $' + atom.structure + '.' + atom.chain + ':' + atom.resi + '@' + atom.name;
            if(ic.resid2Residhash[oriResidName] === undefined) ic.resid2Residhash[oriResidName] = {};

            for (let j in atomHbond) {
              if(bSaltbridge) {
                  // skip both positive orboth negative cases
                  if( ( (atom.resn === 'LYS' || atom.resn === 'ARG') && (atomHbond[j].resn === 'LYS' || atomHbond[j].resn === 'ARG') ) ||
                    ( (atom.resn === 'GLU' || atom.resn === 'ASP') && (atomHbond[j].resn === 'GLU' || atomHbond[j].resn === 'ASP') ) ) {
                        continue;
                    }
              }

              if(!ic.crossstrucinter && atom.structure != atomHbond[j].structure) continue;

              // skip same residue
              if(chain_resi == j.substr(0, j.lastIndexOf('_') ) ) continue;

              let xdiff = Math.abs(atom.coord.x - atomHbond[j].coord.x);
              if(xdiff > threshold) continue;

              let ydiff = Math.abs(atom.coord.y - atomHbond[j].coord.y);
              if(ydiff > threshold) continue;

              let zdiff = Math.abs(atom.coord.z - atomHbond[j].coord.z);
              if(zdiff > threshold) continue;

              let dist = xdiff * xdiff + ydiff * ydiff + zdiff * zdiff;
              if(dist > maxlengthSq) continue;

              if(ic.proteins.hasOwnProperty(atom.serial) && ic.proteins.hasOwnProperty(atomHbond[j].serial)
                && (atom.name === 'N' || atom.name === 'O') && (atomHbond[j].name === 'O' || atomHbond[j].name === 'N') ) {

                if(atom.name === atomHbond[j].name) continue;
                if(atom.structure == atomHbond[j].structure && atom.chain == atomHbond[j].chain && Math.abs(atom.resi - atomHbond[j].resi) <= 1) continue; // peptide bond

                // protein backbone hydrogen
                // https://en.wikipedia.org/wiki/DSSP_(hydrogen_bond_estimation_algorithm)
                let result;

                let inDonor = (atom.name === 'N') ? atom : atomHbond[j];
                let inAcceptor = (atom.name === 'O') ? atom : atomHbond[j];

                if (inDonor.resn === 'Pro') {
                    continue;
                }
                else if (inDonor.hcoord === undefined) {
                    if(!this.isValidHbond(atom, atomHbond[j], threshold)) continue;
                }
                else {
                    let inDonorH = inDonor.hcoord;
                    let inDonorN = inDonor.coord;

                    let resid = inAcceptor.structure + "_" + inAcceptor.chain + "_" + inAcceptor.resi;
                    let C_atom;
                    for(let serial in ic.residues[resid]) {
                        if(ic.atoms[serial].name === 'C') {
                            C_atom = ic.atoms[serial];
                            break;
                        }
                    }

                    if(!C_atom) continue;

                    let inAcceptorC = C_atom.coord;
                    let inAcceptorO = inAcceptor.coord;

                    let distanceHO = inDonorH.distanceTo(inAcceptorO);
                    let distanceHC = inDonorH.distanceTo(inAcceptorC);
                    let distanceNC = inDonorN.distanceTo(inAcceptorC);
                    let distanceNO = inDonorN.distanceTo(inAcceptorO);

                    if (distanceHO < kMinimalDistance || distanceHC < kMinimalDistance || distanceNC < kMinimalDistance || distanceNO < kMinimalDistance) {
                        result = kMinHBondEnergy;
                    }
                    else {
                        result = kCouplingConstant / distanceHO - kCouplingConstant / distanceHC + kCouplingConstant / distanceNC - kCouplingConstant / distanceNO;
                    }

                    //if(result > kMaxHBondEnergy) {
                    if(atom.ss == 'helix' && atomHbond[j].ss == 'helix' && result > kMaxHBondEnergy) ;
                }
              }
              else {
                  if(!this.isValidHbond(atom, atomHbond[j], threshold)) continue;
              }

              // too many hydrogen bonds for one atom
              if(hbondCnt[atom.serial] > 2 || hbondCnt[atomHbond[j].serial] > 2) {
                  continue;
              }

              if(hbondCnt[atom.serial] === undefined) {
                  hbondCnt[atom.serial] = 1;
              }
              else {
                  ++hbondCnt[atom.serial];
              }

              if(hbondCnt[atomHbond[j].serial] === undefined) {
                  hbondCnt[atomHbond[j].serial] = 1;
              }
              else {
                  ++hbondCnt[atomHbond[j].serial];
              }

              // output hydrogen bonds
              if(type !== 'graph') {
                  if(bSaltbridge) {
                      ic.saltbridgepnts.push({'serial': atom.serial, 'coord': atom.coord});
                      ic.saltbridgepnts.push({'serial': atomHbond[j].serial, 'coord': atomHbond[j].coord});
                  }
                  else {
                      ic.hbondpnts.push({'serial': atom.serial, 'coord': atom.coord});
                      ic.hbondpnts.push({'serial': atomHbond[j].serial, 'coord': atomHbond[j].coord});
                  }
              }

              let chain_resi2 = atomHbond[j].structure + "_" + atomHbond[j].chain + "_" + atomHbond[j].resi;
              hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[chain_resi]);
              hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[chain_resi2]);

              residueHash[chain_resi] = 1;
              residueHash[chain_resi2] = 1;

              //var residName = atomHbond[j].resn + " " + atomHbond[j].structure + "_" + atomHbond[j].chain + "_" + atomHbond[j].resi + '_' + atomHbond[j].name;
              let residName = atomHbond[j].resn + ' $' + atomHbond[j].structure + '.' + atomHbond[j].chain + ':' + atomHbond[j].resi + '@' + atomHbond[j].name;

              let resids = chain_resi + '_' + atom.resn + ',' + chain_resi2 + '_' + atomHbond[j].resn;

              if(ic.resids2interAll[resids] === undefined
                || ic.resids2interAll[resids]['ionic'] === undefined
                || !ic.resids2interAll[resids]['ionic'].hasOwnProperty(oriResidName + ',' + residName) ) {
                  ic.resid2Residhash[oriResidName][residName] = dist.toFixed(1);

                  if(!bInternal) {
                      if(ic.resids2inter[resids] === undefined) ic.resids2inter[resids] = {};
                      if(ic.resids2inter[resids]['hbond'] === undefined) ic.resids2inter[resids]['hbond'] = {};
                      ic.resids2inter[resids]['hbond'][oriResidName + ',' + residName] = dist.toFixed(1);
                  }

                  if(ic.resids2interAll[resids] === undefined) ic.resids2interAll[resids] = {};
                  if(ic.resids2interAll[resids]['hbond'] === undefined) ic.resids2interAll[resids]['hbond'] = {};
                  ic.resids2interAll[resids]['hbond'][oriResidName + ',' + residName] = dist.toFixed(1);
              }
            } // end of for (let j in atomHbond) {
          }
        } // end of for (let i in targetAtoms) {

        let residueArray = Object.keys(residueHash);

        // draw sidec for these residues
        if(type !== 'graph') {
            for(let i = 0, il = residueArray.length; i < il; ++i) {
                for(let j in ic.residues[residueArray[i]]) {
                    // all atoms should be shown for hbonds
                    ic.atoms[j].style2 = 'stick';
                }
            }
        }

        return hbondsAtoms;
    }

    setHbondsContacts(options, type) { let ic = this.icn3d; ic.icn3dui;
        let hbond_contact = type;
        let hbonds_contact = (type == 'hbond') ? 'hbonds' : type;

        ic.lines[hbond_contact] = [];

        if (options[hbonds_contact].toLowerCase() === 'yes') {
            let color;
            let pnts;
            if(type == 'hbond') {
                pnts = ic.hbondpnts;
                color = '#0F0';
            }
            else if(type == 'saltbridge') {
                pnts = ic.saltbridgepnts;
                color = '#0FF';
            }
            else if(type == 'contact') {
                pnts = ic.contactpnts;
                color = '#888';
            }
            else if(type == 'halogen') {
                pnts = ic.halogenpnts;
                color = '#F0F';
            }
            else if(type == 'pi-cation') {
                pnts = ic.picationpnts;
                color = '#F00';
            }
            else if(type == 'pi-stacking') {
                pnts = ic.pistackingpnts;
                color = '#00F';
            }

             for (let i = 0, lim = Math.floor(pnts.length / 2); i < lim; i++) {
                let line = {    };
                line.position1 = pnts[2 * i].coord;
                line.serial1 = pnts[2 * i].serial;
                line.position2 = pnts[2 * i + 1].coord;
                line.serial2 = pnts[2 * i + 1].serial;
                line.color = color;
                line.dashed = true;

                // only draw bonds connected with currently displayed atoms
                if(line.serial1 !== undefined && line.serial2 !== undefined && !ic.dAtoms.hasOwnProperty(line.serial1) && !ic.dAtoms.hasOwnProperty(line.serial2)) continue;

                //if(ic.lines[hbond_contact] === undefined) ic.lines[hbond_contact] = [];
                ic.lines[hbond_contact].push(line);
             }
        }
    }

    //Remove hydrogen bonds.
    hideHbonds() { let ic = this.icn3d; ic.icn3dui;
        ic.opts["hbonds"] = "no";
        if(ic.lines === undefined) ic.lines = { };
        ic.lines['hbond'] = [];
        ic.hbondpnts = [];

        for(let i in ic.atoms) {
            ic.atoms[i].style2 = 'nothing';
        }

        for(let i in ic.sidec) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style2 = ic.opts["sidec"];
            }
        }

        for(let i in ic.water) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style = ic.opts["water"];
            }
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class PiHalogen {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // get halogen, pi-cation,and pi-stacking
    calculateHalogenPiInteractions(startAtoms, targetAtoms, threshold, type, interactionType, bInternal) { let ic = this.icn3d, me = ic.icn3dui;
        if(Object.keys(startAtoms).length === 0 || Object.keys(targetAtoms).length === 0) return;

        let atoms1a = {}, atoms1b = {}, atoms2a = {}, atoms2b = {};
        if(interactionType == 'halogen') {
            for (let i in startAtoms) {
              let atom = startAtoms[i];

              atoms1a = me.hashUtilsCls.unionHash(atoms1a, this.getHalogenDonar(atom));
              atoms2a = me.hashUtilsCls.unionHash(atoms2a, this.getHalogenAcceptor(atom));
            }

            for (let i in targetAtoms) {
              let atom = targetAtoms[i];

              atoms2b = me.hashUtilsCls.unionHash(atoms2b, this.getHalogenDonar(atom));
              atoms1b = me.hashUtilsCls.unionHash(atoms1b, this.getHalogenAcceptor(atom));
            }
        }
        else if(interactionType == 'pi-cation') {
            ic.processedRes = {};
            for (let i in startAtoms) {
              let atom = startAtoms[i];

              atoms1a = me.hashUtilsCls.unionHash(atoms1a, this.getPi(atom, false));
              atoms2a = me.hashUtilsCls.unionHash(atoms2a, this.getCation(atom));
            }

            ic.processedRes = {};
            for (let i in targetAtoms) {
              let atom = targetAtoms[i];

              atoms2b = me.hashUtilsCls.unionHash(atoms2b, this.getPi(atom, false));
              atoms1b = me.hashUtilsCls.unionHash(atoms1b, this.getCation(atom));
            }
        }
        else if(interactionType == 'pi-stacking') {
            ic.processedRes = {};
            for (let i in startAtoms) {
              let atom = startAtoms[i];
              atoms1a = me.hashUtilsCls.unionHash(atoms1a, this.getPi(atom, true));
            }

            ic.processedRes = {};
            for (let i in targetAtoms) {
              let atom = targetAtoms[i];

              atoms1b = me.hashUtilsCls.unionHash(atoms1b, this.getPi(atom, true));
            } // for
        }

        let hbondsAtoms = {};
        let residueHash = {};

        ic.resid2Residhash = {};

        let maxlengthSq = threshold * threshold;

        for (let i in atoms1a) {
            let atom1 = atoms1a[i];
            let oriResidName = atom1.resn + ' $' + atom1.structure + '.' + atom1.chain + ':' + atom1.resi + '@' + atom1.name;
            if(ic.resid2Residhash[oriResidName] === undefined) ic.resid2Residhash[oriResidName] = {};

            for (let j in atoms1b) {
              let atom2 = atoms1b[j];

              if(!ic.crossstrucinter && atom1.structure != atom2.structure) continue;

              // skip same residue
              if(i.substr(0, i.lastIndexOf('_')) == j.substr(0, j.lastIndexOf('_')) ) continue;

              // available in 1b and 2a
              if(interactionType == 'pi-cation' && atom2.resn === 'ARG' && atom2.name === "NH1") {
                let resid2 = atom2.structure + '_' + atom2.chain + '_' + atom2.resi;
                let otherAtom = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid2], 'NH2');

                let coord = atom2.coord.clone().add(otherAtom.coord).multiplyScalar(0.5);
                atom2 = me.hashUtilsCls.cloneHash(atom2);
                atom2.coord = coord;
              }

              // available in 1a and 1b
              // only parallel or perpendicular
              if(interactionType == 'pi-stacking' && atom1.normal !== undefined && atom2.normal !== undefined) {
                  Math.abs(atom1.normal.dot(atom2.normal));
                  // perpendicular 30 degree || parellel, 30 degree
                  // remove this condition on Nov 19, 2021
                  //if(dotResult > 0.5 && dotResult < 0.866) continue;
              }

              let bResult = this.getHalogenPiInteractions(atom1, atom2, type, interactionType, threshold, maxlengthSq, oriResidName, bInternal);

              if(bResult) {
                  hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[atom1.structure + "_" + atom1.chain + "_" + atom1.resi]);
                  hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[atom2.structure + "_" + atom2.chain + "_" + atom2.resi]);

                  residueHash[atom1.structure + "_" + atom1.chain + "_" + atom1.resi] = 1;
                  residueHash[atom2.structure + "_" + atom2.chain + "_" + atom2.resi] = 1;
              }
            }
        }

        for (let i in atoms2a) {
            let atom1 = atoms2a[i];
            let oriResidName = atom1.resn + ' $' + atom1.structure + '.' + atom1.chain + ':' + atom1.resi + '@' + atom1.name;
            if(ic.resid2Residhash[oriResidName] === undefined) ic.resid2Residhash[oriResidName] = {};

            // available in 1b and 2a
            if(interactionType == 'pi-cation' && atom1.resn === 'ARG' && atom1.name === "NH1") {
                let resid1 = atom1.structure + '_' + atom1.chain + '_' + atom1.resi;
                let otherAtom = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid1], 'NH2');

                let coord = atom1.coord.clone().add(otherAtom.coord).multiplyScalar(0.5);
                atom1 = me.hashUtilsCls.cloneHash(atom1);
                atom1.coord = coord;
            }

            for (let j in atoms2b) {
              let atom2 = atoms2b[j];

              if(!ic.crossstrucinter && atom1.structure != atom2.structure) continue;

              // skip same residue
              if(i.substr(0, i.lastIndexOf('_')) == j.substr(0, j.lastIndexOf('_')) ) continue;

              let bResult = this.getHalogenPiInteractions(atom1, atom2, type, interactionType, threshold, maxlengthSq, oriResidName, bInternal);

              if(bResult) {
                  hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[atom1.structure + "_" + atom1.chain + "_" + atom1.resi]);
                  hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[atom2.structure + "_" + atom2.chain + "_" + atom2.resi]);

                  residueHash[atom1.structure + "_" + atom1.chain + "_" + atom1.resi] = 1;
                  residueHash[atom2.structure + "_" + atom2.chain + "_" + atom2.resi] = 1;
              }
            }
        }

        let residueArray = Object.keys(residueHash);

        // draw sidec for these residues
        if(type !== 'graph') {
            for(let i = 0, il = residueArray.length; i < il; ++i) {
                for(let j in ic.residues[residueArray[i]]) {
                    // all atoms should be shown for hbonds
                    ic.atoms[j].style2 = 'stick';
                    if(ic.ions.hasOwnProperty(j)) ic.atoms[j].style2 = 'sphere';
                }
            }
        }

        return hbondsAtoms;
    }

    getHalogenDonar(atom) { let ic = this.icn3d; ic.icn3dui;
          let name2atom = {};
          //if(atom.elem === "F" || atom.elem === "CL" || atom.elem === "BR" || atom.elem === "I") {
          if(atom.elem === "CL" || atom.elem === "BR" || atom.elem === "I") {
              let chain_resi_atom = atom.structure + "_" + atom.chain + "_" + atom.resi + "_" + atom.name;
              name2atom[chain_resi_atom] = atom;
          }

          return name2atom;
    }

    getHalogenAcceptor(atom) { let ic = this.icn3d; ic.icn3dui;
          let name2atom = {};
          let bAtomCond = (atom.elem === "N" || atom.elem === "O" || atom.elem === "S");
          bAtomCond = (ic.bOpm) ? bAtomCond && atom.resn !== 'DUM' : bAtomCond;
          if(bAtomCond) {
              let chain_resi_atom = atom.structure + "_" + atom.chain + "_" + atom.resi + "_" + atom.name;
              name2atom[chain_resi_atom] = atom;
          }

          return name2atom;
    }

    getPi(atom, bStacking) { let ic = this.icn3d, me = ic.icn3dui;
          let name2atom = {};

          let chain_resi = atom.structure + "_" + atom.chain + "_" + atom.resi;

          let bAromatic = atom.het || ic.nucleotides.hasOwnProperty(atom.serial) || atom.resn === "PHE"
            || atom.resn === "TYR" || atom.resn === "TRP";
          if(bStacking) bAromatic = bAromatic || atom.resn === "HIS";

          if(bAromatic) {
              if(!ic.processedRes.hasOwnProperty(chain_resi)) {

                  if(atom.het) { // get aromatic for ligands
                      let currName2atom = this.getAromaticPisLigand(chain_resi);
                      name2atom = me.hashUtilsCls.unionHash(name2atom, currName2atom);
                  }
                  else {
                      let piPosArray = undefined, normalArray = undefined, result = undefined;
                      if(ic.nucleotides.hasOwnProperty(atom.serial)) {
                          result = this.getAromaticRings(atom.resn, chain_resi, 'nucleotide');
                      }
                      else {
                          result = this.getAromaticRings(atom.resn, chain_resi, 'protein');
                      }

                      if(result !== undefined) {
                          piPosArray = result.piPosArray;
                          normalArray = result.normalArray;
                      }

                      for(let i = 0, il = piPosArray.length; i < il; ++i) {
                        name2atom[chain_resi + '_pi' + i] = {resn: atom.resn, name: 'pi' + i, coord: piPosArray[i], serial: atom.serial,
                        structure: atom.structure, chain: atom.chain, resi: atom.resi, normal: normalArray[i]};
                      }
                  }

                  ic.processedRes[chain_resi] = 1;
              }
          }

          return name2atom;
    }

    getCation(atom) { let ic = this.icn3d, me = ic.icn3dui;
          let name2atom = {};

          // use of the two atoms
          if( atom.resn === 'ARG' && atom.name === "NH2") return;

          // remove HIS:  || atom.resn === 'HIS'
          // For ligands, "N" with one single bond only may be positively charged. => to be improved
          let bAtomCond = ( atom.resn === 'LYS' && atom.elem === "N" && atom.name !== "N")
            || ( atom.resn === 'ARG' && (atom.name === "NH1" || atom.name === "NH2"))
            || (atom.het && me.parasCls.cationsTrimArray.indexOf(atom.elem) !== -1)
            || (atom.het && atom.elem === "N" && (atom.bonds.length == 1 || atom.bonds.length == 4) ); // ligand in PDB 2ACE
          bAtomCond = (ic.bOpm) ? bAtomCond && atom.resn !== 'DUM' : bAtomCond;
          if(bAtomCond) {
              let chain_resi_atom = atom.structure + "_" + atom.chain + "_" + atom.resi + "_" + atom.name;
              name2atom[chain_resi_atom] = atom;
          }

          return name2atom;
    }

    getHalogenPiInteractions(atom1, atom2, type, interactionType, threshold, maxlengthSq, oriResidName, bInternal) { let ic = this.icn3d; ic.icn3dui;
          let xdiff = Math.abs(atom1.coord.x - atom2.coord.x);
          if(xdiff > threshold) return false;

          let ydiff = Math.abs(atom1.coord.y - atom2.coord.y);
          if(ydiff > threshold) return false;

          let zdiff = Math.abs(atom1.coord.z - atom2.coord.z);
          if(zdiff > threshold) return false;

          let dist = xdiff * xdiff + ydiff * ydiff + zdiff * zdiff;
          if(dist > maxlengthSq) return false;

          // output salt bridge
          if(type !== 'graph') {
              if(interactionType == 'halogen') {
                  ic.halogenpnts.push({'serial': atom1.serial, 'coord': atom1.coord});
                  ic.halogenpnts.push({'serial': atom2.serial, 'coord': atom2.coord});
              }
              else if(interactionType == 'pi-cation') {
                  ic.picationpnts.push({'serial': atom1.serial, 'coord': atom1.coord});
                  ic.picationpnts.push({'serial': atom2.serial, 'coord': atom2.coord});
              }
              else if(interactionType == 'pi-stacking') {
                  ic.pistackingpnts.push({'serial': atom1.serial, 'coord': atom1.coord});
                  ic.pistackingpnts.push({'serial': atom2.serial, 'coord': atom2.coord});
              }
          }

          let residName = atom2.resn + ' $' + atom2.structure + '.' + atom2.chain + ':' + atom2.resi + '@' + atom2.name;

          //if(ic.resid2Residhash[oriResidName][residName] === undefined || ic.resid2Residhash[oriResidName][residName] > dist) {
              ic.resid2Residhash[oriResidName][residName] = dist.toFixed(1);
          //}

          let resids = atom1.structure + "_" + atom1.chain + "_" + atom1.resi + "_" + atom1.resn
            + ',' + atom2.structure + "_" + atom2.chain + "_" + atom2.resi + "_" + atom2.resn;

          if(!bInternal) {
              if(ic.resids2inter[resids] === undefined) ic.resids2inter[resids] = {};
              if(ic.resids2inter[resids][interactionType] === undefined) ic.resids2inter[resids][interactionType] = {};
              ic.resids2inter[resids][interactionType][oriResidName + ',' + residName] = dist.toFixed(1);
          }

          if(ic.resids2interAll[resids] === undefined) ic.resids2interAll[resids] = {};
          if(ic.resids2interAll[resids][interactionType] === undefined) ic.resids2interAll[resids][interactionType] = {};
          ic.resids2interAll[resids][interactionType][oriResidName + ',' + residName] = dist.toFixed(1);

          return true;
    }

    getRingNormal(coordArray) { let ic = this.icn3d; ic.icn3dui;
        if(coordArray.length < 3) return undefined;

        let v1 = coordArray[0].clone().sub(coordArray[1]);
        let v2 = coordArray[1].clone().sub(coordArray[2]);

        return v1.cross(v2).normalize();
    }

    getAromaticRings(resn, resid, type) { let ic = this.icn3d; ic.icn3dui;
        let piPosArray = [];
        let normalArray = [];

        let coordArray1 = [];
        let coordArray2 = [];

        if(type == 'nucleotide') {
            let pos1 = new THREE.Vector3(), pos2 = new THREE.Vector3();
            if(resn.trim().toUpperCase() == 'A' || resn.trim().toUpperCase() == 'DA'
              || resn.trim().toUpperCase() == 'G' || resn.trim().toUpperCase() == 'DG') {
                for(let i in ic.residues[resid]) {
                    let atom = ic.atoms[i];
                    if(atom.name == 'N1' || atom.name == 'C2' || atom.name == 'N3' || atom.name == 'C6') {
                        pos1.add(atom.coord);

                        coordArray1.push(atom.coord);
                    }
                    else if(atom.name == 'C4' || atom.name == 'C5') {
                        pos1.add(atom.coord);
                        pos2.add(atom.coord);

                        coordArray1.push(atom.coord);
                        coordArray2.push(atom.coord);
                    }
                    else if(atom.name == 'N7' || atom.name == 'C8' || atom.name == 'N9') {
                        pos2.add(atom.coord);

                        coordArray2.push(atom.coord);
                    }
                }

                if(coordArray1.length == 6) {
                    pos1.multiplyScalar(1.0 / 6);
                    piPosArray.push(pos1);
                    normalArray.push(this.getRingNormal(coordArray1));
                }

                if(coordArray2.length == 5) {
                    pos2.multiplyScalar(1.0 / 5);
                    piPosArray.push(pos2);
                    normalArray.push(this.getRingNormal(coordArray2));
                }
            }
            else if(resn.trim().toUpperCase() == 'C' || resn.trim().toUpperCase() == 'DC'
              || resn.trim().toUpperCase() == 'T' || resn.trim().toUpperCase() == 'DT'
              || resn.trim().toUpperCase() == 'U' || resn.trim().toUpperCase() == 'DU') {
                for(let i in ic.residues[resid]) {
                    let atom = ic.atoms[i];
                    if(atom.name == 'N1' || atom.name == 'C2' || atom.name == 'N3' || atom.name == 'C6') {
                        pos1.add(atom.coord);

                        coordArray1.push(atom.coord);
                    }
                    else if(atom.name == 'C4' || atom.name == 'C5') {
                        pos1.add(atom.coord);

                        coordArray1.push(atom.coord);
                    }
                }

                if(coordArray1.length == 6) {
                    pos1.multiplyScalar(1.0 / 6);

                    piPosArray.push(pos1);

                    normalArray.push(this.getRingNormal(coordArray1));
                }
            }
        }
        else if(type == 'protein') {
            let pos1 = new THREE.Vector3(), pos2 = new THREE.Vector3();

            if(resn.toUpperCase() == 'PHE' || resn.toUpperCase() == 'TYR') {
                for(let i in ic.residues[resid]) {
                    let atom = ic.atoms[i];
                    if(atom.name == 'CG' || atom.name == 'CD1' || atom.name == 'CE1'
                      || atom.name == 'CZ' || atom.name == 'CE2' || atom.name == 'CD2') {
                        pos1.add(atom.coord);
                        coordArray1.push(atom.coord);
                    }
                }

                if(coordArray1.length == 6) {
                    pos1.multiplyScalar(1.0 / 6);

                    piPosArray.push(pos1);
                    normalArray.push(this.getRingNormal(coordArray1));
                }
            }
            else if(resn.toUpperCase() == 'HIS') {
                for(let i in ic.residues[resid]) {
                    let atom = ic.atoms[i];
                    if(atom.name == 'CG' || atom.name == 'ND1' || atom.name == 'CE1'
                      || atom.name == 'NE2' || atom.name == 'CD2') {
                        pos1.add(atom.coord);
                        coordArray1.push(atom.coord);
                    }
                }

                if(coordArray1.length == 5) {
                    pos1.multiplyScalar(1.0 / 5);

                    piPosArray.push(pos1);
                    normalArray.push(this.getRingNormal(coordArray1));
                }
            }
            else if(resn.toUpperCase() == 'TRP') {
                for(let i in ic.residues[resid]) {
                    let atom = ic.atoms[i];
                    if(atom.name == 'CZ2' || atom.name == 'CH2' || atom.name == 'CZ3' || atom.name == 'CE3') {
                        pos1.add(atom.coord);
                        coordArray1.push(atom.coord);
                    }
                    else if(atom.name == 'CD2' || atom.name == 'CE2') {
                        pos1.add(atom.coord);
                        pos2.add(atom.coord);
                        coordArray1.push(atom.coord);
                        coordArray2.push(atom.coord);
                    }
                    else if(atom.name == 'CG' || atom.name == 'CD1' || atom.name == 'NE1') {
                        pos2.add(atom.coord);
                        coordArray2.push(atom.coord);
                    }
                }

                if(coordArray1.length == 6) {
                    pos1.multiplyScalar(1.0 / 6);
                    piPosArray.push(pos1);
                    normalArray.push(this.getRingNormal(coordArray1));
                }

                if(coordArray2.length == 5) {
                    pos2.multiplyScalar(1.0 / 5);
                    piPosArray.push(pos2);
                    normalArray.push(this.getRingNormal(coordArray2));
                }
            }
        }

        return {piPosArray: piPosArray, normalArray: normalArray} ;
    }

    // https://www.geeksforgeeks.org/print-all-the-cycles-in-an-undirected-graph/

    // Function to mark the vertex with
    // different colors for different cycles
    dfs_cycle(u, p, cyclenumber) { let ic = this.icn3d; ic.icn3dui;
        // already (completely) visited vertex.
        if (ic.ring_color[u] == 2) {
            return cyclenumber;
        }

        // seen vertex, but was not completely visited -> cycle detected.
        // backtrack based on parents to find the complete cycle.
        if (ic.ring_color[u] == 1) {

            cyclenumber++;
            let cur = p;
            ic.ring_mark[cur] = cyclenumber;

            // backtrack the vertex which are
            // in the current cycle thats found
            while (cur != u) {
                cur = ic.ring_par[cur];
                ic.ring_mark[cur] = cyclenumber;
            }
            return cyclenumber;
        }
        ic.ring_par[u] = p;

        // partially visited.
        ic.ring_color[u] = 1;

        // simple dfs on graph
        if(ic.atoms[u] !== undefined) {
            for(let k = 0, kl = ic.atoms[u].bonds.length; k < kl; ++k) {
                let v = ic.atoms[u].bonds[k];

                // if it has not been visited previously
                if (v == ic.ring_par[u]) {
                    continue;
                }
                cyclenumber = this.dfs_cycle(v, u, cyclenumber);
            }
        }

        // completely visited.
        ic.ring_color[u] = 2;

        return cyclenumber;
    }

    getAromaticPisLigand(resid) { let ic = this.icn3d; ic.icn3dui;
        let name2atom = {};

        let serialArray = Object.keys(ic.residues[resid]);
        let n = serialArray.length;

        // arrays required to color the
        // graph, store the parent of node
        ic.ring_color = {};
        ic.ring_par = {};

        // mark with unique numbers
        ic.ring_mark = {};

        // store the numbers of cycle
        let cyclenumber = 0;
        //var edges = 13;

        // call DFS to mark the cycles
        //cyclenumber = this.dfs_cycle(1, 0, cyclenumber);
        cyclenumber = this.dfs_cycle(serialArray[1], serialArray[0], cyclenumber);

        let cycles = {};

        // push the edges that into the
        // cycle adjacency list
        for (let i = 0; i < n; i++) {
            let serial = serialArray[i];
            //if (ic.ring_mark[serial] != 0) {
            if (ic.ring_mark[serial]) {
                if(cycles[ic.ring_mark[serial]] === undefined) cycles[ic.ring_mark[serial]] = [];
                cycles[ic.ring_mark[serial]].push(serial);
            }
        }

        // print all the vertex with same cycle
        for (let i = 1; i <= cyclenumber; i++) {
            // Print the i-th cycle
            let coord = new THREE.Vector3();
            let cnt = 0, serial;
            let coordArray = [];
            if(cycles.hasOwnProperty(i)) {
                for (let j = 0, jl = cycles[i].length; j < jl; ++j) {
                    serial = cycles[i][j];
                    coord.add(ic.atoms[serial].coord);
                    coordArray.push(ic.atoms[serial].coord);
                    ++cnt;
                }
            }

            //if(cnt == 5 || cnt == 6) {
            if(cnt >= 3 && cnt <= 6 && coordArray[0] && coordArray[1] && coordArray[2] && coordArray[3]) { // two neighboring cycles 5 and 6 in caffeine (CID 2519) will get reported as 5 and 4 atoms. The shared two atoms are reported only once.
                let v1 = coordArray[0].clone().sub(coordArray[1]).normalize();
                let v2 = coordArray[1].clone().sub(coordArray[2]).normalize();
                let v3 = coordArray[2].clone().sub(coordArray[3]).normalize();

                let normal = v1.cross(v2).normalize();
                let bPlane = normal.dot(v3);

                //if(Math.abs(bPlane) < 0.017) { // same plane, 89-90 degree
                if(Math.abs(bPlane) < 0.052) { // same plane, 87-90 degree
                    coord.multiplyScalar(1.0 / cnt);

                    let atom = ic.atoms[serial];
                    name2atom[resid + '_pi' + serial] = {resn: atom.resn, name: 'pi' + serial, coord: coord, serial: atom.serial,
                      structure: atom.structure, chain: atom.chain, resi: atom.resi, normal: normal};
                }
            }
        }

        return name2atom;
    }

    hideHalogenPi() { let ic = this.icn3d; ic.icn3dui;
        ic.opts["halogen"] = "no";
        ic.opts["pi-cation"] = "no";
        ic.opts["pi-stacking"] = "no";
        if(ic.lines === undefined) ic.lines = { };
        ic.lines['halogen'] = [];
        ic.lines['pi-cation'] = [];
        ic.lines['pi-stacking'] = [];
        ic.halogenpnts = [];
        ic.picationpnts = [];
        ic.pistackingpnts = [];

        for(let i in ic.atoms) {
            ic.atoms[i].style2 = 'nothing';
        }

        for(let i in ic.sidec) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style2 = ic.opts["sidec"];
            }
        }

        for(let i in ic.water) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style = ic.opts["water"];
            }
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Saltbridge {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // get ionic interactions, including salt bridge (charged hydrogen bonds)
    calculateIonicInteractions(startAtoms, targetAtoms, threshold, bSaltbridge, type, bInternal) { let ic = this.icn3d, me = ic.icn3dui;
        if(Object.keys(startAtoms).length === 0 || Object.keys(targetAtoms).length === 0) return;

        ic.resid2Residhash = {};

        let atomCation = {}, atomAnion = {};
        let chain_resi, chain_resi_atom;

        let maxlengthSq = threshold * threshold;

        for (let i in startAtoms) {
          let atom = startAtoms[i];

          // only use one of the two atoms
          if( ( atom.resn === 'ARG' && atom.name === "NH2")
            || ( atom.resn === 'GLU' && atom.name === "OE2")
            || ( atom.resn === 'ASP' && atom.name === "OD2") ) {
              continue;
          }

          // For ligand, "N" with one single bond only may be positively charged. => to be improved
          let bAtomCondCation = ( (atom.resn === 'LYS' || atom.resn === 'HIS') && atom.elem === "N" && atom.name !== "N")
            || ( atom.resn === 'ARG' && (atom.name === "NH1" || atom.name === "NH2"))
            || (atom.het && me.parasCls.cationsTrimArray.indexOf(atom.elem) !== -1)
            || (atom.het && atom.elem === "N" && atom.bonds.length == 1);

          let bAtomCondAnion = this.isAnion(atom);

          bAtomCondCation = (ic.bOpm) ? bAtomCondCation && atom.resn !== 'DUM' : bAtomCondCation;
          bAtomCondAnion = (ic.bOpm) ? bAtomCondAnion && atom.resn !== 'DUM' : bAtomCondAnion;

          if(bAtomCondCation || bAtomCondAnion) {
            chain_resi = atom.structure + "_" + atom.chain + "_" + atom.resi;
            chain_resi_atom = chain_resi + "_" + atom.name;

            if(bAtomCondCation) atomCation[chain_resi_atom] = atom;
            if(bAtomCondAnion) atomAnion[chain_resi_atom] = atom;
          }
        } // end of for (let i in startAtoms) {

        let hbondsAtoms = {};
        let residueHash = {};

        for (let i in targetAtoms) {
          let atom = targetAtoms[i];

          // only use one of the two atoms
          if( ( atom.resn === 'ARG' && atom.name === "NH2")
            || ( atom.resn === 'GLU' && atom.name === "OE2")
            || ( atom.resn === 'ASP' && atom.name === "OD2") ) {
              continue;
          }

          let bAtomCondCation = ( (atom.resn === 'LYS' || atom.resn === 'HIS') && atom.elem === "N" && atom.name !== "N")
            || ( atom.resn === 'ARG' && (atom.name === "NH1" || atom.name === "NH2"))
            || (atom.het && me.parasCls.cationsTrimArray.indexOf(atom.elem) !== -1);

          let bAtomCondAnion = this.isAnion(atom);

          bAtomCondCation = (ic.bOpm) ? bAtomCondCation && atom.resn !== 'DUM' : bAtomCondCation;
          bAtomCondAnion = (ic.bOpm) ? bAtomCondAnion && atom.resn !== 'DUM' : bAtomCondAnion;
          if(bAtomCondCation || bAtomCondAnion) {
            chain_resi = atom.structure + "_" + atom.chain + "_" + atom.resi;
            chain_resi_atom = chain_resi + "_" + atom.name;

            let oriResidName = atom.resn + ' $' + atom.structure + '.' + atom.chain + ':' + atom.resi + '@' + atom.name;
            if(ic.resid2Residhash[oriResidName] === undefined) ic.resid2Residhash[oriResidName] = {};

            let atomHbond = {};
            if(bAtomCondCation) atomHbond = atomAnion;
            else if(bAtomCondAnion) atomHbond = atomCation;

            let otherAtom1 = undefined, resid1 = atom.structure + '_' + atom.chain + '_' + atom.resi;
            if( bAtomCondCation && atom.resn === 'ARG' && atom.name === "NH1") {
                otherAtom1 = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid1], 'NH2');
            }
            else if( bAtomCondAnion && atom.resn === 'GLU' && atom.name === "OE1") {
                otherAtom1 = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid1], 'OE2');
            }
            else if( bAtomCondAnion && atom.resn === 'ASP' && atom.name === "OD1") {
                otherAtom1 = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid1], 'OD2');
            }

            let coord1 = (otherAtom1 === undefined) ? atom.coord : atom.coord.clone().add(otherAtom1.coord).multiplyScalar(0.5);

            for (let j in atomHbond) {
              // skip same residue
              if(chain_resi == j.substr(0, j.lastIndexOf('_') )) continue;

              if(!ic.crossstrucinter && atom.structure != atomHbond[j].structure) continue;

                let otherAtom2 = undefined, resid2 = atomHbond[j].structure + '_' + atomHbond[j].chain + '_' + atomHbond[j].resi;
                if( bAtomCondAnion && atomHbond[j].resn === 'ARG' && atomHbond[j].name === "NH1") {
                    otherAtom2 = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid2], 'NH2');
                }
                else if( bAtomCondCation && atomHbond[j].resn === 'GLU' && atomHbond[j].name === "OE1") {
                    otherAtom2 = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid2], 'OE2');
                }
                else if( bAtomCondCation && atomHbond[j].resn === 'ASP' && atomHbond[j].name === "OD1") {
                    otherAtom2 = ic.firstAtomObjCls.getFirstAtomObjByName(ic.residues[resid2], 'OD2');
                }

                let coord2 = (otherAtom2 === undefined) ? atomHbond[j].coord : atomHbond[j].coord.clone().add(otherAtom2.coord).multiplyScalar(0.5);

              let xdiff = Math.abs(coord1.x - coord2.x);
              if(xdiff > threshold) continue;

              let ydiff = Math.abs(coord1.y - coord2.y);
              if(ydiff > threshold) continue;

              let zdiff = Math.abs(coord1.z - coord2.z);
              if(zdiff > threshold) continue;

              let dist = xdiff * xdiff + ydiff * ydiff + zdiff * zdiff;
              if(dist > maxlengthSq) continue;

              // output salt bridge
              if(type !== 'graph') {
                  ic.saltbridgepnts.push({'serial': atom.serial, 'coord': coord1});
                  ic.saltbridgepnts.push({'serial': atomHbond[j].serial, 'coord': coord2});
              }

              let chain_resi2 = atomHbond[j].structure + "_" + atomHbond[j].chain + "_" + atomHbond[j].resi;

              hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[chain_resi]);
              hbondsAtoms = me.hashUtilsCls.unionHash(hbondsAtoms, ic.residues[chain_resi2]);

              residueHash[chain_resi] = 1;
              residueHash[chain_resi2] = 1;

              let residName = atomHbond[j].resn + ' $' + atomHbond[j].structure + '.' + atomHbond[j].chain + ':' + atomHbond[j].resi + '@' + atomHbond[j].name;

              //if(ic.resid2Residhash[oriResidName][residName] === undefined || ic.resid2Residhash[oriResidName][residName] > dist) {
                  ic.resid2Residhash[oriResidName][residName] = dist.toFixed(1);
              //}

              let resids = chain_resi + '_' + atom.resn + ',' + chain_resi2 + '_' + atomHbond[j].resn;

              if(!bInternal) {
                  if(ic.resids2inter[resids] === undefined) ic.resids2inter[resids] = {};
                  if(ic.resids2inter[resids]['ionic'] === undefined) ic.resids2inter[resids]['ionic'] = {};
                  ic.resids2inter[resids]['ionic'][oriResidName + ',' + residName] = dist.toFixed(1);
              }

              if(ic.resids2interAll[resids] === undefined) ic.resids2interAll[resids] = {};
              if(ic.resids2interAll[resids]['ionic'] === undefined) ic.resids2interAll[resids]['ionic'] = {};
              ic.resids2interAll[resids]['ionic'][oriResidName + ',' + residName] = dist.toFixed(1);

            } // end of for (let j in atomHbond) {
          }
        } // end of for (let i in targetAtoms) {

        let residueArray = Object.keys(residueHash);

        // draw sidec for these residues
        if(type !== 'graph') {
            for(let i = 0, il = residueArray.length; i < il; ++i) {
                for(let j in ic.residues[residueArray[i]]) {
                    // all atoms should be shown for hbonds
                    ic.atoms[j].style2 = 'stick';
                    if(ic.ions.hasOwnProperty(j)) ic.atoms[j].style2 = 'sphere';
                }
            }
        }

        return hbondsAtoms;
    }

    isAnion(atom) { let ic = this.icn3d, me = ic.icn3dui;
      // For ligand, "O" in carboxy group may be negatively charged. => to be improved
      let bLigNeg = undefined;
      if(atom.het && atom.elem === "O" && atom.bonds.length == 1) {
            let cAtom = ic.atoms[atom.bonds[0]];
            for(let j = 0; j < cAtom.bonds.length; ++j) {
                let serial = cAtom.bonds[j];
                if(ic.atoms[serial].elem == "O" && serial != atom.serial) {
                    bLigNeg = true;
                    break;
                }
            }
      }

      // "O" in phosphae or sulfate group is neagatively charged
      if(atom.elem === "O" && atom.bonds.length == 1) {
        let pAtom = ic.atoms[atom.bonds[0]];
        if(pAtom.elem == "P" || pAtom.elem == "S") bLigNeg = true;      
      }          

      let bAtomCondAnion = ( atom.resn === 'GLU' && (atom.name === "OE1" || atom.name === "OE2") )
        || ( atom.resn === 'ASP' && (atom.name === "OD1" || atom.name === "OD2") )
        || ( ic.nucleotides.hasOwnProperty(atom.serial) && (atom.name === "OP1" || atom.name === "OP2" || atom.name === "O1P" || atom.name === "O2P"))
        || (atom.het && me.parasCls.anionsTrimArray.indexOf(atom.elem) !== -1)
        || bLigNeg;
          
      return bAtomCondAnion;
    }
    
    hideSaltbridge() { let ic = this.icn3d; ic.icn3dui;
        ic.opts["saltbridge"] = "no";
        if(ic.lines === undefined) ic.lines = { };
        ic.lines['saltbridge'] = [];
        ic.saltbridgepnts = [];

        for(let i in ic.atoms) {
            ic.atoms[i].style2 = 'nothing';
        }

        for(let i in ic.sidec) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style2 = ic.opts["sidec"];
            }
        }

        for(let i in ic.water) {
            if(ic.hAtoms.hasOwnProperty(i)) {
                ic.atoms[i].style = ic.opts["water"];
            }
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SetStyle {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //For a list of atoms, set the hash with style as key and atom serial as value.
    setStyle2Atoms(atoms) { let ic = this.icn3d; ic.icn3dui;
          ic.style2atoms = {};

          for(let i in atoms) {
            // do not show water in assemly
            //if(ic.bAssembly && ic.water.hasOwnProperty(i)) {
            //    ic.atoms[i].style = 'nothing';
            //}

            if(ic.style2atoms[ic.atoms[i].style] === undefined) ic.style2atoms[ic.atoms[i].style] = {};

            ic.style2atoms[ic.atoms[i].style][i] = 1;

            // side chains
            if(ic.atoms[i].style2 !== undefined && ic.atoms[i].style2 !== 'nothing') {
                if(ic.style2atoms[ic.atoms[i].style2] === undefined) ic.style2atoms[ic.atoms[i].style2] = {};

                ic.style2atoms[ic.atoms[i].style2][i] = 1;
            }
          }
    }

    // set atom style when loading a structure
    //Set atom style according to the definition in options (options.secondaryStructure, etc).
    setAtomStyleByOptions(options) { let ic = this.icn3d, me = ic.icn3dui;
        if(options === undefined) options = ic.opts;

        let selectedAtoms;

        if (options.proteins !== undefined) {
            selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.proteins);
            for(let i in selectedAtoms) {
              ic.atoms[i].style = options.proteins.toLowerCase();
            }
        }

        // side chain use style2
        if (options.sidec !== undefined && options.sidec !== 'nothing') {
            selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.sidec);
            //var sidec_calpha = me.hashUtilsCls.unionHash(ic.calphas, ic.sidec);
            //selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, sidec_calpha);

            for(let i in selectedAtoms) {
              ic.atoms[i].style2 = options.sidec.toLowerCase();
            }
        }

        if (options.ntbase !== undefined && options.ntbase !== 'nothing') {
          selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.ntbase);

          for(let i in selectedAtoms) {
            ic.atoms[i].style2 = options.ntbase.toLowerCase();
          }
        }

        if (options.chemicals !== undefined) {
            selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.chemicals);
            for(let i in selectedAtoms) {
              ic.atoms[i].style = options.chemicals.toLowerCase();
            }
        }

        if (options.ions !== undefined) {
            selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.ions);
            for(let i in selectedAtoms) {
              ic.atoms[i].style = options.ions.toLowerCase();
            }
        }

        if (options.water !== undefined) {
            selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.water);
            for(let i in selectedAtoms) {
              ic.atoms[i].style = options.water.toLowerCase();
            }
        }

        if (options.nucleotides !== undefined) {
            selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.nucleotides);
            for(let i in selectedAtoms) {
              ic.atoms[i].style = options.nucleotides.toLowerCase();
            }
        }
    }

    setBackground(color) {var ic = this.icn3d, me = ic.icn3dui;
      
       ic.setOptionCls.setOption('background', color);
       me.htmlCls.clickMenuCls.setLogCmd('set background ' + color, true);
       //let titleColor =(color == 'black' || color == 'transparent') ? me.htmlCls.GREYD : 'black';
       let titleColor = (color == 'black') ? me.htmlCls.GREYD : 'black';
       $("#" + ic.pre + "title").css("color", titleColor);
       $("#" + ic.pre + "titlelink").css("color", titleColor);
    }

    //Save the command history to session storage so that the viewer can show the previous state when refreshing the same page.
    saveCommandsToSession() {var ic = this.icn3d; ic.icn3dui;
        let dataStr = ic.commands.join('\n');
        let data = decodeURIComponent(dataStr);
        sessionStorage.setItem('commands', data);
    }

    //http://jasonjl.me/blog/2015/06/21/taking-action-on-browser-crashes/
    //Set the commands before the browser crashed. These commands are used to restore your previous
    //state by refreshing the crashed page. It works in Chrome, Firefox, and Internet Explorer in PC,
    //but neither Safari nor Mac.
    getCommandsBeforeCrash() {var ic = this.icn3d, me = ic.icn3dui;
       window.addEventListener('load', function() {
          sessionStorage.setItem('good_exit', 'pending');
       });
       window.addEventListener('beforeunload', function() {
          sessionStorage.setItem('good_exit', 'true');
       });
       if(sessionStorage.getItem('good_exit') && sessionStorage.getItem('good_exit') === 'pending') {
          if(!me.utilsCls.isMac()) ic.bCrashed = true;  // this doesn't work in mac
          ic.commandsBeforeCrash = sessionStorage.getItem('commands');
          if(!ic.commandsBeforeCrash) ic.commandsBeforeCrash = '';
       }
    }

    handleContextLost() {var ic = this.icn3d; ic.icn3dui;
        //https://www.khronos.org/webgl/wiki/HandlingContextLost
        // 1 add a lost context handler and tell it to prevent the default behavior

        let canvas = $("#" + ic.pre + "canvas")[0];
        canvas.addEventListener("webglcontextlost", function(event) {
            event.preventDefault();
        }, false);

        // 2 re-setup all your WebGL state and re-create all your WebGL resources when the context is restored.
        canvas.addEventListener("webglcontextrestored", function(event) {
            // IE11 error: WebGL content is taking too long to render on your GPU. Temporarily switching to software rendering.
            console.log("WebGL context was lost. Reset WebGLRenderer and launch iCn3D again.");

            ic.renderer = new THREE.WebGLRenderer({
              canvas: ic.oriContainer.get(0), //this.container.get(0),
              antialias: true,
              preserveDrawingBuffer: true,
              sortObjects: false,
              alpha: true
            });
            // Enable VR
            ic.renderer.xr.enabled = true;

            ic.drawCls.draw();

        }, false);
    }

    adjustIcon() {var ic = this.icn3d; ic.icn3dui;
      if(ic.STATENUMBER === 1) {
        if($("#" + ic.pre + "back").hasClass('icn3d-middleIcon')) {
          $("#" + ic.pre + "back").toggleClass('icn3d-middleIcon');
          $("#" + ic.pre + "back").toggleClass('icn3d-endIcon');
        }
      }
      else {
        if($("#" + ic.pre + "back").hasClass('icn3d-endIcon')) {
          $("#" + ic.pre + "back").toggleClass('icn3d-middleIcon');
          $("#" + ic.pre + "back").toggleClass('icn3d-endIcon');
        }
      }
      if(ic.STATENUMBER === ic.commands.length) {
        if($("#" + ic.pre + "forward").hasClass('icn3d-middleIcon')) {
          $("#" + ic.pre + "forward").toggleClass('icn3d-middleIcon');
          $("#" + ic.pre + "forward").toggleClass('icn3d-endIcon');
        }
      }
      else {
        if($("#" + ic.pre + "forward").hasClass('icn3d-endIcon')) {
          $("#" + ic.pre + "forward").toggleClass('icn3d-middleIcon');
          $("#" + ic.pre + "forward").toggleClass('icn3d-endIcon');
        }
      }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SetColor {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    colorSpectrum(atoms) { let ic = this.icn3d, me = ic.icn3dui;
        let idx = 0;
        let cnt = 0;

        // for selected atoms
        atoms = me.hashUtilsCls.intHash(atoms, ic.hAtoms);

        for (let i in atoms) {
            let atom = ic.atoms[i];
            if(!atom.het) ++cnt;
        }

        let lastTerSerialInv = (cnt > 1) ? 1 / (cnt - 1) : 1;
        for (let i in atoms) {
            let atom = ic.atoms[i];
            //atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.thr().setHSL(2 / 3 * (1 - idx++ * lastTerSerialInv), 1, 0.45);
            atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.thr().setHSL(3 / 4 * (1 - idx++ * lastTerSerialInv), 1, 0.45);

            ic.atomPrevColors[i] = atom.color;
        }
    }

    colorRainbow(atoms) { let ic = this.icn3d, me = ic.icn3dui;
        let idx = 0;
        let cnt = 0;

        // for selected atoms
        atoms = me.hashUtilsCls.intHash(atoms, ic.hAtoms);

        for (let i in atoms) {
            let atom = ic.atoms[i];
            if(!atom.het) ++cnt;
        }

        let lastTerSerialInv = (cnt > 1) ? 1 / (cnt - 1) : 1;
        for (let i in atoms) {
            let atom = ic.atoms[i];
            atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.thr().setHSL(3 / 4 *  idx++ * lastTerSerialInv, 1, 0.45);

            ic.atomPrevColors[i] = atom.color;
        }
    }

    setColorAcrossSets(nameArray, bSpectrum) { let ic = this.icn3d, me = ic.icn3dui;
        let idx = 0;
        let cnt = nameArray.length;

        let lastTerSerialInv = (cnt > 1) ? 1 / (cnt - 1) : 1;
        for(let i = 0, il = nameArray.length; i < il; ++i) {
            let atomSet = ic.definedSetsCls.getAtomsFromNameArray([nameArray[i]]);
            for (let serial in atomSet) {
                let atom = ic.atoms[serial];

                if(bSpectrum) {
                    atom.color = me.parasCls.thr().setHSL(3 / 4 * (1 - idx * lastTerSerialInv), 1, 0.45);
                }
                else { // rainbow
                    atom.color = me.parasCls.thr().setHSL(3 / 4 *  idx * lastTerSerialInv, 1, 0.45);
                }

                ic.atomPrevColors[serial] = atom.color;
            }
            ++idx;
        }

        ic.drawCls.draw();
    }

    setColorBySets(nameArray, bSpectrum) { let ic = this.icn3d; ic.icn3dui;
        for(let i = 0, il = nameArray.length; i < il; ++i) {
            let atoms = ic.definedSetsCls.getAtomsFromNameArray([nameArray[i]]);

            if(bSpectrum) {
                this.colorSpectrum(atoms);
            }
            else { // rainbow
                this.colorRainbow(atoms);
            }
        }

        ic.drawCls.draw();
    }

    //Set atom color according to the definition in options (options.color).
    setColorByOptions(options, atoms, bUseInputColor) { let ic = this.icn3d, me = ic.icn3dui;
     if(options !== undefined) {
      if(bUseInputColor) {
        for (let i in atoms) {
            let atom = ic.atoms[i];

            ic.atomPrevColors[i] = atom.color;
        }
      }
      else if(options.color.indexOf("#") === 0) {
        for (let i in atoms) {
            let atom = ic.atoms[i];
            atom.color = me.parasCls.thr().setStyle(options.color.toLowerCase());

            ic.atomPrevColors[i] = atom.color;
        }
      }
      else {
        let idx, cnt, lastTerSerialInv;
        let minB, maxB;

        if(options.color.toLowerCase() == 'confidence') {
            $("#" + me.pre + "legend").show();
        }
        else {
            $("#" + me.pre + "legend").hide();
        }

        switch (options.color.toLowerCase()) {
            case 'rainbow':
                this.colorRainbow(atoms);
                break;
            case 'rainbow for chains':
                for(let chainid in ic.chains) {
                    this.colorRainbow(ic.chains[chainid]);
                }
                break;
            case 'spectrum':
                this.colorSpectrum(atoms);
                break;
            case 'spectrum for chains':
                for(let chainid in ic.chains) {
                    this.colorSpectrum(ic.chains[chainid]);
                }
                break;

            case 'structure':
                let colorArray = (ic.bAfMem) ? [me.parasCls.thr(0xFF00FF), me.parasCls.thr(0x00FF00)] : me.parasCls.stdChainColors;
                let index = -1, prevStructure = '', colorLength = colorArray.length;
                for (let i in atoms) {
                    let atom = ic.atoms[i];

                    if(atom.structure != prevStructure) {
                        ++index;

                        index = index % colorLength;
                    }

                    if(!atom.het) {
                        atom.color = colorArray[index];
                        ic.atomPrevColors[i] = atom.color;
                    }
                    else {
                        atom.color = me.parasCls.atomColors[atom.elem];
                        ic.atomPrevColors[i] = atom.color;
                    }

                    prevStructure = atom.structure;
                }
                break;

            case 'chain':
                if(ic.chainsColor !== undefined && Object.keys(ic.chainsColor).length > 0) { // mmdb input   
                    this.setMmdbChainColor();
                }
                else {
                    let index = -1, prevChain = '', colorLength = me.parasCls.stdChainColors.length;
                    for (let i in atoms) {
                        let atom = ic.atoms[i];

                        if(atom.chain != prevChain) {
                            ++index;

                            index = index % colorLength;
                        }

                        //if(atom.color === undefined) atom.color = me.parasCls.stdChainColors[index];
                        if(!atom.het) {
                            atom.color = me.parasCls.stdChainColors[index];

                            if(Object.keys(ic.chainsColor).length > 0) this.updateChainsColor(atom);
                            ic.atomPrevColors[i] = atom.color;
                        }
                        else {
                            atom.color = me.parasCls.atomColors[atom.elem];
                            ic.atomPrevColors[i] = atom.color;
                        }

                        prevChain = atom.chain;
                    }
                }
                break;

            case 'domain':
                idx = 0;
                cnt = 0;
                let domainArray = Object.keys(ic.tddomains);
                cnt = domainArray.length;
                lastTerSerialInv = (cnt > 1) ? 1 / (cnt - 1) : 1;
                for (let i = 0, il = domainArray.length; i < il; ++i) {
                    let color = me.parasCls.thr().setHSL(3 / 4 * (1 - idx++ * lastTerSerialInv), 1, 0.45);

                    for(let resid in ic.tddomains[domainArray[i]]) {
                        for(let serial in ic.residues[resid]) {
                            let atom = ic.atoms[serial];
                            atom.color = color;
                            ic.atomPrevColors[serial] = atom.color;
                        }
                    }
                }
                break;

            case 'defined sets':
                idx = 0;

                if(!ic.nameArray || ic.nameArray.length == 0) {
                    alert('Please first select sets in "Analysis > Defined Sets", and try it again.');
                }
                else {
                    cnt = ic.nameArray.length;
                    lastTerSerialInv = (cnt > 1) ? 1 / (cnt - 1) : 1;
                    for (let i = 0; i < cnt; ++i) {
                        let definedSetName = ic.nameArray[i];
                        let definedSet = ic.definedSetsCls.getAtomsFromNameArray([definedSetName]);

                        let color = me.parasCls.thr().setHSL(3 / 4 * idx++ * lastTerSerialInv, 1, 0.45);

                        for(let serial in definedSet) {
                            let atom = ic.atoms[serial];
                            atom.color = color;
                            ic.atomPrevColors[serial] = atom.color;
                        }
                    }
                }

                break;

            case 'secondary structure green':
                ic.sheetcolor = 'green';
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    // secondary color of nucleotide: blue (me.parasCls.thr(0x0000FF))
                    atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.ssColors[atom.ss] || me.parasCls.thr(0xFF00FF);

                    ic.atomPrevColors[i] = atom.color;
                }

                break;

            case 'secondary structure yellow':
            case 'secondary structure':
                ic.sheetcolor = 'yellow';
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    // secondary color of nucleotide: blue (me.parasCls.thr(0x0000FF))
                    atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.ssColors2[atom.ss] || me.parasCls.thr(0xFF00FF);

                    ic.atomPrevColors[i] = atom.color;
                }

                break;

            case 'secondary structure spectrum':
                idx = 0;
                cnt = 0;

                let ssArray = [];
                let prevI = -9999, start;
                let prevAtom;
                for (let i in atoms) {
                    // only for proteins
                    if(!ic.proteins.hasOwnProperty(i)) continue;

                    let atom = ic.atoms[i];

                    if(prevI == -9999) start = parseInt(i);

                    if(prevI != -9999 && (atom.ss != prevAtom.ss || Math.abs(atom.resi - prevAtom.resi) > 1 || (atom.ssbegin && prevAtom.ssend) ) ) {
                        if(prevAtom.ss == 'coil') ;
                        else {
                            ssArray.push([start, prevI]);
                        }
                        start = i;
                    }

                    prevI = parseInt(i);
                    prevAtom = atom;
                }

                if(prevAtom.ss == 'coil') ;
                else {
                    ssArray.push([start, prevI]);
                }

                cnt = ssArray.length;
                lastTerSerialInv = (cnt > 1) ? 1 / (cnt - 1) : 1;
                for (let i = 0, il = ssArray.length; i < il; ++i) {
                    //var color = me.parasCls.thr().setHSL(2 / 3 * (1 - idx++ * lastTerSerialInv), 1, 0.45);
                    let color = me.parasCls.thr().setHSL(3 / 4 * (1 - idx++ * lastTerSerialInv), 1, 0.45);

                    for(let serial = ssArray[i][0]; serial <= ssArray[i][1]; ++serial) {
                        let atom = ic.atoms[serial];
                        atom.color = color;
                        ic.atomPrevColors[serial] = atom.color;
                    }
                }

                // keep the color of coils untouched
/*
                let color = me.parasCls.ssColors2['coil']
                for (let i = 0, il = coilArray.length; i < il; ++i) {
                    for(let serial = coilArray[i][0]; serial <= coilArray[i][1]; ++serial) {
                        let atom = ic.atoms[serial];
                        atom.color = color;
                        ic.atomPrevColors[serial] = atom.color;
                    }
                }
*/
                break;

            case 'residue':
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.residueColors[atom.resn] || me.parasCls.defaultResidueColor;

                    ic.atomPrevColors[i] = atom.color;
                }

                break;

            case 'residue custom':
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : ic.customResidueColors[atom.resn] || me.parasCls.defaultResidueColor;

                    ic.atomPrevColors[i] = atom.color;
                }
                break;

            case 'align custom':
                // http://proteopedia.org/wiki/index.php/Temperature_color_schemes
                // Fixed: Middle (white): 50, red: >= 100, blue: 0
                ic.middB = 50;
                ic.spanBinv1 = 0.02;
                ic.spanBinv2 = 0.02;

                for(let serial in atoms) {
                    let chainid = ic.atoms[serial].structure + '_' + ic.atoms[serial].chain;
                    if(ic.queryresi2score === undefined || !ic.queryresi2score.hasOwnProperty(chainid)) continue;

                    //var resi = ic.atoms[serial].resi - 1;
                    let color;
                    //if(ic.target2queryHash.hasOwnProperty(resi) && ic.target2queryHash[resi] !== -1) { // -1 means gap
                        //var queryresi = ic.target2queryHash[resi] + 1;
                        //var queryresi = ic.atoms[serial].resi;
                    let queryresi = ic.atoms[serial].resi;

                    if(ic.queryresi2score[chainid].hasOwnProperty(queryresi)) {
                        let b = ic.queryresi2score[chainid][queryresi];

                        if(b > 100) b = 100;

                        let s1 = (ic.middB - b) * ic.spanBinv1;
                        let s2 = (b - ic.middB) * ic.spanBinv2;
                        if(b < ic.middB) {
                            if(ic.startColor == 'blue') {
                                color = (ic.midColor == 'white') ? me.parasCls.thr().setRGB(1 - s1, 1 - s1, 1) : me.parasCls.thr().setRGB(0, 0, s1);
                            }
                            else if(ic.startColor == 'red') {
                                color = (ic.midColor == 'white') ? me.parasCls.thr().setRGB(1, 1 - s1, 1 - s1) : me.parasCls.thr().setRGB(s1, 0, 0);
                            }
                            else if(ic.startColor == 'green') {
                                color = (ic.midColor == 'white') ? me.parasCls.thr().setRGB(1 - s1, 1, 1 - s1) : me.parasCls.thr().setRGB(0, s1, 0);
                            }
                        }
                        else {
                            if(ic.endColor == 'red') {
                                color = (ic.midColor == 'white') ? me.parasCls.thr().setRGB(1, 1 - s2, 1 - s2) : me.parasCls.thr().setRGB(s2, 0, 0);
                            }
                            else if(ic.endColor == 'green') {
                                color = (ic.midColor == 'white') ? me.parasCls.thr().setRGB(1 - s2, 1, 1 - s2) : me.parasCls.thr().setRGB(0, s2, 0);
                            }
                            else if(ic.endColor == 'blue') {
                                color = (ic.midColor == 'white') ? me.parasCls.thr().setRGB(1 - s2, 1 - s2, 1) : me.parasCls.thr().setRGB(0, 0, s2);
                            }
                        }
                    }
                    else {
                        color = me.parasCls.defaultAtomColor;
                    }
                    //}
                    //else {
                    //    color = me.parasCls.defaultAtomColor;
                    //}

                    ic.atoms[serial].color = color;
                    ic.atomPrevColors[serial] = color;
                }

                //ic.updateHlAll();
                break;

            case 'charge':
                for (let i in atoms) {
                    let atom = ic.atoms[i];

                    //atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.chargeColors[atom.resn] || me.parasCls.defaultResidueColor;
                    atom.color = atom.het ? me.parasCls.defaultAtomColor : me.parasCls.chargeColors[atom.resn] || me.parasCls.defaultResidueColor;

                    ic.atomPrevColors[i] = atom.color;
                }

                break;
            case 'hydrophobic':
                for (let i in atoms) {
                    let atom = ic.atoms[i];

                    //atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.chargeColors[atom.resn] || me.parasCls.defaultResidueColor;
                    atom.color = atom.het ? me.parasCls.defaultAtomColor : me.parasCls.hydrophobicColors[atom.resn] || me.parasCls.defaultResidueColor;

                    ic.atomPrevColors[i] = atom.color;
                }
                break;
            case 'normalized hydrophobic':
                for (let i in atoms) {
                    let atom = ic.atoms[i];

                    //atom.color = atom.het ? me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor : me.parasCls.chargeColors[atom.resn] || me.parasCls.defaultResidueColor;
                    atom.color = atom.het ? me.parasCls.defaultAtomColor : me.parasCls.normalizedHPColors[atom.resn] || me.parasCls.defaultResidueColor;

                    ic.atomPrevColors[i] = atom.color;
                }

                break;
            case 'atom':
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    atom.color = me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor;

                    ic.atomPrevColors[i] = atom.color;
                }

                break;

            case 'confidence':
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    if(atom.b === undefined || isNaN(atom.b) || parseInt(atom.b * 1000) == 0) { // invalid b-factor
                        atom.color =  me.parasCls.thr().setRGB(0, 1, 0);
                    }
                    else {
                        let b = atom.b;

                        // PDB
                        b = (atom.structure.length < 6) ? 100 - b : b;

                        if(b >= 90) {
                            atom.color = me.parasCls.thr().setRGB(0, 0.325, 0.839);
                        }
                        else if(b >= 70 && b < 90) {
                            atom.color = me.parasCls.thr().setRGB(0.396, 0.572, 0.953);
                        }
                        else if(b >= 50 && b < 70) {
                            atom.color = me.parasCls.thr().setRGB(1, 0.859, 0.075);
                        }
                        else if(b < 50) {
                            atom.color = me.parasCls.thr().setRGB(1, 0.490, 0.271);
                        }
                    }

                    ic.atomPrevColors[i] = atom.color;
                }

                break;

            case 'b factor':
                // http://proteopedia.org/wiki/index.php/Temperature_color_schemes
                // Fixed: Middle (white): 50, red: >= 100, blue: 0
                ic.middB = 50;
                ic.spanBinv1 = 0.02;
                ic.spanBinv2 = 0.02;

                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    if(atom.b === undefined || isNaN(atom.b) || parseInt(atom.b * 1000) == 0) { // invalid b-factor
                        atom.color =  me.parasCls.thr().setRGB(0, 1, 0);
                    }
                    else {
                        let b = atom.b;
                        if(b > 100) b = 100;

                        // AlphaFold
                        b = (atom.structure.length > 5) ? 100 - b : b;

                        let s1 = (ic.middB - b) * ic.spanBinv1;
                        let s2 = (b - ic.middB) * ic.spanBinv2;

                        atom.color = b < ic.middB ? me.parasCls.thr().setRGB(1 - s1, 1 - s1, 1) : me.parasCls.thr().setRGB(1, 1 - s2, 1 - s2);
                    }

                    if(ic.bOpm && atom.resn == 'DUM') atom.color = me.parasCls.atomColors[atom.elem];

                    ic.atomPrevColors[i] = atom.color;
                }

                break;

            case 'b factor percentile':
                //http://proteopedia.org/wiki/index.php/Disorder
                // percentile normalize B-factor values from 0 to 1
                minB = 1000;
                maxB = -1000;
                if (!ic.bfactorArray) {
                    ic.bfactorArray = [];
                    for (let i in ic.atoms) {
                        let atom = ic.atoms[i];
                        if (minB > atom.b) minB = atom.b;
                        if (maxB < atom.b) maxB = atom.b;

                        ic.bfactorArray.push(atom.b);
                    }

                    ic.bfactorArray.sort(function(a, b) { return a - b; });
                }

                let totalCnt = ic.bfactorArray.length;
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    if(atom.b === undefined || isNaN(atom.b) || parseInt(atom.b * 1000) == 0 || ic.bfactorArray.length == 0) { // invalid b-factor
                        atom.color =  me.parasCls.thr().setRGB(0, 1, 0);
                    }
                    else {
                        // AlphaFold
                        let b = (atom.structure > 5) ? 100 - atom.b : atom.b;

                        let percentile = ic.bfactorArray.indexOf(b) / totalCnt;

                        atom.color = percentile < 0.5 ? me.parasCls.thr().setRGB(percentile * 2, percentile * 2, 1) : me.parasCls.thr().setRGB(1, (1 - percentile) * 2, (1 - percentile) * 2);
                    }

                    ic.atomPrevColors[i] = atom.color;
                }

                break;

            case 'area':
                if(ic.resid2area === undefined) {
                    // calculate area to set up ic.resid2area
                    let currHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

                    // calculate area for all
                    ic.hAtoms = me.hashUtilsCls.cloneHash(ic.atoms);

                    ic.bCalcArea = true;
                    ic.opts.surface = 'solvent accessible surface';
                    ic.applyMapCls.applySurfaceOptions();
                    ic.bCalcArea = false;

                    ic.hAtoms = me.hashUtilsCls.cloneHash(currHAtoms);
                }

                // http://proteopedia.org/wiki/index.php/Temperature_color_schemes
                // Fixed: Middle (white): 50, red: >= 100, blue: 0
                let middB = (ic.midpercent !== undefined) ? ic.midpercent : 35;
                ic.spanBinv1 = 0.02;
                ic.spanBinv2 = 0.02;

                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    let resid = atom.structure + '_' + atom.chain + '_' + atom.resi + '_' + atom.resn;

                    let b = (me.parasCls.residueArea.hasOwnProperty(atom.resn)) ? ic.resid2area[resid] / me.parasCls.residueArea[atom.resn] * 100 : middB;

                    if(b > 100) b = 100;

                    let s1 = (middB - b) * ic.spanBinv1;
                    let s2 = (b - middB) * ic.spanBinv2;

                    atom.color = b < middB ? me.parasCls.thr().setRGB(1 - s1, 1 - s1, 1) : me.parasCls.thr().setRGB(1, 1 - s2, 1 - s2);

                    if(ic.bOpm && atom.resn == 'DUM') atom.color = me.parasCls.atomColors[atom.elem];

                    ic.atomPrevColors[i] = atom.color;
                }
                break;

            case 'identity':
                this.setConservationColor(atoms, true);
                break;

            case 'conserved': // backward-compatible, "conserved" was changed to "identity"
                this.setConservationColor(atoms, true);
                break;

            case 'conservation':
                this.setConservationColor(atoms, false);
                break;

            case 'white':
                this.setAtmClr(atoms, 0xFFFFFF);
                break;

            case 'grey':
                this.setAtmClr(atoms, 0x888888);
                break;

            case 'red':
                this.setAtmClr(atoms, 0xFF0000);
                break;
            case 'green':
                this.setAtmClr(atoms, 0x00FF00);
                break;
            case 'blue':
                this.setAtmClr(atoms, 0x0000FF);
                break;
            case 'magenta':
                this.setAtmClr(atoms, 0xFF00FF);
                break;
            case 'yellow':
                this.setAtmClr(atoms, 0xFFFF00);
                break;
            case 'cyan':
                this.setAtmClr(atoms, 0x00FFFF);
                break;
            case 'custom':
                // do the coloring separately
                break;

            default: // the "#" was missed in order to make sharelink work
                for (let i in atoms) {
                    let atom = ic.atoms[i];
                    atom.color = me.parasCls.thr().setStyle("#" + options.color.toLowerCase());

                    ic.atomPrevColors[i] = atom.color;
                }

                break;
        }

        ic.legendTableCls.showColorLegend(options.color.toLowerCase());
      }
     }
    }

    setAtmClr(atoms, hex) { let ic = this.icn3d, me = ic.icn3dui;
        for (let i in atoms) {
            let atom = ic.atoms[i];
            atom.color = me.parasCls.thr().setHex(hex);

            ic.atomPrevColors[i] = atom.color;
        }
    }

    updateChainsColor(atom) { let ic = this.icn3d; ic.icn3dui;
        let chainid = atom.structure + '_' + atom.chain;
        if(ic.chainsColor[chainid] !== undefined) {  // for mmdbid and align input
            ic.chainsColor[chainid] = atom.color;
        }
    }

    setMmdbChainColor(inAtoms) { let ic = this.icn3d, me = ic.icn3dui;
        let atoms = (inAtoms === undefined) ? ic.hAtoms : inAtoms;
        this.applyOriginalColor(me.hashUtilsCls.hash2Atoms(atoms, ic.atoms));

        // atom color
        let atomHash;
        atomHash = me.hashUtilsCls.unionHash(atomHash, ic.chemicals);
        atomHash = me.hashUtilsCls.unionHash(atomHash, ic.ions);

        for (let i in atomHash) {
            let atom = ic.atoms[i];
            atom.color = me.parasCls.atomColors[atom.elem] || me.parasCls.defaultAtomColor;

            ic.atomPrevColors[i] = atom.color;
        }
    }

    setConservationColor(atoms, bIdentity) { let ic = this.icn3d, me = ic.icn3dui;
        this.setMmdbChainColor(atoms);

        for(let chainid in ic.alnChainsSeq) {
            let resObjectArray = ic.alnChainsSeq[chainid];

            for(let i = 0, il = resObjectArray.length; i < il; ++i) {
                let residueid = chainid + '_' + resObjectArray[i].resi;

                for(let j in ic.residues[residueid]) {
                    if(atoms.hasOwnProperty(j)) {
                        let color = (bIdentity) ? me.parasCls.thr(resObjectArray[i].color) : me.parasCls.thr(resObjectArray[i].color2);
                        ic.atoms[j].color = color;
                        ic.atomPrevColors[j] = color;
                    }
                }
            }
        }
    }

    applyOriginalColor(atoms) { let ic = this.icn3d, me = ic.icn3dui;
        if(atoms === undefined) atoms = ic.atoms;

        for (let i in atoms) {
            let atom = atoms[i];
            let chainid = atom.structure + '_' + atom.chain;

            if(ic.chainsColor.hasOwnProperty(chainid)) {
                atom.color = ic.chainsColor[chainid];
            }
            else {
                atom.color = me.parasCls.atomColors[atom.elem];
                //break;
            }

            ic.atomPrevColors[i] = atom.color;
        }
    }

    applyPrevColor() { let ic = this.icn3d; ic.icn3dui;
        for (let i in ic.atoms) {
            let atom = ic.atoms[i];
            atom.color = ic.atomPrevColors[i];
        }
    }

    //Set the outline color when highlighting atoms. The available options are "yellow", "green", and "red".
    setOutlineColor(colorStr) { let ic = this.icn3d; ic.icn3dui;
        // outline using ShaderMaterial: http://jsfiddle.net/Eskel/g593q/9/
        let shader = {
            'outline' : {
                vertex_shader: [
                    "uniform float offset;",
                    "void main() {",
                        "vec4 pos = modelViewMatrix * vec4( position + normal * offset, 1.0 );",
                        "gl_Position = projectionMatrix * pos;",
                    "}"
                ].join("\n"),

                fragment_shader: [
                    "void main(){",
                        "gl_FragColor = vec4( 1.0, 1.0, 0.0, 1.0 );",
                    "}"
                ].join("\n")
            }
        };

        if(colorStr === 'yellow') {
           shader.outline.fragment_shader = [
               "void main(){",
                   "gl_FragColor = vec4( 1.0, 1.0, 0.0, 1.0 );",
               "}"
           ].join("\n");
        }
        else if(colorStr === 'green') {
           shader.outline.fragment_shader = [
               "void main(){",
                   "gl_FragColor = vec4( 0.0, 1.0, 0.0, 1.0 );",
               "}"
           ].join("\n");
        }
        else if(colorStr === 'red') {
           shader.outline.fragment_shader = [
               "void main(){",
                   "gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );",
               "}"
           ].join("\n");
        }

        // shader
        let uniforms = {offset: {
            type: "f",
            //value: 1
            value: 0.5
          }
        };

        let outShader = shader['outline'];

        let matShader = new THREE.ShaderMaterial({
            uniforms: uniforms,
            vertexShader: outShader.vertex_shader,
            fragmentShader: outShader.fragment_shader,
            depthTest: false,
            depthWrite: false,
            //needsUpdate: true
        });

        return matShader;
    }
}

/**
* @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
*/

class SetOption {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Modify the display options, e.g., setOption('color', 'green')
    setOption(id, value) {var ic = this.icn3d; ic.icn3dui;
      //var options2 = {}
      //options2[id] = value;
      // remember the options
      ic.opts[id] = value;
      ic.selectionCls.saveSelectionIfSelected();
      if(id === 'color') {
          ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);
          ic.drawCls.draw();
          //let residueHash = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(ic.hAtoms);
          //ic.hlUpdateCls.changeSeqColor(Object.keys(residueHash));

          //ic.hlUpdateCls.updateHlAll(ic.nameArray);
          ic.hlUpdateCls.updateHlAll();

          // change graph color
          ic.getGraphCls.updateGraphColor();
      }
      else if(id === 'surface' || id === 'opacity' || id === 'wireframe') {
          if(id === 'opacity' || id === 'wireframe') {
              ic.applyMapCls.removeLastSurface();
          }
          ic.applyMapCls.applySurfaceOptions();
          //if(ic.bRender) ic.drawCls.render();
          ic.drawCls.draw(); // to make surface work in assembly
      }
      else if(id === 'map' || id === 'mapwireframe') {
          if(id === 'mapwireframe') {
              ic.applyMapCls.removeLastMap();
          }
          ic.applyMapCls.applyMapOptions();
          //if(ic.bRender) ic.drawCls.render();
          ic.drawCls.draw(); // to make surface work in assembly
      }
      else if(id === 'emmap' || id === 'emmapwireframe') {
          if(id === 'emmapwireframe') {
              ic.applyMapCls.removeLastEmmap();
          }
          ic.applyMapCls.applyEmmapOptions();
          //if(ic.bRender) ic.drawCls.render();
          ic.drawCls.draw(); // to make surface work in assembly
      }
      else if(id === 'phimap' || id === 'phimapwireframe') {
          if(id === 'phimapwireframe') {
              ic.applyMapCls.removeLastPhimap();
          }
          ic.applyMapCls.applyPhimapOptions();
          //if(ic.bRender) ic.drawCls.render();
          ic.drawCls.draw(); // to make surface work in assembly
      }
      else if(id === 'phisurface') {
          ic.applyMapCls.applyphisurfaceOptions();
          //if(ic.bRender) ic.drawCls.render();
          ic.drawCls.draw(); // to make surface work in assembly
      }
      else if(id === 'chemicalbinding') {
          ic.bSkipChemicalbinding = false;
          ic.drawCls.draw();
      }
      else {
          ic.drawCls.draw();
      }
    }

    //Set the styles of predefined "protein", "nucleotides", etc.
    setStyle(selectionType, style) {var ic = this.icn3d, me = ic.icn3dui;
      let atoms = {};
      switch(selectionType) {
          case 'proteins':
              atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.proteins);
              if(Object.keys(ic.hAtoms).length < Object.keys(ic.proteins).length) ;

              // remove disulfide bonds
              if(style == 'nothing') {
                ic.opts["ssbonds"] = "no";
                ic.lines['ssbond'] = [];
                for(let i in atoms) {
                    ic.atoms[i].style2 = 'nothing';
                }
              }
              else {
                ic.opts["ssbonds"] = "yes";
              }

              break;
          case 'sidec':
              atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.sidec);
              //calpha_atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.calphas);
              // include calphas
              //atoms = me.hashUtilsCls.unionHash(atoms, calpha_atoms);
              break;
          case 'nucleotides':
              atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.nucleotides);
              if(Object.keys(ic.hAtoms).length < Object.keys(ic.nucleotides).length) ;
              break;
          case 'ntbase':
              atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.ntbase);
              break;
          case 'chemicals':
              atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.chemicals);
              break;
          case 'ions':
              atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.ions);
              break;
          case 'water':
              atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.water);
              break;
      }
      // draw sidec separatedly
      if(selectionType === 'sidec' || selectionType === 'ntbase') {
          for(let i in atoms) {
            ic.atoms[i].style2 = style;
          }
      }
      else {
          for(let i in atoms) {
            ic.atoms[i].style = style;
          }
      }
      ic.opts[selectionType] = style;
      ic.selectionCls.saveSelectionIfSelected();
      ic.drawCls.draw();
    }

    //Save the current style setting so that these styles can be restored later by clicking "Apply Saved Style" in the Style menu.
    saveStyle() {var ic = this.icn3d; ic.icn3dui;
       for(let i in ic.atoms) {
           let atom = ic.atoms[i];
           atom.styleSave = atom.style;
           if(atom.style2 !== undefined) atom.style2Save = atom.style2;
       }
    }

    //Restore the previously saved style.
    applySavedStyle() {var ic = this.icn3d; ic.icn3dui;
       for(let i in ic.atoms) {
           let atom = ic.atoms[i];
           if(atom.styleSave !== undefined) {
               atom.style = atom.styleSave;
           }
           if(atom.style2Save !== undefined) {
               atom.style2 = atom.style2Save;
           }
       }
       ic.drawCls.draw();
    }

    //Save the current color setting so that these colors can be restored later by clicking "Apply Saved Color" in the Color menu.
    saveColor() {var ic = this.icn3d; ic.icn3dui;
       for(let i in ic.atoms) {
           let atom = ic.atoms[i];
           atom.colorSave = atom.color.clone();
       }
    }

    //Restore the previously saved color.
    applySavedColor() {var ic = this.icn3d; ic.icn3dui;
       for(let i in ic.atoms) {
           let atom = ic.atoms[i];
           if(atom.colorSave !== undefined) {
               atom.color = atom.colorSave.clone();
               ic.atomPrevColors[i] = atom.color;
           }
       }
       
       ic.hlUpdateCls.changeSeqColor(Object.keys(ic.residues));
       ic.drawCls.draw();
    }
}

/**
 * @author Jack Lin <th3linja@yahoo.com> / https://github.com/ncbi/icn3d
 */

 class LegendTable {
     constructor(icn3d) {
         this.icn3d = icn3d;
     }

     showColorLegend(colorType) { let ic = this.icn3d, me = ic.icn3dui;
        let bClose = false;

        let colorLabel = colorType.substr(0, 1).toUpperCase() + colorType.substr(1);
        if(colorType == 'confidence') {
            colorLabel = 'AlphaFold Confidence';
        }
        else if(colorType == 'normalized hydrophobic') {
            colorLabel = 'Normalized Hydrophobicity';
        }
        else if(colorType == 'hydrophobic') {
            colorLabel = 'Hydrophobicity';
        }

        let html = "Color by <b>" + colorLabel + "</b><br><br>";
 
        //if (ic.legendClick == 1){
        if (colorType == 'atom'){  
            let categoryArray = ['proteins', 'nucleotides', 'chemicals', 'ions', 'water'];
            for(let i = 0, il = categoryArray.length; i < il; ++i) {
                let category = categoryArray[i];
                let atomHash = me.hashUtilsCls.intHash(ic[category], ic.hAtoms);
                html += this.getColorLegendForElem(category, atomHash);
            }
        }
        //else if (ic.legendClick == 2){
        else if (colorType == 'residue'){
            html += this.getColorLegendForResidue(ic.hAtoms);
        }
        //else if (ic.legendClick == 3){
        else if (colorType == 'charge'){
            html += this.getColorLegendForCharge(ic.hAtoms);
        }
        //else if (ic.legendClick == 4){
        else if (colorType == 'normalized hydrophobic' || colorType == 'hydrophobic') {
            let bOriResn = true;
            let resSet = this.getRes2color(ic.hAtoms, bOriResn);

            // polar first - most to least
            // create hydrophobic table
            var items = Object.keys(resSet).map(
                //(key) => { return [key, Object.keys(resSet[key])[0]] 
                (key) => { return [key, me.parasCls.hydrophobicValues[key]] 
            });

            // items.sort(
            //     (first, second) => { 
            //         return ((parseInt(second[1].substring(2,4), 16) - parseInt(second[1].substring(4,6), 16)) - (parseInt(first[1].substring(2,4), 16) - parseInt(first[1].substring(4,6), 16)));
            //     }
            // );

            items.sort(
                (first, second) => { 
                    return parseFloat(first[1]) - parseFloat(second[1]);
                }
            );

            var keys = items.map(
                //(e) => { return [e[0], e[1]]
                (e) => { return [e[0], Object.keys(resSet[e[0]])[0]]
            });

            html += "<div>";
            
            if(colorType == 'normalized hydrophobic') {
                html += "Dark green (W, F, Y, L, I, C, M): Hydrophobic<br>";
                html += "Light green (G, V, S, T, A, N, P, Q): Polar<br>";
                html += "Grey: Charged, not hydrophobic<br><br>";
            }
            else {
                html += "Green (W, F, Y, L, I, C, M): Hydrophobic<br>";
                html += "Yellow (G, V, S, T, A, N, P, Q): Polar<br>";
                html += "Red: Negatively Charged<br>";
                html += "Blue: Positively Charged<br><br>";
            }

            let cnt = 0;
            for (let key of keys) {
                if(!me.parasCls.residueAbbrev[key[0]]) continue;

                html += "<div style='display:inline-block; width:100px'>";
                html += "<div style='width: 10px; height: 10px; background-color:#" + key[1] + "; border: 0px;display:inline-block;' ></div> ";
                html +=  me.parasCls.residueAbbrev[key[0]] + "</div>";

                if(cnt % 4 == 3) html += "<br>";

                ++cnt;
            }
            html += "</div>";
        }
        //else if (ic.legendClick == 5){
        else if (colorType == 'b factor') {
            html += "<div style='width:450px'>B factor quantitates the uncertainty for each atom. A high B factor reflects that the position is less certain.</div><br>";
            html += me.htmlCls.clickMenuCls.setLegendHtml();
        }
        //else if (ic.legendClick == 6){
        else if (colorType == 'confidence') {
            html += me.htmlCls.clickMenuCls.setLegendHtml(true);
        }
        else {
            html = '';
            bClose = true;
        }

        $("#" + me.pre + "dl_legend").html(html);
        me.htmlCls.dialogCls.openDlg('dl_legend', 'Color Legend');

        if(bClose) {
            if(window.dialog) window.dialog.dialog( "close" );
        }
     }

     getColorLegendForElem(category, atomHash) { let ic = this.icn3d, me = ic.icn3dui;
        let html = '';
        let elemSet = {};

        for (let serial in atomHash){
            // atom = ic.atoms[Object.keys(atomHash)[k]];
            let atom = ic.atoms[serial];
            let temp = (atom === undefined || atom.color === undefined || atom.color.getHexString().toUpperCase() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
            if (elemSet[atom.elem] === undefined){
                elemSet[atom.elem] = {};
            }
            elemSet[atom.elem][temp] = 1;
        }

        if(Object.keys(elemSet).length > 0) {
            //html += "<button value='" + category + "' display='block'>" + category + "</button><br>";
            html += "<b>" + category + "</b><br>";
            let elemArray = Object.keys(elemSet).sort();
            //for (let k in elemSet) {
            for(let i = 0, il = elemArray.length; i < il; ++i) {
                let k = elemArray[i];

                html += "<span>";
                for (let v in elemSet[k]) {
                    html += "<div style='width: 10px; height: 10px; background-color:#" + v + "; border: 0px;display:inline-block;' ></div> ";
                }
                html +=  me.parasCls.atomnames[k.toUpperCase()] + "</span><br>";
            }
            html += "<br>";
        }

        return html;
     }

     getRes2color(atomHash, bOriResn) { let ic = this.icn3d, me = ic.icn3dui;
        let resSet = {};

        let residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(atomHash);
        for(let resid in residueHash){
            let atomHash = ic.residues[resid];

            let atom = ic.firstAtomObjCls.getFirstAtomObj(atomHash);
            let resiLabel = (bOriResn) ? atom.resn : me.parasCls.residueAbbrev[atom.resn];
            let temp = (atom === undefined || atom.color === undefined || atom.color.getHexString().toUpperCase() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
            
            if (resiLabel != undefined){
                if (resSet[resiLabel] === undefined){
                    resSet[resiLabel] = {};
                }
                resSet[resiLabel][temp] = 1;
            }
        }

        return resSet;
     }

     getColorLegendForResidue(atomHash) { let ic = this.icn3d; ic.icn3dui;
        let html = '';

        let resSet = this.getRes2color(atomHash);

        if(Object.keys(resSet).length > 0) {
            //html += "<button value='" + pdbid + "' display='block'>" + pdbid + "</button><br>";
            html += "<div>";
            let residueArray = Object.keys(resSet).sort();
            //for (let k in resSet) {
            let dnaHtml = '';
            let cnt = 0;
            for(let i = 0, il = residueArray.length; i < il; ++i) {
                let htmlTmp = '';
                let k = residueArray[i];
                htmlTmp += "<div style='display:inline-block; width:100px'>";
                for (let v in resSet[k]) {
                    htmlTmp += "<div style='width: 10px; height: 10px; background-color:#" + v + "; border: 0px;display:inline-block;' ></div> ";
                }
                htmlTmp +=  k + "</div>";

                if(cnt % 4 == 3) htmlTmp += "<br>";

                if(k.indexOf('(') != -1) {
                    html += htmlTmp;
                    ++cnt;
                }
                else {
                    dnaHtml += htmlTmp;
                }
            }

            if(dnaHtml) html += "<br>" + dnaHtml;

            html += "</div>";
        }

        return html;
     }

     getColorLegendForCharge(atomHash) { let ic = this.icn3d; ic.icn3dui;
        let html = '';

        let residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(atomHash);

        let chargeHash = {};
        for(let resid in residueHash){
            let atomHash = ic.residues[resid];

            let atom = ic.firstAtomObjCls.getFirstAtomObj(atomHash);
            if(atom.resn == 'ARG' || atom.resn == 'LYS') {
                chargeHash['Positive'] = 1;
            }
            else if(atom.resn == 'HIS') {
                chargeHash['Partial-Positive'] = 1;
            }
            else if(atom.resn == 'ASP' || atom.resn == 'GLU' || ic.nucleotides[atom.serial]) {
                chargeHash['Negative'] = 1;
            }
            else {
                chargeHash['Neutral'] = 1;
            }
        }

        const charge2color = {
            "Positive": "0000ff",
            "Partial-Positive": "8080ff",
            "Negative": "ff0000",
            "Neutral": "888888"
        };

        let chargeOrder = ["Positive", "Partial-Positive", "Negative", "Neutral"];
 
        html += "<div>";
        for (let i = 0, il = chargeOrder.length; i < il; ++i) {
            let charge = chargeOrder[i];
            if (chargeHash[charge]){
                html += "<span>";
                html += "<div style='width: 10px; height: 10px; background-color:#" + charge2color[charge] + "; border: 0px;display:inline-block;' ></div> ";
                html += charge;
                html +=  "</span><br>";
            }
        }
        html += "<br>(Charges are at pH 7)";
        html += "</div>";

        return html;
     }
 }

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoCddSite {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Show the annotations of CDD domains and binding sites.
    async showCddSiteAll() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        ic.chainid2pssmid = {};

        let chnidBaseArray = $.map(ic.protein_chainid, function(v) { return v; });
        let chnidArray = Object.keys(ic.protein_chainid);
        // show conserved domains and binding sites
        // live search
        let url = "https://www.ncbi.nlm.nih.gov/Structure/cdannots/cdannots.fcgi?fmt&frclive&live=lcl&queries=" + chnidBaseArray;     
        // precalculated
        //let url = "https://www.ncbi.nlm.nih.gov/Structure/cdannots/cdannots.fcgi?fmt&live=lcl&queries=" + chnidBaseArray;
        // live search for AlphaFold structures
        //if(me.cfg.afid) {

        // use precalculated CDD annotation if
        if( (Object.keys(ic.structures).length == 1 && (me.cfg.mmtfid || me.cfg.pdbid || me.cfg.opmid || me.cfg.mmdbid || me.cfg.gi || me.cfg.uniprotid || me.cfg.blast_rep_id || me.cfg.cid || me.cfg.mmcifid))
            || (Object.keys(ic.structures).length == 2 && me.cfg.align) ) {
                let data = {};
                try {
                    data.value = await me.getAjaxPromise(url, 'jsonp');
                 
                    thisClass.parseCddData([data], chnidArray);
                    /// if(ic.deferredAnnoCddSite !== undefined) ic.deferredAnnoCddSite.resolve();
                }
                catch(err) {
                    thisClass.getNoCdd(chnidBaseArray);
                    /// if(ic.deferredAnnoCddSite !== undefined) ic.deferredAnnoCddSite.resolve();

                    return;
                }
        }
        else {
            let ajaxArray = [];

            for(let i = 0, il = chnidArray.length; i < il; ++i) {
                //let seq = Array.isArray(ic.giSeq[chnidArray[i]]) ? ic.giSeq[chnidArray[i]].join('') : ic.giSeq[chnidArray[i]];
                let seq = Array.isArray(ic.giSeq[chnidArray[i]]) ? ic.giSeq[chnidArray[i]].join('').toUpperCase() : ic.giSeq[chnidArray[i]].toUpperCase();

                // remove water molecules
                seq = seq.replace(/O/g, '');

                //url = "https://www.ncbi.nlm.nih.gov/Structure/cdannots/cdannots.fcgi?fmt&live=lcl&queries=" + ic.giSeq[chnidArray[0]].join('');
                // live searchE
                url = "https://www.ncbi.nlm.nih.gov/Structure/cdannots/cdannots.fcgi?fmt&frclive&live=lcl&queries=" + seq;             
                // precalculated
                //url = "https://www.ncbi.nlm.nih.gov/Structure/cdannots/cdannots.fcgi?fmt&live=lcl&queries=" + seq;

                let cdd = me.getAjaxPromise(url, 'jsonp');

                ajaxArray.push(cdd);
            }

            let allPromise = Promise.allSettled(ajaxArray);
            try {
                let dataArray = await allPromise;

                thisClass.parseCddData(dataArray, chnidArray, true);
                /// if(ic.deferredAnnoCddSite !== undefined) ic.deferredAnnoCddSite.resolve();
            }
            catch(err) {
                
            }            
        }
    }

    parseCddData(dataArray, chnidArray, bSeq) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        let chainWithData = {};

        if(me.bNode) {
            if(!ic.resid2cdd) ic.resid2cdd = {};
            if(!ic.resid2site) ic.resid2site = {};
        }

        for(let i = 0, il = dataArray.length; i < il; ++i) {
            //let data = (bSeq) ? dataArray[i][0] : dataArray[i];
            let data = dataArray[i].value;

            for(let chainI = 0, chainLen = data.data.length; chainI < chainLen; ++chainI) {
                let cddData = data.data[chainI];
                cddData._id;
                //var pos = chnidBaseArray.indexOf(chnidBase);
                //var chnid = chnidArray[pos];
                //let chnid = chnidArray[chainI];
                let chnid = (bSeq) ? chnidArray[i] : chnidArray[chainI];
                chainWithData[chnid] = 1;
                let html = '<div id="' + ic.pre + chnid + '_cddseq_sequence" class="icn3d-cdd icn3d-dl_sequence">';
                let html2 = html;
                let html3 = html;
                let domainArray = cddData.doms;
                if(me.bNode && !ic.resid2cdd[chnid]) ic.resid2cdd[chnid] = [];
                let result = thisClass.setDomainFeature(domainArray, chnid, true, html, html2, html3);

                ic.chainid2pssmid[chnid] = {pssmid2name: result.pssmid2name, pssmid2fromArray: result.pssmid2fromArray, pssmid2toArray: result.pssmid2toArray};

                let acc2domain = result.acc2domain;
                html = result.html + '</div>';
                html2 = result.html2 + '</div>';
                html3 = result.html3 + '</div>';
                $("#" + ic.pre + "dt_cdd_" + chnid).html(html);
                $("#" + ic.pre + "ov_cdd_" + chnid).html(html2);
                $("#" + ic.pre + "tt_cdd_" + chnid).html(html3);

                html = '<div id="' + ic.pre + chnid + '_siteseq_sequence" class="icn3d-dl_sequence">';
                html2 = html;
                html3 = html;

                // features
                let featuteArray = cddData.motifs;
                if(me.bNode && !ic.resid2site[chnid]) ic.resid2site[chnid] = [];
                result = thisClass.setDomainFeature(featuteArray, chnid, false, html, html2, html3, acc2domain);

                html = result.html; // + '</div>';
                html2 = result.html2; // + '</div>';
                html3 = result.html3; // + '</div>';

                let siteArray = data.data[chainI].sites;
                let indexl =(siteArray !== undefined) ? siteArray.length : 0;
                for(let index = 0; index < indexl; ++index) {
                    siteArray[index].srcdom;
                    siteArray[index].type;
                    let resCnt = siteArray[index].sz;
                    let title = 'site: ' + siteArray[index].title;
                    if(title.length > 17) title = title.substr(0, 17) + '...';
                    //var fulltitle = "site: " + siteArray[index].title + "(domain: " + domain + ")";
                    let fulltitle = siteArray[index].title;
                    let resPosArray, adjustedResPosArray = [];
                    for(let i = 0, il = siteArray[index].locs.length; i < il; ++i) {
                        resPosArray = siteArray[index].locs[i].coords;
                        for(let j = 0, jl = resPosArray.length; j < jl; ++j) {
                            // if(ic.bNCBI) {
                            //     adjustedResPosArray.push(Math.round(resPosArray[j]));
                            // }
                            // else {
                            //     adjustedResPosArray.push(thisClass.getAdjustedResi(Math.round(resPosArray[j]), chnid, ic.matchedPos, ic.chainsSeq, ic.baseResi) - 1);
                            // }
                            adjustedResPosArray.push(ic.ParserUtilsCls.getResi(chnid, Math.round(resPosArray[j])) );
                        }
                    }

                    let bCoordinates = false;
                    for(let i = 0, il = adjustedResPosArray.length; i < il; ++i) {
                        let resid = chnid + "_" + adjustedResPosArray[i];
                        if(ic.residues.hasOwnProperty(resid)) {
                            bCoordinates = true;
                            break;
                        }
                    }
    
                    let linkStr = (bCoordinates) ? 'icn3d-link icn3d-blue' : '';

                    let htmlTmp2 = '<div class="icn3d-seqTitle ' + linkStr + '" site="site" posarray="' + adjustedResPosArray.toString() + '" shorttitle="' + title + '" setname="' + chnid + '_site_' + index + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + title + ' </div>';
                    let htmlTmp3 = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Res</span>';
                    let htmlTmp = '<span class="icn3d-seqLine">';
                    html3 += htmlTmp2 + htmlTmp3 + '<br>';
                    html += htmlTmp2 + htmlTmp3 + htmlTmp;
                    html2 += htmlTmp2 + htmlTmp3 + htmlTmp;
                    let pre = 'site' + index.toString();
                    //var widthPerRes = ic.seqAnnWidth / ic.maxAnnoLength;
                    let prevEmptyWidth = 0;
                    let prevLineWidth = 0;
                    let widthPerRes = 1;
                    for(let i = 0, il = ic.giSeq[chnid].length; i < il; ++i) {
                        html += ic.showSeqCls.insertGap(chnid, i, '-');
                        if(resPosArray.indexOf(i) != -1) {
                            let cFull = ic.giSeq[chnid][i];
                            let c = cFull;
                            if(cFull.length > 1) {
                                c = cFull[0] + '..';
                            }
                            //let pos = thisClass.getAdjustedResi(i, chnid, ic.matchedPos, ic.chainsSeq, ic.baseResi);
                            let pos = ic.ParserUtilsCls.getResi(chnid, i);
                            
                            html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" title="' + c + pos + '" class="icn3d-residue">' + cFull + '</span>';
                            if(me.bNode) {
                                let obj = {};
                                obj[chnid + '_' + pos] = 'site: ' + siteArray[index].title;
                                ic.resid2site[chnid].push(obj);
                            }

                            html2 += ic.showSeqCls.insertGapOverview(chnid, i);
                            let emptyWidth =(me.cfg.blast_rep_id == chnid) ? Math.round(ic.seqAnnWidth * i /(ic.maxAnnoLength + ic.nTotalGap) - prevEmptyWidth - prevLineWidth) : Math.round(ic.seqAnnWidth * i / ic.maxAnnoLength - prevEmptyWidth - prevLineWidth);
                            //if(emptyWidth < 0) emptyWidth = 0;
                            if(emptyWidth >= 0) {
                                html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                                html2 += '<div style="display:inline-block; background-color:#000; width:' + widthPerRes + 'px;" title="' + c + pos + '">&nbsp;</div>';
                                prevEmptyWidth += emptyWidth;
                                prevLineWidth += widthPerRes;
                            }
                        }
                        else {
                            html += '<span>-</span>'; //'<span>-</span>';
                        }
                    }
                    htmlTmp = '<span class="icn3d-residueNum" title="residue count">&nbsp;' + resCnt.toString() + ' Residues</span>';
                    htmlTmp += '</span>';
                    htmlTmp += '<br>';
                    html += htmlTmp;
                    html2 += htmlTmp;
                }
                html += '</div>';
                html2 += '</div>';
                html3 += '</div>';
                $("#" + ic.pre + "dt_site_" + chnid).html(html);
                $("#" + ic.pre + "ov_site_" + chnid).html(html2);
                $("#" + ic.pre + "tt_site_" + chnid).html(html3);
            }
        } // outer for loop

        // missing CDD data
        for(let chnid in ic.protein_chainid) {
            if(!chainWithData.hasOwnProperty(chnid)) {
                $("#" + ic.pre + "dt_cdd_" + chnid).html('');
                $("#" + ic.pre + "ov_cdd_" + chnid).html('');
                $("#" + ic.pre + "tt_cdd_" + chnid).html('');
                $("#" + ic.pre + "dt_site_" + chnid).html('');
                $("#" + ic.pre + "ov_site_" + chnid).html('');
                $("#" + ic.pre + "tt_site_" + chnid).html('');
            }
        }
        // add here after the ajax call
        ic.showAnnoCls.enableHlSeq();
        ic.bAjaxCddSite = true;
    }

    getNoCdd(chnidBaseArray) { let ic = this.icn3d; ic.icn3dui;
        console.log( "No CDD data were found for the protein " + chnidBaseArray + "..." );
        for(let chnid in ic.protein_chainid) {
            $("#" + ic.pre + "dt_cdd_" + chnid).html('');
            $("#" + ic.pre + "ov_cdd_" + chnid).html('');
            $("#" + ic.pre + "tt_cdd_" + chnid).html('');
            $("#" + ic.pre + "dt_site_" + chnid).html('');
            $("#" + ic.pre + "ov_site_" + chnid).html('');
            $("#" + ic.pre + "tt_site_" + chnid).html('');
        }
        // add here after the ajax call
        ic.showAnnoCls.enableHlSeq();
        ic.bAjaxCddSite = true;
    }

    setDomainFeature(domainArray, chnid, bDomain, html, html2, html3, acc2domain) { let ic = this.icn3d, me = ic.icn3dui;

        let pssmid2name, pssmid2fromArray, pssmid2toArray;
        if(bDomain) {
            acc2domain = {};
            pssmid2name = {};
            pssmid2fromArray = {};
            pssmid2toArray = {};
        }

        let indexl =(domainArray !== undefined) ? domainArray.length : 0;
        let maxTextLen =(bDomain) ? 14 : 19;
        let titleSpace =(bDomain) ? 100 : 120;
        for(let index = 0; index < indexl; ++index) {
            let pssmid = (bDomain) ? domainArray[index].pssmid : 0;

            let acc =(bDomain) ? domainArray[index].acc : domainArray[index].srcdom;
            let type = domainArray[index].type;
            type =(bDomain) ? 'domain' : 'feat';
            let domain =(bDomain) ? domainArray[index].title.split(':')[0] : domainArray[index].title;
            // convert double quote
            domain = domain.replace(/\"/g, "``");
            // convert singe quote
            domain = domain.replace(/'/g, "`");

            if(bDomain) acc2domain[acc] = domain;

            let defline =(bDomain) ? domainArray[index].defline : '';
            let title = type + ': ' + domain;

            if(title.length > maxTextLen) title = title.substr(0, maxTextLen) + '...';
            let fulltitle = type + ": " + domain;

            if(bDomain) pssmid2name[pssmid] = domain;

            // each domain may have several repeat. Treat each repeat as a domain
            let domainRepeatArray = domainArray[index].locs;

            if(!domainRepeatArray) continue;

            for(let r = 0, rl = domainRepeatArray.length; r < rl; ++r) {
                // each domain repeat or domain may have several segments, i.e., a domain may not be continous
                let fromArray = [], toArray = [];
                let resiHash = {};
                let resCnt = 0;
                let segArray =(bDomain) ? domainRepeatArray[r].segs : [domainRepeatArray[r]];
                for(let s = 0, sl = segArray.length; s < sl; ++s) {
                    let domainFrom = Math.round(segArray[s].from);
                    let domainTo = Math.round(segArray[s].to);

                    // if(ic.bNCBI) {
                    //     fromArray.push(domainFrom);
                    //     toArray.push(domainTo);
                    // }
                    // else {
                    //     fromArray.push(thisClass.getAdjustedResi(domainFrom, chnid, ic.matchedPos, ic.chainsSeq, ic.baseResi) - 1);
                    //     toArray.push(thisClass.getAdjustedResi(domainTo, chnid, ic.matchedPos, ic.chainsSeq, ic.baseResi) - 1);
                    // }

                    // fromArray.push(ic.ParserUtilsCls.getResi(chnid, domainFrom));
                    // toArray.push(ic.ParserUtilsCls.getResi(chnid, domainTo));

                    fromArray.push(domainFrom);
                    toArray.push(domainTo);

                    for(let i = domainFrom; i <= domainTo; ++i) {
                        resiHash[i] = 1;
                    }
                    resCnt += domainTo - domainFrom + 1;
                }

                //var setname = chnid + "_" + domain + "_" + index + "_" + r; //chnid + "_" + type + "_" + index + "_" + r;
                let setname = chnid + "_" + domain;
                if(!bDomain) setname += "_" + index + "_" + r; // + acc2domain[acc];

                if(bDomain) pssmid2fromArray[pssmid] = fromArray;
                if(bDomain) pssmid2toArray[pssmid] = toArray;

                let bCoordinates = false;
                for(let i = 0, il = fromArray.length; i < il; ++i) {
                    let from = parseInt(fromArray[i]), to = parseInt(toArray[i]);
                                       
                    for(let j = from; j <= to; ++j) {
                        
                        let resid = chnid + "_" + j;
                        
                        if(ic.residues.hasOwnProperty(resid)) {
                            bCoordinates = true;
                            break;
                        }
                    }

                    if(bCoordinates) {
                        break;
                    }
                }

                let linkStr = (bCoordinates) ? 'icn3d-link icn3d-blue' : '';

                let htmlTmp2 = '<div class="icn3d-seqTitle ' + linkStr + '" ' + type + '="' + acc + '" from="' + fromArray + '" to="' + toArray + '" shorttitle="' + title + '" setname="' + setname + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + title + ' </div>';
                let htmlTmp3 = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Res</span>';
                html3 += htmlTmp2 + htmlTmp3 + '<br>';
                let htmlTmp = '<span class="icn3d-seqLine">';
                html += htmlTmp2 + htmlTmp3 + htmlTmp;
                if(bDomain) {
                    html2 += '<div style="width:20px; display:inline-block;"><span id="' + ic.pre + chnid + '_' + acc + '_' + r + '_cddseq_expand" class="ui-icon ui-icon-plus icn3d-expand icn3d-link" style="width:15px;" title="Expand"></span><span id="' + ic.pre + chnid + '_' + acc + '_' + r + '_cddseq_shrink" class="ui-icon ui-icon-minus icn3d-shrink icn3d-link" style="display:none; width:15px;" title="Shrink"></span></div>';
                }
                html2 += '<div style="width:' + titleSpace + 'px!important;" class="icn3d-seqTitle ' + linkStr + '" ' + type + '="' + acc + '" from="' + fromArray + '" to="' + toArray + '" shorttitle="' + title + '" index="' + index + '" setname="' + setname + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + title + ' </div>';
                html2 += htmlTmp3 + htmlTmp;
                let pre = type + index.toString();
                for(let i = 0, il = ic.giSeq[chnid].length; i < il; ++i) {
                  html += ic.showSeqCls.insertGap(chnid, i, '-');

                  if(resiHash.hasOwnProperty(i)) {
                      let cFull = ic.giSeq[chnid][i];
                      let c = cFull;
                      if(cFull.length > 1) {
                          c = cFull[0] + '..';
                      }
                      // let pos = thisClass.getAdjustedResi(i, chnid, ic.matchedPos, ic.chainsSeq, ic.baseResi);
                      let pos = ic.ParserUtilsCls.getResi(chnid, i);
                      html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" title="' + c + pos + '" class="icn3d-residue">' + cFull + '</span>';
                      if(me.bNode) {
                        let obj = {};
                        obj[chnid + '_' + pos] = fulltitle;
                        if(bDomain) {
                            ic.resid2cdd[chnid].push(obj);
                        }
                        else {
                            ic.resid2site[chnid].push(obj);
                        }
                      }
                  }
                  else {
                      html += '<span>-</span>'; //'<span>-</span>';
                  }
                }
                let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chnid]);
                let colorStr =(atom.color === undefined || atom.color.getHexString() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
                let color =(atom.color !== undefined) ? colorStr : "CCCCCC";
                if(me.cfg.blast_rep_id != chnid) { // regular
                    for(let i = 0, il = fromArray.length; i < il; ++i) {
                        let emptyWidth =(i == 0) ? Math.round(ic.seqAnnWidth *(fromArray[i] - ic.baseResi[chnid] - 1) / ic.maxAnnoLength) : Math.round(ic.seqAnnWidth *(fromArray[i] - toArray[i-1] - 1) / ic.maxAnnoLength);
                        html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                        html2 += '<div style="display:inline-block; color:white!important; font-weight:bold; background-color:#' + color + '; width:' + Math.round(ic.seqAnnWidth *(toArray[i] - fromArray[i] + 1) / ic.maxAnnoLength) + 'px;" class="icn3d-seqTitle ' + linkStr + '" domain="' +(index+1).toString() + '" from="' + fromArray + '" to="' + toArray + '" shorttitle="' + title + '" index="' + index + '" setname="' + setname + '" id="' + chnid + '_domain_' + index + '_' + r + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + domain + ' </div>';
                    }
                }
                else { // with potential gaps
                    let fromArray2 = [], toArray2 = [];
                    for(let i = 0, il = fromArray.length; i < il; ++i) {
                        fromArray2.push(fromArray[i]);
                        for(let j = parseInt(fromArray[i]); j <= parseInt(toArray[i]); ++j) {
                            if(ic.targetGapHash !== undefined && ic.targetGapHash.hasOwnProperty(j)) {
                                toArray2.push(j - 1);
                                fromArray2.push(j);
                            }
                        }
                        toArray2.push(toArray[i]);
                    }
                    for(let i = 0, il = fromArray2.length; i < il; ++i) {
                        html2 += ic.showSeqCls.insertGapOverview(chnid, fromArray2[i]);
                        let emptyWidth =(i == 0) ? Math.round(ic.seqAnnWidth *(fromArray2[i] - ic.baseResi[chnid] - 1) /(ic.maxAnnoLength + ic.nTotalGap)) : Math.round(ic.seqAnnWidth *(fromArray2[i] - toArray2[i-1] - 1) /(ic.maxAnnoLength + ic.nTotalGap));
                        html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                        html2 += '<div style="display:inline-block; color:white!important; font-weight:bold; background-color:#' + color + '; width:' + Math.round(ic.seqAnnWidth *(toArray2[i] - fromArray2[i] + 1) /(ic.maxAnnoLength + ic.nTotalGap)) + 'px;" class="icn3d-seqTitle ' + linkStr + '" domain="' +(index+1).toString() + '" from="' + fromArray2 + '" to="' + toArray2 + '" shorttitle="' + title + '" index="' + index + '" setname="' + setname + '" id="' + chnid + '_domain_' + index + '_' + r + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + domain + ' </div>';
                    }
                }
                htmlTmp = '<span class="icn3d-residueNum" title="residue count">&nbsp;' + resCnt.toString() + ' Residues</span>';
                htmlTmp += '</span>';
                htmlTmp += '<br>';
                html += htmlTmp;
                html2 += htmlTmp;
                if(bDomain) {
                    html2 += '<div id="' + ic.pre + chnid + '_' + acc + '_' + r + '_cddseq" style="display:none; white-space:normal;" class="icn3d-box">' + defline + '(<a href="' + me.htmlCls.baseUrl + 'cdd/cddsrv.cgi?uid=' + acc + '" target="_blank" class="icn3d-blue">open details view...</a>)</div>';
                }
            } // for(let r = 0,
        }

        return {html: html, html2: html2, html3: html3, acc2domain: acc2domain,
          pssmid2name: pssmid2name, pssmid2fromArray: pssmid2fromArray, pssmid2toArray: pssmid2toArray}
    }

    // getAdjustedResi(resi, chnid, matchedPos, chainsSeq, baseResi) { let ic = this.icn3d, me = ic.icn3dui;
    //     return (resi >= matchedPos[chnid] && resi - matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][resi - matchedPos[chnid]].resi : baseResi[chnid] + 1 + resi;
    // }

    showAnnoType(chnid, chnidBase, type, title, residueArray, resid2resids) { let ic = this.icn3d, me = ic.icn3dui;
        let html = '<div id="' + ic.pre + chnid + '_' + type + 'seq_sequence" class="icn3d-dl_sequence">';
        let html2 = html;
        let html3 = html;
        if(residueArray.length == 0) {
            $("#" + ic.pre + "dt_" + type + "_" + chnid).html('');
            $("#" + ic.pre + "ov_" + type + "_" + chnid).html('');
            $("#" + ic.pre + "tt_" + type + "_" + chnid).html('');
            return;
        }
        let fulltitle = title;
        if(title.length > 17) title = title.substr(0, 17) + '...';
        let resPosArray = [];
        for(let i = 0, il = residueArray.length; i < il; ++i) {
            let resid = residueArray[i];
            //let resi = Math.round(resid.substr(residueArray[i].lastIndexOf('_') + 1) );
            let resi = resid.substr(residueArray[i].lastIndexOf('_') + 1);
            resPosArray.push( resi );
        }
        let resCnt = resPosArray.length;
        let chainnameNospace = type;
        let htmlTmp2 = '<div class="icn3d-seqTitle icn3d-link icn3d-blue" ' + type + '="" posarray="' + resPosArray.toString() + '" shorttitle="' + title + '" setname="' + chnid + '_' + chainnameNospace + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + title + ' </div>';
        let htmlTmp3 = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Res</span>';
        html3 += htmlTmp2 + htmlTmp3 + '<br>';
        let htmlTmp = '<span class="icn3d-seqLine">';
        html += htmlTmp2 + htmlTmp3 + htmlTmp;
        html2 += htmlTmp2 + htmlTmp3 + htmlTmp;
        let pre = type;
        let prevEmptyWidth = 0;
        let prevLineWidth = 0;
        let widthPerRes = 1;
        for(let i = 0, il = ic.giSeq[chnid].length; i < il; ++i) {
          html += ic.showSeqCls.insertGap(chnid, i, '-');
          let resi = ic.ParserUtilsCls.getResi(chnid, i);
          //if(resPosArray.indexOf(i+1 + ic.baseResi[chnid]) != -1) {
          if(resPosArray.indexOf(resi) != -1) {
              let cFull = ic.giSeq[chnid][i];
              let c = cFull;
              if(cFull.length > 1) {
                  c = cFull[0] + '..';
              }
            //   let pos =(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i;
            //   let resid = chnid + '_' +(i+1 + ic.baseResi[chnid]).toString();
            //   let title = cFull +(i+1 + ic.baseResi[chnid]).toString();
              let pos = resi;
              let resid = chnid + '_' + resi;
              let title = cFull + resi;
              
              if(type == 'ssbond') {
                  title = 'Residue ' + resid + ' has disulfide bond with';
                  let sstitle = '';
                  if(resid2resids[resid] !== undefined) {
                      for(let j = 0, jl = resid2resids[resid].length; j < jl; ++j) {
                        sstitle += ' residue ' + resid2resids[resid][j];
                      }
                  }
                  title += sstitle;

                  if(me.bNode) {
                    let obj = {};
                    obj[resid] = 'disulfide bond with' + sstitle;
                    ic.resid2ssbond[chnid].push(obj);
                  }
              }
              else if(type == 'crosslink') {
                  title = 'Residue ' + resid + ' has cross-linkage with';
                  let cltitle = '';
                  if(resid2resids[resid] !== undefined) {
                      for(let j = 0, jl = resid2resids[resid].length; j < jl; ++j) {
                        cltitle += ' residue ' + resid2resids[resid][j];
                      }
                  }
                  title += cltitle;

                  if(me.bNode) {
                    let obj = {};
                    obj[resid] = 'cross-linkage with' + cltitle;
                    ic.resid2crosslink[chnid].push(obj);
                  }
              }
              else {
                title = 'Residue ' + resid + ' has connection with';
                let cltitle = '';
                if(resid2resids && resid2resids[resid] !== undefined) {
                    for(let j = 0, jl = resid2resids[resid].length; j < jl; ++j) {
                      cltitle += ' residue ' + resid2resids[resid][j];
                    }
                }
                title += cltitle;
              }

              html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" title="' + title + '" class="icn3d-residue">' + c + '</span>';
              html2 += ic.showSeqCls.insertGapOverview(chnid, i);
              let emptyWidth =(me.cfg.blast_rep_id == chnid) ? Math.round(ic.seqAnnWidth * i /(ic.maxAnnoLength + ic.nTotalGap) - prevEmptyWidth - prevLineWidth) : Math.round(ic.seqAnnWidth * i / ic.maxAnnoLength - prevEmptyWidth - prevLineWidth);
                //if(emptyWidth < 0) emptyWidth = 0;
                if(emptyWidth >= 0) {
                    html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                    html2 += '<div style="display:inline-block; background-color:#000; width:' + widthPerRes + 'px;" title="' + title + '">&nbsp;</div>';
                    prevEmptyWidth += emptyWidth;
                    prevLineWidth += widthPerRes;
                }
          }
          else {
            html += '<span>-</span>'; //'<span>-</span>';
          }
        }
        htmlTmp = '<span class="icn3d-residueNum" title="residue count">&nbsp;' + resCnt.toString() + ' Residues</span>';
        htmlTmp += '</span>';
        htmlTmp += '<br>';
        html += htmlTmp;
        html2 += htmlTmp;
        html += '</div>';
        html2 += '</div>';
        html3 += '</div>';
        $("#" + ic.pre + "dt_" + type + "_" + chnid).html(html);
        $("#" + ic.pre + "ov_" + type + "_" + chnid).html(html2);
        $("#" + ic.pre + "tt_" + type + "_" + chnid).html(html3);
    }

    // jquery tooltip
    //https://stackoverflow.com/questions/18231315/jquery-ui-tooltip-html-with-links
    setToolTip() {  let ic = this.icn3d; ic.icn3dui;
      $("[id^=" + ic.pre + "snp]").add("[id^=" + ic.pre + "clinvar]").add("[id^=" + ic.pre + "ssbond]").add("[id^=" + ic.pre + "crosslink]").tooltip({
        content: function() {
            return $(this).prop('title');
        },
        show: null,
        close: function(event, ui) {
            ui.tooltip.hover(
            function() {
                $(this).stop(true).fadeTo(400, 1);
            },
            function() {
                $(this).fadeOut("400", function() {
                    $(this).remove();
                });
            });
        }
      });
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoContact {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Show the residues interacting with the chain.
    showInteraction(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        // let thisClass = this;
        // if(ic.chainname2residues === undefined &&(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined || me.cfg.blast_rep_id !== undefined || me.cfg.align !== undefined || me.cfg.chainalign !== undefined) ) {
        //     // 2d interaction didn't finish loading data yet
        //     setTimeout(function(){
        //       thisClass.showInteraction_base(chnid, chnidBase);
        //     }, 1000);
        // }
        // else {
        //     this.showInteraction_base(chnid, chnidBase);
        // }

        this.showInteraction_base(chnid, chnidBase);
    }
    showInteraction_base(chnid, chnidBase) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) {
            if(!ic.resid2contact) ic.resid2contact = {};
            if(!ic.resid2contact[chnid]) ic.resid2contact[chnid] = [];
        }
        // set interaction
        if(ic.chainname2residues === undefined) ic.chainname2residues = {};
        let radius = 4;
        let chainArray = Object.keys(ic.chains);
        let chainid = chnid;
        let pos = Math.round(chainid.indexOf('_'));
//        if(pos > 4) return; // NMR structures with structure id such as 2K042,2K043, ...
        ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chainid]);
        if(ic.chainname2residues[chainid] === undefined) {
            ic.chainname2residues[chainid] = {};
            let jl = chainArray.length;
            if(jl > 100 && me.cfg.mmdbid === undefined && me.cfg.gi === undefined && me.cfg.blast_rep_id === undefined && me.cfg.align === undefined && me.cfg.chainalign === undefined) {
            //if(jl > 100) {
                //console.log("Do not show interactions if there are more than 100 chains");
                $("#" + ic.pre + "dt_interaction_" + chnid).html("");
                $("#" + ic.pre + "ov_interaction_" + chnid).html("");
                return; // skip interactions if there are more than 100 chains
            }
            for(let j = 0; j < jl; ++j) {
                let chainid2 = chainArray[j];
                if(chainid2 === chainid) continue;
                // interactions should be on the same structure
                if(chainid2.substr(0, chainid2.indexOf('_')) !== chainid.substr(0, chainid.indexOf('_'))) continue;
                pos = Math.round(chainid.indexOf('_'));
                if(pos > 4) continue; // NMR structures with structure id such as 2K042,2K043, ...
                let atom2 = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chainid2]);
                //if(ic.chainname2residues[chainid2] === undefined) ic.chainname2residues[chainid2] = {}
                let type2;
                if(ic.chemicals.hasOwnProperty(atom2.serial)) { // 1. chemical interacting with proteins
                    type2 = 'chemical';
                }
                else if(ic.nucleotides.hasOwnProperty(atom2.serial)) { // 2. DNA interacting with proteins
                    type2 = 'nucleotide';
                }
                else if(ic.ions.hasOwnProperty(atom2.serial)) { // 3. ions interacting with proteins
                    type2 = 'ion';
                }
                else if(ic.proteins.hasOwnProperty(atom2.serial)) { // 4. protein interacting with proteins
                    type2 = 'protein';
                }
                else if(ic.water.hasOwnProperty(atom2.serial)) { // 5. water interacting with proteins
                    type2 = 'water';
                }
                // find atoms in chainid1, which interact with chainid2
                let atomsChainid1 = ic.contactCls.getAtomsWithinAtom(me.hashUtilsCls.hash2Atoms(ic.chains[chainid], ic.atoms), me.hashUtilsCls.hash2Atoms(ic.chains[chainid2], ic.atoms), radius);
                if(Object.keys(atomsChainid1).length == 0) continue;
                let residues = {};
                for(let k in atomsChainid1) {
                    let atom = ic.atoms[k];
                    let residueid = atom.structure + '_' + atom.chain + '_' + atom.resi;
                    residues[residueid] = 1;
                }
                let name = chainid2.substr(chainid2.indexOf('_') + 1) + "(" + type2 + ")";
                ic.chainname2residues[chainid][name] = Object.keys(residues);
            } // for
        }
        let html = '<div id="' + ic.pre + chnid + '_interseq_sequence" class="icn3d-dl_sequence">';
        let html2 = html;
        let html3 = html;
        let index = 0;
        for(let chainname in ic.chainname2residues[chnid]) {
            let residueArray = ic.chainname2residues[chnid][chainname];
            if(!residueArray) continue; // same chain

            let title = "Interact ." + chainname;
            if(title.length > 17) title = title.substr(0, 17) + '...';
            let fulltitle = "Interact ." + chainname;
            let resPosArray = [];
            for(let i = 0, il = residueArray.length; i < il; ++i) {
                let resid = residueArray[i];
                //let resi = Math.round(resid.substr(residueArray[i].lastIndexOf('_') + 1) );
                let resi = resid.substr(residueArray[i].lastIndexOf('_') + 1);

//                resid = chnid + '_' + (resiNcbi + ic.baseResi[chnid]).toString();

                // exclude chemical, water and ions
                if(ic.residues[resid]) {
                    let serial = Object.keys(ic.residues[resid])[0];
                    if(ic.proteins.hasOwnProperty(serial) || ic.nucleotides.hasOwnProperty(serial)) {
//                        resPosArray.push( resiNcbi );
                        resPosArray.push( resi );
                    }
                }
            }
            let resCnt = resPosArray.length;
            if(resCnt == 0) continue;
            let chainnameNospace = chainname.replace(/\s/g, '');
            let htmlTmp2 = '<div class="icn3d-seqTitle icn3d-link icn3d-blue" interaction="' +(index+1).toString() + '" posarray="' + resPosArray.toString() + '" shorttitle="' + title + '" setname="' + chnid + '_' + chainnameNospace + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + title + ' </div>';
            let htmlTmp3 = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Res</span>';
            html3 += htmlTmp2 + htmlTmp3 + '<br>';
            let htmlTmp = '<span class="icn3d-seqLine">';
            html += htmlTmp2 + htmlTmp3 + htmlTmp;
            html2 += htmlTmp2 + htmlTmp3 + htmlTmp;
            let pre = 'inter' + index.toString();
            let prevEmptyWidth = 0;
            let prevLineWidth = 0;
            let widthPerRes = 1;

            for(let i = 0, il = ic.giSeq[chnid].length; i < il; ++i) {
              html += ic.showSeqCls.insertGap(chnid, i, '-');
              let resi = ic.ParserUtilsCls.getResi(chnid, i);           
            //   if(resPosArray.indexOf(i+1 + ic.baseResi[chnid]) != -1) {
              if(resPosArray.indexOf(resi) != -1) {
//              if(resPosArray.indexOf(i+1) != -1) {
                  let cFull = ic.giSeq[chnid][i];
                  let c = cFull;
                  if(cFull.length > 1) {
                      c = cFull[0] + '..';
                  }
                  
                //   let pos =(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i;
                  let pos = resi;
                  html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" title="' + cFull + pos + '" class="icn3d-residue">' + c + '</span>';
                  if(me.bNode) {
                      let obj = {};
                      obj[chnid + '_' + pos] = fulltitle;
                      ic.resid2contact[chnid].push(obj);
                  }
                  
                  html2 += ic.showSeqCls.insertGapOverview(chnid, i);
                  let emptyWidth =(me.cfg.blast_rep_id == chnid) ? Math.round(ic.seqAnnWidth * i /(ic.maxAnnoLength + ic.nTotalGap) - prevEmptyWidth - prevLineWidth) : Math.round(ic.seqAnnWidth * i / ic.maxAnnoLength - prevEmptyWidth - prevLineWidth);
                    //if(emptyWidth < 0) emptyWidth = 0;
                    if(emptyWidth >= 0) {
                    html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                    html2 += '<div style="display:inline-block; background-color:#000; width:' + widthPerRes + 'px;" title="' + c + pos + '">&nbsp;</div>';
                    prevEmptyWidth += emptyWidth;
                    prevLineWidth += widthPerRes;
                    }
              }
              else {
                html += '<span>-</span>'; //'<span>-</span>';
              }
            }
            htmlTmp = '<span class="icn3d-residueNum" title="residue count">&nbsp;' + resCnt.toString() + ' Residues</span>';
            htmlTmp += '</span>';
            htmlTmp += '<br>';
            html += htmlTmp;
            html2 += htmlTmp;
            ++index;
        }
        html += '</div>';
        html2 += '</div>';
        html3 += '</div>';
        $("#" + ic.pre + "dt_interaction_" + chnid).html(html);
        $("#" + ic.pre + "ov_interaction_" + chnid).html(html2);
        $("#" + ic.pre + "tt_interaction_" + chnid).html(html3);
        // add here after the ajax call
        if(! me.utilsCls.isMobile()) {
            ic.hlSeqCls.selectSequenceNonMobile();
        }
        else {
            ic.hlSeqCls.selectSequenceMobile();
            ic.hlSeqCls.selectChainMobile();
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoPTM {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Show the annotations of CDD domains and binding sites.
    async showPTM(chnid, chnidBase, type, begin, end) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        // UniProt ID
        let structure = chnid.substr(0, chnid.indexOf('_'));
        let chain = chnid.substr(chnid.indexOf('_') + 1);

        if(type == 'afmem') {
            let ptmHash = {'Transmembrane': [{'begin': begin, 'end': end}]};
            this.setAnnoPtmTransmem('transmem', ptmHash, chnid);        
        }
        // UniProt ID
        else if( structure.length > 5 ) {
            let url =  "https://www.ebi.ac.uk/proteins/api/features/" + structure; 
            let data;
            try {
                data = await me.getAjaxPromise(url, 'json');

                thisClass.parsePTM(data, chnid, type);
                /// if(ic.deferredPTM !== undefined) ic.deferredPTM.resolve();
            }
            catch {
                thisClass.getNoPTM(chnid, type);

                return;
            }
        }
        else { // PDB
            // get PDB to UniProt mapping
            // https://www.ebi.ac.uk/pdbe/api/doc/sifts.html
            // https://www.ebi.ac.uk/pdbe/api/doc/
            let structLower = structure.substr(0, 4).toLowerCase();
            let urlMap = "https://www.ebi.ac.uk/pdbe/api/mappings/uniprot/" + structLower;

            let dataMap;
            try {
                dataMap = await me.getAjaxPromise(urlMap, 'json');

                let UniProtID = '';
                if(!ic.UPResi2ResiPosPerChain) ic.UPResi2ResiPosPerChain = {};
                ic.UPResi2ResiPosPerChain[chnid] = {};
                let mapping = dataMap[structLower].UniProt;

                let bFound = false;
                for(let up in mapping) {
                    let chainArray = mapping[up].mappings;
                    //if(bFound) break;

                    for(let i = 0, il = chainArray.length; i < il; ++i) {
                    //"entity_id": 3, "end": { "author_residue_number": null, "author_insertion_code": "", "residue_number": 219 }, "chain_id": "A", "start": { "author_residue_number": 94, "author_insertion_code": "", "residue_number": 1 }, "unp_end": 312, "unp_start": 94, "struct_asym_id": "C"
                        let chainObj = chainArray[i];
                        if(chainObj.chain_id == chain) {
                            let start = chainObj.unp_start;
                            let end = chainObj.unp_end;
                            let posStart = chainObj.start.residue_number;
                            let posEnd = chainObj.end.residue_number;

                            if(posEnd - posStart != end - start) {
                                console.log("There might be some issues in the PDB to UniProt residue mapping.");
                            }

                            for(let j = 0; j <= end - start; ++j) {
                                ic.UPResi2ResiPosPerChain[chnid][j + start] = j + posStart - 1; // 0-based
                            }

                            if(UniProtID == '' || UniProtID.length != 6) UniProtID = up;
                            bFound = true;
                            //break;
                        }
                    }
                }

                if(!ic.annoPtmData) ic.annoPtmData = {};

                if(UniProtID == '') {
                    thisClass.getNoPTM(chnid, type);
                }
                else {
                    // call just once for one UniProt ID
                    if(ic.annoPtmData.hasOwnProperty(UniProtID)) {
                        thisClass.parsePTM(ic.annoPtmData[UniProtID], chnid, type);
                    }
                    else {
                        
                        let url =  "https://www.ebi.ac.uk/proteins/api/features/" + UniProtID;     
                        let data;
                        try {
                            data = await me.getAjaxPromise(url, 'json');
                            ic.annoPtmData[UniProtID] = data;

                            thisClass.parsePTM(data, chnid, type);
                            /// if(ic.deferredPTM !== undefined) ic.deferredPTM.resolve();
                        }
                        catch(err) {
                            thisClass.getNoPTM(chnid, type);
                            return;
                        }
                    }
                }
            }
            catch(err) {
                thisClass.getNoPTM(chnid, type);
                return;
            }
        }
    }

    parsePTM(data, chnid, type) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) {
            if(type == 'ptm') {
                ic.resid2ptm = {};
                ic.resid2ptm[chnid] = [];
            }
            else {
                ic.resid2transmem = {};
                ic.resid2transmem[chnid] = [];
            }
        }

        let ptmHash = {}, transmemHash = {};
        for(let i = 0, il = data.features.length; i < il; ++i) {
            let feature = data.features[i];

            if(type == 'ptm' && feature.category == 'PTM' && feature.type != 'DISULFID' && feature.type != 'CROSSLNK') {
                let title = '';
                if(feature.type == 'CARBOHYD') {
                    //title = 'Glycosylation, ' + feature.description;
                    title = 'Glycosylation';
                }
                else if(feature.type == 'LIPID') {
                    title = 'Lipidation, ' + feature.description;
                }
                else if(feature.description.indexOf('Phospho') == 0) {
                    title = 'Phosphorylation';
                }
                else if(feature.description) {
                    title = feature.description;
                }
                else {
                    title = feature.type;
                }

                if(!ptmHash[title]) ptmHash[title] = [];
                ptmHash[title].push(feature);
            }
            else if(type == 'transmem' && feature.category == 'TOPOLOGY' && feature.type == 'TRANSMEM') {
                let title = 'Transmembrane';
                if(!transmemHash[title]) transmemHash[title] = [];
                transmemHash[title].push(feature);
            }
        }

        if(type == 'ptm') {
            this.setAnnoPtmTransmem('ptm', ptmHash, chnid);
        }
        else {
            this.setAnnoPtmTransmem('transmem', transmemHash, chnid);
        }

        // add here after the ajax call
        ic.showAnnoCls.enableHlSeq();
        ic.bAjaxPTM = true;
    }

    setAnnoPtmTransmem(type, ptmHash, chnid) { let ic = this.icn3d, me = ic.icn3dui;
        let index = 0;
        let html = '', html2 = '', html3 = ''; 
        html += '<div id="' + ic.pre + chnid + '_' + type + 'seq_sequence" class="icn3d-cdd icn3d-dl_sequence">';
        html2 += html;
        html3 += html;
        let stucture = chnid.substr(0, chnid.indexOf('_'));

        for(let ptm in ptmHash) {
            let ptmArray = ptmHash[ptm];
            //"type": "MOD_RES", "category": "PTM", "description": "4-hydroxyproline", "begin": "382", "end": "382",
            let resPosArray = [];
            let bCoordinates = false;
            for(let i = 0, il = ptmArray.length; i < il; ++i) {
                let begin = parseInt(ptmArray[i].begin);
                let end = parseInt(ptmArray[i].end);

                for(let j = begin; j <= end; ++j) {
                    if(stucture.length > 5) { // UniProt
                        resPosArray.push(j - 1); // 0-based
                    } 
                    else { // PDB                       
                        if(ic.UPResi2ResiPosPerChain && ic.UPResi2ResiPosPerChain[chnid][j]) resPosArray.push(ic.UPResi2ResiPosPerChain[chnid][j]);
                    }
                    
                    if(!bCoordinates && ic.residues.hasOwnProperty(chnid + '_' + j)) {
                        bCoordinates = true;
                    }
                }
            }

            if(resPosArray.length == 0) continue;

            let resCnt = resPosArray.length;
            let title = (type == 'ptm') ? 'PTM: ' + ptm : 'Transmembrane';
            if(title.length > 17) title = title.substr(0, 17) + '...';
            let fulltitle = ptm;

            let linkStr = (bCoordinates) ? 'icn3d-link icn3d-blue' : '';

            let htmlTmp2 = '<div class="icn3d-seqTitle ' + linkStr + '" ' + type + '="' + type + '" posarray="' 
                + resPosArray.toString() + '" shorttitle="' + title + '" setname="' + chnid + '_' + type + '_' 
                + index + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + title + ' </div>';
            let htmlTmp3 = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Res</span>';
            let htmlTmp = '<span class="icn3d-seqLine">';
            html3 += htmlTmp2 + htmlTmp3 + '<br>';
            html += htmlTmp2 + htmlTmp3 + htmlTmp;
            html2 += htmlTmp2 + htmlTmp3 + htmlTmp;
            let pre = type + index.toString();
            //var widthPerRes = ic.seqAnnWidth / ic.maxAnnoLength;
            let prevEmptyWidth = 0;
            let prevLineWidth = 0;
            let widthPerRes = 1;
            for(let i = 0, il = ic.giSeq[chnid].length; i < il; ++i) {
                html += ic.showSeqCls.insertGap(chnid, i, '-');
                if(resPosArray.indexOf(i) != -1) {
                    let cFull = ic.giSeq[chnid][i];
                    let c = cFull;
                    if(cFull.length > 1) {
                        c = cFull[0] + '..';
                    }
                    // let pos = ic.annoCddSiteCls.getAdjustedResi(i, chnid, ic.matchedPos, ic.chainsSeq, ic.baseResi);
                    let pos = ic.ParserUtilsCls.getResi(chnid, i);
                    
                    html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" title="' + c + pos + '" class="icn3d-residue">' + cFull + '</span>';
                    if(me.bNode) {
                        let obj = {};
                        obj[chnid + '_' + pos] = title;
                        ic.resid2ptm[chnid].push(obj);
                    }

                    html2 += ic.showSeqCls.insertGapOverview(chnid, i);
                    let emptyWidth =(me.cfg.blast_rep_id == chnid) ? Math.round(ic.seqAnnWidth * i /(ic.maxAnnoLength + ic.nTotalGap) - prevEmptyWidth - prevLineWidth) : Math.round(ic.seqAnnWidth * i / ic.maxAnnoLength - prevEmptyWidth - prevLineWidth);
                    //if(emptyWidth < 0) emptyWidth = 0;
                    if(emptyWidth >= 0) {
                        html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                        html2 += '<div style="display:inline-block; background-color:#000; width:' + widthPerRes + 'px;" title="' + c + pos + '">&nbsp;</div>';
                        prevEmptyWidth += emptyWidth;
                        prevLineWidth += widthPerRes;
                    }
                }
                else {
                    html += '<span>-</span>'; //'<span>-</span>';
                }
            }
            htmlTmp = '<span class="icn3d-residueNum" title="residue count">&nbsp;' + resCnt.toString() + ' Residues</span>';
            htmlTmp += '</span>';
            htmlTmp += '<br>';
            html += htmlTmp;
            html2 += htmlTmp;

            ++index;
        }

        html += '</div>';
        html2 += '</div>';
        html3 += '</div>';

        $("#" + ic.pre + "dt_" + type + "_" + chnid).html(html);
        $("#" + ic.pre + "ov_" + type + "_" + chnid).html(html2);
        $("#" + ic.pre + "tt_" + type + "_" + chnid).html(html3);
    }

    getNoPTM(chnid, type) { let ic = this.icn3d; ic.icn3dui;
        console.log( "No PTM data were found for the chain " + chnid + "..." );

        let idStr = (type == 'ptm') ? 'ptm' : 'transmem';
   
        $("#" + ic.pre + "dt_" + idStr + "_" + chnid).html('');
        $("#" + ic.pre + "ov_" + idStr + "_" + chnid).html('');
        $("#" + ic.pre + "tt_" + idStr + "_" + chnid).html('');

        // add here after the ajax call
        ic.showAnnoCls.enableHlSeq();
        ic.bAjaxPTM = true;
        /// if(ic.deferredPTM !== undefined) ic.deferredPTM.resolve();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoCrossLink {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    showCrosslink(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        if(ic.clbondpnts === undefined) {
            // didn't finish loading atom data yet
            setTimeout(function(){
              thisClass.showCrosslink_base(chnid, chnidBase);
            }, 1000);
        }
        else {
            this.showCrosslink_base(chnid, chnidBase);
        }
    }
    showCrosslink_base(chnid, chnidBase) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) {
            if(!ic.resid2crosslink) ic.resid2crosslink = {};
            if(!ic.resid2crosslink[chnid]) ic.resid2crosslink[chnid] = [];
        }

        let chainid = chnidBase;
        let resid2resids = {};
        let structure = chainid.substr(0, chainid.indexOf('_'));
        let clbondArray = ic.clbondpnts[structure];

        if(clbondArray === undefined) {
            $("#" + ic.pre + "dt_crosslink_" + chnid).html('');
            $("#" + ic.pre + "ov_crosslink_" + chnid).html('');
            $("#" + ic.pre + "tt_crosslink_" + chnid).html('');
            return;
        }
        for(let i = 0, il = clbondArray.length; i < il; i = i + 2) {
            let resid1 = clbondArray[i]; // chemical
            let resid2 = clbondArray[i+1]; // protein or chemical
            resid1.substr(0, resid1.lastIndexOf('_'));
            let chainid2 = resid2.substr(0, resid2.lastIndexOf('_'));
            //if(chainid === chainid1) {
            //    if(resid2resids[resid1] === undefined) resid2resids[resid1] = [];
            //    resid2resids[resid1].push(resid2);
            //}
            if(chainid === chainid2) {
                if(resid2resids[resid2] === undefined) resid2resids[resid2] = [];
                resid2resids[resid2].push(resid1);
            }
        }
        let residueArray = Object.keys(resid2resids);
        let title = "Cross-Linkages";
        ic.annoCddSiteCls.showAnnoType(chnid, chnidBase, 'crosslink', title, residueArray, resid2resids);
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoDomain {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    showDomainPerStructure(index) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        //var chnid = Object.keys(ic.protein_chainid)[0];
        //var pdbid = chnid.substr(0, chnid.indexOf('_'));
        let pdbArray = Object.keys(ic.structures);
        // show 3D domains
        let pdbid = pdbArray[index];
        //let url = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&domain&molinfor&uid=" + pdbid;

        if(index == 0 && ic.mmdb_data !== undefined) {      
            for(let chnid in ic.protein_chainid) {
                if(chnid.indexOf(pdbid) !== -1) {
                    this.showDomainWithData(chnid, ic.mmdb_data);
                }
            }
        }
        else if(ic.mmdb_dataArray[index] !== undefined) {
            for(let chnid in ic.protein_chainid) {
                if(chnid.indexOf(pdbid) !== -1) {
                   this.showDomainWithData(chnid, ic.mmdb_dataArray[index]);
                }
            }
        }
        else {
            // calculate 3D domains on-the-fly
            //ic.protein_chainid[chainArray[i]] 
            let data = {};
            data.domains = {};
            for(let chainid in ic.chains) {
                let structure = chainid.substr(0, chainid.indexOf('_'));
                if(pdbid == structure && ic.protein_chainid.hasOwnProperty(chainid)) {
                    data.domains[chainid] = {};
                    data.domains[chainid].domains = [];

                    let atoms = ic.chains[chainid];

                    let result = ic.domain3dCls.c2b_NewSplitChain(atoms);
                    let subdomains = result.subdomains;
                    //let substruct = result.substruct;
                    //let jsonStr = ic.domain3dCls.getDomainJsonForAlign(atoms);

                    for(let i = 0, il = subdomains.length; i < il; ++i) {
                        // domain item: {"sdid":1722375,"intervals":[[1,104],[269,323]]}
                        let domain = {};
                        domain.intervals = [];

                        for(let j = 0, jl = subdomains[i].length; j < jl; j += 2) {
                            domain.intervals.push([subdomains[i][j], subdomains[i][j+1]]);
                        }

                        data.domains[chainid].domains.push(domain);
                    }
                }
            }

            ic.mmdb_dataArray[index] = data;
            let bCalcDirect = true;
            for(let chnid in ic.protein_chainid) {
                if(chnid.indexOf(pdbid) !== -1) {
                    thisClass.showDomainWithData(chnid, ic.mmdb_dataArray[index], bCalcDirect);
                }
            }

            ic.bAjax3ddomain = true;
            ic.bAjaxDoneArray[index] = true;          
        }
    }

    //Show the annotations of 3D domains.
    showDomainAll() { let ic = this.icn3d; ic.icn3dui;
        //var chnid = Object.keys(ic.protein_chainid)[0];
        //var pdbid = chnid.substr(0, chnid.indexOf('_'));
        let pdbArray = Object.keys(ic.structures);
        // show 3D domains
        ic.mmdb_dataArray = [];
        ic.bAjaxDoneArray = [];
        for(let i = 0, il = pdbArray.length; i < il; ++i) {
            ic.bAjaxDoneArray[i] = false;
        }

        for(let i = 0, il = pdbArray.length; i < il; ++i) {
            this.showDomainPerStructure(i);
        }
    }
    showDomainWithData(chnid, data, bCalcDirect) { let ic = this.icn3d, me = ic.icn3dui;
        let html = '<div id="' + ic.pre + chnid + '_domainseq_sequence" class="icn3d-dl_sequence">';
        let html2 = html;
        let html3 = html;
        let domainArray, proteinname;
        let pos = chnid.indexOf('_');
        let chain = chnid.substr(pos + 1);

        if(bCalcDirect) {
            proteinname = chnid;
            domainArray = (data.domains[chnid]) ? data.domains[chnid].domains : [];
        }
        else {
            let molinfo = data.moleculeInfor;
            let currMolid;
            for(let molid in molinfo) {
                if(molinfo[molid].chain === chain) {
                currMolid = molid;
                proteinname = molinfo[molid].name;
                break;
                }
            }
            if(currMolid !== undefined && data.domains[currMolid] !== undefined) {
                domainArray = data.domains[currMolid].domains;
            }
            if(domainArray === undefined) {
                domainArray = [];
            }
        }

        for(let index = 0, indexl = domainArray.length; index < indexl; ++index) {
            //var fulltitle = '3D domain ' +(index+1).toString() + ' of ' + proteinname + '(PDB ID: ' + data.pdbId + ')';
            let fulltitle = '3D domain ' +(index+1).toString() + ' of ' + proteinname;
            let title =(fulltitle.length > 17) ? fulltitle.substr(0,17) + '...' : fulltitle;
            let subdomainArray = domainArray[index].intervals;
            // remove duplicate, e.g., at https://www.ncbi.nlm.nih.gov/Structure/mmdb/mmdb_strview.cgi?v=2&program=icn3d&domain&molinfor&uid=1itw
            let domainFromHash = {}, domainToHash = {};
            let fromArray = [], toArray = [];
            let resiHash = {};
            let resCnt = 0;

            for(let i = 0, il = subdomainArray.length; i < il; ++i) {
                let domainFrom = Math.round(subdomainArray[i][0]) - 1; // convert 1-based to 0-based
                let domainTo = Math.round(subdomainArray[i][1]) - 1;
                if(domainFromHash.hasOwnProperty(domainFrom) || domainToHash.hasOwnProperty(domainTo)) {
                    continue; // do nothing for duplicated "from" or "to", e.g, PDBID 1ITW, 5FWI
                }
                else {
                    domainFromHash[domainFrom] = 1;
                    domainToHash[domainTo] = 1;
                }

                // use the NCBI residue number, and convert to PDB residue number during selection
                // if(ic.bNCBI || bCalcDirect) {
                    fromArray.push(domainFrom);
                    toArray.push(domainTo);
                // }
                // else {
                //     fromArray.push(domainFrom + ic.baseResi[chnid]);
                //     toArray.push(domainTo + ic.baseResi[chnid]);
                // }

                resCnt += domainTo - domainFrom + 1;
                for(let j = domainFrom; j <= domainTo; ++j) {
                    resiHash[j+1] = 1;
                }
            }

            // save 3D domain info for node.js script
            if(me.bNode) {
                let domainName = '3D domain ' +(index+1).toString();
                            
                if(!ic.resid2domain) ic.resid2domain = {};
                if(!ic.resid2domain[chnid]) ic.resid2domain[chnid] = [];
                for(let i = 0, il = fromArray.length; i < il; ++i) {
                    let from = parseInt(fromArray[i]);
                    let to = parseInt(toArray[i]);
                    for(let j = from; j <= to; ++j) {
                        // 0-based
                        let obj = {};
                        obj[chnid + '_' + (j+1).toString()] = domainName;
                        ic.resid2domain[chnid].push(obj);
                    }
                }
            }

            let htmlTmp2 = '<div class="icn3d-seqTitle icn3d-link icn3d-blue" 3ddomain="' +(index+1).toString() + '" from="' + fromArray + '" to="' + toArray + '" shorttitle="' + title + '" index="' + index + '" setname="' + chnid + '_3d_domain_' +(index+1).toString() + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">' + title + ' </div>';
            let htmlTmp3 = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Res</span>';
            html3 += htmlTmp2 + htmlTmp3 + '<br>';
            let htmlTmp = '<span class="icn3d-seqLine">';
            html += htmlTmp2 + htmlTmp3 + htmlTmp;
            html2 += htmlTmp2 + htmlTmp3 + htmlTmp;
            let pre = 'domain3d' + index.toString();
            for(let i = 0, il = ic.giSeq[chnid].length; i < il; ++i) {
              html += ic.showSeqCls.insertGap(chnid, i, '-');
              //if(i >= domainFrom && i <= domainTo) {
              let resi = ic.ParserUtilsCls.getResi(chnid, i);
              if(resiHash.hasOwnProperty(i+1)) {
                  let cFull = ic.giSeq[chnid][i];
                  let c = cFull;
                  if(cFull.length > 1) {
                      c = cFull[0] + '..';
                  }
                  
                //   let pos =(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i;
                  let pos = resi;
                  html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" title="' + c + pos + '" class="icn3d-residue">' + cFull + '</span>';
              }
              else {
                html += '<span>-</span>'; //'<span>-</span>';
              }
            }
            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chnid]);
            let colorStr =(atom.color === undefined || atom.color.getHexString() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
            let color =(atom.color !== undefined) ? colorStr : "CCCCCC";
            if(me.cfg.blast_rep_id != chnid) { // regular
                for(let i = 0, il = fromArray.length; i < il; ++i) {
                    let emptyWidth =(i == 0) ? Math.round(ic.seqAnnWidth *(fromArray[i] - ic.baseResi[chnid] - 1) / ic.maxAnnoLength) : Math.round(ic.seqAnnWidth *(fromArray[i] - toArray[i-1] - 1) / ic.maxAnnoLength);
                    html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                    html2 += '<div style="display:inline-block; color:white!important; font-weight:bold; background-color:#' + color + '; width:' + Math.round(ic.seqAnnWidth *(toArray[i] - fromArray[i] + 1) / ic.maxAnnoLength) + 'px;" class="icn3d-seqTitle icn3d-link icn3d-blue" 3ddomain="' +(index+1).toString() + '" from="' + fromArray + '" to="' + toArray + '" shorttitle="' + title + '" index="' + index + '" setname="' + chnid + '_3d_domain_' +(index+1).toString() + '" id="' + chnid + '_3d_domain_' + index + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">3D domain ' +(index+1).toString() + '</div>';
                }
            }
            else { // with potential gaps
                let fromArray2 = [], toArray2 = [];
                for(let i = 0, il = fromArray.length; i < il; ++i) {
                    fromArray2.push(fromArray[i]);
                    for(let j = parseInt(fromArray[i]); j <= parseInt(toArray[i]); ++j) {
                        if(ic.targetGapHash !== undefined && ic.targetGapHash.hasOwnProperty(j)) {
                            toArray2.push(j - 1);
                            fromArray2.push(j);
                        }
                    }
                    toArray2.push(toArray[i]);
                }
                for(let i = 0, il = fromArray2.length; i < il; ++i) {
                    html2 += ic.showSeqCls.insertGapOverview(chnid, fromArray2[i]);
                    let emptyWidth =(i == 0) ? Math.round(ic.seqAnnWidth *(fromArray2[i] - ic.baseResi[chnid] - 1) /(ic.maxAnnoLength + ic.nTotalGap)) : Math.round(ic.seqAnnWidth *(fromArray2[i] - toArray2[i-1] - 1) /(ic.maxAnnoLength + ic.nTotalGap));
                    html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                    html2 += '<div style="display:inline-block; color:white!important; font-weight:bold; background-color:#' + color + '; width:' + Math.round(ic.seqAnnWidth *(toArray2[i] - fromArray2[i] + 1) /(ic.maxAnnoLength + ic.nTotalGap)) + 'px;" class="icn3d-seqTitle icn3d-link icn3d-blue" 3ddomain="' +(index+1).toString() + '" from="' + fromArray2 + '" to="' + toArray2 + '" shorttitle="' + title + '" index="' + index + '" setname="' + chnid + '_3d_domain_' +(index+1).toString() + '" id="' + chnid + '_3d_domain_' + index + '" anno="sequence" chain="' + chnid + '" title="' + fulltitle + '">3D domain ' +(index+1).toString() + '</div>';
                }
            }
            htmlTmp = '<span class="icn3d-residueNum" title="residue count">&nbsp;' + resCnt.toString() + ' Residues</span>';
            htmlTmp += '</span>';
            htmlTmp += '<br>';
            html += htmlTmp;
            html2 += htmlTmp;
        }
        html += '</div>';
        html2 += '</div>';
        html3 += '</div>';
        $("#" + ic.pre + "dt_domain_" + chnid).html(html);
        $("#" + ic.pre + "ov_domain_" + chnid).html(html2);
        $("#" + ic.pre + "tt_domain_" + chnid).html(html3);
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoSnpClinVar {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async showSnp(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        await this.showSnpClinvar(chnid, chnidBase, true);
    }
    async showClinvar(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        await this.showSnpClinvar(chnid, chnidBase, false);
    }

    //Show the annotations of SNPs and ClinVar.
    async showSnpClinvar(chnid, chnidBase, bSnpOnly) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        // get gi from acc
        //var url2 = "https://www.ncbi.nlm.nih.gov/Structure/icn3d/chainid2repgi.txt";
        let url2 = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?chainid=" + chnidBase;
        try {
            let data2 = await me.getAjaxPromise(url2, 'jsonp');

            //ic.chainid2repgi = JSON.parse(data2);
            //var gi = ic.chainid2repgi[chnidBase];
            let snpgi = data2.snpgi;
            let gi = data2.gi;
            if(bSnpOnly) {
                await thisClass.showSnpPart2(chnid, chnidBase, snpgi);
            }
            else {
                let specialGiArray = [6137708,1942289,224510717,2624886,253723219,2554905,75765331,3660278,312207882,319443632,342350956,1827805,109157826,1065265,40889086,6730307,163931185,494469,163931091,60594093,55669745,18655489,17942684,6980537,166235465,6435586,4139398,4389047,364506122,78101667,262118402,20664221,2624640,158430173,494395,28948777,34810587,13399647,3660342,261278854,342350965,384482350,378792570,15988303,213424334,4558333,2098365,10835631,3318817,374074330,332639529,122919696,4389286,319443573,2781341,67464020,194709238,210061039,364506106,28949044,40889076,161172338,17943181,4557976,62738484,365813173,6137343,350610552,17942703,576308,223674070,15826518,1310997,93279697,4139395,255311799,157837067,361132363,357380836,146387678,383280379,1127268,299856826,13786789,1311054,46015217,3402130,381353319,30750059,218766885,340707375,27065817,355333104,2624634,62738384,241913553,304446010];
                let giUsed = snpgi;
                if(specialGiArray.includes(gi)) giUsed = gi;
                await thisClass.showClinvarPart2(chnid, chnidBase, giUsed);
            }
        }
        catch(err) {
            if(bSnpOnly) {
                thisClass.processNoSnp(chnid);
            }
            else {             
                thisClass.processNoClinvar(chnid);
            }
            return;
        }
    }

    navClinVar(chnid) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        ic.currClin[chnid] = - 1;
        //me.myEventCls.onIds("#" + ic.pre + chnid + "_prevclin", "click", function(e) { let ic = thisClass.icn3d;
        $(document).on("click", "#" + ic.pre + chnid + "_prevclin", function(e) { let ic = thisClass.icn3d;
          e.stopImmediatePropagation();
          //e.preventDefault();
          let maxLen =(ic.resi2disease_nonempty[chnid] !== undefined) ? Object.keys(ic.resi2disease_nonempty[chnid]).length : 0;
          --ic.currClin[chnid];
          if(ic.currClin[chnid] < 0) ic.currClin[chnid] = maxLen - 1; // 0;
          thisClass.showClinVarLabelOn3D(chnid);
        });
        //me.myEventCls.onIds("#" + ic.pre + chnid + "_nextclin", "click", function(e) { let ic = thisClass.icn3d;
        $(document).on("click", "#" + ic.pre + chnid + "_nextclin", function(e) { let ic = thisClass.icn3d;
          e.stopImmediatePropagation();
          //e.preventDefault();
          let maxLen =(ic.resi2disease_nonempty[chnid] !== undefined) ? Object.keys(ic.resi2disease_nonempty[chnid]).length : 0;
          ++ic.currClin[chnid];

          if(ic.currClin[chnid] > maxLen - 1) ic.currClin[chnid] = 0; // ic.resi2disease_nonempty[chnid].length - 1;
          thisClass.showClinVarLabelOn3D(chnid);
        });
    }
    showClinVarLabelOn3D(chnid) { let ic = this.icn3d, me = ic.icn3dui;
          let resiArray = Object.keys(ic.resi2disease_nonempty[chnid]);

          let chainid, residueid;
          chainid = chnid;
          residueid = chainid + '_' + (parseInt(resiArray[ic.currClin[chnid]]) + ic.baseResi[chnid]).toString();
 
          let label = '';
          let diseaseArray = ic.resi2disease_nonempty[chnid][resiArray[ic.currClin[chnid]]];
          for(let k = 0, kl = diseaseArray.length; k < kl; ++k) {
              if(diseaseArray[k] != '' && diseaseArray[k] != 'not specified' && diseaseArray[k] != 'not provided') {
                label = diseaseArray[k];
                break;
              }
          }
          if(label == '') label = (diseaseArray.length > 0) ? diseaseArray[0] : "N/A";

          let position = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
          //position.center.add(new THREE.Vector3(3.0, 3.0, 3.0)); // shift a little bit
          let maxlen = 30;
          if(label.length > maxlen) label = label.substr(0, maxlen) + '...';
          ic.selectionCls.removeSelection();
          if(ic.labels == undefined) ic.labels = {};
          ic.labels['clinvar'] = [];
          //var size = Math.round(ic.LABELSIZE * 10 / label.length);
          let size = ic.LABELSIZE;
          let color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd; //"#FFFF00";
          ic.analysisCls.addLabel(label, position.center.x + 1, position.center.y + 1, position.center.z + 1, size, color, undefined, 'clinvar');
          ic.hAtoms = {};
          for(let j in ic.residues[residueid]) {
              ic.hAtoms[j] = 1;
          }
          //ic.residueLabelsCls.addResidueLabels(ic.hAtoms);
          $("#clinvar_" + ic.pre + residueid).addClass('icn3d-highlightSeq');
          if($("#" + ic.pre + "modeswitch")[0] !== undefined && !$("#" + ic.pre + "modeswitch")[0].checked) {
              ic.definedSetsCls.setMode('selection');
          }
          ic.drawCls.draw();
    }

   //getSnpLine(2, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, undefined, bSnpOnly);
 
    getSnpLine(line, totalLineNum, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, bStartEndRes, chnid, bOverview, bClinvar, bTitleOnly, bSnpOnly) { let ic = this.icn3d, me = ic.icn3dui;
        let html = '';
        let altName = bClinvar ? 'clinvar' : 'snp';
        // determine whether the SNPis from virus directly
        let bVirus = false;
        for(let resi in resi2rsnum) {
            for(let i = 0, il = resi2rsnum[resi].length; i < il; ++i) {
                if(resi2rsnum[resi][i] == 0) {
                    bVirus = true;
                    break;
                }
            }
            if(bVirus) break;
        }
           
        if(bStartEndRes) {
            let title1 = 'ClinVar', title2 = 'SNP', warning = "", warning2 = "";
            if(!bVirus && ic.organism !== undefined && ic.organism !== 'human' && ic.organism !== 'homo sapiens') {
                warning = " <span style='color:#FFA500'>(from human)</span>";
                warning2 = " <span style='color:#FFA500'>(based on human sequences and mapped to this structure by sequence similarity)</span>";
            }
            if(bClinvar) {
                html += '<div class="icn3d-seqTitle icn3d-link icn3d-blue icn3d-clinvar-path" clinvar="clinvar" posarray="' + posClinArray + '" shorttitle="' + title1 + '" setname="' + chnid + '_' + title1 + '" anno="sequence" chain="' + chnid + '" title="' + title1 + warning2 + '">' + title1 + warning + '</div>';
            }
            else {
                html += '<div class="icn3d-seqTitle icn3d-link icn3d-blue" clinvar="clinvar" posarray="' + posarray + '" shorttitle="' + title2 + '" setname="' + chnid + '_' + title2 + '" anno="sequence" chain="' + chnid + '" title="' + title2 + warning2 + '">' + title2 + warning + '</div>';
            }
        }
        else if(line == 2 && bClinvar) {
            let buttonStyle = me.utilsCls.isMobile() ? 'none' : 'button';
            html += '<div id="' + ic.pre + chnid + '_prevclin" style="display:inline-block; font-size:11px; font-weight:bold; width:60px!important;"><button class="link" style="-webkit-appearance:' + buttonStyle + '; height:18px; width:55px;"><span style="white-space:nowrap; margin-left:-40px;" title="Show the previous ClinVar on structure">&lt; ClinVar</span></button></div>';
            html += '<div id="' + ic.pre + chnid + '_nextclin" style="display:inline-block; font-size:11px; font-weight:bold; width:60px!important;"><button class="link" style="-webkit-appearance:' + buttonStyle + '; height:18px; width:55px;"><span style="white-space:nowrap; margin-left:-40px;" title="Show the next ClinVar on structure">ClinVar &gt;</span></button></div>';
        }
        else {
            html += '<div class="icn3d-seqTitle"></div>';
        }
        
        let pre = altName;
        let snpCnt = 0, clinvarCnt = 0;
        let snpTypeHash = {}, currSnpTypeHash = {};
        for(let i = 1, il = ic.giSeq[chnid].length; i <= il; ++i) {
            if(resi2index[i] !== undefined) {            
                ++snpCnt;
                let allDiseaseTitle = '';
                for(let j = 0, jl = resi2snp[i].length; j < jl && !bSnpOnly; ++j) {
                    let diseaseArray = resi2disease[i][j].split('; ');
                    let sigArray = resi2sig[i][j].split('; ');
                    let diseaseTitle = '';
                    for(let k = 0, kl = diseaseArray.length; k < kl; ++k) {   
                        // relax the restriction to show all clinvar    
                        //if(diseaseArray[k] != '' && diseaseArray[k] != 'not specified' && diseaseArray[k] != 'not provided') {
                            diseaseTitle += diseaseArray[k] + '(' + sigArray[k] + '); ';
                        //}
                    }
                    
                    if(diseaseTitle != '') {
                        snpTypeHash[i] = 'icn3d-clinvar';
                        if(j == line - 2) { // just check the current line, "line = 2" means the first SNP
                            currSnpTypeHash[i] = 'icn3d-clinvar';
                            if(diseaseTitle.indexOf('Pathogenic') != -1) {
                                currSnpTypeHash[i] = 'icn3d-clinvar-path';
                            }
                        }
                    }
                    
                    allDiseaseTitle += diseaseTitle + ' | ';
                }
                if(allDiseaseTitle.indexOf('Pathogenic') != -1) {
                    snpTypeHash[i] = 'icn3d-clinvar-path';
                }
               
                if(snpTypeHash[i] == 'icn3d-clinvar' || snpTypeHash[i] == 'icn3d-clinvar-path') {
                    ++clinvarCnt;
                }
            }
        }
        
        if(snpCnt == 0 && !bClinvar) {
            $("#" + ic.pre + 'dt_clinvar_' + chnid).html('');
            $("#" + ic.pre + 'ov_clinvar_' + chnid).html('');
            $("#" + ic.pre + 'tt_clinvar_' + chnid).html('');
            $("#" + ic.pre + 'dt_snp_' + chnid).html('');
            $("#" + ic.pre + 'ov_snp_' + chnid).html('');
            $("#" + ic.pre + 'tt_snp_' + chnid).html('');
            return '';
        }
            
        if(clinvarCnt == 0 && bClinvar) {
            $("#" + ic.pre + 'dt_clinvar_' + chnid).html('');
            $("#" + ic.pre + 'ov_clinvar_' + chnid).html('');
            $("#" + ic.pre + 'tt_clinvar_' + chnid).html('');
            return '';
        }
        let cnt = bClinvar ? clinvarCnt : snpCnt;
        if(line == 1) {
            html += '<span class="icn3d-residueNum" title="residue count">' + cnt + ' Res</span>';
        }
        else {
            html += '<span class="icn3d-residueNum"></span>';
        }
        if(bTitleOnly) {
            return html + '<br>';
        }
        html += '<span class="icn3d-seqLine">';
        
        let diseaseStr = '';
        let prevEmptyWidth = 0;
        let prevLineWidth = 0;
        let widthPerRes = 1;
        for(let i = 1, il = ic.giSeq[chnid].length; i <= il; ++i) {
            if(bOverview) {
                if(resi2index[i] !== undefined) {
                    // get the mouse over text
                    let cFull = ic.giSeq[chnid][i-1];
                    let c = cFull;
                    if(cFull.length > 1) {
                        c = cFull[0] + '..';
                    }
                    // let pos =(i >= ic.matchedPos[chnid] && i-1 - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i-1 - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i-1;
                    let pos = ic.ParserUtilsCls.getResi(chnid, i - 1);
                    let snpTitle = pos + c + '>';
                    for(let j = 0, jl = resi2snp[i].length; j < jl; ++j) {
                        snpTitle += resi2snp[i][j];
                        if(!bSnpOnly) {
                            let diseaseArray = resi2disease[i][j].split('; ');
                            let sigArray = resi2sig[i][j].split('; ');
                            let diseaseTitle = '';
                            for(let k = 0, kl = diseaseArray.length; k < kl; ++k) {
                                // relax the restriction to show all clinvar
                                //if(diseaseArray[k] != '' && diseaseArray[k] != 'not specified' && diseaseArray[k] != 'not provided') {
                                    diseaseTitle += diseaseArray[k] + '(' + sigArray[k] + '); ';
                                //}
                            }
                        }
                    }
                    html += ic.showSeqCls.insertGapOverview(chnid, i-1);
                    let emptyWidth =(me.cfg.blast_rep_id == chnid) ? Math.round(ic.seqAnnWidth *(i-1) /(ic.maxAnnoLength + ic.nTotalGap) - prevEmptyWidth - prevLineWidth) : Math.round(ic.seqAnnWidth *(i-1) / ic.maxAnnoLength - prevEmptyWidth - prevLineWidth);
                    //if(emptyWidth < 0) emptyWidth = 0;
                    if(bClinvar) {
                        if(snpTypeHash[i] == 'icn3d-clinvar' || snpTypeHash[i] == 'icn3d-clinvar-path') {
                            if(emptyWidth >= 0) {
                                html += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                                html += '<div style="display:inline-block; background-color:#000; width:' + widthPerRes + 'px;" title="' + snpTitle + '">&nbsp;</div>';
                                prevEmptyWidth += emptyWidth;
                                prevLineWidth += widthPerRes;
                            }
                        }
                    }
                    else {
                        if(emptyWidth > 0) {
                            html += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                            html += '<div style="display:inline-block; background-color:#000; width:' + widthPerRes + 'px;" title="' + snpTitle + '">&nbsp;</div>';
                            prevEmptyWidth += emptyWidth;
                            prevLineWidth += widthPerRes;
                        }
                    }
                }
            }
            else { // detailed view
              html += ic.showSeqCls.insertGap(chnid, i-1, '-');

              if(resi2index[i] !== undefined) {
                  if(!bClinvar && line == 1) {
                      html += '<span>&dArr;</span>'; // or down triangle &#9660;
                  }
                  else {
                    let cFull = ic.giSeq[chnid][i-1];
                    let c = cFull;
                    if(cFull.length > 1) {
                      c = cFull[0] + '..';
                    }
                    // let pos =(i >= ic.matchedPos[chnid] && i-1 - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i-1 - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i-1;
                    let pos = ic.ParserUtilsCls.getResi(chnid, i - 1);
                    let snpStr = "", snpTitle = "<div class='snptip'>";
                    //var snpType = '';
                    let jl = resi2snp[i].length;
                    let start = 0, end = 0;
                    let shownResCnt;
                    if(line == 2) {
                        start = 0;
                        //end = 1;
                        end = jl;
                    }
                    //else if(line == 3) {
                    //    start = 1;
                    //    end = jl;
                    //}
                    if(!bClinvar) {
                        //shownResCnt = 2;
                        shownResCnt = 1;
                        for(let j = start; j < jl && j < end; ++j) {
                            let snpTmpStr = chnid + "_" + pos + "_" + resi2snp[i][j];
                            let buttonStyle = me.utilsCls.isMobile() ? 'none' : 'button';

                            let bCoord = true;
                            if( !ic.residues.hasOwnProperty(chnid + '_' + pos) ) {
                                bCoord = false;
                            }

                            if(j < shownResCnt) snpStr += resi2snp[i][j];
                            snpTitle += pos + c + '>' + resi2snp[i][j];

                            if(!bSnpOnly) {
                                // disease and significace
                                let diseaseArray = resi2disease[i][j].split('; ');
                                let sigArray = resi2sig[i][j].split('; ');
                                let diseaseTitle = '';
                                let index = 0;
                                for(let k = 0, kl = diseaseArray.length; k < kl; ++k) {
                                    // relax the restriction to show all clinvar
                                    //if(diseaseArray[k] != '' && diseaseArray[k] != 'not specified' && diseaseArray[k] != 'not provided') {
                                        if(index > 0) {
                                            diseaseTitle += '; ';
                                        }
                                        else {
                                            if( j === 0 || j === 1) diseaseStr = 'disease="' + diseaseArray[k] + '"';
                                        }
                                        diseaseTitle += diseaseArray[k] + '(' + sigArray[k] + ')';
                                        ++index;
                                    //}
                                }

                                //resi2rsnum, resi2clinAllele,
                                if(diseaseTitle != '') {
                                    //snpType = 'icn3d-clinvar';
                                    snpTitle += ': ' + diseaseTitle;

                                    if(bCoord && !me.cfg.hidelicense) {
                                        snpTitle += '<br>' + ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpin3d', '3D with scap', 'SNP in 3D with scap', 70, buttonStyle) + '&nbsp;&nbsp;';
                                        snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpinter', 'Interactions', 'SNP Interactions in 3D', 70, buttonStyle) + '&nbsp;&nbsp;';
                                        snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snppdb', 'PDB', 'Download SNP PDB', 35, buttonStyle);
                                    }

                                    //snpTitle += "<br>Links: <span class='" + ic.pre + "snpin3d icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP in 3D with scap</span>, <span class='" + ic.pre + "snpinter icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP Interactions in 3D</span>, <span class='" + ic.pre + "snppdb icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP PDB</span>, <a href='https://www.ncbi.nlm.nih.gov/clinvar/?term=" + resi2clinAllele[i][j] + "[AlleleID]' target='_blank'>ClinVar</a>, <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>";
                                    snpTitle += "<br>Links: <a href='https://www.ncbi.nlm.nih.gov/clinvar/?term=" + resi2clinAllele[i][j] + "[AlleleID]' target='_blank'>ClinVar</a>, <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>";
                                }
                                else {
                                    if(bCoord && !me.cfg.hidelicense) {
                                        snpTitle += '<br>' + ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpin3d', '3D with scap', 'SNP in 3D with scap', 70, buttonStyle) + '&nbsp;&nbsp;';
                                        snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpinter', 'Interactions', 'SNP Interactions in 3D', 70, buttonStyle) + '&nbsp;&nbsp;';
                                        snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snppdb', 'PDB', 'Download SNP PDB', 35, buttonStyle);
                                    }

                                    //snpTitle += "<br>Links: <span class='" + ic.pre + "snpin3d icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP in 3D with scap</span>, <span class='" + ic.pre + "snpinter icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP Interactions in 3D</span>, <span class='" + ic.pre + "snppdb icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP PDB</span>, <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>"
                                    snpTitle += "<br>Link: <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>";
                                }
                                if(j < jl - 1) {
                                    //if(j < 1) snpStr += ';';
                                    snpTitle += '<br><br>';
                                }
                            }
                            else { //if(bSnpOnly) {
                                if(bCoord && !me.cfg.hidelicense) {
                                    snpTitle += '<br>' + ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpin3d', '3D with scap', 'SNP in 3D with scap', 70, buttonStyle) + '&nbsp;&nbsp;';
                                    snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpinter', 'Interactions', 'SNP Interactions in 3D', 70, buttonStyle) + '&nbsp;&nbsp;';
                                    snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snppdb', 'PDB', 'Download SNP PDB', 35, buttonStyle);
                                }

                                if(resi2rsnum[i][j] != 0) {
                                    //snpTitle += "<br>Links: <span class='" + ic.pre + "snpin3d icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP in 3D with scap</span>, <span class='" + ic.pre + "snpinter icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP Interactions in 3D</span>, <span class='" + ic.pre + "snppdb icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP PDB</span>, <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>";
                                    snpTitle += "<br>Link: <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>";
                                }

                                if(j < jl - 1) {
                                    snpTitle += '<br><br>';
                                }
                            }
                        }
                        //if(jl > shownResCnt && line == 3) snpStr += '..';
                        if(jl > shownResCnt && line == 2) snpStr += '..';
                    }
                    else { // if(bClinvar)       
                        shownResCnt = 1;
                        let diseaseCnt = 0;
                        for(let j = start; j < jl && j < end; ++j) {
                            let snpTmpStr = chnid + "_" + pos + "_" + resi2snp[i][j];
                            let buttonStyle = me.utilsCls.isMobile() ? 'none' : 'button';

                            let bCoord = true;
                            if( !ic.residues.hasOwnProperty(chnid + '_' + pos) ) {
                                bCoord = false;
                            }

                            // disease and significace
                            let diseaseArray = resi2disease[i][j].split('; ');
                            let sigArray = resi2sig[i][j].split('; ');
                            let diseaseTitle = '';
                            let index = 0;
                            
                            for(let k = 0, kl = diseaseArray.length; k < kl; ++k) {
                                // relax the restriction to show all clinvar
                                //if(diseaseArray[k] != '' && diseaseArray[k] != 'not specified' && diseaseArray[k] != 'not provided') {
                                    if(index > 0) {
                                        diseaseTitle += '; ';
                                    }
                                    else {
                                        if( j === 0 || j === 1) diseaseStr = 'disease="' + diseaseArray[k] + '"';
                                    }
                                    diseaseTitle += diseaseArray[k] + '(' + sigArray[k] + ')';
                                    ++index;
                                //}
                            }

                            if(diseaseTitle != '') {
                                if(diseaseCnt < shownResCnt) snpStr += resi2snp[i][j];
                                snpTitle += pos + c + '>' + resi2snp[i][j];
                                //snpType = 'icn3d-clinvar';
                                snpTitle += ': ' + diseaseTitle;

                                if(bCoord && !me.cfg.hidelicense) {
                                    snpTitle += '<br>' + ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpin3d', '3D with scap', 'SNP in 3D with scap', 70, buttonStyle) + '&nbsp;&nbsp;';
                                    snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snpinter', 'Interactions', 'SNP Interactions in 3D', 70, buttonStyle) + '&nbsp;&nbsp;';
                                    snpTitle += ic.showAnnoCls.addSnpButton(snpTmpStr, 'snppdb', 'PDB', 'Download SNP PDB', 35, buttonStyle);
                                }

                                //snpTitle += "<br>Links: <span class='" + ic.pre + "snpin3d icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP in 3D with scap</span>, <span class='" + ic.pre + "snpinter icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP Interactions in 3D</span>, <span class='" + ic.pre + "snppdb icn3d-snplink' snp='" + chnid + "_" + pos + "_" + resi2snp[i][j] + "'>SNP PDB</span>, <a href='https://www.ncbi.nlm.nih.gov/clinvar/?term=" + resi2clinAllele[i][j] + "[AlleleID]' target='_blank'>ClinVar</a>, <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>";
                                snpTitle += "<br>Links: <a href='https://www.ncbi.nlm.nih.gov/clinvar/?term=" + resi2clinAllele[i][j] + "[AlleleID]' target='_blank'>ClinVar</a>, <a href='https://www.ncbi.nlm.nih.gov/snp/?term=" + resi2rsnum[i][j] + "' target='_blank'>dbSNP(rs" + resi2rsnum[i][j] + ")</a>";
                                if(j < jl - 1) {
                                    snpTitle += '<br><br>';
                                }
                                ++diseaseCnt;
                            } // if(diseaseTitle != '') {
                        } // for(let j = start; j < jl && j < end; ++j) {
                        //if(diseaseCnt > shownResCnt && line == 3) snpStr += '..';
                        if(diseaseCnt > shownResCnt && line == 2) snpStr += '..';
                    } // else { // if(bClinvar)
                    snpTitle += '</div>';
                    if(bClinvar) {                
                        if(snpTypeHash[i] == 'icn3d-clinvar' || snpTypeHash[i] == 'icn3d-clinvar-path') {
                            if(line == 1) {
                                html += '<span>&dArr;</span>'; // or down triangle &#9660;
                            }
                            else {
                                if(snpStr == '' || snpStr == ' ') {
                                    html += '<span>-</span>';
                                }
                                else {
                                    html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" label title="' + snpTitle + '" ' + diseaseStr + ' class="icn3d-tooltip icn3d-residue ' + currSnpTypeHash[i] + '">' + snpStr + '</span>';
                                }
                            }
                        }
                        else {
                            html += '<span>-</span>';
                        }
                    }
                    else {
                        if(snpStr == '' || snpStr == ' ') {
                            html += '<span>-</span>';
                        }
                        else {
                            if(!bSnpOnly) {
                                html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" label title="' + snpTitle + '" ' + diseaseStr + ' class="icn3d-tooltip icn3d-residue ' + currSnpTypeHash[i] + '">' + snpStr + '</span>';
                            }
                            else {
                                html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" label title="' + snpTitle + '" class="icn3d-tooltip icn3d-residue ' + currSnpTypeHash[i] + '">' + snpStr + '</span>';
                            }
                        }
                    }
                  } // if(!bClinvar && line == 1) {
              }
              else {
                html += '<span>-</span>'; //'<span>-</span>';
              }
            } // if(bOverview) {
        } // for
        
        //var end = bStartEndRes ? ic.chainsSeq[chnid][ic.giSeq[chnid].length - 1 - ic.matchedPos[chnid] ].resi : '';
        if(line == 1) {
            html += '<span class="icn3d-residueNum" title="residue count">&nbsp;' + cnt + ' Residues</span>';
        }
        else {
            html += '<span class="icn3d-residueNum"></span>';
        }
        html += '</span>';
        html += '<br>';

        return html;
    }
    processSnpClinvar(data, chnid, chnidBase, bSnpOnly, bVirus) { let ic = this.icn3d, me = ic.icn3dui;    
        let html = '<div id="' + ic.pre + chnid + '_snpseq_sequence" class="icn3d-dl_sequence">';
        let html2 = html;
        let html3 = html;
        let htmlClinvar = '<div id="' + ic.pre + chnid + '_clinvarseq_sequence" class="icn3d-dl_sequence">';
        let htmlClinvar2 = htmlClinvar;
        let htmlClinvar3 = htmlClinvar;
        let lineArray =(!bSnpOnly || bVirus) ? data.data : data.split('\n');
        let resi2snp = {};
        let resi2index = {};
        let resi2disease = {};
        if(ic.resi2disease_nonempty[chnid] === undefined) ic.resi2disease_nonempty[chnid] = {};
        let resi2sig = {};
        let resi2rsnum = {};
        let resi2clinAllele = {};
        let posHash = {}, posClinHash = {};
        let prevSnpStr = '';
        if(me.bNode) {
            if(bSnpOnly) {
                if(!ic.resid2snp) ic.resid2snp = {};
                if(!ic.resid2snp[chnid]) ic.resid2snp[chnid] = [];
            }
            else {
                if(!ic.resid2clinvar) ic.resid2clinvar = {};
                if(!ic.resid2clinvar[chnid]) ic.resid2clinvar[chnid] = [];
            }
        }

        for(let i = 0, il = lineArray.length; i < il; ++i) {
         //bSnpOnly: false
         //1310770    13    14    14Y>H    368771578    150500    Hereditary cancer-predisposing syndrome; Li-Fraumeni syndrome; not specified; Li-Fraumeni syndrome 1    Likely benign; Uncertain significance; Uncertain significance; Uncertain significance    1TSR_A    120407068    NP_000537.3
         //Pdb_gi, Pos from, Pos to, Pos & Amino acid change, rs#, ClinVar Allele ID, Disease name, Clinical significance, master accession, master_gi, master_accession.version
         //bSnpOnly: true
         //1310770    13    14    14Y>H    1111111
         if(lineArray[i] != '') {
          let fieldArray =(!bSnpOnly || bVirus) ? lineArray[i] : lineArray[i].split('\t');
          let snpStr = fieldArray[3];
          if(snpStr == prevSnpStr) continue;
          prevSnpStr = snpStr;
          let resiStr = snpStr.substr(0, snpStr.length - 3);
          let resi = Math.round(resiStr);

          if(me.bNode) {
              let obj = {};
              obj[chnid + '_' + resi] = snpStr;
                
              if(bSnpOnly) {
                ic.resid2snp[chnid].push(obj);
              }
              else {
                ic.resid2clinvar[chnid].push(obj);
              }
          }

          snpStr.substr(snpStr.length - 3, 1);
          let snpRes = snpStr.substr(snpStr.indexOf('>') + 1); //snpStr.substr(snpStr.length - 1, 1);
          //var rsnum = bSnpOnly ? '' : fieldArray[4];
          let rsnum = fieldArray[4];
          let clinAllele = bSnpOnly ? '' : fieldArray[5];
          let disease = bSnpOnly ? '' : fieldArray[6];  // When more than 2+ diseases, they are separated by "; "
                                        // Some are "not specified", "not provided"
          let clinSig = bSnpOnly ? '' : fieldArray[7];     // Clinical significance, When more than 2+ diseases, they are separated by "; "
          // "*" means terminating codon, "-" means deleted codon
          //if(currRes !== '-' && currRes !== '*' && snpRes !== '-' && snpRes !== '*') {
                let realResi = ic.ParserUtilsCls.getResi(chnid, resi - 1);
                // posHash[resi + ic.baseResi[chnid]] = 1;
                // if(disease != '') posClinHash[resi + ic.baseResi[chnid]] = 1;
                posHash[realResi] = 1;
                if(disease != '') posClinHash[realResi] = 1;
                resi2index[resi] = i + 1;
                if(resi2snp[resi] === undefined) {
                    resi2snp[resi] = [];
                }
                resi2snp[resi].push(snpRes);
                if(resi2rsnum[resi] === undefined) {
                    resi2rsnum[resi] = [];
                }
                resi2rsnum[resi].push(rsnum);
                if(resi2clinAllele[resi] === undefined) {
                    resi2clinAllele[resi] = [];
                }
                resi2clinAllele[resi].push(clinAllele);
                if(resi2disease[resi] === undefined) {
                    resi2disease[resi] = [];
                }
                resi2disease[resi].push(disease);
                if(disease != '') {
                    if(ic.resi2disease_nonempty[chnid][resi] === undefined) {
                        ic.resi2disease_nonempty[chnid][resi] = [];
                    }
                    ic.resi2disease_nonempty[chnid][resi].push(disease);
                }
                if(resi2sig[resi] === undefined) {
                    resi2sig[resi] = [];
                }
                resi2sig[resi].push(clinSig);
          //}
         }
        }
          
        let posarray = Object.keys(posHash);
        let posClinArray = Object.keys(posClinHash);
        if(bSnpOnly) {
            let bClinvar = false;
            html += this.getSnpLine(1, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 1, chnid, false, bClinvar, undefined, bSnpOnly);
            html += this.getSnpLine(2, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, undefined, bSnpOnly);
            //html += this.getSnpLine(3, 3, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, undefined, bSnpOnly);
            html3 += this.getSnpLine(1, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 1, chnid, false, bClinvar, true, bSnpOnly);
            html3 += this.getSnpLine(2, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, true, bSnpOnly);
            //html3 += this.getSnpLine(3, 3, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, true, bSnpOnly);
            html2 += this.getSnpLine(1, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 1, chnid, true, bClinvar, undefined, bSnpOnly);
            html += '</div>';
            html2 += '</div>';
            html3 += '</div>';
            $("#" + ic.pre + 'dt_snp_' + chnid).html(html);
            $("#" + ic.pre + 'ov_snp_' + chnid).html(html2);
            $("#" + ic.pre + 'tt_snp_' + chnid).html(html3);
        }
        else {           
        //if(!bSnpOnly && ic.bClinvarCnt) {
            let bClinvar = true;
            htmlClinvar += this.getSnpLine(1, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 1, chnid, false, bClinvar, undefined, bSnpOnly);
            htmlClinvar += this.getSnpLine(2, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, undefined, bSnpOnly);
            //htmlClinvar += this.getSnpLine(3, 3, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, undefined, bSnpOnly);
            htmlClinvar3 += this.getSnpLine(1, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 1, chnid, false, bClinvar, true, bSnpOnly);
            htmlClinvar3 += this.getSnpLine(2, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, true, bSnpOnly);
            //htmlClinvar3 += this.getSnpLine(3, 3, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 0, chnid, false, bClinvar, true, bSnpOnly);
            htmlClinvar2 += this.getSnpLine(1, 2, resi2snp, resi2rsnum, resi2clinAllele, resi2disease, resi2index, resi2sig, posarray, posClinArray, 1, chnid, true, bClinvar, undefined, bSnpOnly);
            htmlClinvar += '</div>';
            htmlClinvar2 += '</div>';
            htmlClinvar3 += '</div>';    
                          
            $("#" + ic.pre + 'dt_clinvar_' + chnid).html(htmlClinvar);
            $("#" + ic.pre + 'ov_clinvar_' + chnid).html(htmlClinvar2);
            $("#" + ic.pre + 'tt_clinvar_' + chnid).html(htmlClinvar3);
            this.navClinVar(chnid, chnidBase);
        }
                   
        // add here after the ajax call
        ic.showAnnoCls.enableHlSeq();
        if(bSnpOnly) {
            ic.bAjaxSnp = true;
            /// if(ic.deferredSnp !== undefined) ic.deferredSnp.resolve();
        }
        else {
            ic.bAjaxClinvar = true;
            /// if(ic.deferredClinvar !== undefined) ic.deferredClinvar.resolve();
        }
    }
    async showClinvarPart2(chnid, chnidBase, gi) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        //var url = "https://www.ncbi.nlm.nih.gov/projects/SNP/beVarSearch_mt.cgi?appname=iCn3D&format=bed&report=pdb2bed&acc=" + chnidBase;
        //var url = "https://www.ncbi.nlm.nih.gov/Structure/icn3d/clinvar.txt";
        let url = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?chainid_clinvar=" + chnidBase;
        if(ic.chainsGene[chnid] && ic.chainsGene[chnid].geneSymbol) {
            url += "&gene=" + ic.chainsGene[chnid].geneSymbol;
        }

        try {
            let indata = await me.getAjaxPromise(url, 'jsonp');

            if(indata && indata.data && indata.data.length > 0) {
                let bSnpOnly = false;
                let data = indata;         
                
                thisClass.processSnpClinvar(data, chnid, chnidBase, bSnpOnly);
            }
            else {               
                thisClass.processNoClinvar(chnid);
            }
        }
        catch(err) {            
            thisClass.processNoClinvar(chnid);
            ///// if(ic.deferredClinvar !== undefined) ic.deferredClinvar.resolve();
            return;
        }
    }

    async showSnpPart2(chnid, chnidBase, gi) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        if(gi !== undefined) {          
            let url4 = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?chainid_snp=" + chnidBase;
            if(ic.chainsGene[chnid] && ic.chainsGene[chnid].geneSymbol) {
                url4 += "&gene=" + ic.chainsGene[chnid].geneSymbol;
            }

            try {
                let data4 = await me.getAjaxPromise(url4, 'jsonp');

                if(data4 && data4.data && data4.data.length > 0) {
                    let bSnpOnly = true;
                    let bVirus = true;
                    
                    thisClass.processSnpClinvar(data4, chnid, chnidBase, bSnpOnly, bVirus);
                } //if(data4 != "") {
                else {
                    thisClass.processNoSnp(chnid);
                }
                ///// if(ic.deferredSnp !== undefined) ic.deferredSnp.resolve();
            }
            catch(err) {
                thisClass.processNoSnp(chnid);
                ///// if(ic.deferredSnp !== undefined) ic.deferredSnp.resolve();
                return;
            }
        }
        else {
            this.processNoSnp(chnid);
            console.log( "No gi was found for the chain " + chnidBase + "..." );
        }
    }
    processNoClinvar(chnid) { let ic = this.icn3d; ic.icn3dui;
            console.log( "No ClinVar data were found for the protein " + chnid + "..." );
            $("#" + ic.pre + 'dt_clinvar_' + chnid).html('');
            $("#" + ic.pre + 'ov_clinvar_' + chnid).html('');
            ic.showAnnoCls.enableHlSeq();
            ic.bAjaxClinvar = true;
            /// if(ic.deferredClinvar !== undefined) ic.deferredClinvar.resolve();
    }
    processNoSnp(chnid) { let ic = this.icn3d; ic.icn3dui;
            console.log( "No SNP data were found for the protein " + chnid + "..." );
            $("#" + ic.pre + 'dt_snp_' + chnid).html('');
            $("#" + ic.pre + 'ov_snp_' + chnid).html('');
            ic.showAnnoCls.enableHlSeq();
            ic.bAjaxSnp = true;
            /// if(ic.deferredSnp !== undefined) ic.deferredSnp.resolve();
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoSsbond {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Show the disulfide bonds and show the side chain in the style of "stick".
    showSsbond(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        if(ic.ssbondpnts === undefined) {
            // didn't finish loading atom data yet
            setTimeout(function(){
              thisClass.showSsbond_base(chnid, chnidBase);
            }, 1000);
        }
        else {
            this.showSsbond_base(chnid, chnidBase);
        }
    }
    showSsbond_base(chnid, chnidBase) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) {
            if(!ic.resid2ssbond) ic.resid2ssbond = {};
            if(!ic.resid2ssbond[chnid]) ic.resid2ssbond[chnid] = [];
        }

        let chainid = chnidBase;
        let resid2resids = {};
        let structure = chainid.substr(0, chainid.indexOf('_'));

        let ssbondArray = ic.ssbondpnts[structure];
        if(ssbondArray === undefined) {
            $("#" + ic.pre + "dt_ssbond_" + chnid).html('');
            $("#" + ic.pre + "ov_ssbond_" + chnid).html('');
            $("#" + ic.pre + "tt_ssbond_" + chnid).html('');
            return;
        }
        for(let i = 0, il = ssbondArray.length; i < il; i = i + 2) {
            let resid1 = ssbondArray[i];
            let resid2 = ssbondArray[i+1];
            let chainid1 = resid1.substr(0, resid1.lastIndexOf('_'));
            let chainid2 = resid2.substr(0, resid2.lastIndexOf('_'));
            if(chainid === chainid1) {
                if(resid2resids[resid1] === undefined) resid2resids[resid1] = [];
                resid2resids[resid1].push(resid2);
            }
            if(chainid === chainid2) {
                if(resid2resids[resid2] === undefined) resid2resids[resid2] = [];
                resid2resids[resid2].push(resid1);
            }
        }
        let residueArray = Object.keys(resid2resids);
        let title = "Disulfide Bonds";
        ic.annoCddSiteCls.showAnnoType(chnid, chnidBase, 'ssbond', title, residueArray, resid2resids);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AnnoTransMem {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    showTransmem(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        if(ic.ssbondpnts === undefined) {
            // didn't finish loading atom data yet
            setTimeout(function(){
              thisClass.showTransmem_base(chnid, chnidBase);
            }, 1000);
        }
        else {
            this.showTransmem_base(chnid, chnidBase);
        }
    }
    showTransmem_base(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        let residHash = {};
        for(let serial in ic.chains[chnidBase]) {
            let atom = ic.atoms[serial];
            if(atom.coord.z < ic.halfBilayerSize && atom.coord.z > -ic.halfBilayerSize) {
                let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
                residHash[resid] = 1;
            }
        }
        let residueArray = Object.keys(residHash);
        let title = "Transmembrane"; //"Transmembrane domain";
        ic.annoCddSiteCls.showAnnoType(chnid, chnidBase, 'transmem', title, residueArray);
    }

}

/*
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 * Modified from Tom Madej's C++ code
*/

class Domain3d {
    constructor(icn3d) {
		this.icn3d = icn3d;

        //this.dcut = 8; // threshold for C-alpha interactions
		// It seemed the threshold 7 angstrom works better
		this.dcut = 7; // threshold for C-alpha interactions

		this.MAX_SSE = 512;

        //let this.ctc_cnt[this.MAX_SSE][this.MAX_SSE];		// contact count matrix
        this.ctc_cnt = [];
        for(let i = 0; i < this.MAX_SSE; ++i) {
            this.ctc_cnt[i] = [];
        }

        //let this.elt_size[this.MAX_SSE];			// element sizes in residues
        this.elt_size = [];
        this.elt_size.length = this.MAX_SSE;

        //let this.group_num[this.MAX_SSE];			// indicates required element groupings
        this.group_num = [];
        this.group_num.length = this.MAX_SSE;

        // this.split_ratio = 0.0;			//let // splitting ratio
        // this.min_size = 0;				// min required size of a domain
        // this.min_sse = 0;				// min number of SSEs required in a domain
        // this.max_csz = 0;				// max size of a cut, i.e. number of points
        // this.mean_cts = 0.0;				// mean number of contacts in a domain
        // this.c_delta = 0;				// cut set parameter
        // this.nc_fact = 0.0;				// size factor for internal contacts

		this.split_ratio = 0.25;			//let // splitting ratio
        this.min_size = 25;				// min required size of a domain
        this.min_sse = 3;				// min number of SSEs required in a domain
        this.max_csz = 4;				// max size of a cut, i.e. number of points
        this.mean_cts = 0.0;				// mean number of contacts in a domain
        this.c_delta = 3;				// cut set parameter
        this.nc_fact = 0.0;				// size factor for internal contacts

        //let this.elements[2*this.MAX_SSE];			// sets of this.elements to be split
        this.elements = [];
        this.elements.length = 2*this.MAX_SSE;

        //let this.stack[this.MAX_SSE];			// this.stack of sets (subdomains) to split
        this.stack = [];
        this.stack.length = this.MAX_SSE;

        this.top = 0;					// this.top of this.stack
        //let this.curr_prt0[this.MAX_SSE];			// current part 0 this.elements
        this.curr_prt0 = [];
        this.curr_prt0.length = this.MAX_SSE;

        //let this.curr_prt1[this.MAX_SSE];			// current part 1 this.elements
        this.curr_prt1 = [];
        this.curr_prt1.length = this.MAX_SSE;

        this.curr_ne0 = 0;				// no. of this.elements in current part 0
        this.curr_ne1 = 0;				// no. of this.elements in current part 1
        this.curr_ratio = 0.0;			// current splitting ratio
        this.curr_msize = 0;				// min of current part sizes
        //let this.parts[2*this.MAX_SSE];			// final partition into domains
        this.parts = [];
        this.parts.length = 2*this.MAX_SSE;

        this.np = 0;					// next free location in this.parts[]
        this.n_doms = 0;				// number of domains
        //let this.save_ratios[this.MAX_SSE];		// this.saved splitting ratios
        this.save_ratios = [];
        this.save_ratios.length = this.MAX_SSE;

        this.saved = 0;				// number of this.saved ratios
	}

	// Partition the set of this.elements on this.top of the this.stack based on the input cut.
	// If the partition is valid and the ratio is smaller than the current one, then
	// save it as the best partition so far encountered.  Various criteria are
	// employed for valid partitions, as described below.
	//

	//update_partition(int* cut, let k, let n) { let ic = this.icn3d, me = ic.icn3dui;
	update_partition(cut, k, n) { let ic = this.icn3d; ic.icn3dui;
		let i, il, j, t, nc0, nc1, ncx, ne, ne0, ne1, elts = [], prt = []; //int
		let size0, size1, prt0 = [], prt1 = []; // int
        prt0.length = this.MAX_SSE;
        prt1.length = this.MAX_SSE;
		let f, r0; //let

		// this.elements from the this.top of the this.stack 
		//elts = &this.elements[this.stack[this.top - 1]];
		for(i = this.stack[this.top - 1], il = this.elements.length; i < il; ++i) {
			elts.push(this.elements[i]);
		}

		// generate the partition based on the cut //
		for (i = ne = ne0 = ne1 = 0, prt = prt0, t = -1; i < k; i++) {
			// write the this.elements into prt //
			for (j = t + 1; j <= cut[i]; j++)
				prt[ne++] = elts[j];

			t = cut[i];

			// switch the partition //
			if (prt == prt0) {
				ne0 = ne;
				prt = prt1;
				ne = ne1;
			}
			else {
				ne1 = ne;
				prt = prt0;
				ne = ne0;
			}
		}

		// finish with the last part //
		for (j = t + 1; j < n; j++)
			prt[ne++] = elts[j];

		if (prt == prt0)
			ne0 = ne;
		else
			ne1 = ne;

		// don't split into two teeny this.parts! //
		if ((ne0 < this.min_sse) && (ne1 < this.min_sse))
			return cut;

		// check to see if the partition splits any required groups //
		for (i = 0; i < ne0; i++) {
			t = this.group_num[prt0[i]];

			for (j = 0; j < ne1; j++) {
				if (t == this.group_num[prt1[j]])
					return cut;
			}
		}

		// compute the sizes of the this.parts //
		for (i = size0 = 0; i < ne0; i++)
			size0 += this.elt_size[prt0[i]];

		for (i = size1 = 0; i < ne1; i++)
			size1 += this.elt_size[prt1[i]];

		// count internal contacts for part 0 //
		for (i = nc0 = 0; i < ne0; i++) {
			for (j = i; j < ne0; j++)
				nc0 += this.ctc_cnt[prt0[i]][prt0[j]];
		}

		// count internal contacts for part 1 //
		for (i = nc1 = 0; i < ne1; i++) {
			for (j = i; j < ne1; j++)
				nc1 += this.ctc_cnt[prt1[i]][prt1[j]];
		}

		// check globularity condition //
		if ((1.0 * nc0 / size0 < this.mean_cts) ||
			(1.0 * nc1 / size1 < this.mean_cts))
			return cut;

		// to handle non-globular pieces make sure nc0, nc1, are large enough //
		nc0 = Math.max(nc0, this.nc_fact*size0);
		nc1 = Math.max(nc1, this.nc_fact*size1);

		// count inter-part contacts //
		for (i = ncx = 0; i < ne0; i++) {
			t = prt0[i];

			for (j = 0; j < ne1; j++)
				ncx += this.ctc_cnt[t][prt1[j]];
		}

		// compute the splitting ratio //
		f = Math.min(nc0, nc1);
		r0 = 1.0 * ncx / (f + 1.0);

		if ((r0 >= this.curr_ratio + 0.01) || (r0 > this.split_ratio))
			return cut;

		// If the difference in the ratios is insignificant then take the split
		// that most evenly partitions the domain.

		if ((r0 > this.curr_ratio - 0.01) && (Math.min(size0, size1) < this.curr_msize))
				return cut;

		// if we get to here then keep this split //
		for (i = 0; i < ne0; i++)
			this.curr_prt0[i] = prt0[i];

		for (i = 0; i < ne1; i++)
			this.curr_prt1[i] = prt1[i];

		this.curr_ne0 = ne0;
		this.curr_ne1 = ne1;
		this.curr_ratio = r0;
		this.curr_msize = Math.min(size0, size1);

		return cut;

	} // end update_partition //



	// // Run through the possible cuts of size k for a set of this.elements of size n.
	//  *
	//  * To avoid small protrusions, no blocks of consecutive this.elements of length <= this.c_delta
	//  * are allowed.  An example where this is desirable is as follows.  Let's say you
	//  * have a protein with 2 subdomains, one of them an alpha-beta-alpha sandwich.  It
	//  * could then happen that one of the helices in the sandwich domain might make more
	//  * contacts with the other subdomain than with the sandwich.  The correct thing to
	//  * do is to keep the helix with the rest of the sandwich, and the "this.c_delta rule"
	//  * enforces this.
	//  //

	cut_size(k, n) { let ic = this.icn3d; ic.icn3dui;
		let i, j, cok, cut0 = []; //int
		cut0.length = this.MAX_SSE;

		for (i = 0; i < k; i++)
			cut0[i] = i;

		// enumerate cuts of length k //
		while (1) {
			// check block sizes in the cut //
			for (i = cok = 1; i < k; i++) {
				if (cut0[i] - cut0[i - 1] <= this.c_delta) {
					cok = 0;
					break;
				}
			}
			if (cok && (cut0[k - 1] < n - 1))
				cut0 = this.update_partition(cut0, k, n);

			// generate the next k-tuple of positions //
			for (j = k - 1; (j >= 0) && (cut0[j] == n - k + j); j--);

			if (j < 0) break;

			cut0[j]++;

			for (i = j + 1; i < k; i++)
				cut0[i] = cut0[i - 1] + 1;
		}

	} // end cut_size //



	// // Process the set of this.elements on this.top of the this.stack.  We generate cut sets in
	//  * a limited size range, generally from 1 to 5.  For each cut the induced
	//  * partition is considered and its splitting parameters computed.  The cut
	//  * that yields the smallest splitting ratio is chosen as the correct one, if
	//  * the ratio is low enough.  The subdomains are then placed on the this.stack for
	//  * further consideration.
	//  *
	//  * Subdomains with < this.min_sse SSEs are not allowed to split further, however,
	//  * it is possible to trim fewer than this.min_sse SSEs from a larger domain.  E.g.
	//  * a chain with 7 SSEs can be split into a subdomain with 5 SSEs and another
	//  * with 2 SSEs, but the one with 2 SSEs cannot be split further.
	//  *
	//  * Note that the invariant is, that this.stack[top] always points to the next free
	//  * location in this.elements[].
	//  //

	process_set() { let ic = this.icn3d; ic.icn3dui;
		let i, il, k, n, t, k0, elts = []; //int

		// count the this.elements //
		//elts = &this.elements[this.stack[this.top - 1]];
		for(i = this.stack[this.top - 1], il = this.elements.length; i < il; ++i) {
			elts.push(this.elements[i]);
		}

		//for (n = 0; *elts > -1; n++, elts++);
		for (n = 0; n < elts.length && elts[n] > -1; n++);

		// try various cut sizes //
		k0 = Math.min(n - 1, this.max_csz);
		this.curr_ne0 = this.curr_ne1 = 0;
		this.curr_ratio = 100.0;

		for (k = 1; k <= k0; k++)
			this.cut_size(k, n);

		// pop this.stack //
		this.top--;

		if (this.curr_ne0 == 0) {
			// no split took place, save part //
			t = this.stack[this.top];

			//for (elts = &this.elements[t]; *elts > -1; elts++)
			//	parts[np++] = *elts;

			for (i = t; i < this.elements.length && this.elements[i] > -1; i++)
				this.parts[this.np++] = this.elements[i];

			this.parts[this.np++] = -1;
			this.n_doms++;
		}
		else {
			this.save_ratios[this.saved++] = this.curr_ratio;

			if (this.curr_ne0 > this.min_sse) {
				// push on part 0 //
				t = this.stack[this.top];

				for (i = 0; i < this.curr_ne0; i++)
					this.elements[t++] = this.curr_prt0[i];

				this.elements[t++] = -1;
				this.stack[++this.top] = t;
			}
			else {
				// save part 0 //
				for (i = 0; i < this.curr_ne0; i++)
					this.parts[this.np++] = this.curr_prt0[i];

				this.parts[this.np++] = -1;
				this.n_doms++;
			}

			if (this.curr_ne1 > this.min_sse) {
				// push on part 1 //
				t = this.stack[this.top];

				for (i = 0; i < this.curr_ne1; i++)
					this.elements[t++] = this.curr_prt1[i];

				this.elements[t++] = -1;
				this.stack[++this.top] = t;
			}
			else {
				// save part 1 //
				for (i = 0; i < this.curr_ne1; i++)
					this.parts[this.np++] = this.curr_prt1[i];

				this.parts[this.np++] = -1;
				this.n_doms++;
			}
		}
	} // end process_set //



	// Main driver for chain splitting. //
	//process_all(let n) { let ic = this.icn3d, me = ic.icn3dui;
	process_all(n) { let ic = this.icn3d; ic.icn3dui;
		let i; //int

		// initialize the this.stack //
		this.top = 1;
		this.stack[0] = this.np = this.n_doms = 0;
		this.saved = 0;

		for (i = 0; i < n; i++)
			this.elements[i] = i;

		this.elements[n] = -1;

		// recursively split the chain into domains //
		while (this.top > 0) {
			this.process_set();
		}
	} // end process_all //

	// Output the domains.  For S we number the this.elements 1, 2, ..., n. //
	//output(let n, int* prts) { let ic = this.icn3d, me = ic.icn3dui;
	output(n) { let ic = this.icn3d; ic.icn3dui;
		let i, k; //int
		
		let prts = [];

		// zap the output array //
		for (i = 0; i < 2*n; i++)
			prts.push(0);

		// now write out the subdomains //
		for (i = k = 0; k < this.n_doms; i++) {
			prts[i] = this.parts[i] + 1;

			if (this.parts[i] < 0)
				k++;
		}

		return prts;
	} // end output //



	// // S-interface to the chain-splitting program.
	//  *
	//  * Explanation of parameters:
	//  *
	//  *	ne - number of secondary structure this.elements (SSEs)
	//  *	cts - contact count matrix
	//  *	elt_sz - sizes of SSEs
	//  *	grps - element group indicators
	//  *	sratio - splitting ratio
	//  *	msize - min size of a split domain
	//  *	m_sse - min number of SSEs required in a split part
	//  *	mcsz - max cut size, i.e. max number of split points
	//  *	avg_cts - mean number of internal contacts for a domain
	//  *	c_delt - cut set parameter
	//  *	ncf0 - size factor for number of internal contacts
	//  *	prts - output listing of domains
	//  *	n_saved - number of this.saved splitting ratios
	//  *	ratios - splitting ratios
	//  *	ret - success/failure indicator
	//  *	verb - flag to turn off/on splitting information
	//  //

	//new_split_chain(let ne, let sratio, let msize, let m_sse, let mcsz, let avg_cts,
	//	let c_delt, let ncf0, int* prts, int* n_saved, let* ratios) { let ic = this.icn3d, me = ic.icn3dui;
	new_split_chain(ne, sratio, msize, m_sse, mcsz, avg_cts,
		c_delt, ncf0, prts, n_saved, ratios) { let ic = this.icn3d; ic.icn3dui;
		let i; //int

		this.split_ratio = sratio;
		this.min_size = msize;
		this.min_sse = m_sse;
		this.max_csz = mcsz;
		this.mean_cts = avg_cts;
		this.c_delta = c_delt;
		this.nc_fact = ncf0;
		
		this.process_all(ne);
		//this.output(ne, prts);
		this.parts = this.output(ne);
		n_saved = this.saved;
		for (i = 0; i < this.saved; i++)
			ratios[i] = this.save_ratios[i];

		return n_saved;

	} // end new_split_chain //

	//
	// Actually, here is a better method that is also simple!
	//
	// If there are N atoms (residues) this algorithm should usually run in
	// time O(N^4/3), and usually even much faster!  In very unusual cases
	// it could take quadratic time.  The key idea is that atoms are not
	// infinitely compressible, i.e. only a fixed number will fit in a given
	// region of space.  So if the protein is roughly spherical, there will
	// only be O(N^1/3) atoms close to any given diameter.  Therefore, a
	// bound on the number of iterations of the inner loop is O(N^1/3).
	//
	// For an elongated protein that happens to have the x-axis normal to
	// the long axis, then it is possible for the inner loop to take time
	// O(N), in which case the whole takes O(N^2).  But this should rarely,
	// if ever, occur in practice.  It would also be possible beforehand to
	// choose the axis with the largest variance.
	//

	// typedef struct res_struct {
	// 	let rnum;
	// 	let x, y, z;
	// } ResRec;

	//list< pair< pair< int, let >, let > >
	//c2b_AlphaContacts(let n0, let* x0, let* y0, let* z0,
	//	const let incr = 4, const let dcut = 8.0) { let ic = this.icn3d, me = ic.icn3dui;
	c2b_AlphaContacts(n0, x0, y0, z0, dcut, resiArray) { let ic = this.icn3d; ic.icn3dui;
		//if(!incr) incr = 4;
		if(!dcut) dcut = this.dcut;

		let list_cts = [], list_rr = [];

		for (let i = 0; i < n0; i++) {
			// don't include residues with missing coordinates
			//if ((x0[i] == MissingCoord) || (y0[i] == MissingCoord) || (z0[i] == MissingCoord))
			if (!x0[i]|| !y0[i] || !z0[i])
				continue;

			//ResRec rr0;
			let rr0 = {};
			//rr0.rnum = i + 1;
			rr0.rnum = resiArray[i];
			rr0.x = x0[i];
			rr0.y = y0[i];
			rr0.z = z0[i];
			list_rr.push(rr0);
		}
		
		list_rr.sort(function(rr1, rr2) {
				return rr1.x - rr2.x;
			});
					
		//let rrit1, rrit2, rrbeg;
		let i, j, len = list_rr.length;

		//for (rrit1 = list_rr.begin(); rrit1 != list_rr.end(); rrit1++) {
		for (i = 0; i < len; ++i) {	
			//ResRec rr1 = *rrit1;
			let rr1 = list_rr[i];
			let x1 = rr1.x;
			let y1 = rr1.y;
			let z1 = rr1.z;
			//rrbeg = rrit1;
			//rrbeg++;

			//for (rrit2 = rrbeg; rrit2 != list_rr.end(); rrit2++) {
			for (j = i + 1; j < len; ++j) {	
				//ResRec rr2 = *rrit2;
				let rr2 = list_rr[j];
				if ((parseInt(rr1.rnum) - parseInt(rr2.rnum) <= 3) && (parseInt(rr2.rnum) - parseInt(rr1.rnum) <= 3)) continue;
				let x2 = rr2.x;
				let y2 = rr2.y;
				let z2 = rr2.z;

				if (x2 > x1 + dcut)
					break;

				// x1 <= x2 <= x1 + dcut so compare
				let sum = (x1 - x2)*(x1 - x2);
				sum += (y1 - y2)*(y1 - y2);
				sum += (z1 - z2)*(z1 - z2);
				let d0 = Math.sqrt(sum);
				if (d0 > dcut) continue;
				//pair< pair< int, let >, let > lpair;
				//pair< int, let > rpair;
				let lpair = {}, rpair = {};

				if (parseInt(rr1.rnum) < parseInt(rr2.rnum)) {
					rpair.first = rr1.rnum;
					rpair.second = rr2.rnum;
				}
				else {
					rpair.first = rr2.rnum;
					rpair.second = rr1.rnum;
				}

				lpair.first = rpair;
				lpair.second = d0;
				list_cts.push(lpair);
			}
		}

		return list_cts;

	} // end c2b_AlphaContacts



	//
	// Creates a table, actually a graph, of the contacts between SSEs.
	//

	//static map< pair< int, let >, let >
	//c2b_ContactTable(vector<int>& v1, vector<int>& v2) { let ic = this.icn3d, me = ic.icn3dui;
	c2b_ContactTable(v1, v2) { let ic = this.icn3d; ic.icn3dui;
		let cmap = {};
		let n0 = v1.length; //unsigned int

		if (n0 != v2.length) {
			// problem!

			return cmap;
		}

		for (let i = 0; i < n0; i++) {
			let e1 = v1[i];
			let e2 = v2[i];
			//pair<int, int> epr;
			//let epr = {};
			//epr.first = e1;
			//epr.second = e2;
			let epr = e1 + '_' + e2;

			//if (cmap.count(epr) == 0) {
			if (!cmap[epr]) {	
				cmap[epr] = 1;
			}
			else
				cmap[epr]++;
		}

		return cmap;

	} // end c2b_ContactTable

	
	//https://www.geeksforgeeks.org/number-groups-formed-graph-friends/
	countUtil(ss1, sheetNeighbor, existing_groups) {
		this.visited[ss1] = true;
		if(!this.groupnum2sheet[existing_groups]) this.groupnum2sheet[existing_groups] = [];
		this.groupnum2sheet[existing_groups].push(parseInt(ss1));

		for(let ss2 in sheetNeighbor[ss1]) {
			if (!this.visited[ss2]) {
				this.countUtil(ss2, sheetNeighbor, existing_groups);  
			}
		}  
	}

	//
	// Residue ranges of the Vast domains, per protein chain.
	//

	//
	// Subdomain definition rules are as follows; let m0 = minSSE:
	//
	//     1. A subdomain with <= m0 SSEs cannot be split.
	//
	//     2. A subdomain cannot be split into two this.parts, both with < m0 SSEs.
	//
	//     3. However, a subdomain can be trimmed, i.e. split into two this.parts,
	//        one with < m0 SSEs.
	//
	//c2b_NewSplitChain(string asymId, let seqLen, let* x0, let* y0, let* z0) { let ic = this.icn3d, me = ic.icn3dui;
	// x0, y0, z0: array of x,y,z coordinates of C-alpha atoms
	//c2b_NewSplitChain(chnid, dcut) { let ic = this.icn3d, me = ic.icn3dui;
	c2b_NewSplitChain(atoms, dcut) { let ic = this.icn3d; ic.icn3dui;
		let x0 = [], y0 = [], z0 = [], resiArray = [];

		//substruct: array of secondary structures, each of which has the keys: From (1-based), To (1-based), Sheet (0 or 1), also add these paras: x1, y1, z1, x2, y2, z2
		let substruct = [];
		// determine residue ranges for each subdomain
		let subdomains = [];

		// sheets: array of sheets, each of which has the key: sheet_num (beta sandwich has two sheets, e.g., 0 and 1), adj_strand1 (not used), adj_strand2
		let sheets = [];

		let residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(atoms);
		let residueArray = Object.keys(residueHash);
		let chnid = residueArray[0].substr(0, residueArray[0].lastIndexOf('_'));

		let substructItem = {};
		for(let i = 0; i < residueArray.length; ++i) {
			let resid = residueArray[i];

            let resi = resid.substr(resid.lastIndexOf('_') + 1);
/*			
			if(i == 0) {
				resiOffset = resi - 1;

				for(let j = 0; j < resiOffset; ++j) {
					x0.push(undefined);
					y0.push(undefined);
					z0.push(undefined);					
				}
			}
*/

			//let resid = chnid + "_" + resi;
			let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[resid]);

			if(!atom) continue;    
/*
			if(atom) {
				x0.push(atom.coord.x);
				y0.push(atom.coord.y);
				z0.push(atom.coord.z);
			}
			else {
				x0.push(undefined);
				y0.push(undefined);
				z0.push(undefined);
			}
*/
			//if(!atom) continue;   
			
			x0.push(atom.coord.x);
			y0.push(atom.coord.y);
			z0.push(atom.coord.z);
			resiArray.push(resi);

			if(atom.ssend) {
				substructItem.To = parseInt(resi);
				substructItem.x2 = atom.coord.x;
				substructItem.y2 = atom.coord.y;
				substructItem.z2 = atom.coord.z;

				substructItem.Sheet = (atom.ss == 'sheet') ? true : false;
				substruct.push(substructItem);
				substructItem = {};				
			}

			// a residue could be both start and end. check ssend first, then check ssbegin 
			if(atom.ssbegin) {
				substructItem.From = parseInt(resi);
				substructItem.x1 = atom.coord.x;
				substructItem.y1 = atom.coord.y;
				substructItem.z1 = atom.coord.z;
			}
        }

		let nsse = substruct.length;

		if (nsse <= 3)
			// too small, can't split or trim
			return {subdomains: subdomains, substruct: substruct};

		if (nsse > this.MAX_SSE) {
			// we have a problem...

			return {subdomains: subdomains, substruct: substruct};
		}

		let seqLen = residueArray.length; // + resiOffset;
		let lastResi = resiArray[seqLen - 1];

		// get a list of Calpha-Calpha contacts
		///list< pair< pair< int, let >, let > >
		let cts = this.c2b_AlphaContacts(seqLen, x0, y0, z0, dcut, resiArray);
		
		//
		// Produce a "map" of the SSEs, i.e. vec_sse[i] = 0 means residue i + 1
		// is in a loop, and vec_sse[i] = k means residue i + 1 belongs to SSE
		// number k.
		//
		let vec_sse = []; //vector<int>

		for (let i = 0; i < seqLen; i++)
			vec_sse.push(0);
			
		let hasSheets = false;

		//substruct: array of secondary structures, each of which has the keys: From, To, Sheet (0, 1)
		for (let i = 0; i < substruct.length; i++) {
			//SSE_Rec sserec = substruct[i];
			let sserec = substruct[i];
			let From = sserec.From;
			let To = sserec.To;
			this.elt_size[i] = To - From + 1;

			// double-check indexing OK???
			for (let j = From; j <= To; j++)
				vec_sse[j - 1] = i + 1;

			//if (sserec.Sheet > 0)
			if (sserec.Sheet)
				hasSheets = true;
		}

		// produce the SSE contact lists
		let vec_cts1 = [], vec_cts2 = [], vec_cts1a = [], vec_cts2a = [];

		//for (ctsit = cts.begin(); ctsit != cts.end(); ctsit++) {
		for (let i = 0, il = cts.length; i < il; ++i) {
			//pair< pair< int, let >, let > epr = *ctsit;
			//pair< int, let > respair = epr.first;
			let epr = cts[i];
			let respair = epr.first;
			let sse1 = vec_sse[respair.first - 1];
			let sse2 = vec_sse[respair.second - 1];
			// could be 0 or null
			if ((sse1 <= 0) || (sse2 <= 0) || !sse1 || !sse2) continue;
			vec_cts1.push(sse1);
			vec_cts2.push(sse2);
			if (sse1 == sse2) continue;
			vec_cts1a.push(sse1);
			vec_cts2a.push(sse2);
		}

		// this symmetrizes the contact data
		for (let i = 0; i < vec_cts1a.length; i++) {
			vec_cts1.push(vec_cts2a[i]);
			vec_cts2.push(vec_cts1a[i]);
		}

		// add dummy contacts
		for (let i = 0; i < nsse; i++) {
			vec_cts1.push(i + 1);
			vec_cts2.push(i + 1);
		}
		
		// create contact counts from the contacts/interactions
		//map< pair< int, let >, let > ctable = this.c2b_ContactTable(vec_cts1, vec_cts2);
		let ctable = this.c2b_ContactTable(vec_cts1, vec_cts2);

		// neighbor list of each sheet
		let sheetNeighbor = {};
		for(let pair in ctable) {
			let ssPair = pair.split('_'); // 1-based
			let ss1 = parseInt(ssPair[0]);
			let ss2 = parseInt(ssPair[1]);

			// both are sheets
			if(substruct[ss1 - 1].Sheet && substruct[ss2 - 1].Sheet) {
				if(!sheetNeighbor[ss1]) sheetNeighbor[ss1] = {};
				if(!sheetNeighbor[ss2]) sheetNeighbor[ss2] = {};

				sheetNeighbor[ss1][ss2] = 1;
				sheetNeighbor[ss2][ss1] = 1;
			}
		}

		//https://www.geeksforgeeks.org/number-groups-formed-graph-friends/
		let existing_groups = 0;
		let sheet2sheetnum = {};
		this.groupnum2sheet = {};
		this.visited = {};
		for (let ss1 in sheetNeighbor) {
			this.visited[ss1] = false;
		}

		// get this.groupnum2sheet
		for (let ss1 in sheetNeighbor) {
			// If not in any group.
			if (this.visited[ss1] == false) {
				existing_groups++;
				 
				this.countUtil(ss1, sheetNeighbor, existing_groups);
			}
		}

		// get sheet2sheetnum
		// each neighboring sheet willbe represented by the sheet with the smallest sse 
		for(let groupnum in this.groupnum2sheet) {
			let ssArray = this.groupnum2sheet[groupnum].sort();
			for(let i = 0, il = ssArray.length; i < il; ++i) {
				sheet2sheetnum[ssArray[i]] = ssArray[0];
			}
		}

		for (let i = 0; i < nsse; i++) {
			if(substruct[i].Sheet) {				
				let sheetsItem = {};
				if(sheet2sheetnum[i+1]) {
					sheetsItem.sheet_num = sheet2sheetnum[i+1];
					sheetsItem.adj_strand2 = 1; 
					sheetsItem.sse = i + 1; 
				}
				else {
					sheetsItem.sheet_num = 0;
					sheetsItem.adj_strand2 = 0; 
					sheetsItem.sse = i + 1; 
				}

				sheets.push(sheetsItem);
			}
		}

		//
		// Correct for dummy contacts; they're present to ensure that the
		// table gives the right result in the possible case there is an
		// element with no contacts.
		//
		for (let i = 0; i < nsse; i++) {
			for (let j = 0; j < nsse; j++) {
				//pair<int, int> epr;
				//let epr = {};
				//epr.first = i + 1;
				//epr.second = j + 1;
				let epr = (i+1).toString() + '_' + (j+1).toString();

				//if (ctable.count(epr) == 0)
				if (!ctable[epr])
					this.ctc_cnt[i][j] = 0;
				else {
					let cnt = ctable[epr];
					if (i == j) cnt--; // subtract dummy contact
					this.ctc_cnt[i][j] = cnt;
					this.ctc_cnt[j][i] = cnt;
				}
			}
		}

		let minStrand = 6;

		if (hasSheets) {
			//sheets: array of sheets, each of which has the key: sheet_num (number of strands), adj_strand1, adj_strand2

			let cnt = 0;

			for (let i = 0; i < sheets.length; i++) {
				//BetaSheet_Rec bsrec = sheets[i];
				let bsrec = sheets[i];

				//if ((bsrec.sheet_num > 0) && (this.elt_size[i] >= minStrand) && (bsrec.adj_strand2 != 0))
				if ((bsrec.sheet_num > 0) && (this.elt_size[bsrec.sse - 1] >= minStrand) && (bsrec.adj_strand2 != 0))
					cnt++;
			}

			for (let i = 0; i < nsse; i++) {
				//this.group_num[i] = (cnt == 0) ? i + 1 : 0;
				this.group_num[i] = i + 1;
			}

			if (cnt> 0) {
				for (let i = 0; i < sheets.length; i++) {
					let bsrec = sheets[i];
					this.group_num[bsrec.sse - 1] = bsrec.sheet_num;
				}
			}
		}
		else {
			for (let i = 0; i < nsse; i++)
				this.group_num[i] = i + 1;
		}

		let sratio = 0.25;
		let minSize = 25;
		let maxCsz = 4;
		let avgCts = 0.0;
		let ncFact = 0.0;
		let cDelta = 3;
		let minSSE = 3;

		// call the domain splitter
		this.parts = [];
		this.parts.length = 2*this.MAX_SSE;
		let ratios = [];
		ratios.length = this.MAX_SSE;
		let n_saved = 0;

		for (let i = 0; i < nsse; i++) {
			this.parts[2*i] = this.parts[2*i + 1] = 0;
			ratios[i] = 0.0;
		}

		n_saved = this.new_split_chain(nsse, sratio, minSize, minSSE, maxCsz, avgCts, cDelta, ncFact, this.parts, n_saved, ratios);

		// save domain data
		//list< vector< let > > list_parts;
		let list_parts = [];

		if (n_saved > 0) {
			// splits occurred...
			let j = 0;
			
			for (let i = 0; i <= n_saved; i++) {
				//vector<int> sselst;
				let sselst = [];
				//sselst.clear();

				while (j < 2*nsse) {
					let sse0 = this.parts[j++];

					if (sse0 == 0) {
						list_parts.push(sselst);
						break;
					}
					else
						sselst.push(sse0);
				}
			}
		}
		
		list_parts.sort(function(v1, v2) {
				return v1[0] - v2[0];
			});

		//for (lplet = list_parts.begin(); lplet != list_parts.end(); lpint++) {
		for (let index = 0, indexl = list_parts.length; index < indexl; ++index) {
			//vector<int> prts = *lpint;
			let prts = list_parts[index];
			//vector<int> resflags;
			//resflags.clear();

			//let resflags = [];
			let resflags = {};

			// a domain must have at least 3 SSEs...
			if (prts.length <= 2) continue;

			for (let i = 0; i < seqLen; i++) {
				//resflags.push(0);
				resflags[resiArray[i]] = 0;
			}

			for (let i = 0; i < prts.length; i++) {
				let k = prts[i] - 1;

				if ((k < 0) || (k >= substruct.length)) {
					return {subdomains: subdomains, substruct: substruct};
				}

				//SSE_Rec sserec = substruct[k];
				let sserec = substruct[k];
				let From = sserec.From;
				let To = sserec.To;

				for (let j = From; j <= To; j++) {
					//resflags[j - 1] = 1;
					resflags[j] = 1;
				}

				if ((k == 0) && (From > 1)) {
					// residues with negative residue numbers will not be included
					for (let j = 1; j < From; j++) {
						//resflags[j - 1] = 1;
						// include at most 10 residues
						if(From - j <= 10) {
							//resflags[j - 1] = 1;
							resflags[j] = 1;
						}
					}
				}

				//if ((k == substruct.length - 1) && (To < seqLen)) {
				if ((k == substruct.length - 1) && (To < parseInt(lastResi))) {
					//for (let j = To + 1; j <= seqLen; j++) {
					for (let j = To + 1; j <= parseInt(lastResi); j++) {
						//resflags[j - 1] = 1;
						// include at most 10 residues
						if(j - To <= 10) {
							//resflags[j - 1] = 1;
							resflags[j] = 1;
						}
					}
				}

				// left side
				if (k > 0) {
					//SSE_Rec sserec1 = substruct[k - 1];
					let sserec1 = substruct[k - 1];
					let To1 = sserec1.To;
					//let ll = (int) floor(0.5*((let) (From - To1 - 1)));
					let ll = parseInt(0.5 * (From - To1 - 1));

					if (ll > 0) {
						for (let j = From - ll; j <= From - 1; j++) {
							//resflags[j - 1] = 1;
							resflags[j] = 1;
						}
					}
				}

				// right side
				if (k < substruct.length - 1) {
					//SSE_Rec sserec1 = substruct[k + 1];
					let sserec1 = substruct[k + 1];
					let From1 = sserec1.From;
					//let ll = (int) ceil(0.5*((let) (From1 - To - 1)));
					// let ft = From1 - To - 1;
					// let ll = parseInt(ft/2);
					// if (ft % 2 == 1) ll++;
					let ll = parseInt(0.5 * (From1 - To - 1) + 0.5);

					if (ll > 0) {
						for (let j = To + 1; j <= To + ll; j++) {
							//resflags[j - 1] = 1;
							resflags[j] = 1;
						}
					}
				}
			}

			// extract the continguous segments
			let inseg = false;
			let startseg;
			//vector<int> segments;
			//segments.clear();
			let segments = [];

			for (let i = 0; i < seqLen; i++) {
				//let rf = resflags[i];
				let rf = resflags[resiArray[i]];

				if (!inseg && (rf == 1)) {
					// new segment starts here
					//startseg = i + 1;
					startseg = resiArray[i];
					inseg = true;
					continue;
				}

				if (inseg && (rf == 0)) {
					// segment ends
					segments.push(startseg);
					//segments.push(i);
					segments.push(resiArray[i]);
					inseg = false;
				}
			}

			// check for the last segment
			if (inseg) {
				segments.push(startseg);
				//segments.push(seqLen);
				segments.push(lastResi);
			}

			subdomains.push(segments);
		}

		// update ic.tddomains
		if(!ic.tddomains) ic.tddomains = {};
		for(let i = 0, il = subdomains.length; i < il; ++i) {
			// domain item: {"sdid":1722375,"intervals":[[1,104],[269,323]]}
			let domainName = 'domain3d-' + Object.keys(ic.tddomains).length;
			ic.tddomains[domainName] = {};

			for(let j = 0, jl = subdomains[i].length; j < jl; j += 2) {
				for(let k = subdomains[i][j]; k <= subdomains[i][j+1]; ++k) {
					let resid = chnid + '_' + k;
					ic.tddomains[domainName][resid] = 1;
				}
			}
		}

		return {subdomains: subdomains, substruct: substruct};
	} // end c2b_NewSplitChain

	getDomainJsonForAlign(atoms) { let ic = this.icn3d, me = ic.icn3dui;
		let result = this.c2b_NewSplitChain(atoms);

		let subdomains = result.subdomains;
		let substruct = result.substruct;

		let residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(atoms);
		let residueArray = Object.keys(residueHash);
		let chnid = residueArray[0].substr(0, residueArray[0].lastIndexOf('_'));

		//the whole structure is also considered as a large domain
		//if(subdomains.length == 0) {
			//subdomains.push([parseInt(ic.chainsSeq[chnid][0].resi), parseInt(ic.chainsSeq[chnid][ic.chainsSeq[chnid].length - 1].resi)]);
			subdomains.push([parseInt(residueArray[0].substr(residueArray[0].lastIndexOf('_') + 1)), 
			 	parseInt(residueArray[residueArray.length-1].substr(residueArray[residueArray.length-1].lastIndexOf('_') + 1))]);
		//}

		// m_domains1: {"data": [ {"ss": [[1,20,30,x,y,z,x,y,z], [2,50,60,x,y,z,x,y,z]], "domain": [[1,43,x,y,z],[2,58,x,y,z], ...]}, {"ss": [[1,20,30,x,y,z,x,y,z], [2,50,60,x,y,z,x,y,z]],"domain": [[1,43,x,y,z],[2,58,x,y,z], ...]} ] }
		let jsonStr = '{"data": [';
		for(let i = 0, il = subdomains.length; i < il; ++i) {
			if(i > 0) jsonStr += ', ';
			//secondary structure
			jsonStr += '{"ss": [';
			let ssCnt = 0;
			for(let j = 0, jl = subdomains[i].length; j < jl; j += 2) {
				let start = subdomains[i][j];
				let end = subdomains[i][j + 1];
			
				for(let k = 0, kl = substruct.length; k < kl; ++k) {
					//ss: sstype	ss_start	ss_end	x1	y1	z1	x2	y2	z2
						//sstype: 1 (helix), 2 (sheet)
					let sstype = (substruct[k].Sheet) ? 2 : 1;
					let from = substruct[k].From;
					let to = substruct[k].To;

					let residFrom = chnid + "_" + from;
					let atomFrom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residFrom]);
					if(!atomFrom || !ic.hAtoms.hasOwnProperty(atomFrom.serial)) continue;

					let residTo = chnid + "_" + to;
					let atomTo = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residTo]);
					if(!atomTo || !ic.hAtoms.hasOwnProperty(atomTo.serial)) continue;

					if(from >= start && to <= end) {
						if(ssCnt > 0) jsonStr += ', ';
						jsonStr += '[' + sstype + ',' + from + ',' + to + ',' + substruct[k].x1.toFixed(2) + ',' + substruct[k].y1.toFixed(2) + ',' 
							+ substruct[k].z1.toFixed(2) + ',' + substruct[k].x2.toFixed(2) + ',' + substruct[k].y2.toFixed(2) + ',' + substruct[k].z2.toFixed(2) + ']';
						++ssCnt;
					}
				}				
			}
			jsonStr += ']';

			// domain
			jsonStr += ', "domain": [';
			let domainCnt = 0;
			for(let j = 0, jl = subdomains[i].length; j < jl; j += 2) {
				let start = subdomains[i][j];
				let end = subdomains[i][j + 1];

				for(let k = 0, kl = residueArray.length; k < kl; ++k) {
					let resid = residueArray[k];

					let resi = resid.substr(resid.lastIndexOf('_') + 1);
		
					//let resid = chnid + "_" + resi;
					let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[resid]);

					if(!atom) continue;
					if(!ic.hAtoms.hasOwnProperty(atom.serial)) continue;

					//domain: resi, restype, x, y, z
					let restype = me.parasCls.resn2restype[atom.resn];
					if(restype !== undefined && resi >= start && resi <= end) {
						if(domainCnt > 0) jsonStr += ', ';
						jsonStr += '[' + resi + ',' + restype + ',' + atom.coord.x.toFixed(2) + ',' 
							+ atom.coord.y.toFixed(2) + ',' + atom.coord.z.toFixed(2) + ']';
						++domainCnt;
					}
				}			
			}
			jsonStr += ']}';
		}
		jsonStr += ']}';

		return jsonStr;
	} 
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AddTrack {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    clickAddTrackButton() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        // ncbi gi/accession
        me.myEventCls.onIds("#" + ic.pre + "addtrack_button1", "click", async function(e) { let ic = thisClass.icn3d;
           e.stopImmediatePropagation();

           //e.preventDefault();
           dialog.dialog( "close" );

           let chainid = $("#" + ic.pre + "track_chainid").val();

           //var gi = $("#" + ic.pre + "track_gi").val().toUpperCase();
           let gi = $("#" + ic.pre + "track_gi").val();
           let title =(isNaN(gi)) ? 'Acc ' + gi : 'gi ' + gi;

           //var text = $("#" + ic.pre + "track_text").val();
           let url = me.htmlCls.baseUrl + 'pwaln/pwaln.fcgi?from=track';

           let dataObj = {'targets': chainid, 'queries': gi};
           let data = await me.getAjaxPostPromise(url, dataObj);

           thisClass.alignSequenceToStructure(chainid, data, title);
        });

        // FASTA
        me.myEventCls.onIds("#" + ic.pre + "addtrack_button2", "click", async function(e) { let ic = thisClass.icn3d;
           e.stopImmediatePropagation();
           //e.preventDefault();
           dialog.dialog( "close" );

           let chainid = $("#" + ic.pre + "track_chainid").val();

           let fasta = $("#" + ic.pre + "track_fasta").val();
           //var title = 'fasta ' + fasta.substr(0, 5);
           let title = $("#" + ic.pre + "fasta_title").val();

           let structure = chainid.substr(0, chainid.indexOf('_'));
           let targets = chainid;
           if(structure.length == 5) { // e.g., 1TUP2
              targets = targets.substr(0,4);
           }
           else if(structure.length > 5) { // AlphaFold UniProt
              targets = '';
              for(let i = 0, il = ic.chainsSeq[chainid].length; i < il; ++i) {
                targets += ic.chainsSeq[chainid][i].name;
              }
           }

           //var text = $("#" + ic.pre + "track_text").val();
           let url = me.htmlCls.baseUrl + 'pwaln/pwaln.fcgi?from=track';
           let dataObj = {'targets': targets, 'queries': fasta};
           let data = await me.getAjaxPostPromise(url, dataObj);

           thisClass.alignSequenceToStructure(chainid, data, title);
        });

        // FASTA Alignment
        me.myEventCls.onIds("#" + ic.pre + "addtrack_button2b", "click", async function(e) { let ic = thisClass.icn3d;
           e.stopImmediatePropagation();
           //e.preventDefault();
           dialog.dialog( "close" );

           let chainid = $("#" + ic.pre + "track_chainid").val();
           let startpos = $("#" + ic.pre + "fasta_startpos").val();
           let colorseqby = $("#" + ic.pre + "colorseqby").val();
           let type =(colorseqby == 'identity') ? 'identity' : 'custom';

           let fastaList = $("#" + ic.pre + "track_fastaalign").val();
           let fastaArray = fastaList.split('>');

           // the first array item is empty
           // the second array item is the sequence of the structure, start with i = 2
           let posFirst = fastaArray[1].indexOf('\n');
           fastaArray[1].substr(0, posFirst);
           let seqFirst = fastaArray[1].substr(posFirst + 1).replace(/\n/g, '');

           let trackTitleArray = [];
           let trackSeqArray = [];
           for(let i = 2, il = fastaArray.length; i < il; ++i) {
               let pos = fastaArray[i].indexOf('\n');
               let title = fastaArray[i].substr(0, pos);
               trackTitleArray.push(title);
               let seq = fastaArray[i].substr(pos + 1).replace(/\n/g, '');
               trackSeqArray.push(seq);
           }

           let startposGiSeq = undefined;
           for(let i = 0, il = ic.giSeq[chainid].length; i < il; ++i) {
               //let pos =(i >= ic.matchedPos[chainid] && i - ic.matchedPos[chainid] < ic.chainsSeq[chainid].length) ? ic.chainsSeq[chainid][i - ic.matchedPos[chainid]].resi : ic.baseResi[chainid] + 1 + i;
               let pos = ic.ParserUtilsCls.getResi(chainid, i);

               if(pos != startpos) {
                   continue;
               }
               else {
                   startposGiSeq = i;
               }
           }

           if(startposGiSeq === undefined) alert("Please double check the start position before clicking \"Add Track\"");


           // set up gap for the master seq
           // don't count gaps in both ends
           ic.targetGapHash = {};
           let prevSeq = '-', prevPos = 0, from, to, cnt = 0, dashCnt = 0;
           let bFound = false, seqStart = 0, seqEnd = 0;
           for(let i = 0, il = seqFirst.length; i < il; ++i) {
              if(seqFirst[i] == '-' && seqFirst[i] != prevSeq) { // start of gap
                  from = cnt;
                  dashCnt = 0;
              }

              if(prevSeq == '-' && seqFirst[i] != prevSeq && cnt > 0) { // end of gap
                  to = prevPos;
                  ic.targetGapHash[from + startposGiSeq] = {'from': from + startposGiSeq, 'to': to + dashCnt - 1 + startposGiSeq};
              }

              prevSeq = seqFirst[i];
              prevPos = cnt;

              if(seqFirst[i] != '-') {
                  ++cnt;
                  seqEnd = i;

                  if(!bFound) {
                      seqStart = i;
                      bFound = true;
                  }
              }
              else {
                  ++dashCnt;
              }
           }

           await ic.annotationCls.resetAnnoAll();

           let targetGapHashStr = '';
           let cntTmp = 0;
           for(let i in ic.targetGapHash) {
               if(cntTmp > 0) targetGapHashStr += ' ';
               targetGapHashStr += i + '_' + ic.targetGapHash[i].from + '_' + ic.targetGapHash[i].to;
               ++cntTmp;
           }

           me.htmlCls.clickMenuCls.setLogCmd("msa | " + targetGapHashStr, true);

           // add tracks
           let resi2cntSameRes = {}; // count of same residue at each position
           for(let j = 0, jl = trackSeqArray.length; j < jl; ++j) {
               let resi = startpos; //startposGiSeq + 1;
               let text = '';
               for(let k = 0; k < startposGiSeq; ++k) {
                   if(ic.targetGapHash.hasOwnProperty(k)) {
                       for(let m = 0; m < ic.targetGapHash[k].to - ic.targetGapHash[k].from + 1; ++m) {
                           text += '-';
                       }
                   }

                   text += '-';
               }

               for(let k = seqStart; k <= seqEnd; ++k) {
                  //if(seqFirst[k] == '-') continue;

                  if(j == 0) resi2cntSameRes[resi] = 0;

                  text += trackSeqArray[j][k]; //ic.giSeq[chainid][i];

                  if(seqFirst[k] != '-') {
                      if(seqFirst[k] == trackSeqArray[j][k]) ++resi2cntSameRes[resi];
                      ++resi;
                  }
               }

               let title =(trackTitleArray[j].length < 20) ? trackTitleArray[j] : trackTitleArray[j].substr(0, 20) + '...';
               let bMsa = true;
               thisClass.showNewTrack(chainid, title, text, undefined, undefined, type, undefined, bMsa);

               me.htmlCls.clickMenuCls.setLogCmd("add track | chainid " + chainid + " | title " + title + " | text " + thisClass.simplifyText(text)
                + " | type " + type + " | color 0 | msa 1", true);
           }

           // set colot for the master seq
           if(trackSeqArray.length > 0) {
                if(ic.queryresi2score === undefined) ic.queryresi2score = {};
                if(ic.queryresi2score[chainid] === undefined) ic.queryresi2score[chainid] = {};

                let nSeq = trackSeqArray.length;
                for(let resi in resi2cntSameRes) {
                    let score = parseInt(resi2cntSameRes[resi] / nSeq * 100);
                    ic.queryresi2score[chainid][resi] = score;
                }

                let resiArray = Object.keys(resi2cntSameRes);
                let start = Math.min.apply(null, resiArray);
                let end = Math.max.apply(null, resiArray);

                let resiScoreStr = '';
                for(let resi = start; resi <= end; ++resi) {
                    if(resi2cntSameRes.hasOwnProperty(resi)) {
                        resiScoreStr += Math.round(resi2cntSameRes[resi] / nSeq * 9); // max 9
                    }
                    else {
                        resiScoreStr += '_';
                    }
                }

                ic.opts['color'] = 'align custom';
                ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

                ic.hlUpdateCls.updateHlAll();

                ic.drawCls.draw();

                me.htmlCls.clickMenuCls.setLogCmd('color align custom | ' + chainid + ' | range ' + start + '_' + end + ' | ' + resiScoreStr, true);
           }
        });

        // BED file
        me.myEventCls.onIds("#" + ic.pre + "addtrack_button3", "click", function(e) { let ic = thisClass.icn3d;
           e.stopImmediatePropagation();
           //e.preventDefault();
           dialog.dialog( "close" );

           let chainid = $("#" + ic.pre + "track_chainid").val();


           let file = $("#" + ic.pre + "track_bed")[0].files[0];

           if(!file) {
             alert("Please select a file...");
           }
           else {
             if(!window.File || !window.FileReader || !window.FileList || !window.Blob) {
                alert('The File APIs are not fully supported in this browser.');
             }

             let reader = new FileReader();
             reader.onload = function(e) {
               let dataStr = e.target.result; // or = reader.result;

               let lineArray = dataStr.split('\n');

               let bItemRgb = false, bColorByStrand = false;
               let strandRgbArray;
               for(let i = 0, il = lineArray.length; i < il; ++i) {
                   if(lineArray[i].substr(0, 7) == 'browser') continue;

                   if(lineArray[i].substr(0, 5) == 'track') {
                       if(lineArray[i].toLowerCase().indexOf('itemrgb') != -1) bItemRgb = true;
                       if(lineArray[i].toLowerCase().indexOf('colorbystrand=') != -1) {
                           bColorByStrand = true;

                           //e.g., colorByStrand="255,0,0 0,0,255"
                           let pos = lineArray[i].toLowerCase().indexOf('colorbystrand=');
                           let restStr = lineArray[i].substr(pos);
                           let quotePos = restStr.indexOf('"');
                           if(quotePos != -1) {
                             let quoteStr = restStr.substr(quotePos + 1);
                             let quotePos2 = quoteStr.indexOf('"');
                             if(quotePos != -1) {
                               let colorList = quoteStr.substr(0, quotePos2);
                               strandRgbArray = colorList.split(' ');
                             }
                           }

                       }
                   }
                   else { // tracks
                          if(lineArray[i] == '') continue;
                          let fieldArray = lineArray[i].replace(/\s+/g, ' ').split(' ');

                          if(fieldArray.length > 8 || fieldArray.length < 6) bColorByStrand = false;
                          if(fieldArray.length < 9) bItemRgb = false;

                          //https://genoic.ucsc.edu/FAQ/FAQformat.html#format1
                          fieldArray[0];
                          let chromStart = fieldArray[1];
                          let chromEnd = fieldArray[2];
                          let trackName = fieldArray[3];

                          let strand, itemRgb;

                          if(fieldArray.length > 4) ;
                          if(fieldArray.length > 5) strand = fieldArray[5]; // ., +, or -
                          if(fieldArray.length > 6) ;
                          if(fieldArray.length > 7) ;
                          if(fieldArray.length > 8) itemRgb = fieldArray[8];
                          if(fieldArray.length > 9) ;
                          if(fieldArray.length > 10) ;
                          if(fieldArray.length > 11) ;

                       let title = trackName;

                       let rgbColor = '51,51,51';
                       if(bItemRgb) {
                           rgbColor = itemRgb;
                       }
                       else if(bColorByStrand) {
                           if(strand == '+' && strandRgbArray.length > 0) {
                               rgbColor = strandRgbArray[0];
                           }
                           else if(strand == '-' && strandRgbArray.length > 1) {
                               rgbColor = strandRgbArray[1];
                           }
                           else if(strand == '.' && strandRgbArray.length > 2) {
                               rgbColor = strandRgbArray[2];
                           }
                       }

                       let text = '';
                       let cssColorArray = [];
                       for(let j = 0, jl = chromEnd; j < jl; ++j) {
                           if(j < chromStart) {
                               text += '-';
                               cssColorArray.push('');
                           }
                           else {
                               text += ic.giSeq[chainid][j];
                               cssColorArray.push('rgb(' + rgbColor + ')');
                           }
                       }

                       thisClass.showNewTrack(chainid, title, text, cssColorArray, undefined, undefined, rgbColor);

                       me.htmlCls.clickMenuCls.setLogCmd("add track | chainid " + chainid + " | title " + title + " | text " + thisClass.simplifyText(text) + " | type bed | color " + rgbColor, true);
                   }
               }
             };

             reader.readAsText(file);
           }
        });

        // custom
        me.myEventCls.onIds("#" + ic.pre + "addtrack_button4", "click", function(e) { let ic = thisClass.icn3d;
           e.stopImmediatePropagation();
           //e.preventDefault();
           dialog.dialog( "close" );

           let chainid = $("#" + ic.pre + "track_chainid").val();
           let title = $("#" + ic.pre + "track_title").val();
           let text = $("#" + ic.pre + "track_text").val(); // input simplifyText

           //this.showNewTrack(chainid, title, text);
           //me.htmlCls.clickMenuCls.setLogCmd("add track | chainid " + chainid + " | title " + title + " | text " + this.simplifyText(text), true);
           let result = thisClass.getFullText(text);

           thisClass.showNewTrack(chainid, title,  result.text, undefined, undefined, 'custom', undefined, undefined, result.fromArray, result.toArray);

           me.htmlCls.clickMenuCls.setLogCmd("add track | chainid " + chainid + " | title " + title + " | text " + thisClass.simplifyText(text) + " | type custom", true);
        });

        // current selection
        me.myEventCls.onIds("#" + ic.pre + "addtrack_button5", "click", function(e) { let ic = thisClass.icn3d;
           e.stopImmediatePropagation();
           //e.preventDefault();
           dialog.dialog( "close" );

           let chainid = $("#" + ic.pre + "track_chainid").val();
           let title = $("#" + ic.pre + "track_selection").val();
           let text = '';

           let selectedAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.chains[chainid]);

           let residueHash = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(selectedAtoms);

           let cssColorArray = [];
           for(let i = 0, il = ic.giSeq[chainid].length; i < il; ++i) {
              let cFull = ic.giSeq[chainid][i];

              let c = cFull;
              if(cFull.length > 1) {
                  //c = cFull[0] + '..';
                  c = cFull[0]; // one letter for each residue
              }

              //let pos =(i >= ic.matchedPos[chainid] && i - ic.matchedPos[chainid] < ic.chainsSeq[chainid].length) ? ic.chainsSeq[chainid][i - ic.matchedPos[chainid]].resi : ic.baseResi[chainid] + 1 + i;
              let pos = ic.ParserUtilsCls.getResi(chainid, i);

              if( residueHash.hasOwnProperty(chainid + '_' + pos) ) {
                  let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[chainid + '_' + pos]);
                  let colorStr =(atom.color === undefined || atom.color.getHexString().toUpperCase() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
                  let color =(atom.color !== undefined) ? colorStr : "CCCCCC";

                  text += c;
                  cssColorArray.push('#' + color);
              }
              else {
                  text += '-';
                  cssColorArray.push('');
              }
           }

           thisClass.showNewTrack(chainid, title, text, cssColorArray, undefined, 'selection', undefined);

           me.htmlCls.clickMenuCls.setLogCmd("add track | chainid " + chainid + " | title " + title + " | text " + thisClass.simplifyText(text) + " | type selection", true);
        });

    }

    showNewTrack(chnid, title, text, cssColorArray, inTarget2queryHash, type, color, bMsa, fromArray, toArray) {  let ic = this.icn3d, me = ic.icn3dui;
        //if(ic.customTracks[chnid] === undefined) {
        //    ic.customTracks[chnid] = {}
        //}

        let bErrorMess = false;
        if(text == 'cannot be aligned') {
            bErrorMess = true;
        }

        let textForCnt = text.replace(/-/g, '');
        let resCnt = textForCnt.length;
        //if(resCnt > ic.giSeq[chnid].length) {
        //    resCnt = ic.giSeq[chnid].length;
        //}

        if(!bMsa) {
            if(text.length > ic.giSeq[chnid].length) {
                text = text.substr(0, ic.giSeq[chnid].length);
            }
            else if(text.length < ic.giSeq[chnid].length && !bErrorMess) {
                // .fill is not supported in IE
                //var extra = Array(ic.giSeq[chnid].length - text.length).fill(' ').join('');
                let extra = '';
                for(let i = 0, il = ic.giSeq[chnid].length - text.length; i < il; ++i) {
                    extra += '-';
                }

                text += extra;
            }
        }

        let simpTitle = title.replace(/\s/g, '_').replace(/\./g, 'dot').replace(/\W/g, '');
        if(simpTitle.length > 20) simpTitle = simpTitle.substr(0, 20);

        //ic.customTracks[chnid][simpTitle] = text;

        let divLength = me.htmlCls.RESIDUE_WIDTH * text.length + 200;

        $("#" + ic.pre + "dt_custom_" + chnid).append("<div id='" + ic.pre + "dt_custom_" + chnid + "_" + simpTitle + "'></div>");
        $("#" + ic.pre + "dt_custom_" + chnid + "_" + simpTitle).width(divLength);

        $("#" + ic.pre + "ov_custom_" + chnid).append("<div id='" + ic.pre + "ov_custom_" + chnid + "_" + simpTitle + "'></div>");
        $("#" + ic.pre + "ov_custom_" + chnid + "_" + simpTitle).width(divLength);

        $("#" + ic.pre + "tt_custom_" + chnid).append("<div id='" + ic.pre + "tt_custom_" + chnid + "_" + simpTitle + "'></div>");
        $("#" + ic.pre + "tt_custom_" + chnid + "_" + simpTitle).width(divLength);

        let html = '<div id="' + ic.pre + 'giseq_sequence" class="icn3d-dl_sequence">';
        let html2 = html;
        let html3 = html;

        //var htmlTmp2 = '<div class="icn3d-seqTitle icn3d-link icn3d-blue" gi="' + chnid + '" anno="sequence" chain="' + chnid + '"><span style="white-space:nowrap;">' + title + '</span></div>';
        //var htmlTmp2 = '<div class="icn3d-seqTitle" gi="' + chnid + '" anno="sequence" chain="' + chnid + '"><span style="white-space:nowrap;">' + title + '</span></div>';
        let index = parseInt(Math.random()*10);
        let htmlTmp2 = '<div class="icn3d-seqTitle icn3d-link icn3d-blue" custom="' +(index+1).toString() + '" from="' + fromArray + '" to="' + toArray + '" shorttitle="' + simpTitle + '" index="' + index + '" setname="' + chnid + '_custom_' +(index+1).toString() + '" anno="sequence" chain="' + chnid + '" title="' + title + '">' + simpTitle + ' </div>';
        let htmlTmp3 = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Pos</span>';

        html3 += htmlTmp2 + htmlTmp3 + '<br>';

        let htmlTmp = '<span class="icn3d-seqLine">';

        html += htmlTmp2 + htmlTmp3 + htmlTmp;
        html2 += htmlTmp2 + htmlTmp3 + htmlTmp;

        //var pre ='cst' + ic.customTracks[chnid].length;
        let posTmp = chnid.indexOf('_');
        //var pre ='cst' + chnid.substr(posTmp);
        let pre ='cst' + chnid.substr(posTmp + 1);

        let prevEmptyWidth = 0;
        let prevLineWidth = 0;
        let widthPerRes = 1;

        let bAlignColor =(type === undefined || type === 'seq' || type === 'custom') && text.indexOf('cannot-be-aligned') == -1 && text.indexOf('cannot be aligned') == -1 ? true : false;

        let bIdentityColor =(type === 'identity') && text.indexOf('cannot-be-aligned') == -1 && text.indexOf('cannot be aligned') == -1 ? true : false;

        let parsedResn = {};
        let gapCnt = 0, currResi = 1;
        htmlTmp2 = '';
        for(let i = 0, il = text.length; i < il; ++i) {
          let resNum = i-gapCnt;

          if(!bMsa) {
              html += ic.showSeqCls.insertGap(chnid, i, '-');
          }
          else {
              if(ic.targetGapHash.hasOwnProperty(resNum) && !parsedResn.hasOwnProperty(resNum)) {
                  gapCnt += ic.targetGapHash[resNum].to - ic.targetGapHash[resNum].from + 1;

                  parsedResn[resNum] = 1;
              }
          }

          let c = text.charAt(i);

          if(c != ' ' && c != '-') {
              let resName =(ic.chainsSeq[chnid][resNum]) ? ic.chainsSeq[chnid][resNum].name : ' ';
              let colorHexStr = ic.showAnnoCls.getColorhexFromBlosum62(c, resName);
              let identityColorStr =(c == resName) ? 'FF0000' : '0000FF';

              //var pos =(resNum >= ic.matchedPos[chnid] && resNum - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][resNum - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + resNum;
              let pos = ic.baseResi[chnid] + currResi;

              if(inTarget2queryHash !== undefined) pos = ic.baseResi[chnid] + inTarget2queryHash[i] + 1; // 0-based

              let tmpStr;
              if(cssColorArray !== undefined && cssColorArray[i] != '') {
                  tmpStr = 'style="color:' + cssColorArray[i] + '"';
              }
              else if(color) {
                  tmpStr = 'style="color:rgb(' + color + ')"';
              }
              else if(bAlignColor || type == 'seq') {
                  tmpStr = 'style="color:#' + colorHexStr + '"';

                  if(type == 'seq') { // reset the color of atoms
                      for(let serial in ic.residues[chnid + '_' + pos]) {
                          let color2 = me.parasCls.thr("#" + colorHexStr);
                          ic.atoms[serial].color = color2;
                          ic.atomPrevColors[serial] = color2;
                      }
                  }
              }
              else if(bIdentityColor) {
                  tmpStr = 'style="color:#' + identityColorStr + '"';
              }
              else {
                  tmpStr = '';
              }

              html += '<span id="' + pre + '_' + ic.pre + chnid + '_' + pos + '" title="' + c + pos + '" class="icn3d-residue" ' + tmpStr + '>' + c + '</span>';

              htmlTmp2 += ic.showSeqCls.insertGapOverview(chnid, i);

              let emptyWidth =(me.cfg.blast_rep_id == chnid) ? Math.round(ic.seqAnnWidth * i /(ic.maxAnnoLength + ic.nTotalGap) - prevEmptyWidth - prevLineWidth) : Math.round(ic.seqAnnWidth * i / ic.maxAnnoLength - prevEmptyWidth - prevLineWidth);
              if(emptyWidth < 0) emptyWidth = 0;

              htmlTmp2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
              if(cssColorArray !== undefined && cssColorArray[i] != '') {
                  tmpStr = cssColorArray[i];
              }
              else if(color) {
                  tmpStr = 'rgb(' + color + ')';
              }
              else if(bAlignColor) {
                  tmpStr = '#' + colorHexStr;
              }
              else {
                  tmpStr = '#333';
              }

              htmlTmp2 += '<div style="display:inline-block; background-color:' + tmpStr + '; width:' + widthPerRes + 'px;" title="' + c +(i+1).toString() + '">&nbsp;</div>';

              prevEmptyWidth += emptyWidth;
              prevLineWidth += widthPerRes;

              ++currResi;
          }
          else {
              if(bErrorMess) {
                  html += '<span>' + c + '</span>';
              }
              else {
                html += '<span>-</span>';
              }
          }
        }

        if(fromArray !== undefined) {
            htmlTmp2 = '';
            let fromArray2 = [], toArray2 = [];
            for(let i = 0, il = fromArray.length; i < il; ++i) {
                fromArray2.push(fromArray[i]);

                for(let j = parseInt(fromArray[i]); j <= parseInt(toArray[i]); ++j) {
                    if(ic.targetGapHash !== undefined && ic.targetGapHash.hasOwnProperty(j)) {
                        toArray2.push(j - 1);
                        fromArray2.push(j);
                    }
                }

                toArray2.push(toArray[i]);
            }

            ic.nTotalGap = 0;
            for(let i in ic.targetGapHash) {
                ic.nTotalGap += ic.targetGapHash[i].to - ic.targetGapHash[i].from + 1;
            }

            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chnid]);
            let colorStr =(atom.color === undefined || atom.color.getHexString() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
            let color =(atom.color !== undefined) ? colorStr : "CCCCCC";

            for(let i = 0, il = fromArray2.length; i < il; ++i) {
                htmlTmp2 += ic.showSeqCls.insertGapOverview(chnid, fromArray2[i]);

                let emptyWidth =(i == 0) ? Math.round(ic.seqAnnWidth *(fromArray2[i] - ic.baseResi[chnid] - 1) /(ic.maxAnnoLength + ic.nTotalGap)) : Math.round(ic.seqAnnWidth *(fromArray2[i] - toArray2[i-1] - 1) /(ic.maxAnnoLength + ic.nTotalGap));
                htmlTmp2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';

                htmlTmp2 += '<div style="display:inline-block; color:white!important; font-weight:bold; background-color:#' + color + '; width:' + Math.round(ic.seqAnnWidth *(toArray2[i] - fromArray2[i] + 1) /(ic.maxAnnoLength + ic.nTotalGap)) + 'px;" class="icn3d-seqTitle icn3d-link icn3d-blue" custom="' +(index+1).toString() + '" from="' + fromArray2 + '" to="' + toArray2 + '" shorttitle="' + simpTitle + '" index="' + index + '" setname="' + chnid + '_custom_' +(index+1).toString() + '" id="' + chnid + '_custom_' + index + '" anno="sequence" chain="' + chnid + '" title="' + title + '">' + title + '</div>';
            }
        }

        htmlTmp = '<span class="icn3d-residueNum" title="residue count">' + resCnt.toString() + ' Pos</span>';
        htmlTmp += '</span>';
        htmlTmp += '<br>';

        htmlTmp += '</div>';

        html += htmlTmp;
        html2 += htmlTmp2 + htmlTmp;

        html3 += '</div>';

        $("#" + ic.pre + "dt_custom_" + chnid + "_" + simpTitle).html(html);
        $("#" + ic.pre + "ov_custom_" + chnid + "_" + simpTitle).html(html2);
        $("#" + ic.pre + "tt_custom_" + chnid + "_" + simpTitle).html(html3);
    }

    alignSequenceToStructure(chainid, data, title) { let ic = this.icn3d, me = ic.icn3dui;
      let query, target, firstKey;

      if(data.data !== undefined) {
          query = data.data[0].query;
          //target = data.data[0].targets[chainid.replace(/_/g, '')];
          //target = data.data[0].targets[chainid];
          firstKey = Object.keys(data.data[0].targets)[0];
          target = data.data[0].targets[firstKey];

          target = target.hsps[0];
      }

      let text = '';

      let cssColorArray = [];
      let target2queryHash = {};
      if(query !== undefined && target !== undefined) {
          let evalue = target.scores.e_value.toPrecision(2);
          if(evalue > 1e-200) evalue = parseFloat(evalue).toExponential();

          target.scores.bit_score;

          //var targetSeq = data.targets[chainid.replace(/_/g, '')].seqdata;
          //let targetSeq = data.targets[chainid].seqdata;
          let targetSeq = data.targets[firstKey].seqdata;
          let querySeq = query.seqdata;

          let segArray = target.segs;
          for(let i = 0, il = segArray.length; i < il; ++i) {
              let seg = segArray[i];
              for(let j = 0; j <= seg.orito - seg.orifrom; ++j) {
                  target2queryHash[j + seg.orifrom] = j + seg.from;
              }
          }

          // the missing residues at the end of the seq will be filled up in the API showNewTrack()
          for(let i = 0, il = targetSeq.length; i < il; ++i) {
              if(target2queryHash.hasOwnProperty(i)) {
                  text += querySeq[target2queryHash[i]];

                  let colorHexStr = ic.showAnnoCls.getColorhexFromBlosum62(targetSeq[i], querySeq[target2queryHash[i]]);
                  cssColorArray.push("#" + colorHexStr);

                //   let resi =  ic.baseResi[chainid] + 1 + i; //i + 1;
                  let resi =  ic.ParserUtilsCls.getResi(chainid, i);
                  for(let serial in ic.residues[chainid + '_' + resi]) {
                      let color = me.parasCls.thr("#" + colorHexStr);
                      ic.atoms[serial].color = color;
                      ic.atomPrevColors[serial] = color;
                  }
              }
              else {
                  text += '-';
                  cssColorArray.push("");
              }
          }

          title += ', E: ' + evalue;
      }
      else {
          text += "cannot be aligned";
      }

      this.showNewTrack(chainid, title, text, cssColorArray, target2queryHash, 'seq');

      ic.hlUpdateCls.updateHlAll();
      ic.drawCls.draw();

      me.htmlCls.clickMenuCls.setLogCmd("add track | chainid " + chainid + " | title " + title + " | text " + this.simplifyText(text) + " | type seq", true);
    }

    defineSecondary(chainid, type) { let ic = this.icn3d, me = ic.icn3dui;
        if(!$('#' + ic.pre + 'dl_definedsets').hasClass('ui-dialog-content') || !$('#' + ic.pre + 'dl_definedsets').dialog( 'isOpen' )) {
            me.htmlCls.dialogCls.openDlg('dl_definedsets', 'Select sets');
            $("#" + ic.pre + "atomsCustom").resizable();
        }

        let selectedResidues = {};
        let bUnion = false, bUpdateHighlight = true;

        let helixCnt = 0, sheetCnt = 0;
        //var prevName = chainid + zero + index + '_L(N', currName, setName;
        let prevName = chainid + '_C(Nterm', currName, setName;

        // clear selection
        ic.hAtoms = {};

        for(let i = 0, il = ic.chainsSeq[chainid].length; i < il; ++i) {
          let currResi = ic.chainsSeq[chainid][i].resi;

          // name of secondary structures
          let residueid = chainid + '_' + currResi;

          if( ic.residues.hasOwnProperty(residueid) ) {
            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
            let currSS = ic.secondaries[residueid];

            if(currSS == 'H') {
                if(atom.ssbegin) {
                    ++helixCnt;

                    if(Object.keys(selectedResidues).length > 0) {
                        setName = currName + 'H' + helixCnt + ')';
                        if(type == 'coil') {
                            ic.selectionCls.selectResidueList(selectedResidues, setName, setName, bUnion, bUpdateHighlight);
                            if(!bUnion) bUnion = true;
                        }
                        selectedResidues = {};
                    }
                }

                //zero =(index < 10) ? '0' : '';
                //currName = chainid + zero + index + '_H' + helixCnt;
                currName = chainid + '_H' + helixCnt;
                selectedResidues[residueid] = 1;

                if(atom.ssend) {
                    //zero =(index < 9) ? '0' : '';
                    //prevName = chainid + zero +(index+1) + '_L(H' + helixCnt;
                    prevName = chainid + '_C(H' + helixCnt;
                    if(type == 'helix') {
                        ic.selectionCls.selectResidueList(selectedResidues, currName, currName, bUnion, bUpdateHighlight);
                        if(!bUnion) bUnion = true;
                    }
                    selectedResidues = {};
                }
            }
            else if(currSS == 'E') {
                if(atom.ssbegin) {
                    ++sheetCnt;

                    if(Object.keys(selectedResidues).length > 0) {
                        setName = currName + 'S' + sheetCnt + ')';
                        if(type == 'coil') {
                            ic.selectionCls.selectResidueList(selectedResidues, setName, setName, bUnion, bUpdateHighlight);
                            if(!bUnion) bUnion = true;
                        }
                        selectedResidues = {};
                    }
                }

                //zero =(index < 10) ? '0' : '';
                //currName = chainid + zero + index + '_S' + sheetCnt;
                currName = chainid + '_S' + sheetCnt;
                selectedResidues[residueid] = 1;

                if(atom.ssend) {
                    //zero =(index < 9) ? '0' : '';
                    //prevName = chainid + zero +(index+1) + '_L(S' + sheetCnt;
                    prevName = chainid + '_C(S' + sheetCnt;
                    if(type == 'sheet') {
                        ic.selectionCls.selectResidueList(selectedResidues, currName, currName, bUnion, bUpdateHighlight);
                        if(!bUnion) bUnion = true;
                    }
                    selectedResidues = {};
                }
            }
            else {
                currName = prevName + '-';
                selectedResidues[residueid] = 1;
            }
          } // end if( ic.residues.hasOwnProperty(residueid) ) {
        } // for loop

        if(Object.keys(selectedResidues).length > 0) {
            setName = currName + 'Cterm)';
            if(type == 'coil') {
                ic.selectionCls.selectResidueList(selectedResidues, setName, setName, bUnion, bUpdateHighlight);
            }
        }
    }

    simplifyText(text) { let ic = this.icn3d; ic.icn3dui;
        let out = ''; // 1-based text positions
        let bFoundText = false;

        // replace 'undefined' to space
        text = text.replace(/undefined/g, ' ');

        let i, il, prevEmptyPos = -1;
        for(i = 0, il = text.length; i < il; ++i) {
            if(text[i] == '-' || text[i] == ' ') {
                if(bFoundText && i !== prevEmptyPos) {
                    if(prevEmptyPos+1 == i-1) {
                        out +=(prevEmptyPos+1 + 1).toString() + ' ' + text.substr(prevEmptyPos+1, i - 1 - prevEmptyPos) + ', ';
                   }
                    else {
                        out +=(prevEmptyPos+1 + 1).toString() + '-' +(i-1 + 1).toString() + ' ' + text.substr(prevEmptyPos+1, i - 1 - prevEmptyPos) + ', ';
                    }
                    bFoundText = false;
                }

                prevEmptyPos = i;
            }
            else {
                bFoundText = true;
            }
        }

        if(bFoundText && i == il) {
            if(prevEmptyPos+1 == i-1) {
                out +=(prevEmptyPos+1 + 1).toString() + ' ' + text.substr(prevEmptyPos+1, i - 1 - prevEmptyPos) + ', ';
            }
            else {
                out +=(prevEmptyPos+1 + 1).toString() + '-' +(i-1 + 1).toString() + ' ' + text.substr(prevEmptyPos+1, i - 1 - prevEmptyPos) + ', ';
            }
        }

        return out;
    }

    checkGiSeq(chainid, title, text, type, color, bMsa, index) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        if(index > 20) return false;

        if(ic.giSeq !== undefined && ic.giSeq[chainid] !== undefined) {
            let result = this.getFullText(text);
            text = result.text;
            this.showNewTrack(chainid, title, text, undefined, undefined, type, color, bMsa);
            return false;
        }

        // wait for ic.giSeq to be available
        setTimeout(function(){ thisClass.checkGiSeq(chainid, title, text, type, color, bMsa, index + 1); }, 100);
    }

    getFullText(text) { let ic = this.icn3d; ic.icn3dui;
        let out = '', fromArray = [], toArray = [];

        let textArray = text.split(',');
        let lastTextPos = -1;
        for(let i = 0, il = textArray.length; i < il; ++i) {
            let eachText = textArray[i].trim();
            if(eachText.length == 0) continue;

            let range_text = eachText.split(' ');
            if(range_text.length !== 2) continue;

            let rangeText = range_text[1];
            let start_end = range_text[0].split('-');

            let start, end;
            if(start_end.length == 2) {
                start = start_end[0] - 1; // 1-based
                end = start_end[1] - 1;
            }
            else if(start_end.length == 1) {
                start = start_end[0] - 1;
                end = start;
            }
            else {
                continue;
            }

            fromArray.push(start);
            toArray.push(end);

            // previous empty text
            for(let j = 0; j < start - lastTextPos - 1; ++j) {
                out += '-';
            }

            let range = end - start + 1;

            if(rangeText.length > range) {
                 out += rangeText.substr(0, range);
            }
            else {
                 out += rangeText;
            }

            // fill up rangeText
            for(let j = 0; j < range - rangeText.length; ++j) {
                out += '-';
            }

            lastTextPos = end;
        }

        return {"text": out, "fromArray": fromArray, "toArray": toArray}
    }

    setCustomFile(type, startColor, midColor, endColor) {var ic = this.icn3d, me = ic.icn3dui;
       let thisClass = this;

       let chainid = $("#" + ic.pre + "customcolor_chainid").val();
       let file = $("#" + ic.pre + "cstcolorfile")[0].files[0];
       if(!file) {
         alert("Please select a file before clicking 'Load'");
       }
       else {
         me.utilsCls.checkFileAPI();
         let reader = new FileReader();
         reader.onload = function(e) { let ic = thisClass.icn3d;
            let dataStr = e.target.result; // or = reader.result;
            let lineArray = dataStr.split('\n');
            if(ic.queryresi2score === undefined) ic.queryresi2score = {};
            //if(ic.queryresi2score[chainid] === undefined) ic.queryresi2score[chainid] = {}
            ic.queryresi2score[chainid] = {};
            for(let i = 0, il = lineArray.length; i < il; ++i) {
                if(lineArray[i].trim() !== '') {
                    let columnArray = lineArray[i].split(/\s+/);
                    ic.queryresi2score[chainid][columnArray[0]] = columnArray[1];
                }
            }
            let resiArray = Object.keys(ic.queryresi2score[chainid]);
            let start = Math.min.apply(null, resiArray);
            let end = Math.max.apply(null, resiArray);
            let resiScoreStr = '';
            for(let resi = start; resi <= end; ++resi) {
                if(ic.queryresi2score[chainid].hasOwnProperty(resi)) {
                    resiScoreStr += Math.round(ic.queryresi2score[chainid][resi]/11); // max 9
                }
                else {
                    resiScoreStr += '_';
                }
            }

            if(type == 'color') {
                ic.opts['color'] = 'align custom';

                ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);
                ic.hlUpdateCls.updateHlAll();
                me.htmlCls.clickMenuCls.setLogCmd('color align custom | ' + chainid + ' | range ' + start + '_' + end + ' | ' + resiScoreStr + ' | colorrange ' + startColor + ' ' + midColor + ' ' + endColor, true);

                let legendHtml = me.htmlCls.clickMenuCls.setLegendHtml();

                //$("#" + me.pre + "legend").html(legendHtml);
                $("#" + me.pre + "dl_legend").html(legendHtml);
                me.htmlCls.dialogCls.openDlg('dl_legend', 'Color range');
            }
            else if(type == 'tube') {
                ic.setOptionCls.setStyle('proteins', 'custom tube');
                me.htmlCls.clickMenuCls.setLogCmd('color tube | ' + chainid + ' | range ' + start + '_' + end + ' | ' + resiScoreStr, true);
            }
            ic.drawCls.draw();
         };
         reader.readAsText(file);
       }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Annotation {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    hideAllAnno() { let ic = this.icn3d; ic.icn3dui;
        this.setAnnoSeqBase(false);
        $("[id^=" + ic.pre + "custom]").hide();
    }
    setAnnoSeqBase(bShow) {  let ic = this.icn3d; ic.icn3dui;
        //let itemArray = ['site', 'ptm', 'snp', 'clinvar', 'cdd', 'domain', 'interaction', 'ssbond', 'crosslink', 'transmem'];
        let itemArray = ['cdd', 'clinvar', 'snp', 'site', 'ptm', 'ssbond', 'crosslink', 'transmem', 'domain', 'interaction'];
        for(let i in itemArray) {
            let item = itemArray[i];
            if(bShow) {
                $("[id^=" + ic.pre + item + "]").show();
            }
            else {
                $("[id^=" + ic.pre + item + "]").hide();
            }
        }
    }
    setAnnoTabBase(bChecked) {  let ic = this.icn3d; ic.icn3dui;
        //let itemArray = ['all', 'binding', 'ptm', 'snp', 'clinvar', 'cdd', '3dd', 'interact', 'custom', 'ssbond', 'crosslink', 'transmem'];
        let itemArray = ['all', 'cdd', 'clinvar', 'snp', 'binding', 'ptm', 'ssbond', 'crosslink', 'transmem', '3dd', 'custom', 'interact'];
        for(let i in itemArray) {
            let item = itemArray[i];
            if($("#" + ic.pre + "anno_" + item).length) $("#" + ic.pre + "anno_" + item)[0].checked = bChecked;
        }
    }
    async setAnnoTabAll() {  let ic = this.icn3d; ic.icn3dui;
        this.setAnnoTabBase(true);
        this.setAnnoSeqBase(true);
        await this.updateClinvar();
        await this.updateSnp();
        this.updateDomain();
        await this.updatePTM();
        this.updateSsbond();
        this.updateCrosslink();
        await this.updateTransmem();
        this.updateInteraction();
    }
    hideAnnoTabAll() {  let ic = this.icn3d; ic.icn3dui;
        this.setAnnoTabBase(false);
        this.hideAllAnno();
    }
    async resetAnnoAll() {  let ic = this.icn3d; ic.icn3dui;
       // reset annotations
       //$("#" + ic.pre + "dl_annotations").html("");
       //ic.bAnnoShown = false;
       //ic.showAnnoCls.showAnnotations();

       $("[id^=" + ic.pre + "dt_]").html("");
       $("[id^=" + ic.pre + "tt_]").html("");
       $("[id^=" + ic.pre + "ov_]").html("");
       await ic.showAnnoCls.processSeqData(ic.chainid_seq);

       //if($("#" + ic.pre + "dt_giseq_" + chainid).css("display") != 'block') {
       //    this.setAnnoViewAndDisplay('overview');
       //}
       //else {
           this.setAnnoViewAndDisplay('detailed view');
       //}
       await this.resetAnnoTabAll();
    }

    async resetAnnoTabAll() {  let ic = this.icn3d; ic.icn3dui;
        if($("#" + ic.pre + "anno_binding").length && $("#" + ic.pre + "anno_binding")[0].checked) {
            $("[id^=" + ic.pre + "site]").show();
        }
        if($("#" + ic.pre + "anno_snp").length && $("#" + ic.pre + "anno_snp")[0].checked) {
            $("[id^=" + ic.pre + "snp]").show();
            ic.bSnpShown = false;
            await this.updateSnp();
        }
        if($("#" + ic.pre + "anno_clinvar").length && $("#" + ic.pre + "anno_clinvar")[0].checked) {
            $("[id^=" + ic.pre + "clinvar]").show();
            ic.bClinvarShown = false;
            await this.updateClinvar();
        }
        if($("#" + ic.pre + "anno_cdd").length && $("#" + ic.pre + "anno_cdd")[0].checked) {
            $("[id^=" + ic.pre + "cdd]").show();
        }
        if($("#" + ic.pre + "anno_3dd").length && $("#" + ic.pre + "anno_3dd")[0].checked) {
            $("[id^=" + ic.pre + "domain]").show();
            ic.bDomainShown = false;
            this.updateDomain();
        }
        if($("#" + ic.pre + "anno_interact").length && $("#" + ic.pre + "anno_interact")[0].checked) {
            $("[id^=" + ic.pre + "interaction]").show();
            ic.bInteractionShown = false;
            this.updateInteraction();
        }
        if($("#" + ic.pre + "anno_ptm").length && $("#" + ic.pre + "anno_ptm")[0].checked) {
            $("[id^=" + ic.pre + "ptm]").show();
            ic.bPTMShown = false;
            await this.updatePTM();
        }
        if($("#" + ic.pre + "anno_custom").length && $("#" + ic.pre + "anno_custom")[0].checked) {
            $("[id^=" + ic.pre + "custom]").show();
        }
        if($("#" + ic.pre + "anno_ssbond").length && $("#" + ic.pre + "anno_ssbond")[0].checked) {
            $("[id^=" + ic.pre + "ssbond]").show();
            ic.bSSbondShown = false;
            this.updateSsbond();
        }
        if($("#" + ic.pre + "anno_crosslink").length && $("#" + ic.pre + "anno_crosslink")[0].checked) {
            $("[id^=" + ic.pre + "crosslink]").show();
            ic.bCrosslinkShown = false;
            this.updateCrosslink();
        }
        if($("#" + ic.pre + "anno_transmem").length && $("#" + ic.pre + "anno_transmem")[0].checked) {
            $("[id^=" + ic.pre + "transmem]").show();
            ic.bTranememShown = false;
            await this.updateTransmem();
        }
    }
    setAnnoTabCustom() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "custom]").show();
        if($("#" + ic.pre + "anno_custom").length) $("#" + ic.pre + "anno_custom")[0].checked = true;
    }
    hideAnnoTabCustom() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "custom]").hide();
        if($("#" + ic.pre + "anno_custom").length) $("#" + ic.pre + "anno_custom")[0].checked = false;
    }
    async setAnnoTabClinvar() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "clinvar]").show();
        if($("#" + ic.pre + "anno_clinvar").length) $("#" + ic.pre + "anno_clinvar")[0].checked = true;
        await this.updateClinvar();
    }
    hideAnnoTabClinvar() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "clinvar]").hide();
        if($("#" + ic.pre + "anno_clinvar").length) $("#" + ic.pre + "anno_clinvar")[0].checked = false;
    }
    async setAnnoTabSnp() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "snp]").show();
        if($("#" + ic.pre + "anno_snp").length) $("#" + ic.pre + "anno_snp")[0].checked = true;
        await this.updateSnp();
    }
    hideAnnoTabSnp() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "snp]").hide();
        if($("#" + ic.pre + "anno_snp").length) $("#" + ic.pre + "anno_snp")[0].checked = false;
    }
    setAnnoTabCdd() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "cdd]").show();
        if($("#" + ic.pre + "anno_cdd").length) $("#" + ic.pre + "anno_cdd")[0].checked = true;
    }
    hideAnnoTabCdd() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "cdd]").hide();
        if($("#" + ic.pre + "anno_cdd").length) $("#" + ic.pre + "anno_cdd")[0].checked = false;
    }
    setAnnoTab3ddomain() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "domain]").show();
        if($("#" + ic.pre + "anno_3dd").length) $("#" + ic.pre + "anno_3dd")[0].checked = true;
        this.updateDomain();
    }
    hideAnnoTab3ddomain() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "domain]").hide();
        if($("#" + ic.pre + "anno_3dd").length) $("#" + ic.pre + "anno_3dd")[0].checked = false;
    }
    setAnnoTabSite() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "site]").show();
        $("[id^=" + ic.pre + "feat]").show();
        if($("#" + ic.pre + "anno_binding").length) $("#" + ic.pre + "anno_binding")[0].checked = true;
    }
    hideAnnoTabSite() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "site]").hide();
        $("[id^=" + ic.pre + "feat]").hide();
        if($("#" + ic.pre + "anno_binding").length) $("#" + ic.pre + "anno_binding")[0].checked = false;
    }
    setAnnoTabInteraction() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "interaction]").show();
        if($("#" + ic.pre + "anno_interact").length) $("#" + ic.pre + "anno_interact")[0].checked = true;
        this.updateInteraction();
    }
    hideAnnoTabInteraction() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "interaction]").hide();
        if($("#" + ic.pre + "anno_interact").length) $("#" + ic.pre + "anno_interact")[0].checked = false;
    }
    async setAnnoTabPTM() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "ptm]").show();
        if($("#" + ic.pre + "anno_ptm").length) $("#" + ic.pre + "anno_ptm")[0].checked = true;
        await this.updatePTM();
    }
    hideAnnoTabPTM() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "ptm]").hide();
        if($("#" + ic.pre + "anno_ptm").length) $("#" + ic.pre + "anno_ptm")[0].checked = false;
    }
    setAnnoTabSsbond() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "ssbond]").show();
        if($("#" + ic.pre + "anno_ssbond").length) $("#" + ic.pre + "anno_ssbond")[0].checked = true;
        this.updateSsbond();
    }
    hideAnnoTabSsbond() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "ssbond]").hide();
        if($("#" + ic.pre + "anno_ssbond").length) $("#" + ic.pre + "anno_ssbond")[0].checked = false;
    }
    setAnnoTabCrosslink() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "crosslink]").show();
        if($("#" + ic.pre + "anno_crosslink").length) $("#" + ic.pre + "anno_crosslink")[0].checked = true;
        this.updateCrosslink();
    }
    hideAnnoTabCrosslink() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "crosslink]").hide();
        if($("#" + ic.pre + "anno_crosslink").length) $("#" + ic.pre + "anno_crosslink")[0].checked = false;
    }
    async setAnnoTabTransmem() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "transmem]").show();
        if($("#" + ic.pre + "anno_transmem").length) $("#" + ic.pre + "anno_transmem")[0].checked = true;
        await this.updateTransmem();
    }
    hideAnnoTabTransmem() {  let ic = this.icn3d; ic.icn3dui;
        $("[id^=" + ic.pre + "transmem]").hide();
        if($("#" + ic.pre + "anno_transmem").length) $("#" + ic.pre + "anno_transmem")[0].checked = false;
    }
    setTabs() {  let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

    //        $("#" + ic.pre + "dl_annotations_tabs").tabs();
        $("#" + ic.pre + "dl_addtrack_tabs").tabs();
        $("#" + ic.pre + "dl_anno_view_tabs").tabs();
        //$("#" + ic.pre + "anno_all", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_all", "click", async function(e) {

        if($("#" + ic.pre + "anno_all")[0].checked) {
            await thisClass.setAnnoTabAll();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation all", true);
        }
        else {
            thisClass.hideAnnoTabAll();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation all", true);
        }
        });

        //$("#" + ic.pre + "anno_binding", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_binding", "click", function(e) {
        if($("#" + ic.pre + "anno_binding")[0].checked) {
            thisClass.setAnnoTabSite();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation site", true);
        }
        else {
            thisClass.hideAnnoTabSite();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation site", true);
        }
        });

        //$("#" + ic.pre + "anno_snp", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_snp", "click", async function(e) {
        if($("#" + ic.pre + "anno_snp")[0].checked) {
            await thisClass.setAnnoTabSnp();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation snp", true);
        }
        else {
            thisClass.hideAnnoTabSnp();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation snp", true);
        }
        });

        //$("#" + ic.pre + "anno_clinvar", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_clinvar", "click", async function(e) {
        if($("#" + ic.pre + "anno_clinvar")[0].checked) {
            await thisClass.setAnnoTabClinvar();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation clinvar", true);
        }
        else {
            thisClass.hideAnnoTabClinvar();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation clinvar", true);
        }
        });

        //$("#" + ic.pre + "anno_cdd", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_cdd", "click", function(e) {
            thisClass.clickCdd();
        });

        //$("#" + ic.pre + "anno_3dd", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_3dd", "click", function(e) {
        if($("#" + ic.pre + "anno_3dd")[0].checked) {
            thisClass.setAnnoTab3ddomain();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation 3ddomain", true);
        }
        else {
            thisClass.hideAnnoTab3ddomain();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation 3ddomain", true);
        }
        });

        //$("#" + ic.pre + "anno_interact", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_interact", "click", function(e) {
        if($("#" + ic.pre + "anno_interact")[0].checked) {
            thisClass.setAnnoTabInteraction();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation interaction", true);
        }
        else {
            thisClass.hideAnnoTabInteraction();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation interaction", true);
        }
        });

        me.myEventCls.onIds("#" + ic.pre + "anno_ptm", "click", async function(e) {
            if($("#" + ic.pre + "anno_ptm")[0].checked) {
                await thisClass.setAnnoTabPTM();
                me.htmlCls.clickMenuCls.setLogCmd("set annotation ptm", true);
            }
            else {
                thisClass.hideAnnoTabPTM();
                me.htmlCls.clickMenuCls.setLogCmd("hide annotation ptm", true);
            }
        });

        //$("#" + ic.pre + "anno_custom", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_custom", "click", function(e) {
        if($("#" + ic.pre + "anno_custom")[0].checked) {
            thisClass.setAnnoTabCustom();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation custom", true);
        }
        else {
            thisClass.hideAnnoTabCustom();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation custom", true);
        }
        });

        //$("#" + ic.pre + "anno_ssbond", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_ssbond", "click", function(e) {
        if($("#" + ic.pre + "anno_ssbond")[0].checked) {
            thisClass.setAnnoTabSsbond();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation ssbond", true);
        }
        else {
            thisClass.hideAnnoTabSsbond();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation ssbond", true);
        }
        });

        //$("#" + ic.pre + "anno_crosslink", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_crosslink", "click", function(e) {
        if($("#" + ic.pre + "anno_crosslink")[0].checked) {
            thisClass.setAnnoTabCrosslink();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation crosslink", true);
        }
        else {
            thisClass.hideAnnoTabCrosslink();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation crosslink", true);
        }
        });

        //$("#" + ic.pre + "anno_transmem", "click", function(e) {
        me.myEventCls.onIds("#" + ic.pre + "anno_transmem", "click", async function(e) {
        if($("#" + ic.pre + "anno_transmem").length && $("#" + ic.pre + "anno_transmem")[0].checked) {
            await thisClass.setAnnoTabTransmem();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation transmembrane", true);
        }
        else {
            thisClass.hideAnnoTabTransmem();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation transmembrane", true);
        }
        });
    }
    clickCdd() { let ic = this.icn3d, me = ic.icn3dui;
      if($("[id^=" + ic.pre + "cdd]").length > 0) {
        if($("#" + ic.pre + "anno_cdd")[0].checked) {
            this.setAnnoTabCdd();
            me.htmlCls.clickMenuCls.setLogCmd("set annotation cdd", true);
        }
        else {
            this.hideAnnoTabCdd();
            me.htmlCls.clickMenuCls.setLogCmd("hide annotation cdd", true);
        }
      }
    }

    showAnnoSelectedChains() {   let ic = this.icn3d, me = ic.icn3dui;
        // show selected chains in annotation window
        let chainHash = {};
        for(let i in ic.hAtoms) {
            let atom = ic.atoms[i];
            let chainid = atom.structure + '_' + atom.chain;
            chainHash[chainid] = 1;
        }
        $("#" + ic.pre + "dl_annotations > .icn3d-annotation").hide();
        for(let chainid in chainHash) {
            if($("#" + ic.pre + "anno_" + chainid).length) {
                $("#" + ic.pre + "anno_" + chainid).show();
            }
            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chainid]);
            if(atom.resn !== undefined) {
                let oneLetterRes = me.utilsCls.residueName2Abbr(atom.resn.substr(0, 3));
                $("#" + ic.pre + "anno_" + oneLetterRes).show();
            }
        }
    }
    showAnnoAllChains() {   let ic = this.icn3d; ic.icn3dui;
        $("#" + ic.pre + "dl_annotations > .icn3d-annotation").show();
    }
    setAnnoView(view) { let ic = this.icn3d; ic.icn3dui;
        if(view === 'detailed view') {
            ic.view = 'detailed view';
            $( "#" + ic.pre + "dl_anno_view_tabs" ).tabs( "option", "active", 1 );
        }
        else { // overview
            ic.view = 'overview';
            $( "#" + ic.pre + "dl_anno_view_tabs" ).tabs( "option", "active", 0 );
        }
    }
    setAnnoDisplay(display, prefix) { let ic = this.icn3d; ic.icn3dui;
        let itemArray = ['giseq', 'custom', 'site', 'ptm', 'snp', 'clinvar', 'cdd', 'domain', 'interaction', 'ssbond', 'crosslink', 'transmem'];
        for(let i in itemArray) {
            let item = itemArray[i];
            $("[id^=" + ic.pre + prefix + "_" + item + "]").attr('style', display);
        }
    }
    showFixedTitle() { let ic = this.icn3d; ic.icn3dui;
            let style = 'display:block;';
            this.setAnnoDisplay(style, 'tt');
    }
    hideFixedTitle() { let ic = this.icn3d; ic.icn3dui;
            let style = 'display:none!important;';
            this.setAnnoDisplay(style, 'tt');
    }
    setAnnoViewAndDisplay(view) { let ic = this.icn3d; ic.icn3dui;
        if(view === 'detailed view') {
            this.setAnnoView('detailed view');
            let style = 'display:block;';
            this.setAnnoDisplay(style, 'dt');
            $("#" + ic.pre + "seqguide_wrapper").attr('style', style);
            style = 'display:none;';
            this.setAnnoDisplay(style, 'ov');
        }
        else { // overview
            this.setAnnoView('overview');
            this.hideFixedTitle();
            let style = 'display:none;';
            this.setAnnoDisplay(style, 'dt');
            $("#" + ic.pre + "seqguide_wrapper").attr('style', style);
            style = 'display:block;';
            this.setAnnoDisplay(style, 'ov');
        }
    }

    // by default, showSeq and showCddSite are called at showAnnotations
    // the following will be called only when the annotation is selected: showSnpClinvar, showDomain, showInteraction
    // showSnpClinvar and showDomain will loop through ic.protein_chainid
    // showInteraction will loop through ic.interactChainbase
    async updateClinvar() { let ic = this.icn3d; ic.icn3dui;
        if(ic.bClinvarShown === undefined || !ic.bClinvarShown) {
            for(let chainid in ic.protein_chainid) {
                let chainidBase = ic.protein_chainid[chainid];
                await ic.annoSnpClinVarCls.showClinvar(chainid, chainidBase);
            }
        }
        ic.bClinvarShown = true;
    }
    async updateSnp() { let ic = this.icn3d; ic.icn3dui;
        if(ic.bSnpShown === undefined || !ic.bSnpShown) {
            for(let chainid in ic.protein_chainid) {
                let chainidBase = ic.protein_chainid[chainid];
                await ic.annoSnpClinVarCls.showSnp(chainid, chainidBase);
            }
        }
        ic.bSnpShown = true;
    }
    updateDomain() { let ic = this.icn3d; ic.icn3dui;
        if(ic.bDomainShown === undefined || !ic.bDomainShown) {
            ic.annoDomainCls.showDomainAll();
        }
        ic.bDomainShown = true;
    }
    updateInteraction() { let ic = this.icn3d; ic.icn3dui;
        if(ic.bInteractionShown === undefined || !ic.bInteractionShown) {
            for(let chainid in ic.interactChainbase) {
                let chainidBase = ic.interactChainbase[chainid];
                ic.annoContactCls.showInteraction(chainid, chainidBase);
            }
        }
        ic.bInteractionShown = true;
    }
    async updatePTM() { let ic = this.icn3d; ic.icn3dui;
        if(ic.bPTMShown === undefined || !ic.bPTMShown) {
            for(let chainid in ic.PTMChainbase) {
                let chainidBase = ic.PTMChainbase[chainid];
                await ic.annoPTMCls.showPTM(chainid, chainidBase, 'ptm');
            }
        }
        ic.bPTMShown = true;
    }
    updateSsbond() { let ic = this.icn3d; ic.icn3dui;
        if(ic.bSSbondShown === undefined || !ic.bSSbondShown) {
            for(let chainid in ic.ssbondChainbase) {
                let chainidBase = ic.ssbondChainbase[chainid];
                ic.annoSsbondCls.showSsbond(chainid, chainidBase);
            }
        }
        ic.bSSbondShown = true;
    }
    updateCrosslink() { let ic = this.icn3d; ic.icn3dui;
        if(ic.bCrosslinkShown === undefined || !ic.bCrosslinkShown) {
            for(let chainid in ic.crosslinkChainbase) {
                let chainidBase = ic.crosslinkChainbase[chainid];
                ic.annoCrossLinkCls.showCrosslink(chainid, chainidBase);
            }
        }
        ic.bCrosslinkShown = true;
    }

    async updateTransmem() { let ic = this.icn3d, me = ic.icn3dui;
        if(ic.bTranememShown === undefined || !ic.bTranememShown) {
            for(let chainid in ic.protein_chainid) {
                let chainidBase = ic.protein_chainid[chainid];
                if(me.cfg.opmid !== undefined) {
                    ic.annoTransMemCls.showTransmem(chainid, chainidBase);
                }
                else if(ic.bAfMem && ic.afmem_start_end) {
                    let begin = ic.afmem_start_end[0];
                    let end = ic.afmem_start_end[1];
                    await ic.annoPTMCls.showPTM(chainid, chainidBase, 'afmem', begin, end);
                }
                else {
                    await ic.annoPTMCls.showPTM(chainid, chainidBase, 'transmem');
                }
            }
        }
        ic.bTranememShown = true;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ShowAnno {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //show annotations such as SNPs, ClinVar, domains, binding sites, etc.
    showAnnotations_part1() { let ic = this.icn3d, me = ic.icn3dui;
        me.htmlCls.dialogCls.openDlg('dl_selectannotations', 'Sequences and Annotations');
        // add note about assembly
        if((ic.bAssemblyNote === undefined || !ic.bAssemblyNote) && ic.asuCnt !== undefined ) {
            let html = "     <br><div id='" + ic.pre + "assembly_note' style='margin-left:5px;'><span class='icn3d-annoLargeTitle'>Assembly Tips:</span> Only the asymmetric unit is shown in the sequence window.<br>Click \"Assembly\" in the menu \"View\" to switch between asymmetric unit and biological assembly(<b>" + ic.asuCnt + "</b> asymmetric unit).</div>";
            $("#" + ic.pre + "dl_annotations_tabs").append(html);
            ic.bAssemblyNote = true;
        }

        if(ic.bResetAnno) {
            //reset Anno when loading another structure
            ic.giSeq = {};
            ic.currClin = {};
            ic.resi2disease_nonempty = {};
            ic.baseResi = {};
            ic.matchedPos = {};

            $("#" + me.pre + "dl_annotations").empty();
            //ic.annotationCls.setAnnoViewAndDisplay('overview');
            ic.annotationCls.setAnnoView('overview');
        }

        let nucleotide_chainid = {}, chemical_chainid = {}, chemical_set = {};
        //ic.protein_chainid = {};

        if(ic.bAnnoShown === undefined || !ic.bAnnoShown || ic.bResetAnno) { // ic.bResetAnno when loading another structure
            ic.protein_chainid = {};

            let chainArray = Object.keys(ic.chains);

            if(ic.giSeq === undefined) ic.giSeq = {};
            if(ic.currClin === undefined) ic.currClin = {};
            if(ic.resi2disease_nonempty === undefined) ic.resi2disease_nonempty = {};
            if(ic.baseResi === undefined) ic.baseResi = {};
            if(ic.matchedPos === undefined) ic.matchedPos = {};
            let dialogWidth;
            if(me.bNode) { // no $().dialog
                dialogWidth = 500;
            }
            else {
                dialogWidth =(me.cfg.notebook) ? me.htmlCls.WIDTH / 2 : $("#" + ic.pre + "dl_selectannotations").dialog( "option", "width" );
            }
            ic.seqAnnWidth = dialogWidth - 120 - 30*2 - 50; // title: 120px, start and end resi: 30px, extra space on the left and right: 50px
            
            for(let i = 0, il = chainArray.length; i < il; ++i) {
                Math.round(chainArray[i].indexOf('_'));
                //if(pos > 4) continue; // NMR structures with structure id such as 2K042,2K043, ...
                let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chainArray[i]]);
                if(atom === undefined) atom = ic.firstAtomObjCls.getFirstAtomObj(ic.chains[chainArray[i]]);
                if(atom === undefined) continue;

                // only single letter chain has accession such as 1P9M_A
                let chainLetter = chainArray[i].substr(chainArray[i].indexOf('_') + 1);
                let chainidBase;
                if(chainLetter.indexOf('_') !== -1) { // NCBI modified chainid, e.g., A_1
                    chainLetter = chainLetter.substr(0, chainLetter.indexOf('_'));
                    chainidBase = chainArray[i].substr(0, chainArray[i].indexOf('_')) + '_' + chainLetter;
                }
                else {
                    chainidBase = chainArray[i];
                }
                //if(me.cfg.mmdbid !== undefined) { // protein and chemicals/ions are in different chains

                if(ic.proteins.hasOwnProperty(atom.serial) && ic.chainsSeq[chainArray[i]].length > 1) {
                    ic.protein_chainid[chainArray[i]] = chainidBase;
                }
                else if(ic.nucleotides.hasOwnProperty(atom.serial) && ic.chainsSeq[chainArray[i]].length > 1) {
                    nucleotide_chainid[chainArray[i]] = chainidBase;
                }
                else {
                    if(ic.chainsSeq[chainArray[i]].length > 1) {
                        chemical_chainid[chainArray[i]] = chainidBase;
                    }
                    else {
                        let name = ic.chainsSeq[chainArray[i]][0].name;
                        let resid = chainArray[i] + '_' + ic.chainsSeq[chainArray[i]][0].resi;
                        if(chemical_set[name] === undefined) chemical_set[name] = [];
                        chemical_set[name].push(resid);
                    }
                }
                //}
                // protein and nucleotide chain may have chemicals/ions attached at the end
                if((me.cfg.pdbid !== undefined || me.cfg.opmid !== undefined || me.cfg.mmcifid !== undefined || me.cfg.mmtfid !== undefined)
                  &&(ic.proteins.hasOwnProperty(atom.serial) || ic.nucleotides.hasOwnProperty(atom.serial)) ) {
                    for(let r = 0, rl = ic.chainsSeq[chainArray[i]].length; r < rl; ++r) {
                        let resObj = ic.chainsSeq[chainArray[i]][r];
                        if(resObj.name !== '' && resObj.name !== '-' && resObj.name == resObj.name.toUpperCase()) {
                            let resid = chainArray[i] + '_' + resObj.resi;
                            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[resid]);
                            if(atom === undefined) atom = ic.firstAtomObjCls.getFirstAtomObj(ic.chains[chainArray[i]]);
                            if(ic.proteins.hasOwnProperty(atom.serial) || ic.nucleotides.hasOwnProperty(atom.serial)) {
                                continue;
                            }
                            else {
                                let name = resObj.name.trim();
                                if(chemical_set[name] === undefined) chemical_set[name] = [];
                                chemical_set[name].push(resid);
                            }
                        } // if(resObj.name !== ''
                    } // for(let r = 0
                } // if(me.cfg.mmdbid
            } // for(let i = 0

            ic.maxAnnoLength = 1;
            for(let chainid in ic.chainsSeq) {
                // use protein or nucleotide as the max length
                if(ic.chainsSeq[chainid].length > ic.maxAnnoLength && (ic.protein_chainid.hasOwnProperty(chainid) || nucleotide_chainid.hasOwnProperty(chainid)) ) {
                    ic.maxAnnoLength = ic.chainsSeq[chainid].length;
                }
            }
        }

        return {'nucleotide_chainid': nucleotide_chainid, 'chemical_chainid': chemical_chainid, 'chemical_set': chemical_set};
    }

    async showAnnotations() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        let result = this.showAnnotations_part1();

        let nucleotide_chainid = result.nucleotide_chainid;
        let chemical_chainid = result.chemical_chainid;
        let chemical_set = result.chemical_set;
            
        if(ic.bAnnoShown === undefined || !ic.bAnnoShown || ic.bResetAnno) { // ic.bResetAnno when loading another structure
            if(me.cfg.blast_rep_id === undefined) {
               if(ic.bFullUi) {
                    if(me.cfg.mmtfid !== undefined) { // mmtf data do NOT have the missing residues
                        //let id = chainArray[0].substr(0, chainArray[0].indexOf('_'));
                        let id = Object.keys(ic.structures)[0];

                        await ic.mmcifParserCls.downloadMmcifSymmetry(id, 'mmtfid');
                    }
                    
                    await this.showAnnoSeqData(nucleotide_chainid, chemical_chainid, chemical_set);
               }
            }
            else if(me.cfg.blast_rep_id !== undefined && !ic.bSmithwm && !ic.bLocalSmithwm) { // align sequence to structure
                let url = me.htmlCls.baseUrl + 'pwaln/pwaln.fcgi?from=querytarget';
                let dataObj = {'targets': me.cfg.blast_rep_id, 'queries': me.cfg.query_id};
                if(me.cfg.query_from_to !== undefined ) {
                    // convert from 1-based to 0-based
                    let query_from_to_array = me.cfg.query_from_to.split(':');
                    for(let i = 0, il = query_from_to_array.length; i < il; ++i) {
                        query_from_to_array[i] = parseInt(query_from_to_array[i]) - 1;
                    }
                    dataObj['queries'] = me.cfg.query_id + ':' + query_from_to_array.join(':');
                }
                if(me.cfg.target_from_to !== undefined) {
                    // convert from 1-based to 0-based
                    let target_from_to_array = me.cfg.target_from_to.split(':');
                    for(let i = 0, il = target_from_to_array.length; i < il; ++i) {
                        target_from_to_array[i] = parseInt(target_from_to_array[i]) - 1;
                    }
                    dataObj['targets'] = me.cfg.blast_rep_id + ':' + target_from_to_array.join(':');
                }

                let data = await me.getAjaxPostPromise(url, dataObj);

                ic.seqStructAlignData = data;
                await thisClass.showAnnoSeqData(nucleotide_chainid, chemical_chainid, chemical_set);
            } // align seq to structure
            else if(me.cfg.blast_rep_id !== undefined && (ic.bSmithwm || ic.bLocalSmithwm)) { // align sequence to structure
                //{'targets': me.cfg.blast_rep_id, 'queries': me.cfg.query_id}
                let idArray = [me.cfg.blast_rep_id];

                let target, query;
                if(me.cfg.query_id.indexOf('>') != -1) { //FASTA with header
                    query = me.cfg.query_id.substr(me.cfg.query_id.indexOf('\n') + 1);
                }
                else if(!(/\d/.test(me.cfg.query_id)) || me.cfg.query_id.length > 50) { //FASTA
                    query = me.cfg.query_id;
                }
                else { // accession
                    idArray.push(me.cfg.query_id);
                }

                // show the sequence and 3D structure
                //var url = "https://eme.utilsCls.ncbi.nlm.nih.gov/entrez/eUtilsCls/efetch.fcgi?db=protein&retmode=json&rettype=fasta&id=" + chnidBaseArray;
                let url = me.htmlCls.baseUrl + "/vastdyn/vastdyn.cgi?chainlist=" + idArray;
                let chainid_seq = await me.getAjaxPromise(url, 'jsonp', false, "Can not retrieve the sequence of the accession(s) " + idArray.join(", "));

                let index = 0;
                for(let acc in chainid_seq) {
                    if(index == 0) {
                        target = chainid_seq[acc];
                    }
                    else if(!query) {
                        query = chainid_seq[acc];
                    }

                    ++index;
                }

                let match_score = 1, mismatch = -1, gap = -1, extension = -1;

                let bLocal = (ic.bLocalSmithwm) ? true : false;
                ic.seqStructAlignDataLocalSmithwm = ic.alignSWCls.alignSW(target, query, match_score, mismatch, gap, extension, bLocal);

                await thisClass.showAnnoSeqData(nucleotide_chainid, chemical_chainid, chemical_set);
             } // align seq to structure
        }
        ic.bAnnoShown = true;
    }

    async showAnnoSeqData(nucleotide_chainid, chemical_chainid, chemical_set) { let ic = this.icn3d, me = ic.icn3dui;
        if(!me.bNode) await this.getAnnotationData();

        let i = 0;
        for(let chain in nucleotide_chainid) {
            this.getSequenceData(chain, nucleotide_chainid[chain], 'nucleotide', i);
            ++i;
        }
        ic.interactChainbase = me.hashUtilsCls.unionHash(ic.interactChainbase, ic.protein_chainid);
        ic.interactChainbase = me.hashUtilsCls.unionHash(ic.interactChainbase, nucleotide_chainid);
        i = 0;
        for(let chain in chemical_chainid) {
            this.getSequenceData(chain, chemical_chainid[chain], 'chemical', i);
            ++i;
        }
        ic.interactChainbase = me.hashUtilsCls.unionHash(ic.interactChainbase, chemical_chainid);
        ic.PTMChainbase = me.hashUtilsCls.unionHash(ic.PTMChainbase, ic.protein_chainid);

        ic.ssbondChainbase = me.hashUtilsCls.unionHash(ic.ssbondChainbase, ic.protein_chainid);
        ic.ssbondChainbase = me.hashUtilsCls.unionHash(ic.ssbondChainbase, chemical_chainid);
        ic.crosslinkChainbase = me.hashUtilsCls.unionHash(ic.crosslinkChainbase, ic.protein_chainid);
        ic.crosslinkChainbase = me.hashUtilsCls.unionHash(ic.crosslinkChainbase, nucleotide_chainid);
        ic.crosslinkChainbase = me.hashUtilsCls.unionHash(ic.crosslinkChainbase, chemical_chainid);
        for(let name in chemical_set) {
            this.getCombinedSequenceData(name, chemical_set[name], i);
            ++i;
        }

        if(!me.bNode) {
            this.enableHlSeq();
            ic.annotationCls.hideAllAnno();

            // setTimeout(function(){
            //     ic.annotationCls.clickCdd();
            // }, 0);

            ic.annotationCls.clickCdd();
        }
    }

    async getAnnotationData() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        let chnidBaseArray = $.map(ic.protein_chainid, function(v) { return v; });
        let index = 0;
        for(let chnid in ic.protein_chainid) {
            let buttonStyle = me.utilsCls.isMobile() ? 'none' : 'button';
            let fullProteinName = ic.showSeqCls.getProteinName(chnid);
            let proteinName = fullProteinName;
            //if(proteinName.length > 40) proteinName = proteinName.substr(0, 40) + "...";
            let categoryStr =(index == 0) ? "<span class='icn3d-annoLargeTitle'><b>Proteins</b>: </span><br><br>" : "";
            let geneLink =(ic.chainsGene[chnid] && ic.chainsGene[chnid].geneId) ? "(Gene: <a href='https://www.ncbi.nlm.nih.gov/gene/" + ic.chainsGene[chnid].geneId + "' target='_blank' title='" + ic.chainsGene[chnid].geneDesc + "'>" + ic.chainsGene[chnid].geneSymbol + "</a>)" : '';
            let structure = chnid.substr(0, chnid.indexOf('_'));
            let chainLink = (structure.length > 5) ? '<a href="https://alphafold.ebi.ac.uk/entry/' + structure + '" target="_blank">' + chnid + '</a>' : chnid;
            let chainHtml = "<div id='" + ic.pre + "anno_" + chnid + "' class='icn3d-annotation'>" + categoryStr
                + "<span style='font-weight:bold;'>Annotations of " + chainLink
                + "</span>: <a class='icn3d-blue' href='https://www.ncbi.nlm.nih.gov/protein?term="
                + chnid + "' target='_blank' title='" + fullProteinName + "'>" + proteinName + "</a>"
                + geneLink + "&nbsp;&nbsp;&nbsp;"
                + this.addButton(chnid, "icn3d-addtrack", "Add Track", "Add a custom track", 60, buttonStyle)
                + "&nbsp;&nbsp;&nbsp;";
            //if(me.cfg.blast_rep_id !== undefined && me.cfg.blast_rep_id == chnid) {
                chainHtml += this.addButton(chnid, "icn3d-customcolor", "Custom Color/Tube", "Use a custom file to define the colors or tubes in 3D structure", 110, buttonStyle) + "&nbsp;&nbsp;&nbsp;";
            //}
                chainHtml += this.addButton(chnid, "icn3d-helixsets", "Helix Sets", "Define sets for each helix in this chain and add them to the menu of \"Defined Sets\"", 60, buttonStyle) + "&nbsp;"
                + this.addButton(chnid, "icn3d-sheetsets", "Sheet Sets", "Define sets for each sheet in this chain and add them to the menu of \"Defined Sets\"", 60, buttonStyle) + "&nbsp;"
                + this.addButton(chnid, "icn3d-coilsets", "Coil Sets", "Define sets for each coil in this chain and add them to the menu of \"Defined Sets\"", 60, buttonStyle);
            $("#" + ic.pre + "dl_annotations").append(chainHtml);
            //let itemArray = ['giseq', 'cdd', 'clinvar', 'snp', 'domain', 'site', 'ptm', 'interaction', 'custom', 'ssbond', 'crosslink', 'transmem'];
            let itemArray = ['giseq', 'cdd', 'clinvar', 'snp', 'site', 'ptm', 'ssbond', 'crosslink', 'transmem', 'domain', 'custom', 'interaction'];
            // dt: detailed view, hide by default; ov: overview, show by default
            for(let i in itemArray) {
                let item = itemArray[i];
                $("#" + ic.pre + "anno_" + chnid).append(this.getAnDiv(chnid, item));
            }
            $("#" + ic.pre + "anno_" + chnid).append("<br><hr><br>");
            ++index;
        }
        
        if(!me.bNode) ic.annoCddSiteCls.setToolTip();

        // show the sequence and 3D structure
        //var url = "https://eme.utilsCls.ncbi.nlm.nih.gov/entrez/eUtilsCls/efetch.fcgi?db=protein&retmode=json&rettype=fasta&id=" + chnidBaseArray;
        let url = me.htmlCls.baseUrl + "/vastdyn/vastdyn.cgi?chainlist=" + chnidBaseArray;

        if(ic.chainid_seq !== undefined) {     
            await this.processSeqData(ic.chainid_seq);
        }
        else {       
            try {
                let data = await me.getAjaxPromise(url, 'jsonp');

                ic.chainid_seq = data;
                await thisClass.processSeqData(ic.chainid_seq);
            }
            catch(err) {
                thisClass.enableHlSeq();
                if(!me.bNode) console.log( "No data were found for the protein " + chnidBaseArray + "..." );
                for(let chnid in ic.protein_chainid) {
                    let chnidBase = ic.protein_chainid[chnid];
                    ic.showSeqCls.setAlternativeSeq(chnid, chnidBase);
                    ic.showSeqCls.showSeq(chnid, chnidBase);
                }
                // get CDD/Binding sites
                await ic.annoCddSiteCls.showCddSiteAll();
                return;
            }
        }
    }

    getSequenceData(chnid, chnidBase, type, index) { let ic = this.icn3d; ic.icn3dui;
        let fullProteinName = ic.showSeqCls.getProteinName(chnid);
        let proteinName = fullProteinName;
        if(proteinName.length > 40) proteinName = proteinName.substr(0, 40) + "...";
        let categoryStr = "";
        if(index == 0) {
            if(type == 'protein') {
                categoryStr = "<span class='icn3d-annoLargeTitle'><b>Proteins</b>: </span><br><br>";
            }
            else if(type == 'nucleotide') {
                categoryStr = "<span class='icn3d-annoLargeTitle'><b>Nucleotides</b>: </span><br><br>";
            }
            else if(type == 'chemical') {
                categoryStr = "<span class='icn3d-annoLargeTitle'><b>Chemicals/Ions/Water</b>: </span><br><br>";
            }
        }
        $("#" + ic.pre + "dl_annotations").append("<div id='" + ic.pre + "anno_" + chnid + "' class='icn3d-annotation'>" + categoryStr + "<b>" + chnid + "</b>: " + "<span title='" + fullProteinName + "'>" + proteinName + "</span> </div>");
        // dt: detailed view, hide by default; ov: overview, show by default
        $("#" + ic.pre + "anno_" + chnid).append(this.getAnDiv(chnid, 'giseq'));
        //$("#" + ic.pre + "anno_" + chnid).append(this.getAnDiv(chnid, 'custom'));
        $("#" + ic.pre + "anno_" + chnid).append(this.getAnDiv(chnid, 'interaction'));
        $("#" + ic.pre + "anno_" + chnid).append("<br><hr><br>");
        // show the sequence and 3D structure
        ic.giSeq[chnid] = [];

        for(let i = 0; i < ic.chainsSeq[chnid].length; ++i) {
            let res = ic.chainsSeq[chnid][i].name;
            //ic.giSeq[chnid][i] =(res.length > 1) ? res.substr(0, 1) : res;
            ic.giSeq[chnid][i] = res;
        }
        ic.matchedPos[chnid] = 0;
        ic.baseResi[chnid] = ic.chainsSeq[chnid][0].resi - ic.matchedPos[chnid] - 1;
        ic.showSeqCls.showSeq(chnid, chnidBase, type);
        //ic.annoContactCls.showInteraction(chnid, chnidBase);
    }
    getCombinedSequenceData(name, residArray, index) { let ic = this.icn3d, me = ic.icn3dui;
        let categoryStr =(index == 0) ? "<span class='icn3d-annoLargeTitle'><b>Chemicals/Ions/Water</b>: </span><br><br>" : "";
        let chemName;
        let pos = residArray[0].lastIndexOf('_');
        let firstChainid = residArray[0].substr(0, pos);
        let sid =(me.cfg.mmdbid !== undefined && ic.chainid2sid !== undefined) ? ic.chainid2sid[firstChainid] : undefined;
        if(sid !== undefined) {
            chemName = "<b>" + name + " <a class='icn3d-blue' href='https://pubchem.ncbi.nlm.nih.gov/substance/" + sid + "#section=2D-Structure' target='_blank'><img src='https://pubchem.ncbi.nlm.nih.gov/image/imgsrv.fcgi?sid=" + sid + "'></a></b>";
        }
        else {
            chemName = "<b>" + name + "</b>";
        }
        $("#" + ic.pre + "dl_annotations").append("<div id='" + ic.pre + "anno_" + name + "' class='icn3d-annotation'>" + categoryStr + chemName + "</div>");
        // dt: detailed view, hide by default; ov: overview, show by default
        $("#" + ic.pre + "anno_" + name).append("<div id='" + ic.pre + "giseq_" + name + "'><div id='" + ic.pre + "dt_giseq_" + name + "' style='display:none'></div><div id='" + ic.pre + "ov_giseq_" + name + "'></div></div>");
        $("#" + ic.pre + "anno_" + name).append("<br><hr><br>");
        // sequence, detailed view
        let htmlTmp = '<div id="' + ic.pre + 'giseq_sequence" class="icn3d-dl_sequence">';
        let chainType = 'Chem.', chainTypeFull = 'Chemical';
        //htmlTmp += '<div class="icn3d-seqTitle2" anno="sequence"><span style="white-space:nowrap;" title="' + chainTypeFull + ' ' + name + '">' + chainType + ' ' + name + '</span></div>';
        htmlTmp += '<div class="icn3d-seqTitle icn3d-link icn3d-blue" anno="sequence" gi="' + name + '" resn="' + name + '"><span style="white-space:nowrap;" title="' + chainTypeFull + ' ' + name + '">' + chainType + ' ' + name + '</span></div>';
        htmlTmp += '<span class="icn3d-residueNum" style="width:60px!important;" title="starting protein sequence number">Count: ' + residArray.length + '</span>';
        htmlTmp += '<span class="icn3d-seqLine">';
        // sequence, overview
        let html = htmlTmp;
        let html2 = htmlTmp;
        for(let i = 0, il = residArray.length; i < il; ++i) {
          let cFull = name;
          let c = cFull;
          if(cFull.length > 3) {
              c = cFull.substr(0,3);
          }
          if(i < residArray.length - 1) c = c + ',';
          let resid = residArray[i];
          let resi = resid.substr(resid.lastIndexOf('_') + 1);
          html += '<span id="giseq_' + ic.pre + resid + '" title="' + cFull + resi + '" class="icn3d-residue icn3d-chemical">' + c + '</span>';
        }
        let color = me.htmlCls.GREY8;
        //html2 += '<div class="icn3d-seqTitle" style="display:inline-block; color:white; font-weight:bold; background-color:' + color + '; width:' + Math.round(ic.seqAnnWidth * residArray.length / ic.maxAnnoLength) + 'px;">' + name + '</div>';
        let width = Math.round(ic.seqAnnWidth * residArray.length / ic.maxAnnoLength);
        if(width < 1) width = 1;
        html2 += '<div class="icn3d-seqTitle" style="display:inline-block; color:white; font-weight:bold; background-color:' + color + '; width:' + width + 'px;">&nbsp;</div>';
        //htmlTmp = '<span class="icn3d-residueNum" title="ending protein sequence number">' + residArray.length + '</span>';
        //htmlTmp += '</span>';
        htmlTmp = '</span>';
        htmlTmp += '<br>';
        htmlTmp += '</div>';
        html += htmlTmp;
        html2 += htmlTmp;
        $("#" + ic.pre + 'dt_giseq_' + name).html(html);
        $("#" + ic.pre + 'ov_giseq_' + name).html(html2);
    }

    async processSeqData(chainid_seq) { let ic = this.icn3d, me = ic.icn3dui;
        for(let chnid in ic.protein_chainid) {
            let chnidBase = ic.protein_chainid[chnid];
            //if(chainid_seq.hasOwnProperty(chnid)) {
            //    let allSeq = chainid_seq[chnid];

            if(chainid_seq.hasOwnProperty(chnidBase)) {
                let allSeq = chainid_seq[chnidBase];
                ic.giSeq[chnid] = allSeq;
                // the first 10 residues from sequences with structure
                let startResStr = '';
                for(let i = 0; i < 10 && i < ic.chainsSeq[chnid].length; ++i) {
                    startResStr += ic.chainsSeq[chnid][i].name.substr(0, 1);
                }
                let pos = allSeq.toLowerCase().indexOf(startResStr.toLowerCase());
                if(pos == -1) {
                    console.log("The gi sequence didn't match the protein sequence. The start of 3D protein sequence: " + startResStr + ". The gi sequence: " + allSeq.substr(0, 10) + ".");
                    ic.showSeqCls.setAlternativeSeq(chnid, chnidBase);
                }
                else {
                    ic.matchedPos[chnid] = pos;
                    ic.baseResi[chnid] = ic.chainsSeq[chnid][0].resi - ic.matchedPos[chnid] - 1;
                }
            }
            else {
                if(!me.bNode) console.log( "No data were found for the chain " + chnid + "..." );
                ic.showSeqCls.setAlternativeSeq(chnid, chnidBase);
            }
            
            if(me.cfg.blast_rep_id != chnid) {
                ic.showSeqCls.showSeq(chnid, chnidBase);
            }
            else if(me.cfg.blast_rep_id == chnid && ic.seqStructAlignData === undefined && ic.seqStructAlignDataSmithwm === undefined) {
              let title;
              if(me.cfg.query_id.length > 14) {
                  title = 'Query: ' + me.cfg.query_id.substr(0, 6) + '...';
              }
              else {
                  title =(isNaN(me.cfg.query_id)) ? 'Query: ' + me.cfg.query_id : 'Query: gi ' + me.cfg.query_id;
              }
              let compTitle = undefined;
              let compText = undefined;
              let text = "cannot be aligned";
              ic.queryStart = '';
              ic.queryEnd = '';
              if(ic.bRender) alert('The sequence can NOT be aligned to the structure');
              ic.showSeqCls.showSeq(chnid, chnidBase, undefined, title, compTitle, text, compText);
            }
            else if(me.cfg.blast_rep_id == chnid && (ic.seqStructAlignData !== undefined || ic.seqStructAlignDataSmithwm !== undefined) ) { // align sequence to structure
              //var title = 'Query: ' + me.cfg.query_id.substr(0, 6);
              let title;
              if(me.cfg.query_id.length > 14) {
                  title = 'Query: ' + me.cfg.query_id.substr(0, 6) + '...';
              }
              else {
                  title =(isNaN(me.cfg.query_id)) ? 'Query: ' + me.cfg.query_id : 'Query: gi ' + me.cfg.query_id;
              }

              
              let evalue, targetSeq, querySeq, segArray;

              if(ic.seqStructAlignData !== undefined) {
                let query, target;
                let data = ic.seqStructAlignData;
                if(data.data !== undefined) {
                    query = data.data[0].query;
                    //target = data.data[0].targets[chnid.replace(/_/g, '')];
                    target = data.data[0].targets[chnid];
                    target =(target !== undefined && target.hsps.length > 0) ? target.hsps[0] : undefined;
                }

                if(query !== undefined && target !== undefined) {
                    evalue = target.scores.e_value.toPrecision(2);
                    if(evalue > 1e-200) evalue = parseFloat(evalue).toExponential();
                    target.scores.bit_score;
                    //var targetSeq = data.targets[chnid.replace(/_/g, '')].seqdata;
                    targetSeq = data.targets[chnid].seqdata;
                    querySeq = query.seqdata;
                    segArray = target.segs;
                }               
              }
              else { // mimic the output of the cgi pwaln.fcgi
                let data = ic.seqStructAlignDataSmithwm;
                evalue = data.score;
                targetSeq = data.target.replace(/-/g, '');
                querySeq = data.query.replace(/-/g, '');
                segArray = [];
                // target, 0-based: orifrom, orito
                // query, 0-based: from, to

                let targetCnt = -1, queryCnt = -1;
                let bAlign = false, seg = {};
                for(let i = 0, il = data.target.length; i < il; ++i) {
                    if(data.target[i] != '-')  ++targetCnt;
                    if(data.query[i] != '-')  ++queryCnt;
                    if(!bAlign && data.target[i] != '-' && data.query[i] != '-') {
                        bAlign = true;
                        seg.orifrom = targetCnt;
                        seg.from = queryCnt;
                    }
                    else if(bAlign && (data.target[i] == '-' || data.query[i] == '-') ) {
                        bAlign = false;
                        seg.orito = (data.target[i] == '-') ? targetCnt : targetCnt - 1;
                        seg.to = (data.query[i] == '-') ? queryCnt : queryCnt - 1;
                        segArray.push(seg);
                        seg = {};
                    }
                }

                // end condition
                if(data.target[data.target.length - 1] != '-' && data.query[data.target.length - 1] != '-') {
                    seg.orito = targetCnt;
                    seg.to = queryCnt;

                    segArray.push(seg);
                }
              }

              let text = '', compText = '';
              ic.queryStart = '';
              ic.queryEnd = '';
              
              if(segArray !== undefined) {
                  let target2queryHash = {};
                  if(ic.targetGapHash === undefined) ic.targetGapHash = {};
                  ic.fullpos2ConsTargetpos = {};
                  ic.consrvResPosArray = [];
                  let prevTargetTo = 0, prevQueryTo = 0;
                  ic.nTotalGap = 0;
                  ic.queryStart = segArray[0].from + 1;
                  ic.queryEnd = segArray[segArray.length - 1].to + 1;
                  for(let i = 0, il = segArray.length; i < il; ++i) {
                      let seg = segArray[i];
                      if(i > 0) { // determine gap
                        if(seg.orifrom - prevTargetTo < seg.from - prevQueryTo) { // gap in target
                            ic.targetGapHash[seg.orifrom] = {'from': prevQueryTo + 1, 'to': seg.from - 1};
                            ic.nTotalGap += ic.targetGapHash[seg.orifrom].to - ic.targetGapHash[seg.orifrom].from + 1;
                        }
                        else if(seg.orifrom - prevTargetTo > seg.from - prevQueryTo) { // gap in query
                            for(let j = prevTargetTo + 1; j < seg.orifrom; ++j) {
                              target2queryHash[j] = -1; // means gap in query
                            }
                        }
                      }
                      for(let j = 0; j <= seg.orito - seg.orifrom; ++j) {
                          target2queryHash[j + seg.orifrom] = j + seg.from;
                      }
                      prevTargetTo = seg.orito;
                      prevQueryTo = seg.to;
                  }

                  // the missing residues at the end of the seq will be filled up in the API showNewTrack()
                  let nGap = 0;
                  ic.alnChainsSeq[chnid] = [];
                  //let offset =(ic.chainid2offset[chnid]) ? ic.chainid2offset[chnid] : 0;                
                  for(let i = 0, il = targetSeq.length; i < il; ++i) {
                      //text += ic.showSeqCls.insertGap(chnid, i, '-', true);
                      if(ic.targetGapHash.hasOwnProperty(i)) {
                          for(let j = ic.targetGapHash[i].from; j <= ic.targetGapHash[i].to; ++j) {
                              text += querySeq[j];
                          }
                      }
                      compText += ic.showSeqCls.insertGap(chnid, i, '-', true);
                      if(ic.targetGapHash.hasOwnProperty(i)) nGap += ic.targetGapHash[i].to - ic.targetGapHash[i].from + 1;
                      //let pos =(ic.bUsePdbNum) ? i+1 + offset : i+1;
                      let pos =(ic.bUsePdbNum) ? ic.ParserUtilsCls.getResi(chnid, i) : i+1;
                      if(target2queryHash.hasOwnProperty(i) && target2queryHash[i] !== -1) {
                          text += querySeq[target2queryHash[i]];
                          let colorHexStr = this.getColorhexFromBlosum62(targetSeq[i], querySeq[target2queryHash[i]]);
                          if(targetSeq[i] == querySeq[target2queryHash[i]]) {
                              compText += targetSeq[i];
                              ic.fullpos2ConsTargetpos[i + nGap] = {'same': 1, 'pos': pos, 'res': targetSeq[i], 'color': colorHexStr};
                              ic.consrvResPosArray.push(pos);
                              ic.alnChainsSeq[chnid].push({'resi': pos, 'color': '#FF0000', 'color2': '#' + colorHexStr});
                          }
                          else if(this.conservativeReplacement(targetSeq[i], querySeq[target2queryHash[i]])) {
                              compText += '+';
                              ic.fullpos2ConsTargetpos[i + nGap] = {'same': 0, 'pos': pos, 'res': targetSeq[i], 'color': colorHexStr};
                              ic.consrvResPosArray.push(pos);
                              ic.alnChainsSeq[chnid].push({'resi': pos, 'color': '#0000FF', 'color2': '#' + colorHexStr});
                          }
                          else {
                              compText += ' ';
                              ic.fullpos2ConsTargetpos[i + nGap] = {'same': -1, 'pos': pos, 'res': targetSeq[i], 'color': colorHexStr};
                              ic.alnChainsSeq[chnid].push({'resi': pos, 'color': me.htmlCls.GREYC, 'color2': '#' + colorHexStr});
                          }
                      }
                      else {
                          text += '-';
                          compText += ' ';
                      }
                  }

                  //title += ', E: ' + evalue;
              }
              else {
                  text += "cannot be aligned";
                  if(ic.bRender) alert('The sequence can NOT be aligned to the structure');
              }
              let compTitle = (ic.seqStructAlignData !== undefined) ? 'BLAST, E: ' + evalue : 'Score: ' + evalue;
              ic.showSeqCls.showSeq(chnid, chnidBase, undefined, title, compTitle, text, compText);
              let residueidHash = {};
              let residueid;
              if(ic.consrvResPosArray !== undefined) {
                for(let i = 0, il = ic.consrvResPosArray.length; i < il; ++i) {
                    residueid = chnidBase + '_' + ic.consrvResPosArray[i];
                    residueidHash[residueid] = 1;
                    //atomHash = me.hashUtilsCls.unionHash(atomHash, ic.residues[residueid]);
                }
              }
              let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
              //ic.selectionCls.selectResidueList(residueidHash, chnidBase + '_blast', compTitle, false);
              ic.selectionCls.selectResidueList(residueidHash, 'protein_aligned', compTitle, false);
              ic.hAtoms = me.hashUtilsCls.cloneHash(prevHAtoms);
            } // align seq to structure
        } // for loop
        
        if(!me.bNode) {
            this.enableHlSeq();
            // get CDD/Binding sites
            await ic.annoCddSiteCls.showCddSiteAll();
        }
    }

    enableHlSeq() { let ic = this.icn3d, me = ic.icn3dui;
        if(! me.utilsCls.isMobile()) {
            ic.hlSeqCls.selectSequenceNonMobile();
        }
        else {
            ic.hlSeqCls.selectSequenceMobile();
            ic.hlSeqCls.selectChainMobile();
        }
        // highlight seq after the ajax calls
        if(Object.keys(ic.hAtoms).length < Object.keys(ic.dAtoms).length) {
            ic.hlUpdateCls.updateHlSeq();
        }
    }

    getAnDiv(chnid, anno) { let ic = this.icn3d; ic.icn3dui;
        let message = 'Loading ' + anno + '...';
        if(anno == 'custom') {
            message = '';
        }
        else if(anno == 'domain') {
            message = 'Loading 3D ' + anno + '...';
        }
        return "<div id='" + ic.pre + anno + "_" + chnid + "'><div id='" + ic.pre + "tt_" + anno + "_" + chnid + "' class='icn3d-fixed-pos' style='display:none!important'></div><div id='" + ic.pre + "dt_" + anno + "_" + chnid + "' style='display:none'>" + message + "</div><div id='" + ic.pre + "ov_" + anno + "_" + chnid + "'>" + message + "</div></div>";
    }
    addButton(chnid, classvalue, name, desc, width, buttonStyle) { let ic = this.icn3d; ic.icn3dui;
        return "<div class='" + classvalue + "' chainid='" + chnid + "' style='display:inline-block; font-size:11px; font-weight:bold; width:" + width + "px!important;'><button style='-webkit-appearance:" + buttonStyle + "; height:18px; width:" + width + "px;'><span style='white-space:nowrap; margin-left:-3px;' title='" + desc + "'>" + name + "</span></button></div>";
    }
    addSnpButton(snp, classvalue, name, desc, width, buttonStyle) { let ic = this.icn3d; ic.icn3dui;
        return "<div class='" + ic.pre + classvalue + "' snp='" + snp + "' style='margin:3px 0 3px 0; display:inline-block; font-size:11px; font-weight:bold; width:" + width + "px!important;'><button style='-webkit-appearance:" + buttonStyle + "; height:18px; width:" + width + "px;'><span style='white-space:nowrap; margin-left:-3px;' title='" + desc + "'>" + name + "</span></button></div>";
    }
    conservativeReplacement(resA, resB) { let ic = this.icn3d, me = ic.icn3dui;
        let iA =(me.parasCls.b62ResArray.indexOf(resA) !== -1) ? me.parasCls.b62ResArray.indexOf(resA) : me.parasCls.b62ResArray.length - 1; // or the last one "*"
        let iB =(me.parasCls.b62ResArray.indexOf(resB) !== -1) ? me.parasCls.b62ResArray.indexOf(resB) : me.parasCls.b62ResArray.length - 1; // or the last one "*"
        let matrixValue = me.parasCls.b62Matrix[iA][iB];
        if(matrixValue > 0) {
            return true;
        }
        else {
            return false;
        }
    }
    getColorhexFromBlosum62(resA, resB) { let ic = this.icn3d, me = ic.icn3dui;
        resA = resA.toUpperCase();
        resB = resB.toUpperCase();

        let iA =(me.parasCls.b62ResArray.indexOf(resA) !== -1) ? me.parasCls.b62ResArray.indexOf(resA) : me.parasCls.b62ResArray.length - 1; // or the last one "*"
        let iB =(me.parasCls.b62ResArray.indexOf(resB) !== -1) ? me.parasCls.b62ResArray.indexOf(resB) : me.parasCls.b62ResArray.length - 1; // or the last one "*"
        let matrixValue = me.parasCls.b62Matrix[iA][iB];
        if(matrixValue === undefined) return '333333';
        // range and color: blue for -4 ~ 0, red for 0 ~ 11
        // max value 221 to avoid white
        let color = '333333';
        if(matrixValue > 0) {
            let c = 221 - parseInt(matrixValue / 11.0 * 221);
            let cStr =(c < 10) ? '0' + c.toString(16) : c.toString(16);
            color = 'DD' + cStr + cStr;
        }
        else {
            let c = 221 - parseInt(-1.0 * matrixValue / 4.0 * 221);
            let cStr =(c < 10) ? '0' + c.toString(16) : c.toString(16);
            color = cStr + cStr + 'DD';
        }
        return color;
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ShowSeq {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Show the sequences and secondary structures.
    showSeq(chnid, chnidBase, type, queryTitle, compTitle, queryText, compText) {  let ic = this.icn3d, me = ic.icn3dui;
        let bNonMmdb = false;
        let giSeq;
        if(me.cfg.mmdbid === undefined && me.cfg.gi === undefined && me.cfg.blast_rep_id === undefined && me.cfg.align === undefined && me.cfg.chainalign === undefined && me.cfg.mmdbafid === undefined) {
            bNonMmdb = true;
            giSeq = [];
            for(let i = 0; i < ic.giSeq[chnid].length; ++i) {
                giSeq.push(ic.chainsSeq[chnid][i]);
            }
        }
        else {
            giSeq = ic.giSeq[chnid];
        }

        // remove null giSeq[i]
        let giSeqTmp = [];
        for(let i = 0, il = giSeq.length; i < il; ++i) {
            if(giSeq[i]) {
                giSeqTmp.push(giSeq[i]);
            }
        }
        giSeq = giSeqTmp;

        //let divLength = me.htmlCls.RESIDUE_WIDTH * ic.giSeq[chnid].length + 200;
        let divLength = me.htmlCls.RESIDUE_WIDTH * (ic.giSeq[chnid].length + ic.nTotalGap) + 200;

        // let seqLength = ic.giSeq[chnid].length
        // if(seqLength > ic.maxAnnoLength) {
        //     ic.maxAnnoLength = seqLength;
        // }

        //let itemArray = ['giseq', 'cddsite', 'ptm', 'clinvar', 'snp', 'domain', 'interaction', 'custom', 'ssbond', 'crosslink', 'transmem'];
        let itemArray = ['giseq', 'cddsite', 'clinvar', 'snp', 'ptm', 'ssbond', 'crosslink', 'transmem', 'domain', 'custom', 'interaction'];
        for(let i in itemArray) {
            let item = itemArray[i];
            if($("#" + ic.pre + item + "_" + chnid).length) $("#" + ic.pre + item + "_" + chnid).width(divLength);
        }
        // gi html
        let html = '', html2 = '', html3 = '', htmlTmp;
        html += '<div class="icn3d-dl_sequence">';
        html3 += '<div class="icn3d-dl_sequence">';
        // html to display protein positions(10, 20, etc)
        //if(Object.keys(ic.chains[chnid]).length > 10) {

        if(ic.giSeq[chnid].length > 10) {
            htmlTmp = '<div class="icn3d-residueLine" style="white-space:nowrap;">';
            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chnid]);
            //if(ic.baseResi[chnid] != 0 &&(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined || me.cfg.align !== undefined)) {
            if((me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined || me.cfg.blast_rep_id !== undefined || me.cfg.align !== undefined || me.cfg.chainalign !== undefined || me.cfg.mmdbafid !== undefined) && atom.resi_ori !== undefined && atom.resi_ori != atom.resi && chnid.indexOf('Misc') == -1 ) {
                htmlTmp += '<div class="icn3d-annoTitle" anno="0" title="NCBI Residue Numbers">NCBI Residue Numbers</div>';
            }
            else {
                htmlTmp += '<div class="icn3d-annoTitle" anno="0"></div>';
            }
            htmlTmp += '<span class="icn3d-residueNum"></span>';
            html3 += htmlTmp + '<br>';
            html += htmlTmp + '<span class="icn3d-seqLine">';
            let helixCnt = 0, sheetCnt = 0;
            let savedSsName = '';
            for(let i = 0, il = giSeq.length; i < il; ++i) {
              html += this.insertGap(chnid, i, '-');
              let currResi;
            //   if(bNonMmdb) {
            //     currResi = giSeq[i].resi;
            //   }
            //   else {
            //     currResi =(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i;
            //   }
              currResi = ic.ParserUtilsCls.getResi(chnid, i);
              html += '<span>';
              if( currResi % 10 === 0) {
                //html += currResi + ' ';
                html += currResi;
              }

              // name of secondary structures
              let residueid = chnid + '_' + currResi;
              // do not overlap residue number with ss label
              let bshowSsName =(currResi % 10 != 0 && currResi % 10 != 1 && currResi % 10 != 9) ? true : false;
              if( ic.residues.hasOwnProperty(residueid) ) {
                let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
                if(ic.secondaries[residueid] == 'H' && atom.ssbegin) {
                    ++helixCnt;

                    savedSsName = '<span class="icn3d-helix-color">H' + helixCnt + '</span>';

                    if(bshowSsName) {
                        html += savedSsName;
                        savedSsName = '';
                    }
                }
                else if(ic.secondaries[residueid] == 'E' && atom.ssbegin) {
                    ++sheetCnt;
                    if(ic.sheetcolor == 'green') {
                        savedSsName = '<span class="icn3d-sheet-color">S' + sheetCnt + '</span>';
                    }
                    else if(ic.sheetcolor == 'yellow') {
                        savedSsName = '<span class="icn3d-sheet-colory">S' + sheetCnt + '</span>';
                    }

                    if(bshowSsName) {
                        html += savedSsName;
                        savedSsName = '';
                    }
                }
                else if(atom.ssend) {
                    savedSsName = '';
                }

                if(savedSsName != '' && bshowSsName) {
                    html += savedSsName;
                    savedSsName = '';
                }
              }
              html += '</span>';
            }
            html += '<span class="icn3d-residueNum"></span>';
            html += '</span>';
            html += '<br>';
            html += '</div>';
            html3 += '</div>';
        }
        // html to display secondary structures
        htmlTmp = '<div class="icn3d-residueLine" style="white-space:nowrap;">';
        htmlTmp += '<div class="icn3d-annoTitle" anno="0"></div>';
        htmlTmp += '<span class="icn3d-residueNum"></span>';
        html3 += htmlTmp + '<br>';
        html += htmlTmp + '<span class="icn3d-seqLine">';
        for(let i = 0, il = giSeq.length; i < il; ++i) {
          html += this.insertGap(chnid, i, '-');
        //   let resi =(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i;
          let resi = ic.ParserUtilsCls.getResi(chnid, i);
          let residueid = chnid + '_' + resi;
          if( ic.residues.hasOwnProperty(residueid) ) {
            if(ic.secondaries[residueid] == 'H') {
                if(i % 2 == 0) {
                    html += '<span class="icn3d-helix">';
                }
                else {
                    html += '<span class="icn3d-helix2">';
                }
                html += '&nbsp;</span>';
            }
            else if(ic.secondaries[residueid] == 'E') {
                let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
                if(atom.ssend) {
                    if(ic.sheetcolor == 'green') {
                        html += '<span class="icn3d-sheet2">';
                    }
                    else if(ic.sheetcolor == 'yellow') {
                        html += '<span class="icn3d-sheet2y">';
                    }
                }
                else {
                    if(ic.sheetcolor == 'green') {
                        html += '<span class="icn3d-sheet">';
                    }
                    else if(ic.sheetcolor == 'yellow') {
                        html += '<span class="icn3d-sheety">';
                    }
                }
                html += '&nbsp;</span>';
            }
            else if(ic.secondaries[residueid] == 'c') {
                html += '<span class="icn3d-coil">&nbsp;</span>';
            }
            else if(ic.secondaries[residueid] == 'o') {
                html += '<span class="icn3d-other">&nbsp;</span>';
            }
          }
          else {
            html += '<span>-</span>'; //'<span>-</span>';
          }
        }
        html += '<span class="icn3d-residueNum"></span>';
        html += '</span>';
        html += '<br>';
        html += '</div>';
        html += '</div>'; // corresponds to above: html += '<div class="icn3d-dl_sequence">';
        html3 += '</div></div>';
        if(me.cfg.blast_rep_id === chnid) {
            htmlTmp = '<div id="' + ic.pre + 'giseq_sequence" class="icn3d-dl_sequence" style="border: solid 1px #000">';
        }
        else {
            htmlTmp = '<div id="' + ic.pre + 'giseq_sequence" class="icn3d-dl_sequence">';
        }
        let chainType = 'Protein', chainTypeFull = 'Protein';
        if(type !== undefined) {
            if(type == 'nucleotide') {
                chainType = 'Nucl.';
                chainTypeFull = 'Nucleotide';
            }
            else if(type == 'chemical') {
                chainType = 'Chem.';
                chainTypeFull = 'Chemical';
            }
        }
        // sequence, detailed view
        htmlTmp += '<div class="icn3d-seqTitle icn3d-link icn3d-blue" gi="' + chnid + '" anno="sequence" chain="' + chnid + '"><span style="white-space:nowrap;" title="' + chainTypeFull + ' ' + chnid + '">' + chainType + ' ' + chnid + '</span></div>';
        htmlTmp += '<span class="icn3d-residueNum" title="starting protein sequence number">' +(ic.baseResi[chnid]+1).toString() + '</span>';
        html3 += htmlTmp + '<br>';
        let htmlTmp2 = '<span class="icn3d-seqLine">';
        html += htmlTmp + htmlTmp2;
        html2 += htmlTmp + htmlTmp2;
        let pos, nGap = 0;

        for(let i = 0, il = giSeq.length; i < il; ++i) {
          html += this.insertGap(chnid, i, '-');
          if(ic.targetGapHash !== undefined && ic.targetGapHash.hasOwnProperty(i)) nGap += ic.targetGapHash[i].to - ic.targetGapHash[i].from + 1;
          let cFull =(bNonMmdb) ? giSeq[i].name : giSeq[i];
          let c = cFull;
          if(cFull.length > 1) {
              c = cFull[0] + '..';
          }
          
        //   pos =(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) ? ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi : ic.baseResi[chnid] + 1 + i;
          pos = ic.ParserUtilsCls.getResi(chnid, i);
              
          if( !ic.residues.hasOwnProperty(chnid + '_' + pos) ) {
              c = c.toLowerCase();
              html += '<span title="' + cFull + pos + '" class="icn3d-residue">' + c + '</span>';
          }
          else {
              let color = '333333';
              if(me.cfg.blast_rep_id == chnid && ic.fullpos2ConsTargetpos !== undefined && ic.fullpos2ConsTargetpos[i + nGap] !== undefined) {
                  color = ic.fullpos2ConsTargetpos[i + nGap].color;
              }
              else {
                  let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[chnid + '_' + pos]);
                  let colorStr =(atom.color === undefined || atom.color.getHexString() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
                  color =(atom.color !== undefined) ? colorStr : "CCCCCC";
              }
              html += '<span id="giseq_' + ic.pre + chnid + '_' + pos + '" title="' + cFull + pos + '" class="icn3d-residue" style="color:#' + color + '">' + c + '</span>';
          }
        }
        if(me.cfg.blast_rep_id == chnid) {
          // change color in 3D
          ic.opts['color'] = 'conservation';
          ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);
          // remove highlight
          //ic.hlUpdateCls.removeHlSeq();
        }
        // sequence, overview
        let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chnid]);
        let color =(atom.color) ? atom.color.getHexString() : "CCCCCC";
        let width = Math.round(ic.seqAnnWidth * giSeq.length / ic.maxAnnoLength);
        if(width < 1) width = 1;
        if(me.cfg.blast_rep_id != chnid) { // regular
            html2 += '<div id="giseq_summary_' + ic.pre + chnid + '" class="icn3d-seqTitle icn3d-link" gi chain="' + chnid + '" style="display:inline-block; color:white; font-weight:bold; background-color:#' + color + '; width:' + width + 'px;">' + chnid + '</div>';
        }
        else { // with potential gaps
            let fromArray2 = [], toArray2 = [];
            fromArray2.push(0);
            for(let i = 0, il = giSeq.length; i < il; ++i) {
                if(ic.targetGapHash !== undefined && ic.targetGapHash.hasOwnProperty(i)) {
                    toArray2.push(i - 1);
                    fromArray2.push(i);
                }
            }
            toArray2.push(giSeq.length - 1);
            html2 += '<div id="giseq_summary_' + ic.pre + chnid + '" class="icn3d-seqTitle icn3d-link" gi chain="' + chnid + '" style="width:' + width + 'px;">';
            for(let i = 0, il = fromArray2.length; i < il; ++i) {
                html2 += this.insertGapOverview(chnid, fromArray2[i]);
                html2 += '<div style="display:inline-block; color:white!important; font-weight:bold; background-color:#' + color + '; width:' + Math.round(ic.seqAnnWidth *(toArray2[i] - fromArray2[i] + 1) /(ic.maxAnnoLength + ic.nTotalGap)) + 'px;" class="icn3d-seqTitle icn3d-link icn3d-blue" anno="sequence" gi chain="' + chnid + '" title="' + chnid + '">' + chnid + '</div>';
            }
            html2 += '</div>';
        }
        htmlTmp = '<span class="icn3d-residueNum" title="ending protein sequence number">' + pos + '</span>';
        htmlTmp += '</span>';
        htmlTmp += '<br>';
        html += htmlTmp;
        html2 += htmlTmp;
        if(me.cfg.blast_rep_id == chnid) {
            // 1. residue conservation
            if(compText !== undefined && compText !== '') {
            // conservation, detailed view
            htmlTmp = '<div class="icn3d-seqTitle icn3d-link icn3d-blue" blast="" posarray="' + ic.consrvResPosArray.toString() + '" title="' + compTitle + '" setname="' + chnid + '_blast" anno="sequence" chain="' + chnid + '"><span style="white-space:nowrap;" title="' + compTitle + '">' + compTitle + '</span></div>';
            htmlTmp += '<span class="icn3d-residueNum"></span>';
            html3 += htmlTmp + '<br>';
            let htmlTmp2 = '<span class="icn3d-seqLine">';
            html += htmlTmp + htmlTmp2;
            html2 += htmlTmp + htmlTmp2;
            let prevEmptyWidth = 0;
            let prevLineWidth = 0;
            let widthPerRes = 1;
            ic.queryStart;
            for(let i = 0, il = compText.length; i < il; ++i) {
              let c = compText[i];
              if(c == '-') {
                  html += '<span>-</span>';
              }
              else if(c == ' ') {
                  html += '<span> </span>';
              }
              else {
                  let pos = ic.fullpos2ConsTargetpos[i].pos;
                  if( !ic.residues.hasOwnProperty(chnid + '_' + pos) ) {
                      c = c.toLowerCase();
                      html += '<span class="icn3d-residue">' + c + '</span>';
                  }
                  else {
                      let color = ic.fullpos2ConsTargetpos[i].color;
                      html += '<span id="giseq_' + ic.pre + chnid + '_' + pos + '" title="' + ic.fullpos2ConsTargetpos[i].res + pos + '" class="icn3d-residue" style="color:#' + color + '">' + c + '</span>';
                  }
                  html2 += this.insertGapOverview(chnid, i);
                  let emptyWidth = Math.round(ic.seqAnnWidth * i /(ic.maxAnnoLength + ic.nTotalGap) - prevEmptyWidth - prevLineWidth);
                  //if(emptyWidth < 0) emptyWidth = 0;
                  if(emptyWidth >= 0) {
                  html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                  html2 += '<div style="display:inline-block; background-color:#F00; width:' + widthPerRes + 'px;" title="' + c + pos + '">&nbsp;</div>';
                  prevEmptyWidth += emptyWidth;
                  prevLineWidth += widthPerRes;
                  }
              }
            }
            htmlTmp = '<span class="icn3d-residueNum"></span>';
            htmlTmp += '</span>';
            htmlTmp += '<br>';
            html += htmlTmp;
            html2 += htmlTmp;
            }
            // 2. Query text
            // query protein, detailed view
            htmlTmp = '<div class="icn3d-annoTitle" anno="sequence" chain="' + chnid + '"><span style="white-space:nowrap;" title="' + queryTitle + '">' + queryTitle + '</span></div>';
            htmlTmp += '<span class="icn3d-residueNum" title="starting protein sequence number">' + ic.queryStart + '</span>';
            html3 += htmlTmp + '<br>';
            //var htmlTmp2 = '<span class="icn3d-seqLine">';
            let htmlTmp2 = '<span class="icn3d-seqLine" style="font-weight: bold;">';
            html += htmlTmp + htmlTmp2;
            html2 += htmlTmp + htmlTmp2;
            let queryPos = ic.queryStart;
            for(let i = 0, il = queryText.length; i < il; ++i) {
              let c = queryText[i];
              if(c == ' ' || c == '-') {
                  html += '<span>-</span>';
              }
              else {
                  if( ic.fullpos2ConsTargetpos !== undefined && ic.fullpos2ConsTargetpos[i] !== undefined && !ic.residues.hasOwnProperty(chnid + '_' + ic.fullpos2ConsTargetpos[i].pos) ) {
                      c = c.toLowerCase();
                      html += '<span title="' + c + queryPos + '" class="icn3d-residue">' + c + '</span>';
                  }
                  else {
                      html += '<span title="' + c + queryPos + '" class="icn3d-residue">' + c + '</span>';
                  }
                  ++queryPos;
              }
            }
            // query protein, overview
            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chnid]);
            let colorStr =(atom.color === undefined || atom.color.getHexString() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
            let color =(atom.color !== undefined) ? colorStr : "CCCCCC";
            let fromArray2 = [], toArray2 = [];
            let prevChar = '-';
            for(let i = 0, il = queryText.length; i < il; ++i) {
                let c = queryText[i];
                if(c != '-' && prevChar == '-') {
                    fromArray2.push(i);
                }
                else if(c == '-' && prevChar != '-' ) {
                    toArray2.push(i-1);
                }
                prevChar = c;
            }
            if(prevChar != '-') {
                toArray2.push(queryText.length - 1);
            }
            for(let i = 0, il = fromArray2.length; i < il; ++i) {
                let emptyWidth =(i == 0) ? Math.round(ic.seqAnnWidth *(fromArray2[i] - ic.baseResi[chnid] - 1) /(ic.maxAnnoLength + ic.nTotalGap)) : Math.round(ic.seqAnnWidth *(fromArray2[i] - toArray2[i-1] - 1) /(ic.maxAnnoLength + ic.nTotalGap));
                html2 += '<div style="display:inline-block; width:' + emptyWidth + 'px;">&nbsp;</div>';
                html2 += '<div style="display:inline-block; color:white!important; font-weight:bold; background-color:#' + color + '; width:' + Math.round(ic.seqAnnWidth *(toArray2[i] - fromArray2[i] + 1) /(ic.maxAnnoLength + ic.nTotalGap)) + 'px;" anno="sequence" chain="' + chnid + '" title="' + queryTitle + '">' + queryTitle + '</div>';
            }
            htmlTmp = '<span class="icn3d-residueNum" title="ending protein sequence number">' + ic.queryEnd + '</span>';
            htmlTmp += '</span>';
            htmlTmp += '<br>';
            html += htmlTmp;
            html2 += htmlTmp;
        }
        html += '</div>';
        html2 += '</div>';
        html3 += '</div>';

        //if(Object.keys(ic.chains[chnid]).length > 10) {
        if(ic.giSeq[chnid].length > 10) {
            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chnid]);
            //if(ic.baseResi[chnid] != 0 &&(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined || me.cfg.align !== undefined)) {
            if((me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined || me.cfg.blast_rep_id !== undefined || me.cfg.align !== undefined || me.cfg.chainalign !== undefined || me.cfg.mmdbafid !== undefined) && atom.resi_ori !== undefined && atom.resi_ori != atom.resi && chnid.indexOf('Misc') == -1 ) {
                htmlTmp = '<div class="icn3d-dl_sequence">';
                htmlTmp += '<div class="icn3d-residueLine" style="white-space:nowrap;">';
                htmlTmp += '<div class="icn3d-annoTitle" anno="0" title="PDB Residue Numbers">PDB Residue Numbers</div>';
                htmlTmp += '<span class="icn3d-residueNum"></span>';
                html3 += htmlTmp + '<br>';
                html += htmlTmp + '<span class="icn3d-seqLine">';
                for(let i = 0, il = giSeq.length; i < il; ++i) {
                    html += this.insertGap(chnid, i, '-');
                    //if(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) {
                    //   let currResi = ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi;
                      let currResi = ic.ParserUtilsCls.getResi(chnid, i);
                      let residueid = chnid + '_' + currResi;
                      if(!ic.residues.hasOwnProperty(residueid)) {
                          html += '<span></span>';
                      }
                      else {
                          let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
                          let resi_ori = atom.resi_ori;
                          html += '<span>';
                          if( resi_ori % 10 === 0) {
                            html += resi_ori + ' ';
                          }
                          html += '</span>';
                      }
                    // }
                    // else {
                    //   html += '<span></span>';
                    // }
                }
                html += '<span class="icn3d-residueNum"></span>';
                html += '</span>';
                html += '<br>';
                html += '</div>';
                html += '</div>';
                html3 += '</div></div>';
            }         
            else if(ic.bShowRefnum && ic.chainid2index.hasOwnProperty(chnid)) {              
                let result = this.showRefNum(giSeq, chnid);
                html += result.html;
                html3 += result.html3;

                let bKabat = true;
                result = this.showRefNum(giSeq, chnid, bKabat);
                html += result.html;
                html3 += result.html3;
            }
            else if(ic.bShowCustomRefnum && ic.chainsMapping.hasOwnProperty(chnid)) {              
                let bCustom = true;
                let result = this.showRefNum(giSeq, chnid, undefined, bCustom);
                html += result.html;
                html3 += result.html3;
            }
        }

        // highlight reference numbers
        if(ic.bShowRefnum) {
            ic.hAtoms = ic.hAtomsRefnum;
            
            let name = 'refnum_anchors';
            ic.selectionCls.saveSelection(name, name);
            
            ic.hlUpdateCls.updateHlAll();
        }

        $("#" + ic.pre + 'dt_giseq_' + chnid).html(html);
        $("#" + ic.pre + 'ov_giseq_' + chnid).html(html2);
        $("#" + ic.pre + 'tt_giseq_' + chnid).html(html3); // fixed title for scrolling
    }

    showRefNum(giSeq, chnid, bKabat, bCustom) {  let ic = this.icn3d, me = ic.icn3dui;
        let html = '', html3 = '';

        let htmlTmp = '<div class="icn3d-dl_sequence">';
        htmlTmp += '<div class="icn3d-residueLine" style="white-space:nowrap;">';
        if(bCustom) {
            htmlTmp += '<div class="icn3d-annoTitle" anno="0" title="Custom Reference Numbers">Custom Ref. No.</div>';
        }
        else if(bKabat) {
            htmlTmp += '<div class="icn3d-annoTitle" anno="0" title="Kabat Reference Numbers">Kabat Ref. No.</div>';
        }
        else {
            htmlTmp += '<div class="icn3d-annoTitle" anno="0" title="IgStRAnD Reference Numbers">IgStRAnD Ref. No.</div>';
        }
        htmlTmp += '<span class="icn3d-residueNum"></span>';
        html3 += htmlTmp + '<br>';
        html += htmlTmp + '<span class="icn3d-seqLine">';
        for(let i = 0, il = giSeq.length; i < il; ++i) {
            html += this.insertGap(chnid, i, '-');
            // if(i >= ic.matchedPos[chnid] && i - ic.matchedPos[chnid] < ic.chainsSeq[chnid].length) {
                // let currResi = ic.chainsSeq[chnid][i - ic.matchedPos[chnid]].resi;
                let currResi = ic.ParserUtilsCls.getResi(chnid, i);
                let residueid = chnid + '_' + currResi;
                let domainid = (bCustom) ? 0 : ic.resid2domainid[residueid];
                if(!ic.residues.hasOwnProperty(residueid)) {
                    html += '<span></span>';
                }
                else {
                    //let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
                    //let resi_ori = atom.resi_ori;

                    //if(ic.resid2refnum.hasOwnProperty(residueid)) {
                    let refnumLabel = ic.resid2refnum[residueid];
                    if(refnumLabel) {
                        let refnumStr_ori = refnumLabel.replace(/'/g, '').substr(1);
                        let refnumStr;
                        if(bCustom) {
                            refnumStr = refnumLabel;
                        }
                        else if(bKabat) {
                            refnumStr = (ic.domainid2ig2kabat[domainid]) ? ic.domainid2ig2kabat[domainid][refnumStr_ori] : undefined;                            
                        }
                        else {
                            refnumStr = refnumStr_ori;
                        }
                    
                        if(bCustom) {
                            if(!refnumStr) {                               
                                html += '<span></span>';
                            }
                            else {
                                let refnum = parseInt(refnumStr);

                                if(refnum % 2 == 0) {
                                    html += '<span title="' + refnumStr + '">' + refnumStr + '</span>';
                                }
                                else {
                                    html += '<span title="' + refnumStr + '">&nbsp;</span>';
                                }
                            }
                        }
                        else if(bKabat) {
                            if(!refnumStr) {                               
                                html += '<span></span>';
                            }
                            else {
                                let refnum = parseInt(refnumStr).toString();
                                let color = this.getRefnumColor(refnumStr_ori);
                                let colorStr = 'style="color:' + color + '"';

                                let lastTwo = parseInt(refnum.substr(refnum.length - 2, 2));

                                if(lastTwo % 2 == 0) {
                                    html += '<span ' + colorStr + ' title="' + refnumStr + '">' + refnumStr + '</span>';
                                }
                                else {
                                    html += '<span ' + colorStr + ' title="' + refnumStr + '">&nbsp;</span>';
                                }
                            }
                        }
                        else {
                            let refnum = parseInt(refnumStr).toString();
                            let color = this.getRefnumColor(refnumStr_ori);
                            let colorStr = 'style="color:' + color + '"';

                            let lastTwo = parseInt(refnum.substr(refnum.length - 2, 2));

                            if(lastTwo == 50) {
                                // highlight the anchor residues
                                ic.hAtomsRefnum = me.hashUtilsCls.unionHash(ic.hAtomsRefnum, ic.residues[residueid]);

                                html += '<span ' + colorStr + ' title="' + refnumLabel + '"><b>' + refnumLabel.substr(0, 1) + '</b>' + refnumLabel.substr(1) + '</span>';
                            }
                            else if(lastTwo % 2 == 0 && lastTwo != 52) {
                                let lastTwoStr = isNaN(refnumStr) ? lastTwo + refnumStr.substr(refnumStr.length - 1, 1) : lastTwo;
                                html += '<span ' + colorStr + ' title="' + refnumLabel + '">' + lastTwoStr + '</span>';
                            }
                            else {
                                html += '<span ' + colorStr + ' title="' + refnumLabel + '">&nbsp;</span>';
                            }
                        }
                    }
                    else {
                        html += '<span></span>';
                    }
                }
            // }
            // else {
            //     html += '<span></span>';
            // }
        }
        html += '<span class="icn3d-residueNum"></span>';
        html += '</span>';
        html += '<br>';
        html += '</div>';
        html += '</div>';
        html3 += '</div></div>';

        return {html: html, html3: html3}
    }

    getRefnumColor(refnumStr) {  let ic = this.icn3d; ic.icn3dui;
        let prefix = refnumStr.substr(0,2);
        if(prefix == '10') {
            return '#000';
        }
        else if(prefix == '12') {
            return '#888';
        }
        else if(prefix == '20') {
            return '#000';
        }
        else if(prefix == '30') {
            return '#888';
        }
        else if(prefix == '32') {
            return '#000';
        }
        else if(prefix == '37') {
            return '#888';
        }
        else if(prefix == '40') {
            return '#000';
        }
        else if(prefix == '50') {
            return '#888';
        }
        else if(prefix == '60') {
            return '#000';
        }
        else if(prefix == '70') {
            return '#888';
        }
        else if(prefix == '72') {
            return '#000';
        }
        else if(prefix == '30') {
            return '#BBB';
        }
    }

    insertGap(chnid, seqIndex, text, bNohtml) {  let ic = this.icn3d; ic.icn3dui;
      let html = '';
      //if(me.cfg.blast_rep_id == chnid && ic.targetGapHash!== undefined && ic.targetGapHash.hasOwnProperty(seqIndex)) {
      if(ic.targetGapHash!== undefined && ic.targetGapHash.hasOwnProperty(seqIndex)) {
          for(let j = 0; j <(ic.targetGapHash[seqIndex].to - ic.targetGapHash[seqIndex].from + 1); ++j) {
              if(bNohtml) {
                 html += text;
              }
              else {
                 html += '<span>' + text + '</span>';
              }
          }
      }
      return html;
    }
    insertGapOverview(chnid, seqIndex) {  let ic = this.icn3d, me = ic.icn3dui;
      let html2 = '';
      if(me.cfg.blast_rep_id == chnid && ic.targetGapHash!== undefined && ic.targetGapHash.hasOwnProperty(seqIndex)) {
          let width = ic.seqAnnWidth *(ic.targetGapHash[seqIndex].to - ic.targetGapHash[seqIndex].from + 1) /(ic.maxAnnoLength + ic.nTotalGap);
          html2 += '<div style="display:inline-block; background-color:#333; width:' + width + 'px; height:3px;">&nbsp;</div>';
      }
      return html2;
    }

    setAlternativeSeq(chnid, chnidBase) { let ic = this.icn3d; ic.icn3dui;
        //if(ic.chainsSeq[chnid] !== undefined) {
        let resArray = ic.chainsSeq[chnid];
        ic.giSeq[chnid] = [];
        for(let i = 0, il = resArray.length; i < il; ++i) {
            let res = resArray[i].name;
            ic.giSeq[chnid][i] = res;
        }
        ic.matchedPos[chnid] = 0;
        ic.baseResi[chnid] = ic.chainsSeq[chnid][0].resi - ic.matchedPos[chnid] - 1;
    }

    getProteinName(chnid) { let ic = this.icn3d, me = ic.icn3dui;
        let fullProteinName = '';
        if((me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined || me.cfg.blast_rep_id !== undefined) && ic.mmdb_data !== undefined) {
            let moleculeInfor = ic.mmdb_data.moleculeInfor;
            let chain = chnid.substr(chnid.indexOf('_') + 1);
            for(let i in moleculeInfor) {
                if(moleculeInfor[i].chain == chain) {
                    fullProteinName = moleculeInfor[i].name.replace(/\'/g, '&prime;');
                    //if(proteinName.length > 40) proteinName = proteinName.substr(0, 40) + "...";
                    break;
                }
            }
        }
        else if((me.cfg.align !== undefined || me.cfg.chainalign !== undefined || me.cfg.mmdbafid !== undefined || ic.bRealign || ic.bSymd) && ic.chainid2title !== undefined) {
            if(ic.chainid2title[chnid] !== undefined) {
                fullProteinName = ic.chainid2title[chnid];
            }
        }
        return fullProteinName;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class HlSeq {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    selectSequenceNonMobile() { let ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      let thisClass = this;
      $("#" + ic.pre + "dl_sequence2").add("[id^=" + ic.pre + "dt_giseq]").add("[id^=" + ic.pre + "dt_custom]").add("[id^=" + ic.pre + "dt_site]").add("[id^=" + ic.pre + "dt_ptm]").add("[id^=" + ic.pre + "dt_snp]").add("[id^=" + ic.pre + "dt_clinvar]").add("[id^=" + ic.pre + "dt_cdd]").add("[id^=" + ic.pre + "dt_domain]").add("[id^=" + ic.pre + "dt_interaction]").add("[id^=" + ic.pre + "dt_ssbond]").add("[id^=" + ic.pre + "dt_crosslink]").add("[id^=" + ic.pre + "dt_transmem]")
      .add("[id^=" + ic.pre + "tt_giseq]").add("[id^=" + ic.pre + "tt_custom]").add("[id^=" + ic.pre + "tt_site]").add("[id^=" + ic.pre + "tt_ptm]").add("[id^=" + ic.pre + "tt_snp]").add("[id^=" + ic.pre + "tt_clinvar]").add("[id^=" + ic.pre + "tt_cdd]").add("[id^=" + ic.pre + "tt_domain]").add("[id^=" + ic.pre + "tt_interaction]").add("[id^=" + ic.pre + "tt_ssbond]").add("[id^=" + ic.pre + "tt_crosslink]").add("[id^=" + ic.pre + "tt_transmem]")
      .selectable({
          distance: 1, //Tolerance, in pixels, for when selecting should start. If specified, selecting will not start until the mouse has been dragged beyond the specified distance.
          stop: function() { let ic = thisClass.icn3d;
              if($(this).attr('id') === ic.pre + "dl_sequence2") {
                  ic.bAlignSeq = true;
                  ic.bAnnotations = false;
              }
              //else if($(this).attr('id') === ic.pre + "dl_annotations") {
              else {
                  ic.bAlignSeq = false;
                  ic.bAnnotations = true;
              }

              if(ic.bSelectResidue === false && !ic.bShift && !ic.bCtrl) {
                  ic.selectionCls.removeSelection();
              }

              // select residues
              $("span.ui-selected", this).each(function() {
                  let id = $(this).attr('id');

                  if(id !== undefined) {
                     thisClass.selectResidues(id, this);
                 }
              });

              //ic.residueLabelsCls.addResidueLabels(ic.hAtoms, false, 0.5);
              ic.hlObjectsCls.addHlObjects();  // render() is called

              // get all chainid in the selected residues
              let chainHash = {};
              for(let residueid in ic.selectedResidues) {
                  let pos = residueid.lastIndexOf('_');
                  let chainid = residueid.substr(0, pos);

                  chainHash[chainid] = 1;
              }

              // highlight the nodes
              let chainArray2d = Object.keys(chainHash);
              ic.hlUpdateCls.updateHl2D(chainArray2d);

              // select annotation title
              //$("#" + ic.pre + "dl_selectannotations div.ui-selected", this).each(function() {
              $("div.ui-selected", this).each(function() {
                  if($(this).attr('chain') !== undefined) {
                      thisClass.selectTitle(this);
                  }
              });
          }
      });

      $("[id^=" + ic.pre + "ov_giseq]").add("[id^=" + ic.pre + "ov_custom]").add("[id^=" + ic.pre + "ov_site]").add("[id^=" + ic.pre + "ov_ptm]").add("[id^=" + ic.pre + "ov_snp]").add("[id^=" + ic.pre + "ov_clinvar]").add("[id^=" + ic.pre + "ov_cdd]").add("[id^=" + ic.pre + "ov_domain]").add("[id^=" + ic.pre + "ov_interaction]").add("[id^=" + ic.pre + "ov_ssbond]").add("[id^=" + ic.pre + "ov_crosslink]").add("[id^=" + ic.pre + "ov_transmem]")
      .add("[id^=" + ic.pre + "tt_giseq]").add("[id^=" + ic.pre + "tt_custom]").add("[id^=" + ic.pre + "tt_site]").add("[id^=" + ic.pre + "tt_ptm]").add("[id^=" + ic.pre + "tt_snp]").add("[id^=" + ic.pre + "tt_clinvar]").add("[id^=" + ic.pre + "tt_cdd]").add("[id^=" + ic.pre + "tt_domain]").add("[id^=" + ic.pre + "tt_interaction]").add("[id^=" + ic.pre + "tt_ssbond]").add("[id^=" + ic.pre + "tt_crosslink]").add("[id^=" + ic.pre + "tt_transmem]")

      .add("#" + ic.pre + "dl_sequence2").add("[id^=" + ic.pre + "dt_giseq]").add("[id^=" + ic.pre + "dt_custom]").add("[id^=" + ic.pre + "dt_site]").add("[id^=" + ic.pre + "dt_ptm]").add("[id^=" + ic.pre + "dt_snp]").add("[id^=" + ic.pre + "dt_clinvar]").add("[id^=" + ic.pre + "dt_cdd]").add("[id^=" + ic.pre + "dt_domain]").add("[id^=" + ic.pre + "dt_interaction]").add("[id^=" + ic.pre + "dt_ssbond]").add("[id^=" + ic.pre + "dt_crosslink]").add("[id^=" + ic.pre + "dt_transmem]")
      .add("[id^=" + ic.pre + "tt_giseq]").add("[id^=" + ic.pre + "tt_custom]").add("[id^=" + ic.pre + "tt_site]").add("[id^=" + ic.pre + "tt_ptm]").add("[id^=" + ic.pre + "tt_snp]").add("[id^=" + ic.pre + "tt_clinvar]").add("[id^=" + ic.pre + "tt_cdd]").add("[id^=" + ic.pre + "tt_domain]").add("[id^=" + ic.pre + "tt_interaction]").add("[id^=" + ic.pre + "tt_ssbond]").add("[id^=" + ic.pre + "tt_crosslink]").add("[id^=" + ic.pre + "tt_transmem]")

      .on('click', '.icn3d-seqTitle', function(e) { let ic = thisClass.icn3d;
          e.stopImmediatePropagation();

          //if($(this).attr('id') === ic.pre + "dl_sequence2") {
          if($(this).parents('div').attr('id') === ic.pre + "dl_sequence2") {
              ic.bAlignSeq = true;
              ic.bAnnotations = false;
          }
          //else if($(this).attr('id') === ic.pre + "dl_annotations") {
          else {
              ic.bAlignSeq = false;
              ic.bAnnotations = true;
          }

          // select annotation title
          //$("div .ui-selected", this).each(function() {
              thisClass.selectTitle(this);

              ic.hlUpdateCls.hlSummaryDomain3ddomain(this);
           //});

            // remove possible text selection
            if(window.getSelection) {
              if(window.getSelection().empty) {  // Chrome
                window.getSelection().empty();
              } else if(window.getSelection().removeAllRanges) {  // Firefox
                window.getSelection().removeAllRanges();
              }
            } else if(document.selection) {  // IE?
              document.selection.empty();
            }
      });
    }

    selectSequenceMobile() { let ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      let thisClass = this;

      $("#" + ic.pre + "dl_sequence2").add("[id^=" + ic.pre + "giseq]").add("[id^=" + ic.pre + "custom]").add("[id^=" + ic.pre + "site]").add("[id^=" + ic.pre + "ptm]").add("[id^=" + ic.pre + "clinvar]").add("[id^=" + ic.pre + "snp]").add("[id^=" + ic.pre + "cdd]").add("[id^=" + ic.pre + "domain]").add("[id^=" + ic.pre + "interaction]").add("[id^=" + ic.pre + "ssbond]").add("[id^=" + ic.pre + "crosslink]").add("[id^=" + ic.pre + "transmem]").on('click', '.icn3d-residue', function(e) { let ic = thisClass.icn3d;
          e.stopImmediatePropagation();

          // select residues
          //$("span.ui-selected", this).each(function() {
              let id = $(this).attr('id');

              if(id !== undefined) {
                   thisClass.selectResidues(id, this);
             }
          //});

          //ic.residueLabelsCls.addResidueLabels(ic.hAtoms, false, 0.5);
           ic.hlObjectsCls.addHlObjects();  // render() is called

          // get all chainid in the selected residues
          let chainHash = {};
          for(let residueid in ic.selectedResidues) {
              let pos = residueid.lastIndexOf('_');
              let chainid = residueid.substr(0, pos);

              chainHash[chainid] = 1;
          }

          // clear nodes in 2d dgm
          ic.hlUpdateCls.removeHl2D();

          // highlight the nodes
          let chainArray2d = Object.keys(chainHash);
          ic.hlUpdateCls.updateHl2D(chainArray2d);
      });
    }

    selectChainMobile() { let ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      let thisClass = this;

      $("#" + ic.pre + "dl_sequence2").add("[id^=" + ic.pre + "giseq]").add("[id^=" + ic.pre + "custom]").add("[id^=" + ic.pre + "site]").add("[id^=" + ic.pre + "ptm]").add("[id^=" + ic.pre + "feat]").add("[id^=" + ic.pre + "clinvar]").add("[id^=" + ic.pre + "snp]").add("[id^=" + ic.pre + "cdd]").add("[id^=" + ic.pre + "domain]").add("[id^=" + ic.pre + "interaction]").add("[id^=" + ic.pre + "ssbond]").add("[id^=" + ic.pre + "crosslink]").add("[id^=" + ic.pre + "transmem]").on('click', '.icn3d-seqTitle', function(e) { let ic = thisClass.icn3d;
          e.stopImmediatePropagation();

          //if($(this).attr('id') === ic.pre + "dl_sequence2") {
          if($(this).parents('div').attr('id') === ic.pre + "dl_sequence2") {
              ic.bAlignSeq = true;
              ic.bAnnotations = false;
          }
          //else if($(this).attr('id') === ic.pre + "dl_annotations") {
          else {
              ic.bAlignSeq = false;
              ic.bAnnotations = true;
          }

          // select annotation title
          //$("div.ui-selected", this).each(function() {
              thisClass.selectTitle(this);

              ic.hlUpdateCls.hlSummaryDomain3ddomain(this);
          //});
      });
    }

    selectTitle(that) { let ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      if($(that).hasClass('icn3d-seqTitle')) {
        let chainid = $(that).attr('chain');
        let resn = $(that).attr('resn');

        if(ic.bAlignSeq) {
            ic.bSelectAlignResidue = false;
        }
        else {
            ic.bSelectResidue = false;
        }

        if(!ic.bAnnotations) {
            ic.hlUpdateCls.removeSeqChainBkgd(chainid);
        }
        //else {
        //    ic.hlUpdateCls.removeSeqChainBkgd();
        //}

        if(!ic.bCtrl && !ic.bShift) {
            ic.hlUpdateCls.removeSeqResidueBkgd();

            ic.hlUpdateCls.removeSeqChainBkgd();

            ic.currSelectedSets = [];
        }

        $(that).toggleClass('icn3d-highlightSeq');
        let commandname, commanddescr, position;
        if(resn) {
            commandname = resn; 
        }
        else {
            if(!ic.bAnnotations) {
                if(ic.bAlignSeq) {
                    commandname = "align_" + chainid;
                }
                else {
                    commandname = chainid;           
                }
            }
            else {
                commandname = $(that).attr('setname');
                commanddescr = $(that).attr('title');
            }
        }

        if($(that).hasClass('icn3d-highlightSeq')) {
            if(!ic.bAnnotations) {
                if(ic.bCtrl || ic.bShift) {
                    ic.currSelectedSets.push(commandname);
                    ic.selectionCls.selectAChain(chainid, commandname, true, true);
                }
                else {
                    ic.currSelectedSets = [commandname];
                    ic.selectionCls.selectAChain(chainid, commandname, ic.bAlignSeq);
                }

                if(ic.bAlignSeq) {
                    me.htmlCls.clickMenuCls.setLogCmd('select alignChain ' + chainid, true);
                }
                else {   
                    me.htmlCls.clickMenuCls.setLogCmd('select chain ' + chainid, true);
                }

                let setNames = ic.currSelectedSets.join(' or ');
                //if(ic.currSelectedSets.length > 1) me.htmlCls.clickMenuCls.setLogCmd('select saved atoms ' + setNames, true);
                if(ic.currSelectedSets.length > 1) me.htmlCls.clickMenuCls.setLogCmd('select sets ' + setNames, true);
            }
            else {
                if($(that).hasClass('icn3d-highlightSeq')) {
                    ic.hlUpdateCls.removeHl2D();

                    if($(that).attr('gi') !== undefined) {
                        if(ic.bCtrl || ic.bShift) {
                            ic.currSelectedSets.push(chainid);
                            if(resn) {
                                let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
                                let bNoUpdateAll = true;
                                ic.selByCommCls.selectBySpec('select :3' + resn, commandname, commandname, false, bNoUpdateAll);
                                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, prevHAtoms);
                                ic.hlUpdateCls.updateHlAll(resn, undefined, true, true);
                            }
                            else {
                                ic.selectionCls.selectAChain(chainid, chainid, false, true);
                            }
                        }
                        else {
                            ic.currSelectedSets = [chainid];
                            if(resn) {
                                let bNoUpdateAll = true;
                                ic.selByCommCls.selectBySpec('select :3' + resn, commandname, commandname, false, bNoUpdateAll);
                                ic.hlUpdateCls.updateHlAll(resn, undefined, true, true);
                            }
                            else {
                                ic.selectionCls.selectAChain(chainid, chainid, false);
                            }
                        }

                        if(resn) {
                            me.htmlCls.clickMenuCls.setLogCmd('select :3' + resn, true);
                        }
                        else {
                            me.htmlCls.clickMenuCls.setLogCmd('select chain ' + chainid, true);
                        }

                        let setNames = ic.currSelectedSets.join(' or ');
                        //if(ic.currSelectedSets.length > 1) me.htmlCls.clickMenuCls.setLogCmd('select saved atoms ' + setNames, true);
                        if(ic.currSelectedSets.length > 1) me.htmlCls.clickMenuCls.setLogCmd('select sets ' + setNames, true);
                    }
                    else {
                        let residueidHash = {};
                        if($(that).attr('domain') !== undefined || $(that).attr('feat') !== undefined || $(that).attr('3ddomain') !== undefined || $(that).attr('custom') !== undefined) {
                            ic.hlUpdateCls.hlSummaryDomain3ddomain(that);

                            let fromArray = $(that).attr('from').split(',');
                            let toArray = $(that).attr('to').split(',');

                            // protein chains
                            let residueid, from, to;
                            chainid.substr(0, chainid.indexOf('_'));
                            for(let i = 0, il = fromArray.length; i < il; ++i) {
                                from = parseInt(fromArray[i]);
                                to = parseInt(toArray[i]);

                                for(let j = from; j <= to; ++j) {
                                    /*
                                    if( ($(that).attr('domain') !== undefined || $(that).attr('feat') !== undefined || $(that).attr('3ddomain') !== undefined) ) {
                                        let residNCBI = chainid + '_' + (j+1).toString();
                                        // AlphaFold domains calculated on-the-fly have no conversion
                                        // if(structure.length > 5) {
                                        //     residueid = residNCBI;
                                        // }
                                        // else if(ic.ncbi2resid[residNCBI]) {
                                        //     residueid = ic.ncbi2resid[residNCBI];
                                        // }
                                        // else {
                                        //     residueid = residNCBI;
                                        // }

                                        residueid = ic.ncbi2resid[residNCBI];
                                    }
                                    */
                                    
                                    if(($(that).attr('domain') !== undefined || $(that).attr('feat') !== undefined) || $(that).attr('3ddomain') !== undefined) {
                                        let residNCBI = chainid + '_' + (j+1).toString();
                                        residueid = ic.ncbi2resid[residNCBI];
                                    }
                                    else {
                                        residueid = chainid + '_' + (j+1).toString();
                                    }

                                    residueidHash[residueid] = 1;

                                    //atomHash = me.hashUtilsCls.unionHash(atomHash, ic.residues[residueid]);
                                }
                            }

                            if(ic.bCtrl || ic.bShift) {
                                ic.selectionCls.selectResidueList(residueidHash, commandname, commanddescr, true);
                            }
                            else {
                                ic.selectionCls.selectResidueList(residueidHash, commandname, commanddescr, false);
                            }
                            //ic.hlUpdateCls.updateHlAll();

                            residueid = chainid + '_' + parseInt((from + to)/2).toString();
                            //residueid = chainid + '_' + from.toString();
                            position = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
                        }
                        //else if($(that).attr('site') !== undefined || $(that).attr('clinvar') !== undefined) {
                        else if($(that).attr('posarray') !== undefined) {
                            let posArray = $(that).attr('posarray').split(',');
                            //ic.hAtoms = {}

                            //removeAllLabels();

                            //var  atomHash = {}, residueidHash = {}
                            let residueid;
                            chainid.substr(0, chainid.indexOf('_'));
                            for(let i = 0, il = posArray.length; i < il; ++i) {
                                if($(that).attr('site') !== undefined || $(that).attr('ptm') !== undefined) {
                                    // if(ic.bNCBI) {
                                        let residNCBI = chainid + '_' +(parseInt(posArray[i])+1).toString();
                                        // AlphaFold domains calculated on-the-fly have no conversion
                                        // if(structure.length > 5) {
                                        //     residueid = residNCBI;
                                        // }
                                        // else if(ic.ncbi2resid[residNCBI]) {
                                        //     residueid = ic.ncbi2resid[residNCBI];
                                        // }
                                        // else {
                                        //     residueid = residNCBI;
                                        // }

                                        residueid = ic.ncbi2resid[residNCBI];
                                    // }
                                    // else {
                                    //     residueid = chainid + '_' +(parseInt(posArray[i])+1).toString();
                                    // }
                                }
                                //else if($(that).attr('clinvar') !== undefined) {
                                else {
                                    residueid = chainid + '_' + posArray[i];
                                }

                                residueidHash[residueid] = 1;
                                //atomHash = me.hashUtilsCls.unionHash(atomHash, ic.residues[residueid]);
                            }

                            if(ic.bCtrl || ic.bShift) {
                                ic.selectionCls.selectResidueList(residueidHash, commandname, commanddescr, true);
                            }
                            else {
                                ic.selectionCls.selectResidueList(residueidHash, commandname, commanddescr, false);
                            }

                            residueid = chainid + '_' + posArray[parseInt((0 + posArray.length)/2)].toString();
                            //residueid = chainid + '_' + posArray[0].toString();
                            position = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
                        }

                        //removeAllLabels
                        for(let name in ic.labels) {
                            if(name !== 'schematic' && name !== 'distance') {
                               ic.labels[name] = [];
                            }
                        }

                        //var size = parseInt(ic.LABELSIZE * 10 / commandname.length);
                        let size = ic.LABELSIZE;
                        let color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd; //"FFFF00";
                        if(position !== undefined) ic.analysisCls.addLabel(commanddescr, position.center.x, position.center.y, position.center.z, size, color, undefined, 'custom');

                        ic.drawCls.draw();

                        me.htmlCls.clickMenuCls.setLogCmd('select ' + ic.resid2specCls.residueids2spec(Object.keys(residueidHash)) + ' | name ' + commandname, true);

                        if(ic.bCtrl || ic.bShift) {
                            ic.currSelectedSets.push(commandname);
                        }
                        else {
                            ic.currSelectedSets = [commandname];
                        }

                        let setNames = ic.currSelectedSets.join(' or ');
                        //if(ic.currSelectedSets.length > 1) me.htmlCls.clickMenuCls.setLogCmd('select saved atoms ' + setNames, true);
                        if(ic.currSelectedSets.length > 1) me.htmlCls.clickMenuCls.setLogCmd('select sets ' + setNames, true);
                    } // if($(that).attr('gi') !== undefined) {
                } // if($(that).hasClass('icn3d-highlightSeq')) {
            } // if(!ic.bAnnotations) {
        } // if($(that).hasClass('icn3d-highlightSeq')) {
        else {
            ic.hlObjectsCls.removeHlObjects();
            ic.hlUpdateCls.removeHl2D();

           $("#" + ic.pre + "atomsCustom").val("");
        }

      }
    }

    selectResidues(id, that) { let ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      if(ic.bSelectResidue === false && !ic.bShift && !ic.bCtrl) {
          ic.selectionCls.removeSelection();
      }

      if(id !== undefined && id !== '') {
        // add "align_" in front of id so that full sequence and aligned sequence will not conflict
        //if(id.substr(0, 5) === 'align') id = id.substr(5);

        // seq_div0_1TSR_A_1, align_div0..., giseq_div0..., snp_div0..., interaction_div0..., cddsite_div0..., domain_div0...
        id = id.substr(id.indexOf('_') + 1);

        ic.bSelectResidue = true;

        $(that).toggleClass('icn3d-highlightSeq');

        let residueid = id.substr(id.indexOf('_') + 1);

        if(ic.residues.hasOwnProperty(residueid)) {
            if($(that).hasClass('icn3d-highlightSeq')) {
              for(let j in ic.residues[residueid]) {
                ic.hAtoms[j] = 1;
              }

              ic.selectedResidues[residueid] = 1;

              if(ic.bAnnotations && $(that).attr('disease') !== undefined) {
                  let label = $(that).attr('disease');

                  let position = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
                  //position.center.add(new THREE.Vector3(3.0, 3.0, 3.0)); // shift a little bit

                  let maxlen = 15;
                  if(label.length > maxlen) label = label.substr(0, maxlen) + '...';

                  //var size = parseInt(ic.LABELSIZE * 10 / label.length);
                  let size = ic.LABELSIZE;
                  let color = me.htmlCls.GREYD;
                  ic.analysisCls.addLabel(label, position.center.x, position.center.y, position.center.z, size, color, undefined, 'custom');
              }
            }
            else {
                for(let i in ic.residues[residueid]) {
                  //ic.hAtoms[i] = undefined;
                  delete ic.hAtoms[i];
                }
                //ic.selectedResidues[residueid] = undefined;
                delete ic.selectedResidues[residueid];

                ic.hlObjectsCls.removeHlObjects();
            }
        }
      }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class HlUpdate {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //The 2D diagram only shows the currently displayed chains when users click the option "View Only Selection".
    //This method is called to dynamically update the content of the 2D interaction diagram.
    update2DdgmContent() { let ic = this.icn3d, me = ic.icn3dui;
       // update 2D diagram to show just the displayed parts
       let html2ddgm = '';
       if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined) {
          html2ddgm += ic.diagram2dCls.draw2Ddgm(ic.interactionData, ic.inputid, undefined, true);
          html2ddgm += ic.diagram2dCls.set2DdgmNote();

          $("#" + ic.pre + "dl_2ddgm").html(html2ddgm);
       }
       else if(ic.mmdbidArray &&(me.cfg.align !== undefined || me.cfg.chainalign !== undefined || ic.bRealign)) {
          html2ddgm += ic.diagram2dCls.draw2Ddgm(ic.interactionData1, ic.mmdbidArray[0].toUpperCase(), 0, true);
          if(ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t) {
              html2ddgm += ic.diagram2dCls.draw2Ddgm(ic.interactionData2, ic.mmdbidArray[0].toUpperCase(), 1, true);
          }
          else {
              html2ddgm += ic.diagram2dCls.draw2Ddgm(ic.interactionData2, ic.mmdbidArray[1].toUpperCase(), 1, true);
          }
          html2ddgm += ic.diagram2dCls.set2DdgmNote(true);

          $("#" + ic.pre + "dl_2ddgm").html(html2ddgm);
       }
    }

    //Change the residue color in the annotation window for the residues in the array "residueArray".
    changeSeqColor(residueArray) { let ic = this.icn3d, me = ic.icn3dui;
       for(let i = 0, il = residueArray.length; i < il; ++i) {
           let pickedResidue = residueArray[i];
           //[id$= is expensive
           //if($("[id$=" + ic.pre + pickedResidue + "]").length !== 0) {
             let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[pickedResidue]);
             if(!atom) continue;

             let colorStr =(atom.color === undefined || atom.color.getHexString().toUpperCase() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
             let color =(atom.color !== undefined) ? colorStr : "CCCCCC";
             // annotations will have their own color, only the chain will have the changed color
             $("[id=giseq_" + ic.pre + pickedResidue + "]").attr('style', 'color:#' + color);
             $("[id=align_" + ic.pre + pickedResidue + "]").attr('style', 'color:#' + color);
             if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined || ic.bRealign || ic.bSymd) $("[id=align_" + ic.pre + pickedResidue + "]").attr('style', 'color:#' + color);
           //}
       }
    }

    //Remove the highlight in 3D structure, 2D interaction, 1D sequence, and the menu of defined sets.
    removeHlAll() { let ic = this.icn3d; ic.icn3dui;
           this.removeHlObjects();
           this.removeHlSeq();
           this.removeHl2D();
           this.removeHlMenus();
    }

    //Remove the highlight in the 3D structure display.
    removeHlObjects() { let ic = this.icn3d; ic.icn3dui;
           ic.hlObjectsCls.removeHlObjects();
    }

    //Remove the highlight in the sequence display of the annotation window.
    removeHlSeq() { let ic = this.icn3d; ic.icn3dui;
    //       this.removeSeqChainBkgd();
           this.removeSeqResidueBkgd();
    }

    //Remove the highlight in the 2D interaction diagram.
    removeHl2D() { let ic = this.icn3d; ic.icn3dui;
          // clear nodes in 2d dgm
          $("#" + ic.pre + "dl_2ddgm rect").attr('stroke', '#000000');
          $("#" + ic.pre + "dl_2ddgm circle").attr('stroke', '#000000');
          $("#" + ic.pre + "dl_2ddgm polygon").attr('stroke', '#000000');

          $("#" + ic.pre + "dl_2ddgm rect").attr('stroke-width', 1);
          $("#" + ic.pre + "dl_2ddgm circle").attr('stroke-width', 1);
          $("#" + ic.pre + "dl_2ddgm polygon").attr('stroke-width', 1);

          if($("#" + ic.pre + "dl_2ddgm circle").length > 0) {
              $("#" + ic.pre + "dl_2ddgm svg line").attr('stroke', '#000000');
              $("#" + ic.pre + "dl_2ddgm line").attr('stroke-width', 1);
          }
    }

    //Remove the selection in the menu of defined sets.
    removeHlMenus() { let ic = this.icn3d; ic.icn3dui;
        $("#" + ic.pre + "atomsCustom").val("");
        $("#" + ic.pre + "atomsCustom")[0].blur();
    }

    //Update the highlight of 3D structure, 2D interaction, sequences, and the menu of defined sets
    //according to the current highlighted atoms.
    updateHlAll(commandnameArray, bSetMenu, bUnion, bForceHighlight) { let ic = this.icn3d, me = ic.icn3dui;
       // update the previously highlisghted atoms for switching between all and selection
       ic.prevHighlightAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

       this.updateHlObjects(bForceHighlight);

       if(commandnameArray !== undefined) {
           this.updateHlSeqInChain(commandnameArray, bUnion);
       }
       else {
           this.updateHlSeq(undefined, undefined, bUnion);
       }

       this.updateHl2D();
       if(bSetMenu === undefined || bSetMenu) this.updateHlMenus(commandnameArray);

       //ic.annotationCls.showAnnoSelectedChains();
    }

    //Update the highlight of 3D structure display according to the current highlighted atoms.
    updateHlObjects(bForceHighlight) { let ic = this.icn3d; ic.icn3dui;
       ic.hlObjectsCls.removeHlObjects();

       if((ic.hAtoms && ic.atoms && Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) || bForceHighlight) {
          ic.hlObjectsCls.addHlObjects();
          ic.definedSetsCls.setMode('selection');
       }
    }

    // update highlight in sequence, slow if sequence is long
    //Update the highlight of sequences in the annotation window according to the current highlighted atoms.
    updateHlSeq(bShowHighlight, residueHash, bUnion) { let ic = this.icn3d; ic.icn3dui;
           if(bUnion === undefined || !bUnion) {
               this.removeHlSeq();
           }

           if(residueHash === undefined) residueHash = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(ic.hAtoms);

           if(ic.hAtoms && ic.atoms && Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) this.hlSequence(Object.keys(residueHash));
           this.changeSeqColor(Object.keys(residueHash));
    }

    updateHlSeqInChain(commandnameArray, bUnion) { let ic = this.icn3d; ic.icn3dui;
           if(bUnion === undefined || !bUnion) {
               this.removeHlSeq();
           }
           //if(residueHash === undefined) residueHash = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(ic.hAtoms);

           if(ic.hAtoms && ic.atoms && Object.keys(ic.hAtoms).length == Object.keys(ic.atoms).length) return;

           //this.hlSequence(Object.keys(residueHash));
           // speed up with chain highlight
           for(let i = 0, il = commandnameArray.length; i < il; ++i) {
               let commandname = commandnameArray[i];
               if(Object.keys(ic.chains).indexOf(commandname) !== -1) {
                   this.hlSeqInChain(commandname);
               }
               else {
                   let residueArray = [];

                   if(ic.defNames2Residues[commandname] !== undefined && ic.defNames2Residues[commandname].length > 0) {
                       residueArray = ic.defNames2Residues[commandname];
                   }

                   let residueHash = {};
                   if(ic.defNames2Atoms[commandname] !== undefined && ic.defNames2Atoms[commandname].length > 0) {
                       for(let j = 0, jl = ic.defNames2Atoms[commandname].length; j < jl; ++j) {
                           let serial = ic.defNames2Atoms[commandname][j];
                           let atom = ic.atoms[serial];
                           let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;

                           residueHash[resid] = 1;
                       }

                       residueArray = residueArray.concat(Object.keys(residueHash));
                   }

                   this.hlSequence(residueArray);
               }
           }

           //this.changeSeqColor(Object.keys(residueHash));
    }

    // update highlight in 2D window
    //Update the highlight of 2D interaction diagram according to the current highlighted atoms.
    updateHl2D(chainArray2d) { let ic = this.icn3d, me = ic.icn3dui;
      this.removeHl2D();

      if(ic.hAtoms && ic.atoms && Object.keys(ic.hAtoms).length == Object.keys(ic.atoms).length) return;

      if(chainArray2d === undefined) {
          let chainHash = ic.firstAtomObjCls.getChainsFromAtoms(ic.hAtoms);
          chainArray2d = Object.keys(chainHash);
      }

      if(chainArray2d !== undefined) {
          for(let i = 0, il = chainArray2d.length; i < il; ++i) {
              let hlatoms = me.hashUtilsCls.intHash(ic.chains[chainArray2d[i]], ic.hAtoms);
              let ratio = 1.0 * Object.keys(hlatoms).length / Object.keys(ic.chains[chainArray2d[i]]).length;

              let firstAtom = ic.firstAtomObjCls.getFirstCalphaAtomObj(hlatoms);
              if(ic.alnChains[chainArray2d[i]] !== undefined) {
                    let alignedAtoms = me.hashUtilsCls.intHash(ic.alnChains[chainArray2d[i]], hlatoms);
                    if(Object.keys(alignedAtoms).length > 0) firstAtom = ic.firstAtomObjCls.getFirstCalphaAtomObj(alignedAtoms);
                }
              let color =(firstAtom !== undefined && firstAtom.color !== undefined) ? '#' + firstAtom.color.getHexString() : '#FFFFFF';

              let target = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] rect[class='icn3d-hlnode']");
              let base = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] rect[class='icn3d-basenode']");
              if(target !== undefined) {
                  ic.diagram2dCls.highlightNode('rect', target, base, ratio);
                  $(target).attr('fill', color);
              }

              target = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] circle[class='icn3d-hlnode']");
              base = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] circle[class='icn3d-basenode']");
              if(target !== undefined) {
                    ic.diagram2dCls.highlightNode('circle', target, base, ratio);
                    $(target).attr('fill', color);
              }

              target = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] ellipse[class='icn3d-hlnode']");
              //base = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] ellipse[class='icn3d-basenode']");
              if(target !== undefined) {
                    ic.diagram2dCls.highlightNode('ellipse', target, undefined, ratio);
                    //$(target).attr('fill', color);
              }

              target = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] polygon[class='icn3d-hlnode']");
              base = $("#" + ic.pre + "dl_2ddgm g[chainid=" + chainArray2d[i] + "] polygon[class='icn3d-basenode']");

              if(target !== undefined) {
                  ic.diagram2dCls.highlightNode('polygon', target, base, ratio);
                  $(target).attr('fill', color);
              }
          }
      }

      if(ic.lineArray2d !== undefined) {
          for(let i = 0, il = ic.lineArray2d.length; i < il; i += 2) {
              $("#" + ic.pre + "dl_2ddgm g[chainid1=" + ic.lineArray2d[i] + "][chainid2=" + ic.lineArray2d[i + 1] + "] line").attr('stroke', me.htmlCls.ORANGE);
          }
      }

      // update the previously highlisghted atoms for switching between all and selection
      ic.prevHighlightAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

      ic.definedSetsCls.setMode('selection');
    }

    // update highlight in the menu of defined sets
    //Update the selection in the menu of defined sets according to the current highlighted atoms.
    updateHlMenus(commandnameArray) { let ic = this.icn3d; ic.icn3dui;
        if(commandnameArray === undefined) commandnameArray = [];

        let definedAtomsHtml = ic.definedSetsCls.setAtomMenu(commandnameArray);

        if($("#" + ic.pre + "atomsCustom").length) {
            $("#" + ic.pre + "atomsCustom").html(definedAtomsHtml);
            $("#" + ic.pre + "atomsCustom")[0].blur();
        }
    }

    hlSequence(residueArray) { let ic = this.icn3d; ic.icn3dui;
       // update annotation windows and alignment sequences
       let chainHash = {};
       for(let i = 0, il = residueArray.length; i < il; ++i) {
           let pickedResidue = residueArray[i].trim();
           //[id$= is expensive to search id ending with
           //var resElem = $("[id$=" + ic.pre + pickedResidue + "]");
           let resElem = $("[id=giseq_" + ic.pre + pickedResidue + "]");
           if(resElem.length !== 0) {
             resElem.addClass('icn3d-highlightSeq');
           }

           resElem = $("[id=align_" + ic.pre + pickedResidue + "]");
           if(resElem.length !== 0) {
             resElem.addClass('icn3d-highlightSeq');
           }

           let pos = pickedResidue.lastIndexOf('_');
           let chainid = pickedResidue.substr(0, pos);

           chainHash[chainid] = 1;
       }

       for(let chainid in chainHash) {
           if($("#giseq_summary_" + ic.pre + chainid).length !== 0) {
             $("#giseq_summary_" + ic.pre + chainid).addClass('icn3d-highlightSeqBox');
           }
       }
    }

    hlSeqInChain(chainid) { let ic = this.icn3d; ic.icn3dui;
       // update annotation windows and alignment sequences
       for(let i = 0, il = ic.chainsSeq[chainid].length; i < il; ++i) {
           let resi = ic.chainsSeq[chainid][i].resi;
           let pickedResidue = chainid + '_' + resi;

           //if($("[id$=" + ic.pre + pickedResidue + "]").length !== 0) {
           //  $("[id$=" + ic.pre + pickedResidue + "]").addClass('icn3d-highlightSeq');
           //}
           // too expensive to highlight all annotations
           if($("#giseq_" + ic.pre + pickedResidue).length !== 0) {
             $("#giseq_" + ic.pre + pickedResidue).addClass('icn3d-highlightSeq');
           }
           if($("#align_" + ic.pre + pickedResidue).length !== 0) {
             $("#align_" + ic.pre + pickedResidue).addClass('icn3d-highlightSeq');
           }
       }

       if($("#giseq_summary_" + ic.pre + chainid).length !== 0) {
         $("#giseq_summary_" + ic.pre + chainid).addClass('icn3d-highlightSeqBox');
       }
    }

    toggleHighlight() { let ic = this.icn3d; ic.icn3dui;
        //me.htmlCls.clickMenuCls.setLogCmd("toggle highlight", true);

        //if(ic.prevHighlightObjects.length > 0 || ic.prevHighlightObjects_ghost.length > 0) { // remove
        if(ic.bShowHighlight) { // remove
            this.clearHighlight();
            ic.bShowHighlight = false;
        }
        else { // add
            this.showHighlight();
            ic.bShowHighlight = true;
        }

        //me.htmlCls.clickMenuCls.setLogCmd("toggle highlight", true);
    }

    clearHighlight() { let ic = this.icn3d; ic.icn3dui;
        ic.labels['picking']=[];
        ic.drawCls.draw();

        ic.hlObjectsCls.removeHlObjects();
        this.removeHl2D();
        if(ic.bRender) ic.drawCls.render();

        this.removeSeqChainBkgd();
        this.removeSeqResidueBkgd();

        ic.bSelectResidue = false;
    }

    showHighlight() { let ic = this.icn3d; ic.icn3dui;
        ic.hlObjectsCls.addHlObjects();
        this.updateHlAll();
        //ic.bSelectResidue = true;
    }

    highlightChains(chainArray) { let ic = this.icn3d; ic.icn3dui;
        ic.hlObjectsCls.removeHlObjects();
        this.removeHl2D();

        ic.hlObjectsCls.addHlObjects();
        this.updateHl2D(chainArray);

        let residueHash = {};
        for(let c = 0, cl = chainArray.length; c < cl; ++c) {
            let chainid = chainArray[c];
            for(let i in ic.chainsSeq[chainid]) { // get residue number
                let resObj = ic.chainsSeq[chainid][i];
                let residueid = chainid + "_" + resObj.resi;

                if(resObj.name !== '' && resObj.name !== '-') {
                  residueHash[residueid] = 1;
                }
            }
        }

        this.hlSequence(Object.keys(residueHash));
    }

    hlSummaryDomain3ddomain(that) { let ic = this.icn3d; ic.icn3dui;
      if($(that).attr('domain') !== undefined) { // domain
        let index = $(that).attr('index');
        let chainid = $(that).attr('chain');

        if($("[id^=" + chainid + "_domain_" + index + "]").length !== 0) {
            $("[id^=" + chainid + "_domain_" + index + "]").addClass('icn3d-highlightSeqBox');
        }
      }

      if($(that).attr('3ddomain') !== undefined) { // 3d domain
        let index = $(that).attr('index');
        let chainid = $(that).attr('chain');

        if($("[id^=" + chainid + "_3d_domain_" + index + "]").length !== 0) {
            $("[id^=" + chainid + "_3d_domain_" + index + "]").addClass('icn3d-highlightSeqBox');
        }
      }
    }

    //Remove the background of the highlighted chain in the sequence dialog.
    removeSeqChainBkgd(currChain) {
      if(currChain === undefined) {
        $( ".icn3d-seqTitle" ).each(function( index ) {
          $( this ).removeClass('icn3d-highlightSeq');
          $( this ).removeClass('icn3d-highlightSeqBox');
        });
      }
      else {
        $( ".icn3d-seqTitle" ).each(function( index ) {
          if($(this).attr('chain') !== currChain) {
              $( this ).removeClass('icn3d-highlightSeq');
              $( this ).removeClass('icn3d-highlightSeqBox');
          }
        });
      }
    }

    //Remove the background of the highlighted residues in the sequence dialog.
    removeSeqResidueBkgd() {
        $( ".icn3d-residue" ).each(function( index ) {
          $( this ).removeClass('icn3d-highlightSeq');
        });
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class HlObjects {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Show the highlight for the selected atoms: hAtoms.
    addHlObjects(color, bRender, atomsHash) { let ic = this.icn3d, me = ic.icn3dui;
       //if(atomsHash === undefined) atomsHash = ic.hAtoms;
       let atomsHashDisplay = (atomsHash) ? me.hashUtilsCls.intHash(atomsHash, ic.dAtoms) : me.hashUtilsCls.intHash(ic.hAtoms, ic.dAtoms);

       ic.applyDisplayCls.applyDisplayOptions(ic.opts, atomsHashDisplay, ic.bHighlight);

       if( (bRender) || (ic.bRender) ) {
           ic.drawCls.render();
       }
    };

    //Remove the highlight. The atom selection does not change.
    removeHlObjects() { let ic = this.icn3d; ic.icn3dui;
       // remove prevous highlight
       for(let i in ic.prevHighlightObjects) {
           if(ic.mdl) ic.mdl.remove(ic.prevHighlightObjects[i]);
       }

       ic.prevHighlightObjects = [];

       // remove prevous highlight
       for(let i in ic.prevHighlightObjects_ghost) {
        if(ic.mdl) ic.mdl.remove(ic.prevHighlightObjects_ghost[i]);
       }

       ic.prevHighlightObjects_ghost = [];
    };

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class LineGraph {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    drawLineGraph(lineGraphStr, bScatterplot) { let ic = this.icn3d, me = ic.icn3dui;
        let html, graph = JSON.parse(lineGraphStr);
        let linkArray = [],
            nodeArray1 = [],
            nodeArray2 = [];
        let name2node = {};
        for(let i = 0, il = graph.nodes.length; i < il; ++i) {
            let node = graph.nodes[i];
            name2node[node.id] = node;
        }
        // only get interaction links
        let nameHash = {};
        for(let i = 0, il = graph.links.length; i < il; ++i) {
            let link = graph.links[i];
            if(link.v == me.htmlCls.hbondValue || link.v == me.htmlCls.ionicValue || link.v == me.htmlCls.halogenValue ||
                link.v == me.htmlCls.picationValue || link.v == me.htmlCls.pistackingValue || link.v == me.htmlCls.contactValue) {
                linkArray.push(link);
                nameHash[link.source] = 1;
                nameHash[link.target] = 1;
            }
        }
        let nodeArrays = ic.getGraphCls.getNodeTopBottom(nameHash, name2node);
        nodeArray1 = nodeArrays.nodeArray1;
        nodeArray2 = nodeArrays.nodeArray2;
        ic.lineGraphStr = '{\n';

        let structureArray = ic.resid2specCls.atoms2structureArray(ic.hAtoms);
        //if(Object.keys(ic.structures).length > 1) {
        if(structureArray.length > 1) {

            let struc2index= {};
            let nodeArray1Split = [], nodeArray2Split = [], linkArraySplit = [], nameHashSplit = [];

            // show common interactions: nodes will be the same. The links/interactins are different.
            // The mapped residue name and number are attached to "id".
            // Original node: {id : "Q24.A.2AJF", r : "1_1_2AJF_A_24", s: "a", ...}
            // Node for common interaction: {id : "Q24.A.2AJF|Q24", r : "1_1_2AJF_A_24", s: "a", ...}
            let nodeArray1SplitCommon = [], nodeArray2SplitCommon = [], linkArraySplitCommon = [], nameHashSplitCommon = [];
            let nodeArray1SplitDiff = [], nodeArray2SplitDiff = [], linkArraySplitDiff = [], nameHashSplitDiff = [];
            let linkedNodeCnt = {};

            for(let i = 0, il = structureArray.length; i < il; ++i) {   
                nodeArray1Split[i] = [];
                nodeArray2Split[i] = [];
                linkArraySplit[i] = [];
                nameHashSplit[i] = {};

                nodeArray1SplitCommon[i] = [];
                nodeArray2SplitCommon[i] = [];
                linkArraySplitCommon[i] = [];
                nameHashSplitCommon[i] = {};

                nodeArray1SplitDiff[i] = [];
                nodeArray2SplitDiff[i] = [];
                linkArraySplitDiff[i] = [];
                nameHashSplitDiff[i] = {};

                struc2index[structureArray[i]] = i;
            }

            for(let i = 0, il = linkArray.length; i < il; ++i) {
                let link = linkArray[i];
                let nodeA = name2node[link.source];
                let nodeB = name2node[link.target];

                if(!nodeA || !nodeB || !nodeA.r || !nodeB.r) {
                    continue;
                }

                let idArrayA = this.getIdArrayFromNode(nodeA);
                let idArrayB = this.getIdArrayFromNode(nodeB);

                let index = struc2index[idArrayA[2]];

                if(idArrayA[2] == structureArray[index] && idArrayB[2] == structureArray[index]) {
                    linkArraySplit[index].push(link);
                    nameHashSplit[index][link.source] = 1;
                    nameHashSplit[index][link.target] = 1;

                    let chainid1 = idArrayA[2] + '_' + idArrayA[3];
                    let chainid2 = idArrayB[2] + '_' + idArrayB[3];
                    let resid1 = chainid1 + '_' + idArrayA[4];
                    let resid2 = chainid2 + '_' + idArrayB[4];

                    let mapping1, mapping2;

                    if(ic.chainsMapping[chainid1] && ic.chainsMapping[chainid1][resid1]
                      && ic.chainsMapping[chainid2] && ic.chainsMapping[chainid2][resid2]) { 
                        mapping1 = (nodeA.s == "a") ? ic.chainsMapping[chainid1][resid1] : ic.chainsMapping[chainid2][resid2];
                        mapping2 = (nodeA.s == "a") ? ic.chainsMapping[chainid2][resid2] : ic.chainsMapping[chainid1][resid1];

                        let mappingid = mapping1 + '_' + mapping2 + '_' + link.c; // link.c determines the interaction type
                        if(!linkedNodeCnt.hasOwnProperty(mappingid)) {
                            linkedNodeCnt[mappingid] = 1;
                        }
                        else {
                            ++linkedNodeCnt[mappingid];
                        }
                    }
                } 
            }

            // set linkArraySplitCommon and nameHashSplitCommon
            // set linkArraySplitDiff and nameHashSplitDiff
            let separatorCommon = "=>", separatorDiff = "==>", postCommon = "-", postDiff = "--";
            for(let i = 0, il = linkArray.length; i < il; ++i) {
                let link = linkArray[i];
                let nodeA = name2node[link.source];
                let nodeB = name2node[link.target];

                if(!nodeA || !nodeB || !nodeA.r || !nodeB.r) {
                    continue;
                }

                let idArrayA = this.getIdArrayFromNode(nodeA);
                let idArrayB = this.getIdArrayFromNode(nodeB);

                let index = struc2index[idArrayA[2]];

                if(idArrayA[2] == structureArray[index] && idArrayB[2] == structureArray[index]) {
                    let chainid1 = idArrayA[2] + '_' + idArrayA[3];
                    let chainid2 = idArrayB[2] + '_' + idArrayB[3];
                    let resid1 = chainid1 + '_' + idArrayA[4];
                    let resid2 = chainid2 + '_' + idArrayB[4];

                    let mapping1, mapping2;
                    
                    if(ic.chainsMapping[chainid1] && ic.chainsMapping[chainid1][resid1]
                      && ic.chainsMapping[chainid2] && ic.chainsMapping[chainid2][resid2]) { 
                        mapping1 = (nodeA.s == "a") ? ic.chainsMapping[chainid1][resid1] : ic.chainsMapping[chainid2][resid2];
                        mapping2 = (nodeA.s == "a") ? ic.chainsMapping[chainid2][resid2] : ic.chainsMapping[chainid1][resid1];

                        let mappingid = mapping1 + '_' + mapping2 + '_' + link.c; // link.c determines the interaction type

                        let linkCommon = me.hashUtilsCls.cloneHash(link);
                        linkCommon.source += (ic.chainsMapping[chainid1][resid1]) ? separatorCommon + ic.chainsMapping[chainid1][resid1] : separatorCommon + postCommon;
                        linkCommon.target += (ic.chainsMapping[chainid2][resid2]) ? separatorCommon + ic.chainsMapping[chainid2][resid2] : separatorCommon + postCommon;

                        let linkDiff = me.hashUtilsCls.cloneHash(link);
                        linkDiff.source += (ic.chainsMapping[chainid1][resid1]) ? separatorDiff + ic.chainsMapping[chainid1][resid1] : separatorDiff + postDiff;
                        linkDiff.target += (ic.chainsMapping[chainid2][resid2]) ? separatorDiff + ic.chainsMapping[chainid2][resid2] : separatorDiff + postDiff;
                    
                        if(linkedNodeCnt[mappingid] == structureArray.length) {
                            linkArraySplitCommon[index].push(linkCommon);
                        }  
                        else {
                            linkArraySplitDiff[index].push(linkDiff);
                        }

                        // use the original node names and thus use the original link
                        nameHashSplitCommon[index][link.source] = ic.chainsMapping[chainid1][resid1];
                        nameHashSplitCommon[index][link.target] = ic.chainsMapping[chainid2][resid2];
 
                        nameHashSplitDiff[index][link.source] = ic.chainsMapping[chainid1][resid1];
                        nameHashSplitDiff[index][link.target] = ic.chainsMapping[chainid2][resid2];
                    }
                } 
            }

            let len1Split = [], len2Split = [], maxWidth = 0;
            let strucArray = [];
            let bCommonDiff = 1;
            for(let i = 0, il = structureArray.length; i < il; ++i) {  
                let nodeArraysTmp = ic.getGraphCls.getNodeTopBottom(nameHashSplit[i], name2node);
                nodeArray1Split[i] = nodeArraysTmp.nodeArray1;
                nodeArray2Split[i] = nodeArraysTmp.nodeArray2;

                if(Object.keys(ic.chainsMapping).length > 0) { 
                    // common interactions
                    bCommonDiff = 1;
                    nodeArraysTmp = ic.getGraphCls.getNodeTopBottom(nameHashSplit[i], name2node, undefined, bCommonDiff, nameHashSplitCommon[i]);
                    nodeArray1SplitCommon[i] = nodeArraysTmp.nodeArray1;
                    nodeArray2SplitCommon[i] = nodeArraysTmp.nodeArray2;
                    name2node = me.hashUtilsCls.unionHash(name2node, nodeArraysTmp.name2node);

                    // different interactions
                    bCommonDiff = 2;
                    nodeArraysTmp = ic.getGraphCls.getNodeTopBottom(nameHashSplit[i], name2node, undefined, bCommonDiff, nameHashSplitDiff[i]);
                    nodeArray1SplitDiff[i] = nodeArraysTmp.nodeArray1;
                    nodeArray2SplitDiff[i] = nodeArraysTmp.nodeArray2;
                    name2node = me.hashUtilsCls.unionHash(name2node, nodeArraysTmp.name2node);
                }
                
                len1Split[i] = nodeArray1Split[i].length;
                len2Split[i] = nodeArray2Split[i].length;
                
                maxWidth = Math.max(maxWidth, len2Split[i]);

                //if(linkArraySplit[i].length > 0) strucArray.push(structureArray[i]);
                strucArray.push(structureArray[i]);
            }

            let factor = 1;
            let r = 3 * factor;
            let gap = 7 * factor;
            let height, width, heightAll;
            let marginX = 10,
                marginY = 10,
                legendWidth = 30,
                textHeight = 20;
            
            if(bScatterplot) {
                //heightAll =(len1a + 2 + len2a + 2) *(r + gap) + 4 * marginY + 2 * legendWidth;
                //width =(Math.max(len1b, len2b) + 2) *(r + gap) + 2 * marginX + legendWidth;
                heightAll =(me.utilsCls.sumArray(len1Split) + 2*strucArray.length) *(r + gap) + 4 * marginY 
                  + 2 * legendWidth + textHeight*strucArray.length;

                width = (maxWidth + 2) * (r + gap) + 2 * marginX + legendWidth;
                  
            } else {
                height = 110 + textHeight;
                heightAll = height * strucArray.length;

                width = (maxWidth + 2) * (r + gap) + 2 * marginX;
            }

            // show common and diff interaction as well
            if(Object.keys(ic.chainsMapping).length > 0) heightAll *= 3;

            let id, graphWidth;
            if(bScatterplot) {
                ic.scatterplotWidth = 2 * width;
                graphWidth = ic.scatterplotWidth;
                id = me.scatterplotid;
            } else {
                ic.linegraphWidth = 2 * width;
                graphWidth = ic.linegraphWidth;
                id = me.linegraphid;
            }
            html =(strucArray.length == 0) ? "No interactions found for each structure<br><br>" :
                "2D integration graph for " + strucArray.length + " structure(s) <b>" + strucArray + "</b>. There are three sections: \"Interactions\", \"Common interactions\", and \"Different interactions\". Each section has " + strucArray.length + " graphs.<br><br>";
            html += "<svg id='" + id + "' viewBox='0,0," + width + "," + heightAll + "' width='" + graphWidth + "px'>";

            let result, heightFinal = 0;            
 
            bCommonDiff = 0; // 0: all interactions, 1: common interactions, 2: different interactions
            result = this.drawGraphPerType(bCommonDiff, structureArray, bScatterplot, nodeArray1Split, nodeArray2Split, linkArraySplit, name2node, heightFinal, height, textHeight, len1Split, r, gap, marginY);

            heightFinal = result.heightFinal;
            html += result.html;

            if(Object.keys(ic.chainsMapping).length > 0) {
                bCommonDiff = 1;
                result = this.drawGraphPerType(bCommonDiff, structureArray, bScatterplot, nodeArray1SplitCommon, nodeArray2SplitCommon, linkArraySplitCommon, name2node, heightFinal, height, textHeight, len1Split, r, gap, marginY);

                heightFinal = result.heightFinal;
                html += result.html;

                bCommonDiff = 2;
                result = this.drawGraphPerType(bCommonDiff, structureArray, bScatterplot, nodeArray1SplitDiff, nodeArray2SplitDiff, linkArraySplitDiff, name2node, heightFinal, height, textHeight, len1Split, r, gap, marginY);

                heightFinal = result.heightFinal;
                html += result.html;
            }
            
            html += "</svg>";
        } else {
            if(!bScatterplot) {
                //let struc1 = Object.keys(ic.structures)[0];
                let struc1 = structureArray[0];

                let len1 = nodeArray1.length,
                    len2 = nodeArray2.length;
                let factor = 1;
                let r = 3 * factor;
                let gap = 7 * factor;
                let height = 110;
                let margin = 10;
                let width =(len1 > len2) ? len1 *(r + gap) + 2 * margin : len2 *(r + gap) + 2 * margin;
                ic.linegraphWidth = 2 * width;
                html =(linkArray.length > 0) ? "" : "No interactions found for these two sets<br><br>";
                html += "<svg id='" + me.linegraphid + "' viewBox='0,0," + width + "," + height + "' width='" + ic.linegraphWidth + "px'>";
                html += this.drawLineGraph_base(nodeArray1, nodeArray2, linkArray, name2node, 0);
                ic.lineGraphStr += ic.getGraphCls.updateGraphJson(struc1, 1, nodeArray1, nodeArray2, linkArray);
                html += "</svg>";
            } else {
                //let struc1 = Object.keys(ic.structures)[0];
                let struc1 = structureArray[0];

                let len1 = nodeArray1.length,
                    len2 = nodeArray2.length;
                let factor = 1;
                let r = 3 * factor;
                let gap = 7 * factor;
                let width, heightAll;
                let marginX = 10,
                    marginY = 10,
                    legendWidth = 30;
                heightAll =(len1 + 2) *(r + gap) + 2 * marginY + legendWidth;
                width =(len2 + 2) *(r + gap) + 2 * marginX + legendWidth;

                let id, graphWidth;
                ic.scatterplotWidth = 2 * width;
                graphWidth = ic.scatterplotWidth;
                id = me.scatterplotid;
                html =(linkArray.length > 0) ? "" : "No interactions found for these two sets<br><br>";
                html += "<svg id='" + id + "' viewBox='0,0," + width + "," + heightAll + "' width='" + graphWidth + "px'>";
                html += this.drawScatterplot_base(nodeArray1, nodeArray2, linkArray, name2node, 0);
                ic.lineGraphStr += ic.getGraphCls.updateGraphJson(struc1, 1, nodeArray1, nodeArray2, linkArray);
                html += "</svg>";
            }
        }
        ic.lineGraphStr += '}\n';
        ic.scatterplotStr = ic.lineGraphStr;
        if(bScatterplot) {
            $("#" + ic.pre + "scatterplotDiv").html(html);
        } else {
            $("#" + ic.pre + "linegraphDiv").html(html);
        }
        return html;
    }

    drawGraphPerType(bCommonDiff, structureArray, bScatterplot, nodeArray1, nodeArray2, linkArray, name2node, heightFinal, height, textHeight, len1Split, r, gap, marginY) { let ic = this.icn3d; ic.icn3dui;
        let html = "";

        // draw common interaction
        let label, postfix;
        if(bCommonDiff == 0) {
            label = "Interactions in ";
            postfix = "";
        }
        else if(bCommonDiff == 1) {
            label = "Common interactions in ";
            postfix = "_common";
        }
        else if(bCommonDiff == 2) {
            label = "Different interactions in ";
            postfix = "_diff";
        }

        for(let i = 0, il = structureArray.length; i < il; ++i) {  
            if(bScatterplot) {
                html += this.drawScatterplot_base(nodeArray1[i], nodeArray2[i], linkArray[i], name2node, heightFinal, undefined, label + structureArray[i], textHeight);
                height =(len1Split[i] + 1) *(r + gap) + 2 * marginY + textHeight;
            } else {
                html += this.drawLineGraph_base(nodeArray1[i], nodeArray2[i], linkArray[i], name2node, heightFinal, label + structureArray[i], textHeight);
            }
            heightFinal += height;

            if(bCommonDiff) { // very beginning
                if(i > 0) ic.lineGraphStr += ', \n';
            }
            else {
                ic.lineGraphStr += ', \n';
            }
            ic.lineGraphStr += ic.getGraphCls.updateGraphJson(structureArray[i], i + postfix, nodeArray1[i], nodeArray2[i], linkArray[i]);
        }

        return {"heightFinal": heightFinal, "html": html};
    }

    getIdArrayFromNode(node) { let ic = this.icn3d, me = ic.icn3dui;
        let idArray = []; // 1_1_1KQ2_A_1
        idArray.push('');
        idArray.push('');

        let tmpStr = node.r.substr(4); 
        idArray = idArray.concat(me.utilsCls.getIdArray(tmpStr));

        return idArray;
    }

    drawLineGraph_base(nodeArray1, nodeArray2, linkArray, name2node, height, label, textHeight) { let ic = this.icn3d, me = ic.icn3dui;
        let html = '';
        let len1 = nodeArray1.length,
            len2 = nodeArray2.length;
        let factor = 1;
        let r = 3 * factor;
        let gap = 7 * factor;
        let margin = 10;
        // draw nodes
        let margin1, margin2;
        if(len1 > len2) {
            margin1 = margin;
            margin2 = Math.abs(len1 - len2) *(r + gap) * 0.5 + margin;
        } else {
            margin2 = margin;
            margin1 = Math.abs(len1 - len2) *(r + gap) * 0.5 + margin;
        }

        // draw label
        if(label) {
            height += textHeight;
            html += "<text x='" + margin + "' y='" + height + "' style='font-size:8px; font-weight:bold'>" + label + "</text>";
        }

        let h1 = 30 + height,
            h2 = 80 + height;
        let nodeHtml = '';
        let node2posSet1 = {},
            node2posSet2 = {};
        for(let i = 0; i < len1; ++i) {
            nodeHtml += ic.getGraphCls.drawResNode(nodeArray1[i], i, r, gap, margin1, h1, 'a');
            node2posSet1[nodeArray1[i].id] = { x: margin1 + i *(r + gap), y: h1 };
        }
        for(let i = 0; i < len2; ++i) {
            nodeHtml += ic.getGraphCls.drawResNode(nodeArray2[i], i, r, gap, margin2, h2, 'b');
            node2posSet2[nodeArray2[i].id] = { x: margin2 + i *(r + gap), y: h2 };
        }
        // draw lines
        for(let i = 0, il = linkArray.length; i < il; ++i) {
            let link = linkArray[i];
            let node1 = name2node[link.source];
            let node2 = name2node[link.target];

            if(node1 === undefined || node2 === undefined) continue;

            let resid1 = node1.r.substr(4);
            let resid2 = node2.r.substr(4);
            let pos1 = node2posSet1[node1.id];
            let pos2 = node2posSet2[node2.id];
            if(pos1 === undefined || pos2 === undefined) continue;
            let linestrokewidth;
            if(link.v == me.htmlCls.contactValue) {
                linestrokewidth = 1;
            } else {
                linestrokewidth = 2;
            }
            let strokecolor;
            if(link.v == me.htmlCls.hbondValue) {
                strokecolor = "#" + me.htmlCls.hbondColor;
            } else if(link.v == me.htmlCls.ionicValue) {
                strokecolor = "#" + me.htmlCls.ionicColor;
            } else if(link.v == me.htmlCls.halogenValue) {
                strokecolor = "#" + me.htmlCls.halogenColor;
            } else if(link.v == me.htmlCls.picationValue) {
                strokecolor = "#" + me.htmlCls.picationColor;
            } else if(link.v == me.htmlCls.pistackingValue) {
                strokecolor = "#" + me.htmlCls.pistackingColor;
            } else if(link.v == me.htmlCls.contactValue) {
                strokecolor = "#" + me.htmlCls.contactColor;
            }
            html += "<g class='icn3d-interaction' resid1='" + resid1 + "' resid2='" + resid2 + "' >";
            html += "<title>Interaction of residue " + node1.id + " with residue " + node2.id + "</title>";
            html += "<line x1='" + pos1.x + "' y1='" + pos1.y + "' x2='" + pos2.x + "' y2='" + pos2.y + "' stroke='" + strokecolor + "' stroke-width='" + linestrokewidth + "' /></g>";
        }
        // show nodes later
        html += nodeHtml;
        return html;
    }

    drawScatterplot_base(nodeArray1, nodeArray2, linkArray, name2node, height, bContactMap, label, textHeight, bAfMap) { let ic = this.icn3d; ic.icn3dui;
        let html = '';
        let len1 = nodeArray1.length,
            len2 = nodeArray2.length;
        let factor = 1;
        let r = 3 * factor;
        let gap = (bContactMap) ? r : 7 * factor;
        let legendWidth = 30;
        let marginX = 10,
            marginY = 20;
        let heightTotal =(len1 + 1) *(r + gap) + legendWidth + 2 * marginY;

        // draw label
        if(label) {
            height += textHeight;
            html += "<text x='" + marginX + "' y='" + (height + 15).toString() + "' style='font-size:8px; font-weight:bold'>" + label + "</text>";
        }

        let margin1 = height + heightTotal -(legendWidth + marginY +(r + gap)); // y-axis
        let margin2 = legendWidth + marginX +(r + gap); // x-axis

        let nodeHtml = '';
        let node2posSet1 = {},
            node2posSet2 = {};
        let x = legendWidth + marginX;
        for(let i = 0; i < len1; ++i) {
            nodeHtml += ic.getGraphCls.drawResNode(nodeArray1[i], i, r, gap, margin1, x, 'a', true, undefined, bAfMap);
            node2posSet1[nodeArray1[i].id] = { x: x, y: margin1 - i *(r + gap) };
        }
        let y = height + heightTotal -(legendWidth + marginY);
        for(let i = 0; i < len2; ++i) {
            nodeHtml += ic.getGraphCls.drawResNode(nodeArray2[i], i, r, gap, margin2, y, 'b', false, bContactMap, bAfMap);
            node2posSet2[nodeArray2[i].id] = { x: margin2 + i *(r + gap), y: y };
        }
        for(let i = 0, il = linkArray.length; i < il; ++i) {
            let link = linkArray[i];
            let node1 = name2node[link.source];
            let node2 = name2node[link.target];

            if(!node1 || !node2) continue;

            html += this.drawOnePairNode(link, node1, node2, node2posSet1, node2posSet2, bContactMap, bAfMap);

            if(bContactMap && !bAfMap) { // draw symmetric contact map, bAfmap just need to draw once          
                html += this.drawOnePairNode(link, node2, node1, node2posSet1, node2posSet2, bContactMap, bAfMap);
            }
        }
        // show nodes later
        html += nodeHtml;
        return html;
    }

    drawOnePairNode(link, node1, node2, node2posSet1, node2posSet2, bContactMap, bAfMap) { let ic = this.icn3d, me = ic.icn3dui;
        let html = '';

        let factor = 1;
        let r = 3 * factor;
        // draw rect
        let rectSize = (bContactMap) ? 2 * r : 1.5 * r;
        let halfSize = 0.5 * rectSize;

        let resid1 = node1.r.substr(4);
        let resid2 = node2.r.substr(4);
        let pos1 = node2posSet1[node1.id];
        let pos2 = node2posSet2[node2.id];
        if(pos1 === undefined || pos2 === undefined) return html;

        let strokecolor;
        if(link.v == me.htmlCls.hbondValue) {
            strokecolor = "#" + me.htmlCls.hbondColor;
        } else if(link.v == me.htmlCls.ionicValue) {
            strokecolor = "#" + me.htmlCls.ionicColor;
        } else if(link.v == me.htmlCls.halogenValue) {
            strokecolor = "#" + me.htmlCls.halogenColor;
        } else if(link.v == me.htmlCls.picationValue) {
            strokecolor = "#" + me.htmlCls.picationColor;
        } else if(link.v == me.htmlCls.pistackingValue) {
            strokecolor = "#" + me.htmlCls.pistackingColor;
        } else if(link.v == me.htmlCls.contactValue) {
            strokecolor = "#" + me.htmlCls.contactColor;
        }

        if(bContactMap) strokecolor = "#" + link.c;

        let linestrokewidth;
        if(link.v == me.htmlCls.contactValue) {
            linestrokewidth = 1;
        } else {
            linestrokewidth = 2;
        }
        
        if(bAfMap && ic.hex2skip[link.c]) ;
        else if(bAfMap && ic.hex2id[link.c]) {
            ic.hex2id[link.c];
//            html += "<use href='#" + id + "' x='" +(pos2.x - halfSize).toString() + "' y='" +(pos1.y - halfSize).toString() + "' />";

            //html += "<g class='icn3d-interaction' resid1='" + resid1 + "' resid2='" + resid2 + "' >";
            //html += "<title>Interaction of residue " + node1.id + " with residue " + node2.id + "</title>";
            html += "<rect class='icn3d-interaction' resid1='" + resid1 + "' resid2='" + resid2 + "' x='" +(pos2.x - halfSize).toString() + "' y='" +(pos1.y - halfSize).toString() + "' width='" + rectSize + "' height='" + rectSize + "' fill='" + strokecolor + "' stroke-width='" + linestrokewidth + "' stroke='" + strokecolor + "' />";
            //html += "</g>";
        }
        else {
            html += "<g class='icn3d-interaction' resid1='" + resid1 + "' resid2='" + resid2 + "' >";
            html += "<title>Interaction of residue " + node1.id + " with residue " + node2.id + "</title>";
            if(bContactMap) {
                html += "<rect x='" +(pos2.x - halfSize).toString() + "' y='" +(pos1.y - halfSize).toString() + "' width='" + rectSize + "' height='" + rectSize + "' fill='" + strokecolor + "' stroke-width='" + linestrokewidth + "' stroke='" + strokecolor + "' />";
            }
            else {
                html += "<rect x='" +(pos2.x - halfSize).toString() + "' y='" +(pos1.y - halfSize).toString() + "' width='" + rectSize + "' height='" + rectSize + "' fill='" + strokecolor + "' fill-opacity='0.6' stroke-width='" + linestrokewidth + "' stroke='" + strokecolor + "' />";
            }
            html += "</g>";
        }

        return html;
    }

    copyStylesInline(destinationNode, sourceNode) { let ic = this.icn3d; ic.icn3dui;
        let containerElements = ["svg", "g"];
        for(let cd = 0; cd < destinationNode.childNodes.length; cd++) {
            let child = destinationNode.childNodes[cd];
            if(containerElements.indexOf(child.tagName) != -1) {
                this.copyStylesInline(child, sourceNode.childNodes[cd]);
                continue;
            }
            let style = sourceNode.childNodes[cd].currentStyle || window.getComputedStyle(sourceNode.childNodes[cd]);
            if(style == "undefined" || style == null) continue;
            for(let st = 0; st < style.length; st++) {
                child.style.setProperty(style[st], style.getPropertyValue(style[st]));
            }
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class GetGraph {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    getGraphData(atomSet2, atomSet1, nameArray2, nameArray, html, labelType, bAnyAtom) { let ic = this.icn3d, me = ic.icn3dui;
       // get the nodes and links data
       let nodeStr = '', linkStr = '';
       let nodeArray = [], linkArray = [];
       let node_link1 = this.getNodesLinksForSet(atomSet2, labelType, 'a', bAnyAtom);
       let node_link2 = this.getNodesLinksForSet(atomSet1, labelType, 'b', bAnyAtom);

       nodeArray = node_link1.node.concat(node_link2.node);
       // removed duplicated nodes
       let nodeJsonArray = [];
       let checkedNodeidHash = {};
       let cnt = 0;
       for(let i = 0, il = nodeArray.length; i < il; ++i) {
           let node = nodeArray[i];
           let nodeJson = JSON.parse(node);
           if(!checkedNodeidHash.hasOwnProperty(nodeJson.id)) {
               nodeJsonArray.push(nodeJson);
               checkedNodeidHash[nodeJson.id] = cnt;
               ++cnt;
           }
           else {
               let pos = checkedNodeidHash[nodeJson.id];
               nodeJsonArray[pos].s = 'ab'; // appear in both sets
           }
       }
       let nodeStrArray = [];
       for(let i = 0, il = nodeJsonArray.length; i < il; ++i) {
           let nodeJson = nodeJsonArray[i];
           nodeStrArray.push(JSON.stringify(nodeJson));
       }
       nodeStr = nodeStrArray.join(', ');
       // linkStr
       linkArray = node_link1.link.concat(node_link2.link);
       linkStr = linkArray.join(', ');
       // add chemicals, no links for chemicals
       let selectedAtoms = me.hashUtilsCls.unionHash(me.hashUtilsCls.cloneHash(atomSet1), atomSet2);
       let chemicalNodeStr = '';
       let hBondLinkStr = '', ionicLinkStr = '', halogenpiLinkStr = '', contactLinkStr = '',
         disulfideLinkStr = '', crossLinkStr = '';
           // add hydrogen bonds for each set
           if(!(nameArray2.length == 1 && nameArray.length == 1 && nameArray2[0] == nameArray[0])) {
               hBondLinkStr += this.getHbondLinksForSet(atomSet2, labelType);
               hBondLinkStr += this.getHbondLinksForSet(atomSet1, labelType);
           }
           // add ionic interaction for each set
           if(!(nameArray2.length == 1 && nameArray.length == 1 && nameArray2[0] == nameArray[0])) {
               ionicLinkStr += this.getIonicLinksForSet(atomSet2, labelType);
               ionicLinkStr += this.getIonicLinksForSet(atomSet1, labelType);
           }
           // add halogen, pi-cation and pi-stacking for each set
           if(!(nameArray2.length == 1 && nameArray.length == 1 && nameArray2[0] == nameArray[0])) {
               halogenpiLinkStr += this.getHalogenPiLinksForSet(atomSet2, labelType);
               halogenpiLinkStr += this.getHalogenPiLinksForSet(atomSet1, labelType);
           }
           // add contacts for each set
           if(!(nameArray2.length == 1 && nameArray.length == 1 && nameArray2[0] == nameArray[0])) {
               contactLinkStr += this.getContactLinksForSet(atomSet2, labelType);
               contactLinkStr += this.getContactLinksForSet(atomSet1, labelType);
           }
           //else {
           //    contactLinkStr += this.getContactLinksForSet(atomSet1, labelType);
           //}
           // add disulfide bonds
           for(let structure in ic.ssbondpnts) {
               for(let i = 0, il = ic.ssbondpnts[structure].length; i < il; i += 2) {
                   let resid1 = ic.ssbondpnts[structure][i]; //1GPK_A_402
                   let resid2 = ic.ssbondpnts[structure][i+1];
                   let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1]);
                   let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2]);
                   if(selectedAtoms.hasOwnProperty(atom1.serial) && selectedAtoms.hasOwnProperty(atom2.serial)) {
                       let resName1 = me.utilsCls.residueName2Abbr(atom1.resn) + atom1.resi;
                       if(labelType == 'chain' || labelType == 'structure') resName1 += '.' + atom1.chain;
                       if(labelType == 'structure') resName1 += '.' + atom1.structure;
                       let resName2 = me.utilsCls.residueName2Abbr(atom2.resn) + atom2.resi; // + '_' + atom.chain;
                       if(labelType == 'chain' || labelType == 'structure') resName2 += '.' + atom2.chain;
                       if(labelType == 'structure') resName2 += '.' + atom2.structure;
                       disulfideLinkStr += ', {"source": "' + resName1 + '", "target": "' + resName2
                           + '", "v": ' + me.htmlCls.ssbondValue + ', "c": "' + me.htmlCls.ssbondColor + '"}';
                   }
               }
           }
           // add cross linkage
           for(let structure in ic.clbondpnts) {
               for(let i = 0, il = ic.clbondpnts[structure].length; i < il; i += 2) {
                   let resid1 = ic.clbondpnts[structure][i]; //1GPK_A_402
                   let resid2 = ic.clbondpnts[structure][i+1];
                   let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1]);
                   let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2]);
                   if(selectedAtoms.hasOwnProperty(atom1.serial) && selectedAtoms.hasOwnProperty(atom2.serial)) {
                       let resName1 = me.utilsCls.residueName2Abbr(atom1.resn) + atom1.resi;
                       if(labelType == 'chain' || labelType == 'structure') resName1 += '.' + atom1.chain;
                       if(labelType == 'structure') resName1 += '.' + atom1.structure;
                       let resName2 = me.utilsCls.residueName2Abbr(atom2.resn) + atom2.resi; // + '_' + atom.chain;
                       if(labelType == 'chain' || labelType == 'structure') resName2 += '.' + atom2.chain;
                       if(labelType == 'structure') resName2 += '.' + atom2.structure;
                       crossLinkStr += ', {"source": "' + resName1 + '", "target": "' + resName2
                           + '", "v": ' + me.htmlCls.clbondValue + ', "c": "' + me.htmlCls.clbondColor + '"}';
                   }
               }
           }
       let resStr = '{"nodes": [' + nodeStr + chemicalNodeStr + '], "links": [';
       //resStr += linkStr + html + hBondLinkStr + ionicLinkStr + halogenpiLinkStr + disulfideLinkStr + crossLinkStr + contactLinkStr;
       if(linkStr == '') {
           resStr += linkStr + html.substr(1) + disulfideLinkStr + crossLinkStr + contactLinkStr + hBondLinkStr + ionicLinkStr + halogenpiLinkStr;
       }
       else {
           resStr += linkStr + html + disulfideLinkStr + crossLinkStr + contactLinkStr + hBondLinkStr + ionicLinkStr + halogenpiLinkStr;
       }
       resStr += ']}';

       return resStr;
    }

    drawResNode(node, i, r, gap, margin, y, setName, bVertical, bContactMap, bAfMap) { let ic = this.icn3d; ic.icn3dui;
        let x, resid = node.r.substr(4);
        if(bVertical) {
            x = margin - i *(r + gap);
        }
        else {
            x = margin + i *(r + gap);
        }
        ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]);
        //var color = "#" + atom.color.getHexString().toUpperCase();
        let color = "#" + node.c.toUpperCase();
        "#" + ic.hColor.getHexString().toUpperCase();
        let pos = node.id.indexOf('.');
        let nodeName =(pos == -1) ? node.id : node.id.substr(0, pos);
        let adjustx = 0, adjusty =(setName == 'a') ? -7 : 10;
        if(i % 2 == 1) adjusty =(setName == 'a') ? adjusty - 7 : adjusty + 7;

        if(bContactMap) {
            nodeName = nodeName.substr(1);
            if(!bVertical) adjusty += 4 * r;
        }

        let strokecolor = '#000';
        let strokewidth = '1';
        let textcolor = '#000';
        let fontsize = '6px'; // '6';
        //let html = (bAfMap) ? "<g>" : "<g class='icn3d-node' resid='" + resid + "' >";
        let html = "<g class='icn3d-node' resid='" + resid + "' >";
        html += "<title>" + node.id + "</title>";
        if(bVertical) {
            html += "<circle cx='" + y + "' cy='" + x + "' r='" + r + "' fill='" + color + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' resid='" + resid + "' />";
            html += "<text x='" +(y - 20).toString() + "' y='" +(x + 2).toString() + "' fill='" + textcolor + "' stroke='none' style='font-size:" + fontsize + "; text-anchor:middle' >" + nodeName + "</text>";
        }
        else {
            html += "<circle cx='" + x + "' cy='" + y + "' r='" + r + "' fill='" + color + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' resid='" + resid + "' />";
            html += "<text x='" +(x + adjustx).toString() + "' y='" +(y + adjusty).toString() + "' fill='" + textcolor + "' stroke='none' style='font-size:" + fontsize + "; text-anchor:middle' >" + nodeName + "</text>";
        }
        html += "</g>";
        return html;
    }
    getNodeTopBottom(nameHash, name2node, bReverseNode, bCommonDiff, nameHashCommon) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        let nodeArray1 = [], nodeArray2 = [], name2nodeCommon = {};

        let separatorCommon = "=>", separatorDiff = "==>", postCommon = "-", postDiff = "--";
        for(let name in nameHash) {
            let node = name2node[name];
            if(!node) continue;

            if(bCommonDiff == 1 || bCommonDiff == 2) {
                node = me.hashUtilsCls.cloneHash(node);

                if(bCommonDiff == 1) {
                    let mapping = (nameHashCommon[name]) ? nameHashCommon[name] : postCommon;
                    node.id += separatorCommon + mapping;
                }
                else {
                    let mapping = (nameHashCommon[name]) ? nameHashCommon[name] : postDiff;
                    node.id += separatorDiff + mapping;
                }

                name2nodeCommon[node.id] = node;
            }

            if(node.s == 'a') {
                nodeArray1.push(node);
            }
            else if(node.s == 'b') {
                nodeArray2.push(node);
            }
            else if(node.s == 'ab') {
                nodeArray1.push(node);
                nodeArray2.push(node);
            }
        }
        // sort array
        nodeArray1.sort(function(a,b) {
          return thisClass.compNode(a, b);
        });
        nodeArray2.sort(function(a,b) {
          return thisClass.compNode(a, b, bReverseNode);
        });
        return {"nodeArray1": nodeArray1, "nodeArray2": nodeArray2, "name2node": name2nodeCommon};
    }
    updateGraphJson(struc, index, nodeArray1, nodeArray2, linkArray) { let ic = this.icn3d, me = ic.icn3dui;
        let lineGraphStr = '';
        lineGraphStr += '"structure' + index + '": {"id": "' + struc + '", "nodes1":[';
        lineGraphStr += me.utilsCls.getJSONFromArray(nodeArray1);
        lineGraphStr += '], \n"nodes2":[';
        lineGraphStr += me.utilsCls.getJSONFromArray(nodeArray2);
        lineGraphStr += '], \n"links":[';
        lineGraphStr += me.utilsCls.getJSONFromArray(linkArray);
        lineGraphStr += ']}';
        return lineGraphStr;
    }

    updateGraphColor() { let ic = this.icn3d, me = ic.icn3dui;
      // change graph color
      if(ic.graphStr !== undefined) {
          let graphJson = JSON.parse(ic.graphStr);
          let resid2color = {};
          for(let resid in ic.residues) {
              let atom = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]);
              resid2color[resid] = atom.color.getHexString().toUpperCase();
          }

          let target2resid = {};
          for(let i = 0, il = graphJson.nodes.length; i < il; ++i) {
              let node = graphJson.nodes[i];
              //node.r: 1_1_1KQ2_A_1
              //var idArray = node.r.split('_');
              let idArray = [];
              idArray.push('');
              idArray.push('');

              let tmpStr = node.r.substr(4);
              idArray = idArray.concat(me.utilsCls.getIdArray(tmpStr));

              let resid = idArray[2] + '_' + idArray[3] + '_' + idArray[4];
              node.c = resid2color[resid];
              target2resid[node.id] = resid;
          }
          for(let i = 0, il = graphJson.links.length; i < il; ++i) {
              let link = graphJson.links[i];
              if(link.v == me.htmlCls.ssValue || link.v == me.htmlCls.coilValue) {
                  let resid = target2resid[link.target];
                  link.c = resid2color[resid];
              }
          }
          ic.graphStr = JSON.stringify(graphJson);
      }

      if(ic.bGraph) ic.drawGraphCls.drawGraph(ic.graphStr, ic.pre + 'dl_graph');
      if(ic.bLinegraph) ic.lineGraphCls.drawLineGraph(ic.graphStr);
      if(ic.bScatterplot) ic.lineGraphCls.drawLineGraph(ic.graphStr, true);
    }

    handleForce() { let ic = this.icn3d, me = ic.icn3dui;
       if(me.htmlCls.force == 0 && ic.simulation !== undefined) {
           ic.simulation.stop();
           ic.simulation.force("charge", null);
           ic.simulation.force("x", null);
           ic.simulation.force("y", null);
           ic.simulation.force("r", null);
           ic.simulation.force("link", null);
       }
       else {
           ic.drawGraphCls.drawGraph(ic.graphStr, ic.pre + 'dl_graph');
       }
    }

    getNodesLinksForSet(atomSet, labelType, setName, bAnyAtom) { let ic = this.icn3d, me = ic.icn3dui;
       //var nodeStr = '', linkStr = '';
       let nodeArray = [], linkArray = [];
       let cnt = 0;
       let thickness = me.htmlCls.coilValue;
       let prevChain = '', prevResName = '', prevResi = 0;
       // add chemicals as well
       let residHash = {};
       for(let i in atomSet) {
           let atom = ic.atoms[i];

           if(atom.chain != 'DUM' && (bAnyAtom || atom.het || (atom.name == "CA" && atom.elem == "C") || atom.name == "O3'" || atom.name == "O3*" || atom.name == "P")) {
           // starting nucleotide have "P"
           //if(atom.chain != 'DUM' &&(atom.name == "CA" || atom.name == "P")) {
               let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
               if(residHash.hasOwnProperty(resid)) {
                   continue;
               }
               else {
                   residHash[resid] = 1;
               }
               let resName = me.utilsCls.residueName2Abbr(atom.resn) + atom.resi;
               if(labelType == 'chain' || labelType == 'structure') resName += '.' + atom.chain;
               if(labelType == 'structure') resName += '.' + atom.structure;
               // add 1_1_ to match other conventionssuch as seq_div0_1KQ2_A_50
               let residLabel = '1_1_' + resid;
               //if(cnt > 0) nodeStr += ', ';
               let colorStr = (atom.color) ? atom.color.getHexString().toUpperCase() : '000';
               nodeArray.push('{"id": "' + resName + '", "r": "' + residLabel + '", "s": "' + setName + '", "x": ' + atom.coord.x.toFixed(0)
                   + ', "y": ' + atom.coord.y.toFixed(0) + ', "c": "' + colorStr + '"}');
               if(cnt > 0 && prevChain == atom.chain &&(ic.resid2ncbi[atom.resi] == ic.resid2ncbi[prevResi] + 1 || ic.resid2ncbi[atom.resi] == ic.resid2ncbi[prevResi]) ) {
                   //if(linkCnt > 0) linkStr += ', ';
                   linkArray.push('{"source": "' + prevResName + '", "target": "' + resName
                       + '", "v": ' + thickness + ', "c": "' + colorStr + '"}');
                   if(atom.ssbegin) thickness = me.htmlCls.ssValue;
                   if(atom.ssend) thickness = me.htmlCls.coilValue;
               }
               prevChain = atom.chain;
               prevResName = resName;
               prevResi = atom.resi;
               ++cnt;
           }
       }

       return {"node": nodeArray, "link":linkArray}
    }
    getHbondLinksForSet(atoms, labelType) { let ic = this.icn3d, me = ic.icn3dui;
        let resid2ResidhashHbond = {};
        let threshold = parseFloat($("#" + ic.pre + "hbondthreshold" ).val());
        // not only protein or nucleotides, could be ligands
        let firstSetAtoms = atoms;
        let complement = firstSetAtoms;
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            let bSaltbridge = false;
            ic.hBondCls.calculateChemicalHbonds(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), parseFloat(threshold), bSaltbridge, 'graph', true );
            resid2ResidhashHbond = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        }
        let hbondStr = this.getGraphLinks(resid2ResidhashHbond, resid2ResidhashHbond, me.htmlCls.hbondInsideColor, labelType, me.htmlCls.hbondValuehbondInsideValue);
        return hbondStr;
    }
    getIonicLinksForSet(atoms, labelType) { let ic = this.icn3d, me = ic.icn3dui;
        let resid2Residhash = {};
        let threshold = parseFloat($("#" + ic.pre + "saltbridgethreshold" ).val());
        // not only protein or nucleotides, could be ligands
        let firstSetAtoms = atoms;
        let complement = firstSetAtoms;
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            let bSaltbridge = false;
            ic.saltbridgeCls.calculateIonicInteractions(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), parseFloat(threshold), bSaltbridge, 'graph', true );
            resid2Residhash = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        }
        let ionicStr = this.getGraphLinks(resid2Residhash, resid2Residhash, me.htmlCls.ionicInsideColor, labelType, me.htmlCls.ionicInsideValue);
        return ionicStr;
    }
    getHalogenPiLinksForSet(atoms, labelType) { let ic = this.icn3d, me = ic.icn3dui;
        let resid2Residhash = {};
        let firstSetAtoms = atoms;
        let complement = firstSetAtoms;
        let halogenpiStr = '', threshold;
        threshold = parseFloat($("#" + ic.pre + "halogenthreshold" ).val());
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            ic.piHalogenCls.calculateHalogenPiInteractions(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), parseFloat(threshold), 'graph', 'halogen', true );
            resid2Residhash = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        }
        halogenpiStr += this.getGraphLinks(resid2Residhash, resid2Residhash, me.htmlCls.halogenInsideColor, labelType, me.htmlCls.halogenInsideValue);
        threshold = parseFloat($("#" + ic.pre + "picationthreshold" ).val());
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            ic.piHalogenCls.calculateHalogenPiInteractions(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), parseFloat(threshold), 'graph', 'pi-cation', true );
            resid2Residhash = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        }
        halogenpiStr += this.getGraphLinks(resid2Residhash, resid2Residhash, me.htmlCls.picationInsideColor, labelType, me.htmlCls.picationInsideValue);
        threshold = parseFloat($("#" + ic.pre + "pistackingthreshold" ).val());
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            ic.piHalogenCls.calculateHalogenPiInteractions(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), parseFloat(threshold), 'graph', 'pi-stacking', true );
            resid2Residhash = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        }
        halogenpiStr += this.getGraphLinks(resid2Residhash, resid2Residhash, me.htmlCls.pistackingInsideColor, labelType, me.htmlCls.pistackingInsideValue);
        return halogenpiStr;
    }
    getContactLinksForSet(atoms, labelType, bCartoon2d) { let ic = this.icn3d; ic.icn3dui;
        let ssAtomsArray = [];
        let prevSS = '', prevChain = '';
        let ssAtoms = {};
        for(let i in atoms) {
            let atom = ic.atoms[i];
            if(atom.ss != prevSS || atom.chain != prevChain) {
                if(Object.keys(ssAtoms).length > 0) ssAtomsArray.push(ssAtoms);
                ssAtoms = {};
            }
            ssAtoms[atom.serial] = 1;
            prevSS = atom.ss;
            prevChain = atom.chain;
        }
        // last ss
        if(Object.keys(ssAtoms).length > 0) ssAtomsArray.push(ssAtoms);
        let len = ssAtomsArray.length;
        let interStr = '';
        for(let i = 0; i < len; ++i) {
            for(let j = i + 1; j < len; ++j) {
                interStr += this.getContactLinks(ssAtomsArray[i], ssAtomsArray[j], labelType, true, bCartoon2d);
            }
        }
        return interStr;
    }
    getContactLinks(atomlistTarget, otherAtoms, labelType, bInternal, bCartoon2d) { let ic = this.icn3d, me = ic.icn3dui;
        let radius = parseFloat($("#" + ic.pre + "contactthreshold" ).val());
        let bGetPairs = true, bInteraction = false;
        ic.contactCls.getAtomsWithinAtom(otherAtoms, atomlistTarget, parseFloat(radius), bGetPairs, bInteraction, bInternal);
        let residHash = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        let interStr = this.getGraphLinks(residHash, residHash, me.htmlCls.contactInsideColor, labelType, me.htmlCls.contactInsideValue, bCartoon2d);
        return interStr;
    }
    compNode(a, b, bReverseChain) { let ic = this.icn3d, me = ic.icn3dui;
      let resid1 = a.r.substr(4); // 1_1_1KQ2_A_1
      let resid2 = b.r.substr(4); // 1_1_1KQ2_A_1
      let aIdArray = me.utilsCls.getIdArray(resid1); //resid1.split('_');
      let bIdArray = me.utilsCls.getIdArray(resid2); //resid2.split('_');
      let aChainid = aIdArray[0] + '_' + aIdArray[1];
      let bChainid = bIdArray[0] + '_' + bIdArray[1];
      let aResi = parseInt(aIdArray[2]);
      let bResi = parseInt(bIdArray[2]);
      if(aChainid > bChainid){
          if(bReverseChain) return -1;
          else return 1;
      }
      else if(aChainid < bChainid){
          if(bReverseChain) return 1;
          else return -1;
      }
      else if(aChainid == bChainid){
        return (aResi > bResi) ? 1 :(aResi < bResi) ? -1 : 0;
      }
    }

    getGraphLinks(hash1, hash2, color, labelType, value, bCartoon2d) {var ic = this.icn3d, me = ic.icn3dui;
        let hbondStr = '';
        value =(value === undefined) ? 1 : value;
        let prevLinkStr = '';
        let sourceTargetHash = {};

        for(let resid1 in hash1) {
            //ASN $1KQ2.A:6@ND2
            //or ASN $1KQ2.A:6
            resid1 = resid1.trim();
            let pos1a = resid1.indexOf(' ');
            let pos1b = resid1.indexOf(':');
            let posTmp1 = resid1.indexOf('@');
            let pos1c =(posTmp1 !== -1) ? posTmp1 : resid1.length;
            let pos1d = resid1.indexOf('.');
            let pos1e = resid1.indexOf('$');
            let resName1 = me.utilsCls.residueName2Abbr(resid1.substr(0, pos1a)) + resid1.substr(pos1b + 1, pos1c - pos1b - 1);
            if(labelType == 'chain' || labelType == 'structure') resName1 += '.' + resid1.substr(pos1d + 1, pos1b - pos1d - 1);
            if(labelType == 'structure') resName1 += '.' + resid1.substr(pos1e + 1, pos1d - pos1e - 1);
            for(let resid2 in hash2[resid1]) {
                resid2 = resid2.trim();
                let pos2a = resid2.indexOf(' ');
                let pos2b = resid2.indexOf(':');
                let posTmp2 = resid2.indexOf('@');
                let pos2c =(posTmp2 !== -1) ? posTmp2 : resid2.length;
                let pos2d = resid2.indexOf('.');
                let pos2e = resid2.indexOf('$');
                let resName2 = me.utilsCls.residueName2Abbr(resid2.substr(0, pos2a)) + resid2.substr(pos2b + 1, pos2c - pos2b - 1); //
                    + '_' + resid2.substr(pos2d + 1, pos2b - pos2d - 1);
                if(labelType == 'chain' || labelType == 'structure') resName2 += '.' + resid2.substr(pos2d + 1, pos2b - pos2d - 1);
                if(labelType == 'structure') resName2 += '.' + resid2.substr(pos2e + 1, pos2d - pos2e - 1);

                if(bCartoon2d) {
                    resName1 = ic.resi2resirange[resName1];
                    resName2 = ic.resi2resirange[resName2];
                }

                if(!sourceTargetHash.hasOwnProperty(resName1 + '_' + resName2) && resName1 !== undefined && resName2 !== undefined ) {
                    let linkStr = ', {"source": "' + resName1 + '", "target": "' + resName2 + '", "v": ' + value + ', "c": "' + color + '"}';
                    if(linkStr != prevLinkStr) hbondStr += linkStr;
                    prevLinkStr = linkStr;

                    sourceTargetHash[resName1 + '_' + resName2] = 1;
                    sourceTargetHash[resName2 + '_' + resName1] = 1;
                }
            }
        }
        return hbondStr;
    }
    convertLabel2Resid(residLabel) {var ic = this.icn3d; ic.icn3dui;
        //ASN $1KQ2.A:6@ND2
        //or ASN $1KQ2.A:6
        residLabel.indexOf(' ');
        let pos2Tmp = residLabel.indexOf('@');
        let pos2 =(pos2Tmp !== -1) ? pos2Tmp : residLabel.length;
        let pos3 = residLabel.indexOf('$');
        let pos4 = residLabel.indexOf('.');
        let pos5 = residLabel.indexOf(':');
        let resid = residLabel.substr(pos3 + 1, pos4 - pos3 - 1) + '_' + residLabel.substr(pos4 + 1, pos5 - pos4 - 1)
            + '_' + residLabel.substr(pos5 + 1, pos2 - pos5 - 1);
        return resid;
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ShowInter {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async showInteractions(type) { let ic = this.icn3d, me = ic.icn3dui;
       let nameArray = $("#" + ic.pre + "atomsCustomHbond").val();
       let nameArray2 = $("#" + ic.pre + "atomsCustomHbond2").val();
       if(nameArray2.length == 0) {
           alert("Please select the first set");
       }
       else {
           ic.definedSetsCls.setMode('selection');
           let bHbond = $("#" + ic.pre + "analysis_hbond")[0].checked;
           let bSaltbridge = $("#" + ic.pre + "analysis_saltbridge")[0].checked;
           let bInteraction = $("#" + ic.pre + "analysis_contact")[0].checked;
           let bHalogen = $("#" + ic.pre + "analysis_halogen")[0].checked;
           let bPication = $("#" + ic.pre + "analysis_pication")[0].checked;
           let bPistacking = $("#" + ic.pre + "analysis_pistacking")[0].checked;
           let thresholdHbond = $("#" + ic.pre + "hbondthreshold").val();
           let thresholdSaltbridge = $("#" + ic.pre + "saltbridgethreshold").val();
           let thresholdContact = $("#" + ic.pre + "contactthreshold").val();
           let thresholdHalogen = $("#" + ic.pre + "halogenthreshold").val();
           let thresholdPication = $("#" + ic.pre + "picationthreshold").val();
           let thresholdPistacking = $("#" + ic.pre + "pistackingthreshold").val();
           let thresholdStr = 'threshold ' + thresholdHbond + ' ' + thresholdSaltbridge + ' ' + thresholdContact
            + ' ' + thresholdHalogen + ' ' + thresholdPication + ' ' + thresholdPistacking;
           let result = await ic.viewInterPairsCls.viewInteractionPairs(nameArray2, nameArray, ic.bHbondCalc, type,
                bHbond, bSaltbridge, bInteraction, bHalogen, bPication, bPistacking);
           let interactionTypes = result.interactionTypes;

           let bHbondCalcStr =(ic.bHbondCalc) ? "true" : "false";
           let tmpStr = nameArray2 + " " + nameArray + " | " + interactionTypes + " | " + bHbondCalcStr + " | " + thresholdStr;
           if(type == '3d') {
               me.htmlCls.clickMenuCls.setLogCmd("display interaction 3d | " + tmpStr, true);
           }
           else if(type == 'view') {
               me.htmlCls.clickMenuCls.setLogCmd("view interaction pairs | " + tmpStr, true);
           }
           else if(type == 'save1') {
               me.htmlCls.clickMenuCls.setLogCmd("save1 interaction pairs | " + tmpStr, true);
           }
           else if(type == 'save2') {
               me.htmlCls.clickMenuCls.setLogCmd("save2 interaction pairs | " + tmpStr, true);
           }
           else if(type == 'linegraph') {
               me.htmlCls.clickMenuCls.setLogCmd("line graph interaction pairs | " + tmpStr, true);
           }
           else if(type == 'scatterplot') {
               me.htmlCls.clickMenuCls.setLogCmd("scatterplot interaction pairs | " + tmpStr, true);
           }
           else if(type == 'graph') { // force-directed graph
                let dist_ss = parseInt($("#" + ic.pre + "dist_ss").val());
                let dist_coil = parseInt($("#" + ic.pre + "dist_coil").val());
                let dist_hbond = parseInt($("#" + ic.pre + "dist_hbond").val());
                let dist_inter = parseInt($("#" + ic.pre + "dist_inter").val());
                let dist_ssbond = parseInt($("#" + ic.pre + "dist_ssbond").val());
                let dist_ionic = parseInt($("#" + ic.pre + "dist_ionic").val());
                let dist_halogen = parseInt($("#" + ic.pre + "dist_halogen").val());
                let dist_pication = parseInt($("#" + ic.pre + "dist_pication").val());
                let dist_pistacking = parseInt($("#" + ic.pre + "dist_pistacking").val());
                me.htmlCls.clickMenuCls.setLogCmd("graph interaction pairs | " + nameArray2 + " " + nameArray + " | " + interactionTypes
                    + " | " + bHbondCalcStr + " | " + thresholdStr + " | " + dist_ss + " " + dist_coil
                    + " " + dist_hbond + " " + dist_inter + " " + dist_ssbond + " " + dist_ionic
                    + " " + dist_halogen + " " + dist_pication + " " + dist_pistacking, true);
           }
           // avoid repeated calculation
           ic.bHbondCalc = true;
       }
    }

    // between the highlighted and atoms in nameArray
    //Show the hydrogen bonds between chemicals and proteins/nucleotides with dashed-lines.
    //"threshold" defines the distance of hydrogen bonds.
    showHbonds(threshold, nameArray2, nameArray, bHbondCalc, bSaltbridge, type) { let ic = this.icn3d, me = ic.icn3dui;
        if(bHbondCalc) return;
        let hbonds_saltbridge, select;
        if(bSaltbridge) {
            hbonds_saltbridge = 'saltbridge';
            select = 'salt bridge ' + threshold + ' | sets ' + nameArray2 + " " + nameArray + " | " + bHbondCalc;
        }
        else {
            hbonds_saltbridge = 'hbonds';
            select = 'hbonds ' + threshold + ' | sets ' + nameArray2 + " " + nameArray + " | " + bHbondCalc;
        }
        ic.opts[hbonds_saltbridge] = "yes";
        ic.opts["water"] = "dot";
        let firstSetAtoms, complement;
        firstSetAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);
        complement = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
        ic.firstAtomObjCls.getFirstAtomObj(firstSetAtoms);
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            let selectedAtoms = ic.hBondCls.calculateChemicalHbonds(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), parseFloat(threshold), bSaltbridge );
            let commanddesc;
            if(bSaltbridge) {
                ic.resid2ResidhashSaltbridge = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
                commanddesc = 'all atoms that have salt bridges with the selected atoms';
            }
            else {
                ic.resid2ResidhashHbond = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
                commanddesc = 'all atoms that are hydrogen-bonded with the selected atoms';
            }
            let residues = {};
            for(let i in selectedAtoms) {
                let residueid = ic.atoms[i].structure + '_' + ic.atoms[i].chain + '_' + ic.atoms[i].resi;
                residues[residueid] = 1;
            }
            ic.hAtoms = {};
            for(let resid in residues) {
                for(let i in ic.residues[resid]) {
                    ic.hAtoms[i] = 1;
                    ic.atoms[i].style2 = 'stick';
                    //ic.atoms[i].style2 = 'lines';
                }
            }

            //let commandname = hbonds_saltbridge + '_' + firstAtom.serial;
            let commandname = hbonds_saltbridge + '_auto';
            ic.selectionCls.addCustomSelection(Object.keys(residues), commandname, commanddesc, select, true);
            ic.selectionCls.saveSelectionIfSelected();
            ic.drawCls.draw();
        }
    }

    showHydrogens() { let ic = this.icn3d, me = ic.icn3dui;
        // get hydrogen atoms for currently selected atoms
        if(me.cfg.cid !== undefined) {
            for(let i in ic.hAtoms) {
                    let atom = ic.atoms[i];
            
                    //if(atom.name !== 'H') {
                    if(atom.elem.substr(0, 1) !== 'H') {
                        ic.atoms[atom.serial].bonds = ic.atoms[atom.serial].bonds2.concat();
                        ic.atoms[atom.serial].bondOrder = ic.atoms[atom.serial].bondOrder2.concat();
                        for(let j = 0, jl = ic.atoms[atom.serial].bonds.length; j < jl; ++j) {
                            let serial = ic.atoms[atom.serial].bonds[j];
                            //if(ic.atoms[serial].name === 'H') {
                            if(ic.atoms[serial].elem.substr(0, 1) === 'H') {
                                ic.dAtoms[serial] = 1;
                                ic.hAtoms[serial] = 1;
                            }
                        }
                    }
            }
        }
        else {
            // for(let serial in ic.atoms) {
            //     ic.dAtoms[serial] = 1;
            //     ic.hAtoms[serial] = 1;
            // }  

            // add bonds in heavy atoms
            //for(let serial in ic.hAtoms) {
            for(let serial in ic.atoms) {
                let atom = ic.atoms[serial];
                //if(atom.name === 'H') {
                if(atom.elem.substr(0, 1) === 'H') {                   
                    if(ic.atoms[serial].bonds.length > 0) {
                        let otherSerial = ic.atoms[serial].bonds[0];
                        ic.atoms[otherSerial].bonds.push(atom.serial);
                        if(ic.atoms[otherSerial].bondOrder) ic.atoms[otherSerial].bondOrder.push(1);
                    }        
                    
                    ic.dAtoms[serial] = 1;
                }
            }
        }

        ic.bShowHighlight = false;
    }

    hideHydrogens() { let ic = this.icn3d; ic.icn3dui;
       // remove hydrogen atoms for currently selected atoms
       for(let i in ic.hAtoms) {
           let atom = ic.atoms[i];
           //if(atom.name === 'H') {
           if(atom.elem.substr(0, 1) === 'H') {
               if(ic.atoms[atom.serial].bonds.length > 0) {
                   let otherSerial = ic.atoms[atom.serial].bonds[0];
                   //ic.atoms[atom.serial].bonds = [];
                   let pos = (ic.atoms[otherSerial].bonds) ? ic.atoms[otherSerial].bonds.indexOf(atom.serial) : -1;
                   if(pos !== -1) {
                       ic.atoms[otherSerial].bonds.splice(pos, 1);
                       if(ic.atoms[otherSerial].bondOrder) ic.atoms[otherSerial].bondOrder.splice(pos, 1);
                   }
               }
               delete ic.dAtoms[atom.serial];
               delete ic.hAtoms[atom.serial];            
           }
       }
    }

    hideHbondsContacts() { let ic = this.icn3d, me = ic.icn3dui;
           let select = "set hbonds off";
           me.htmlCls.clickMenuCls.setLogCmd(select, true);
           ic.hBondCls.hideHbonds();
           //ic.drawCls.draw();
           select = "set salt bridge off";
           me.htmlCls.clickMenuCls.setLogCmd(select, true);
           ic.saltbridgeCls.hideSaltbridge();
           select = "set contact off";
           me.htmlCls.clickMenuCls.setLogCmd(select, true);
           ic.contactCls.hideContact();
           select = "set halogen pi off";
           me.htmlCls.clickMenuCls.setLogCmd(select, true);
           ic.piHalogenCls.hideHalogenPi();
    }

    showIonicInteractions(threshold, nameArray2, nameArray, bHbondCalc, bSaltbridge, type) { let ic = this.icn3d, me = ic.icn3dui;
        if(bHbondCalc) return;
        let hbonds_saltbridge, select;
        hbonds_saltbridge = 'saltbridge';
        select = 'salt bridge ' + threshold + ' | sets ' + nameArray2 + " " + nameArray + " | " + bHbondCalc;
        ic.opts[hbonds_saltbridge] = "yes";
        let firstSetAtoms, complement;
        firstSetAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);
        complement = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
        ic.firstAtomObjCls.getFirstAtomObj(firstSetAtoms);
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            let selectedAtoms = ic.saltbridgeCls.calculateIonicInteractions(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), parseFloat(threshold), bSaltbridge );
            let commanddesc;
            ic.resid2ResidhashSaltbridge = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
            commanddesc = 'all atoms that have ionic interactions with the selected atoms';
            let residues = {};
            for(let i in selectedAtoms) {
                let residueid = ic.atoms[i].structure + '_' + ic.atoms[i].chain + '_' + ic.atoms[i].resi;
                residues[residueid] = 1;
            }
            ic.hAtoms = {};
            for(let resid in residues) {
                for(let i in ic.residues[resid]) {
                    ic.hAtoms[i] = 1;
                    ic.atoms[i].style2 = 'stick';
                    if(ic.ions.hasOwnProperty(i)) ic.atoms[i].style2 = 'sphere';
                    //ic.atoms[i].style2 = 'lines';
                }
            }
            //let commandname = hbonds_saltbridge + '_' + firstAtom.serial;
            let commandname = hbonds_saltbridge + '_auto';
            ic.selectionCls.addCustomSelection(Object.keys(residues), commandname, commanddesc, select, true);
            ic.selectionCls.saveSelectionIfSelected();
            ic.drawCls.draw();
        }
    }

    showHalogenPi(threshold, nameArray2, nameArray, bHbondCalc, type, interactionType) { let ic = this.icn3d, me = ic.icn3dui;
        if(bHbondCalc) return;
        let select = interactionType + ' ' + threshold + ' | sets ' + nameArray2 + " " + nameArray + " | " + bHbondCalc;
        ic.opts[interactionType] = "yes";
        let firstSetAtoms, complement;
        firstSetAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);
        complement = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
        ic.firstAtomObjCls.getFirstAtomObj(firstSetAtoms);
        if(Object.keys(complement).length > 0 && Object.keys(firstSetAtoms).length > 0) {
            let selectedAtoms = ic.piHalogenCls.calculateHalogenPiInteractions(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, firstSetAtoms, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, complement, ic.atoms), parseFloat(threshold), type, interactionType );
            let commanddesc;
            if(interactionType == 'halogen') {
                ic.resid2ResidhashHalogen = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
                commanddesc = 'all atoms that have halogen bonds with the selected atoms';
            }
            else if(interactionType == 'pi-cation') {
                ic.resid2ResidhashPication = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
                commanddesc = 'all atoms that have pi-cation interactions with the selected atoms';
            }
            else if(interactionType == 'pi-stacking') {
                ic.resid2ResidhashPistacking = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
                commanddesc = 'all atoms that have pi-stacking with the selected atoms';
            }
            let residues = {};
            for(let i in selectedAtoms) {
                let residueid = ic.atoms[i].structure + '_' + ic.atoms[i].chain + '_' + ic.atoms[i].resi;
                residues[residueid] = 1;
            }
            ic.hAtoms = {};
            for(let resid in residues) {
                for(let i in ic.residues[resid]) {
                    ic.hAtoms[i] = 1;
                    ic.atoms[i].style2 = 'stick';
                    if(ic.ions.hasOwnProperty(i)) ic.atoms[i].style2 = 'sphere';
                    //ic.atoms[i].style2 = 'lines';
                }
            }
            //let commandname = interactionType + '_' + firstAtom.serial;
            let commandname = interactionType + '_auto';
            ic.selectionCls.addCustomSelection(Object.keys(residues), commandname, commanddesc, select, true);
            ic.selectionCls.saveSelectionIfSelected();
            ic.drawCls.draw();
        }
    }

    // show all cross-linkages bonds
    showClbonds() { let ic = this.icn3d, me = ic.icn3dui;
         ic.opts["clbonds"] = "yes";
         let select = 'cross linkage';
         // find all bonds to chemicals
         let residues = ic.applyClbondsCls.applyClbondsOptions();
         for(let resid in residues) {
             ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid]);
         }
         if(Object.keys(residues).length > 0) {
            let commandname = 'clbonds';
            let commanddesc = 'all atoms that have cross-linkages';
            ic.selectionCls.addCustomSelection(Object.keys(residues), commandname, commanddesc, select, true);
            //ic.changeCustomResidues(nameArray);
            ic.selectionCls.saveSelectionIfSelected();
            // show side chains for the selected atoms
            //ic.setOptionCls.setStyle('sidec', 'stick');
            ic.drawCls.draw();
         }
    }

    // show all disulfide bonds
    showSsbonds() { let ic = this.icn3d, me = ic.icn3dui;
         ic.opts["ssbonds"] = "yes";
         let select = 'disulfide bonds';
    //         ic.hlUpdateCls.removeHlMenus();
         let residues = {};
         let structureArray = Object.keys(ic.structures);
         for(let s = 0, sl = structureArray.length; s < sl; ++s) {
             let structure = structureArray[s];
             if(ic.ssbondpnts[structure] === undefined) continue;
             for(let i = 0, lim = Math.floor(ic.ssbondpnts[structure].length / 2); i < lim; i++) {
                let res1 = ic.ssbondpnts[structure][2 * i], res2 = ic.ssbondpnts[structure][2 * i + 1];
                residues[res1] = 1;
                residues[res2] = 1;
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[res1]);
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[res2]);
            }
        }
        if(Object.keys(residues).length > 0) {
            let commandname = 'ssbonds';
            let commanddesc = 'all atoms that have disulfide bonds';
            ic.selectionCls.addCustomSelection(Object.keys(residues), commandname, commanddesc, select, true);
            //ic.changeCustomResidues(nameArray);
            ic.selectionCls.saveSelectionIfSelected();
            // show side chains for the selected atoms
            //ic.setOptionCls.setStyle('sidec', 'stick');
            ic.drawCls.draw();
        }
    }

    //Select a sphere around the highlight atoms with a predefined distance.
    pickCustomSphere(radius, nameArray2, nameArray, bSphereCalc, bInteraction, type) {  let ic = this.icn3d, me = ic.icn3dui;  // ic.pAtom is set already
        if(bSphereCalc) return;
        let select = "select zone cutoff " + radius + " | sets " + nameArray2 + " " + nameArray + " | " + bSphereCalc;
        if(bInteraction) {
            select = "interactions " + radius + " | sets " + nameArray2 + " " + nameArray + " | " + bSphereCalc;
            ic.opts['contact'] = "yes";
        }
        let atomlistTarget, otherAtoms;
        // could be ligands
        atomlistTarget = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);
        otherAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
        let bGetPairs = true;
        let result = this.pickCustomSphere_base(radius, atomlistTarget, otherAtoms, bSphereCalc, bInteraction, type, select, bGetPairs);
        let residueArray = Object.keys(result.residues);
        ic.hAtoms = {};
        for(let index = 0, indexl = residueArray.length; index < indexl; ++index) {
          let residueid = residueArray[index];
          for(let i in ic.residues[residueid]) {
            ic.hAtoms[i] = 1;
          }
        }
        // do not change the set of displaying atoms
        //ic.dAtoms = me.hashUtilsCls.cloneHash(ic.atoms);
        let commandname, commanddesc;
        let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(atomlistTarget);
        if(firstAtom !== undefined) {
            commandname = "sphere." + firstAtom.chain + ":" + me.utilsCls.residueName2Abbr(firstAtom.resn.substr(0, 3)).trim() + firstAtom.resi + "-" + radius + "A";
            if(bInteraction) commandname = "interactions." + firstAtom.chain + ":" + me.utilsCls.residueName2Abbr(firstAtom.resn.substr(0, 3)).trim() + firstAtom.resi + "-" + $("#" + ic.pre + "contactthreshold").val() + "A";
            commanddesc = commandname;
            ic.selectionCls.addCustomSelection(residueArray, commandname, commanddesc, select, true);
        }
        ic.selectionCls.saveSelectionIfSelected();
        ic.drawCls.draw();
    }
    pickCustomSphere_base(radius, atomlistTarget, otherAtoms, bSphereCalc, bInteraction, type, select, bGetPairs, bIncludeTarget) {  let ic = this.icn3d, me = ic.icn3dui;  // ic.pAtom is set already
        let atoms;
        if(bInteraction) {
            atoms = ic.contactCls.getAtomsWithinAtom(me.hashUtilsCls.intHash2Atoms(ic.dAtoms, otherAtoms, ic.atoms), me.hashUtilsCls.intHash2Atoms(ic.dAtoms, atomlistTarget, ic.atoms), parseFloat(radius), bGetPairs, bInteraction, undefined, bIncludeTarget);
            ic.resid2ResidhashInteractions = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        }
        else {
            atoms = ic.contactCls.getAtomsWithinAtom(otherAtoms, atomlistTarget, parseFloat(radius), bGetPairs, bInteraction);
            ic.resid2ResidhashSphere = me.hashUtilsCls.cloneHash(ic.resid2Residhash);
        }
        let residues = {};
        for(let i in atoms) {
            let atom = atoms[i];
            if(ic.bOpm && atom.resn === 'DUM') continue;
            let residueid = atom.structure + '_' + atom.chain + '_' + atom.resi;
            residues[residueid] = 1;
        }
        return {"residues": residues, "resid2Residhash": ic.resid2Residhash}
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ViewInterPairs {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async viewInteractionPairs(nameArray2, nameArray, bHbondCalc, type,
      bHbond, bSaltbridge, bInteraction, bHalogen, bPication, bPistacking, contactDist) { let ic = this.icn3d, me = ic.icn3dui;
       let bondCnt;

       // reset
       ic.hbondpnts = [];
       ic.saltbridgepnts = [];
       ic.contactpnts = [];
       ic.halogenpnts = [];
       ic.picationpnts = [];
       ic.pistackingpnts = [];

       // type: view, save, forcegraph
       ic.bRender = false;
       let hAtoms = {};
       let prevHatoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

       let bContactMapLocal = (type == 'calpha' || type == 'cbeta' || type == 'heavyatoms');

       let atomSet1 = {}, atomSet2 = {};
       if(bContactMapLocal) { // contact map
           for(let i in ic.hAtoms) {
               let atom = ic.atoms[i];

               // skip solvent
               if(atom.resn == 'HOH' || atom.resn == 'WAT' || atom.resn == 'SOL') continue;

               if( (type == 'calpha' && ( atom.het || atom.name == "CA" || atom.name == "O3'" || atom.name == "O3*"))
                   || (type == 'cbeta' && ( atom.het || atom.name == "CB" || atom.name == "O3'" || atom.name == "O3*"))
                   || (type == 'heavyatoms' && atom.elem != "H")
               ) {
                   atomSet1[i] = atom;
                   atomSet2[i] = atom;
               }
           }
       }
       else {
           atomSet1 = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);
           atomSet2 = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
       }

       let labelType; // residue, chain, structure
       let cntChain = 0, cntStructure = 0;
       for(let structure in ic.structures) {
           for(let i = 0, il = ic.structures[structure].length; i < il; ++i) {
               let chainid = ic.structures[structure][i];
               for(let serial in ic.chains[chainid]) {
                   if(atomSet1.hasOwnProperty(serial) || atomSet2.hasOwnProperty(serial)) {
                       ++cntChain;
                       break;
                   }
               }
           }
           ++cntStructure;
       }
       if(cntStructure > 1) labelType = 'structure';
       else if(cntChain > 1) labelType = 'chain';
       else labelType = 'residue';
       // fixed order of interaction type
       let interactionTypes = [];
       if(bHbond) {
           interactionTypes.push('hbonds');
       }
       if(bSaltbridge) {
           interactionTypes.push('salt bridge');
       }
       if(bInteraction) {
           interactionTypes.push('interactions');
       }
       if(bHalogen) {
           interactionTypes.push('halogen');
       }
       if(bPication) {
           interactionTypes.push('pi-cation');
       }
       if(bPistacking) {
           interactionTypes.push('pi-stacking');
       }
       if(!bHbondCalc) {
           ic.resids2inter = {};
           ic.resids2interAll = {};
       }
       if(bSaltbridge) {
           let threshold = parseFloat($("#" + ic.pre + "saltbridgethreshold" ).val());
           if(!threshold || isNaN(threshold)) threshold = ic.tsIonic;
           if(!bHbondCalc) {
               ic.hAtoms = me.hashUtilsCls.cloneHash(prevHatoms);
               //ic.showInterCls.showHbonds(threshold, nameArray2, nameArray, bHbondCalc, true, type);
               ic.showInterCls.showIonicInteractions(threshold, nameArray2, nameArray, bHbondCalc, true, type);
           }
           hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.hAtoms);
       }
       if(bHbond) {
           let threshold = parseFloat($("#" + ic.pre + "hbondthreshold" ).val());
           if(!threshold || isNaN(threshold)) threshold = ic.tsHbond;
           if(!bHbondCalc) {
               ic.hAtoms = me.hashUtilsCls.cloneHash(prevHatoms);
               ic.showInterCls.showHbonds(threshold, nameArray2, nameArray, bHbondCalc, undefined, type);
           }
           hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.hAtoms);
       }
       // switch display order, show hydrogen first
       let tableHtml = '';
       if(bHbond) {
           tableHtml += this.exportHbondPairs(type, labelType);
       }
       if(bSaltbridge) {
           tableHtml += this.exportSaltbridgePairs(type, labelType);
       }
       if(bHalogen) {
           let threshold = parseFloat($("#" + ic.pre + "halogenthreshold" ).val());
           if(!threshold || isNaN(threshold)) threshold = ic.tsHalogen;
           if(!bHbondCalc) {
               ic.hAtoms = me.hashUtilsCls.cloneHash(prevHatoms);
               ic.showInterCls.showHalogenPi(threshold, nameArray2, nameArray, bHbondCalc, type, 'halogen');
           }
           hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.hAtoms);
           tableHtml += this.exportHalogenPiPairs(type, labelType, 'halogen');
       }
       if(bPication) {
           let threshold = parseFloat($("#" + ic.pre + "picationthreshold" ).val());
           if(!threshold || isNaN(threshold)) threshold = ic.tsPication;
           if(!bHbondCalc) {
               ic.hAtoms = me.hashUtilsCls.cloneHash(prevHatoms);
               ic.showInterCls.showHalogenPi(threshold, nameArray2, nameArray, bHbondCalc, type, 'pi-cation');
           }
           hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.hAtoms);
           tableHtml += this.exportHalogenPiPairs(type, labelType, 'pi-cation');
       }
       if(bPistacking) {
           let threshold = parseFloat($("#" + ic.pre + "pistackingthreshold" ).val());
           if(!threshold || isNaN(threshold)) threshold = ic.tsPistacking;
           if(!bHbondCalc) {
               ic.hAtoms = me.hashUtilsCls.cloneHash(prevHatoms);
               ic.showInterCls.showHalogenPi(threshold, nameArray2, nameArray, bHbondCalc, type, 'pi-stacking');
           }
           hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.hAtoms);
           //tableHtml += this.exportHalogenPiPairs(type, labelType, 'pi-stacking');
           let tmp = this.exportHalogenPiPairs(type, labelType, 'pi-stacking');
           tableHtml += tmp;
       }
       if(bInteraction) {
           let threshold = (bContactMapLocal) ? contactDist : parseFloat($("#" + ic.pre + "contactthreshold" ).val());
           if(!threshold || isNaN(threshold)) threshold = ic.tsContact;
           if(!(nameArray2.length == 1 && nameArray.length == 1 && nameArray2[0] == nameArray[0])) {
                if(!bHbondCalc) {
                    ic.hAtoms = me.hashUtilsCls.cloneHash(prevHatoms);
                    ic.showInterCls.pickCustomSphere(threshold, nameArray2, nameArray, bHbondCalc, true, type);
                }
                hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.hAtoms);
                tableHtml += this.exportSpherePairs(true, type, labelType);
           }
           else { // contact in a set, atomSet1 same as atomSet2
                if(!bHbondCalc) {
                    let residues = {};
                    let resid2ResidhashInteractions = {};

                    if(bContactMapLocal) {
                        let bIncludeTarget = true;
                        let result = ic.showInterCls.pickCustomSphere_base(threshold, atomSet1, atomSet2, bHbondCalc, true, undefined, undefined, true, bIncludeTarget);
                        residues = me.hashUtilsCls.unionHash(residues, result.residues);
                        for(let resid in result.resid2Residhash) {
                            resid2ResidhashInteractions[resid] = me.hashUtilsCls.unionHash(resid2ResidhashInteractions[resid], result.resid2Residhash[resid]);
                        }
                    }
                    else {
                        let ssAtomsArray = [];
                        let prevSS = '', prevChain = '';
                        let ssAtoms = {};
                        for(let i in atomSet1) {
                            let atom = ic.atoms[i];
                            if(atom.ss != prevSS || atom.chain != prevChain) {
                                if(Object.keys(ssAtoms).length > 0) ssAtomsArray.push(ssAtoms);
                                ssAtoms = {};
                            }
                            ssAtoms[atom.serial] = 1;
                            prevSS = atom.ss;
                            prevChain = atom.chain;
                        }
                        // last ss
                        if(Object.keys(ssAtoms).length > 0) ssAtomsArray.push(ssAtoms);
                        let len = ssAtomsArray.length;
                        select = "interactions " + threshold + " | sets " + nameArray2 + " " + nameArray + " | true";
                        ic.opts['contact'] = "yes";

                        for(let i = 0; i < len; ++i) {
                            for(let j = i + 1; j < len; ++j) {
                                ic.hAtoms = me.hashUtilsCls.cloneHash(prevHatoms);
                                let result = ic.showInterCls.pickCustomSphere_base(threshold, ssAtomsArray[i], ssAtomsArray[j], bHbondCalc, true, type, select, true);
                                residues = me.hashUtilsCls.unionHash(residues, result.residues);
                                for(let resid in result.resid2Residhash) {
                                    resid2ResidhashInteractions[resid] = me.hashUtilsCls.unionHash(resid2ResidhashInteractions[resid], result.resid2Residhash[resid]);
                                }
                            }
                        }
                    }

                    ic.resid2ResidhashInteractions = resid2ResidhashInteractions;
                    let residueArray = Object.keys(residues);
                    ic.hAtoms = {};
                    for(let index = 0, indexl = residueArray.length; index < indexl; ++index) {
                      let residueid = residueArray[index];
                      for(let i in ic.residues[residueid]) {
                        ic.hAtoms[i] = 1;
                      }
                    }
                    // do not change the set of displaying atoms
                    //ic.dAtoms = me.hashUtilsCls.cloneHash(ic.atoms);
                    let commandname, commanddesc;
                    let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(residues);
                    if(firstAtom !== undefined) {
                        commandname = "sphere." + firstAtom.chain + ":" + me.utilsCls.residueName2Abbr(firstAtom.resn.substr(0, 3)).trim() + firstAtom.resi + "-" + radius + "A";
                        if(bInteraction) commandname = "interactions." + firstAtom.chain + ":" + me.utilsCls.residueName2Abbr(firstAtom.resn.substr(0, 3)).trim() + firstAtom.resi + "-" + $("#" + ic.pre + "contactthreshold").val() + "A";
                        commanddesc = commandname;
                        ic.selectionCls.addCustomSelection(residueArray, commandname, commanddesc, select, true);
                    }
                    ic.selectionCls.saveSelectionIfSelected();
                    ic.drawCls.draw();
                }
                hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.hAtoms);
                tableHtml += this.exportSpherePairs(true, type, labelType);
           } // same set
       }
       ic.hAtoms = me.hashUtilsCls.cloneHash(hAtoms);
       ic.bRender = true;
       //ic.hlUpdateCls.updateHlAll();
       ic.drawCls.draw();
       let residHash, select, commandname, commanddesc;
       residHash = ic.firstAtomObjCls.getResiduesFromAtoms(hAtoms);
       select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residHash));
       commandname = 'interface_all';
       commanddesc = commandname;
       ic.selectionCls.addCustomSelection(Object.keys(residHash), commandname, commanddesc, select, true);
       let interface1 = me.hashUtilsCls.intHash(hAtoms, atomSet1);
       residHash = ic.firstAtomObjCls.getResiduesFromAtoms(interface1);
       select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residHash));
       commandname = 'interface_1';
       commanddesc = commandname;
       ic.selectionCls.addCustomSelection(Object.keys(residHash), commandname, commanddesc, select, true);
       let interface2 = me.hashUtilsCls.intHash(hAtoms, atomSet2);
       residHash = ic.firstAtomObjCls.getResiduesFromAtoms(interface2);
       select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residHash));
       commandname = 'interface_2';
       commanddesc = commandname;
       ic.selectionCls.addCustomSelection(Object.keys(residHash), commandname, commanddesc, select, true);
       //var html = '<div style="text-align:center"><b>Hydrogen Bonds, Salt Bridges, Contacts, Halogen Bonds, &pi;-cation, &pi;-stacking between Two Sets:</b><br>';
       let html = '<div style="text-align:center"><b>' + interactionTypes.join(', ') + ' between Two Sets:</b><br>';
       let residueArray1 = ic.resid2specCls.atoms2residues(Object.keys(atomSet1));
       let residueArray2 = ic.resid2specCls.atoms2residues(Object.keys(atomSet2));
       let cmd1 = 'select ' + ic.resid2specCls.residueids2spec(residueArray1);
       let cmd2 = 'select ' + ic.resid2specCls.residueids2spec(residueArray2);
       html += 'Set 1: ' + nameArray2 + ' <button class="' + ic.pre + 'selset" cmd="' + cmd1 + '">Highlight in 3D</button><br>';
       html += 'Set 2: ' + nameArray + ' <button class="' + ic.pre + 'selset" cmd="' + cmd2 + '">Highlight in 3D</button><br><br></div>';
       html += '<div style="text-align:center"><b>The interfaces are:</b><br>';
       let residueArray3 = ic.resid2specCls.atoms2residues(Object.keys(interface1));
       let residueArray4 = ic.resid2specCls.atoms2residues(Object.keys(interface2));
       let cmd3 = 'select ' + ic.resid2specCls.residueids2spec(residueArray3);
       let cmd4 = 'select ' + ic.resid2specCls.residueids2spec(residueArray4);
       html += 'interface_1 <button class="' + ic.pre + 'selset" cmd="' + cmd3 + '">Highlight in 3D</button><br>';
       html += 'interface_2 <button class="' + ic.pre + 'selset" cmd="' + cmd4 + '">Highlight in 3D</button><br><br></div>';
       html += '<div><b>Note</b>: Each checkbox below selects the corresponding residue. '
         + 'You can click "Save Selection" in the "Select" menu to save the selection '
         + 'and click on "Highlight" button to clear the checkboxes.</div><br>';
       let header = html;
       if(type == 'graph' || type == 'linegraph' || type == 'scatterplot' || bContactMapLocal) html = '';
       html += tableHtml;

       if(type == 'save1' || type == 'save2') {
           html = header;
           let tmpText = '';
           if(type == 'save1') {
               tmpText = 'Set 1';
           }
           else if(type == 'save2') {
               tmpText = 'Set 2';
           }
           html += '<div style="text-align:center"><br><b>Interactions Sorted on ' + tmpText + '</b>: <button class="' + ic.pre + 'showintercntonly" style="margin-left:20px">Show Count Only</button><button class="' + ic.pre + 'showinterdetails" style="margin-left:20px">Show Details</button></div>';
           let result = this.getAllInteractionTable(type);
           html += result.html;
           bondCnt = result.bondCnt;

           $("#" + ic.pre + "dl_interactionsorted").html(html);
           me.htmlCls.dialogCls.openDlg('dl_interactionsorted', 'Show sorted interactions');
       }
       else if(type == 'view') {
           $("#" + ic.pre + "dl_allinteraction").html(html);
           me.htmlCls.dialogCls.openDlg('dl_allinteraction', 'Show interactions');
       }
       else if(type == 'linegraph') {
           me.htmlCls.dialogCls.openDlg('dl_linegraph', 'Show interactions between two lines of residue nodes');
           ic.graphStr = ic.getGraphCls.getGraphData(atomSet1, atomSet2, nameArray2, nameArray, html, labelType);
           ic.bLinegraph = true;
           // draw SVG
           let svgHtml = ic.lineGraphCls.drawLineGraph(ic.graphStr);
           $("#" + ic.pre + "linegraphDiv").html(svgHtml);
       }
       else if(type == 'scatterplot') {
           me.htmlCls.dialogCls.openDlg('dl_scatterplot', 'Show interactions as scatterplot');
           ic.graphStr = ic.getGraphCls.getGraphData(atomSet1, atomSet2, nameArray2, nameArray, html, labelType);
           ic.bScatterplot = true;
           // draw SVG
           let svgHtml = ic.lineGraphCls.drawLineGraph(ic.graphStr, true);
           $("#" + ic.pre + "scatterplotDiv").html(svgHtml);
       }
       else if(bContactMapLocal) {
           me.htmlCls.dialogCls.openDlg('dl_contactmap', 'Show contact map');
           let bAnyAtom = true;
           let graphStr = ic.getGraphCls.getGraphData(atomSet1, atomSet2, nameArray2, nameArray, html, labelType, bAnyAtom);
           ic.bContactMap = true;
           // draw SVG
           let svgHtml = ic.contactMapCls.drawContactMap(graphStr);
           $("#" + ic.pre + "contactmapDiv").html(svgHtml);
       }
       else if(type == 'graph') {
           // atomSet1 and atomSet2 are in the right order here
           ic.graphStr = ic.getGraphCls.getGraphData(atomSet1, atomSet2, nameArray2, nameArray, html, labelType);
           ic.bGraph = true;
           // show only displayed set in 2D graph
           if(Object.keys(atomSet2).length + Object.keys(atomSet1).length > Object.keys(ic.dAtoms).length) {
               ic.graphStr = ic.selectionCls.getGraphDataForDisplayed();
           }

           if(ic.bD3 === undefined) {
                //var url = "https://d3js.org/d3.v4.min.js";
                var url = "https://www.ncbi.nlm.nih.gov/Structure/icn3d/script/d3v4-force-all.min.js";
                await me.getAjaxPromise(url, 'script');

                ic.bD3 = true;

                $("#" + me.svgid).empty();
                me.htmlCls.dialogCls.openDlg('dl_graph', 'Force-directed graph');
                ic.drawGraphCls.drawGraph(ic.graphStr, ic.pre + 'dl_graph');
                /// if(ic.deferredGraphinteraction !== undefined) ic.deferredGraphinteraction.resolve();
           }
           else {
               $("#" + me.svgid).empty();
               me.htmlCls.dialogCls.openDlg('dl_graph', 'Force-directed graph');
               ic.drawGraphCls.drawGraph(ic.graphStr, ic.pre + 'dl_graph');
           }
       }
       return {interactionTypes: interactionTypes.toString(), bondCnt: bondCnt};
    }

    clearInteractions() { let ic = this.icn3d; ic.icn3dui;
        ic.lines['hbond'] = [];
        ic.hbondpnts = [];
        ic.lines['saltbridge'] = [];
        ic.saltbridgepnts = [];
        ic.lines['contact'] = [];
        ic.contactpnts = [];

        ic.lines['halogen'] = [];
        ic.lines['pi-cation'] = [];
        ic.lines['pi-stacking'] = [];
        ic.halogenpnts = [];
        ic.picationpnts = [];
        ic.pistackingpnts = [];
    }

    resetInteractionPairs() { let ic = this.icn3d; ic.icn3dui;
       ic.bHbondCalc = false;
       //me.htmlCls.clickMenuCls.setLogCmd('set calculate hbond false', true);
       ic.showInterCls.hideHbondsContacts();
       ic.hlUpdateCls.clearHighlight();
       // reset the interaction pairs
       ic.resids2inter = {};
       ic.resids2interAll = {};
    }

    async retrieveInteractionData() { let ic = this.icn3d, me = ic.icn3dui;
         if(!ic.b2DShown) {
             if(me.cfg.align !== undefined) {
                 let structureArray = Object.keys(ic.structures);

                 if(me.cfg.atype == 2) {
                    let bDiagramOnly = true;
                    await ic.alignParserCls.downloadAlignment(structureArray[0] + ',' + structureArray[1], bDiagramOnly);
                 }
                 
                 await ic.ParserUtilsCls.set2DDiagramsForAlign(structureArray[0].toUpperCase(), structureArray[1].toUpperCase());
             }
             else if(me.cfg.chainalign !== undefined) {
                 Object.keys(ic.structures);
                 //if(structureArray.length == 2) {
                 //   ic.ParserUtilsCls.set2DDiagramsForAlign(structureArray[1].toUpperCase(), structureArray[0].toUpperCase());
                 //}
                 //else if(structureArray.length == 1) {
                 //   ic.ParserUtilsCls.set2DDiagramsForAlign(structureArray[0].toUpperCase(), structureArray[0].toUpperCase());
                 //}

                 await ic.ParserUtilsCls.set2DDiagramsForChainalign(ic.chainidArray);
             }
             else {
                 ic.ParserUtilsCls.download2Ddgm(ic.inputid.toUpperCase());
             }
         }
    }

    getAllInteractionTable(type) { let ic = this.icn3d, me = ic.icn3dui;
        let bondCnt = [];

        let residsArray = Object.keys(ic.resids2inter);
        if(type == 'save1') {
           residsArray.sort(function(a,b) {
              return me.utilsCls.compResid(a, b, type);
           });
        }
        else if(type == 'save2') {
           residsArray.sort(function(a,b) {
              return me.utilsCls.compResid(a, b, type);
           });
        }
        //ic.resids2inter
        let tmpText = '';
        let prevResidname1 = '', prevIds = '';
        let strHbond = '', strIonic = '', strContact = '', strHalegen = '', strPication = '', strPistacking = '';
        let cntHbond = 0, cntIonic = 0, cntContact = 0, cntHalegen = 0, cntPication = 0, cntPistacking = 0;
        for(let i = 0, il = residsArray.length; i < il; ++i) {
            let resids = residsArray[i];
            let residname1_residname2 = resids.split(',');
            let residname1 =(type == 'save1') ? residname1_residname2[0] : residname1_residname2[1];
            (type == 'save1') ? residname1_residname2[1] : residname1_residname2[0];
            // stru_chain_resi_resn
            let ids = residname1.split('_');
            if(i > 0 && residname1 != prevResidname1) {
                bondCnt.push({cntHbond: cntHbond, cntIonic: cntIonic, cntContact: cntContact, cntHalegen: cntHalegen, cntPication: cntPication, cntPistacking: cntPistacking});

                tmpText += this.getInteractionPerResidue(prevIds, strHbond, strIonic, strContact, strHalegen, strPication, strPistacking,
                  cntHbond, cntIonic, cntContact, cntHalegen, cntPication, cntPistacking);
                strHbond = ''; strIonic = ''; strContact = ''; strHalegen = ''; strPication = ''; strPistacking = '';
                cntHbond = 0; cntIonic = 0; cntContact = 0; cntHalegen = 0; cntPication = 0; cntPistacking = 0;
            }
            let labels2dist, result;
            labels2dist = ic.resids2inter[resids]['hbond'];
            result = this.getInteractionPairDetails(labels2dist, type, 'hbond');
            strHbond += result.html;
            cntHbond += result.cnt;
            labels2dist = ic.resids2inter[resids]['ionic'];
            result = this.getInteractionPairDetails(labels2dist, type, 'ionic');
            strIonic += result.html;
            cntIonic += result.cnt;
            labels2dist = ic.resids2inter[resids]['contact'];
            result = this.getContactPairDetails(labels2dist, type, 'contact');
            strContact += result.html;
            cntContact += result.cnt;
            labels2dist = ic.resids2inter[resids]['halogen'];
            result = this.getInteractionPairDetails(labels2dist, type, 'halogen');
            strHalegen += result.html;
            cntHalegen += result.cnt;
            labels2dist = ic.resids2inter[resids]['pi-cation'];
            result = this.getInteractionPairDetails(labels2dist, type, 'pi-cation');
            strPication += result.html;
            cntPication += result.cnt;
            labels2dist = ic.resids2inter[resids]['pi-stacking'];
            result = this.getInteractionPairDetails(labels2dist, type, 'pi-stacking');
            strPistacking += result.html;
            cntPistacking += result.cnt;
            prevResidname1 = residname1;
            prevIds = ids;
        }
        bondCnt.push({cntHbond: cntHbond, cntIonic: cntIonic, cntContact: cntContact, cntHalegen: cntHalegen, cntPication: cntPication, cntPistacking: cntPistacking});

        tmpText += this.getInteractionPerResidue(prevIds, strHbond, strIonic, strContact, strHalegen, strPication, strPistacking,
          cntHbond, cntIonic, cntContact, cntHalegen, cntPication, cntPistacking);
        let html = '';
        if(residsArray.length > 0) {
            html += '<br><table class="icn3d-sticky" align=center border=1 cellpadding=10 cellspacing=0><thead>';
            html += '<tr><th rowspan=2>Residue</th><th rowspan=2># Hydrogen<br>Bond</th><th rowspan=2># Salt Bridge<br>/Ionic Interaction</th><th rowspan=2># Contact</th>';
            html += '<th rowspan=2># Halogen<br>Bond</th><th rowspan=2># &pi;-Cation</th><th rowspan=2># &pi;-Stacking</th>';
            html += '<th>Hydrogen Bond (backbone atoms: @CA, @N, @C, @O)</th><th>Salt Bridge/Ionic Interaction</th><th>Contact</th>';
            html += '<th>Halogen Bond</th><th>&pi;-Cation</th><th>&pi;-Stacking</th></tr>';
            html += '<tr>';
            let tmpStr = '<td><table width="100%" class="icn3d-border"><tr><td>Atom1</td><td>Atom2</td><td>Distance(&#8491;)</td><td>Highlight in 3D</td></tr></table></td>';
            html += tmpStr;
            html += tmpStr;
            html += '<td><table width="100%" class="icn3d-border"><tr><td>Atom1</td><td>Atom2</td><td># Contacts</td><td>Min Distance(&#8491;)</td><td>C-alpha Distance(&#8491;)</td><td>Highlight in 3D</td></tr></table></td>';
            html += tmpStr;
            html += tmpStr;
            html += tmpStr;
            html += '</tr>';
            html += '</thead><tbody>';
            html += tmpText;
            html += '</tbody></table><br/>';
        }
        return  {html: html, bondCnt: bondCnt};
    }
    getInteractionPerResidue(prevIds, strHbond, strIonic, strContact, strHalegen, strPication, strPistacking,
      cntHbond, cntIonic, cntContact, cntHalegen, cntPication, cntPistacking) { let ic = this.icn3d; ic.icn3dui;
        let tmpText = '';
        tmpText += '<tr align="center"><th>' + prevIds[3] + prevIds[2] + '</th><td>' + cntHbond + '</td><td>' + cntIonic + '</td><td>' + cntContact + '</td><td>' + cntHalegen + '</td><td>' + cntPication + '</td><td>' + cntPistacking + '</td>';

        let itemArray = [strHbond, strIonic, strContact, strHalegen, strPication, strPistacking];
        for(let i in itemArray) {
            let item = itemArray[i];
            tmpText += '<td valign="top"><table width="100%" class="icn3d-border">' + item + '</table></td>';
        }
        tmpText += '</tr>';
        return tmpText;
    }
    getInteractionPairDetails(labels2dist, type, interactionType) { let ic = this.icn3d; ic.icn3dui;
        let tmpText = '', cnt = 0;
        let colorText1 = ' <span style="background-color:#';
        let colorText2 = '">&nbsp;&nbsp;&nbsp;</span>';
        if(labels2dist !== undefined) {
            for(let labels in labels2dist) {
                let resid1_resid2 = labels.split(',');
                let resid1 =(type == 'save1') ? resid1_resid2[0] : resid1_resid2[1];
                let resid2 =(type == 'save1') ? resid1_resid2[1] : resid1_resid2[0];
                let resid1Real = ic.getGraphCls.convertLabel2Resid(resid1);
                let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1Real]);
                let color1 = (atom1.color) ? atom1.color.getHexString() : '';
                let resid2Real = ic.getGraphCls.convertLabel2Resid(resid2);
                let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2Real]);
                let color2 = (atom2.color) ? atom2.color.getHexString() : '';
                let dist = Math.sqrt(labels2dist[labels]).toFixed(1);
                tmpText += '<tr><td><span style="white-space:nowrap"><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + interactionType + '2_' +  cnt + 'a" resid="' + resid1 + '"/> ' + resid1 + colorText1 + color1 + colorText2 + '</span></td><td><span style="white-space:nowrap"><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + interactionType + '2_' +  cnt + 'b" resid="' + resid2 + '"/> ' + resid2 + colorText1 + color2 + colorText2 + '</span></td><td align="center">' + dist + '</td>';
                tmpText += '<td align="center"><button class="' + ic.pre + 'selres" resid="' + resid1 + '|' + resid2 + '">Highlight</button></td>';
                tmpText += '</tr>';
                ++cnt;
            }
        }
        return {html: tmpText, cnt: cnt}
    }
    getContactPairDetails(labels2dist, type) { let ic = this.icn3d; ic.icn3dui;
        let tmpText = '', cnt = 0;
        let colorText1 = ' <span style="background-color:#';
        let colorText2 = '">&nbsp;&nbsp;&nbsp;</span>';
        if(labels2dist !== undefined) {
            for(let labels in labels2dist) {
                let resid1_resid2 = labels.split(',');
                let resid1 =(type == 'save1') ? resid1_resid2[0] : resid1_resid2[1];
                let resid2 =(type == 'save1') ? resid1_resid2[1] : resid1_resid2[0];
                let resid1Real = ic.getGraphCls.convertLabel2Resid(resid1);
                let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1Real]);
                let color1 = (atom1.color) ? atom1.color.getHexString() : '';
                let resid2Real = ic.getGraphCls.convertLabel2Resid(resid2);
                let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2Real]);
                let color2 = (atom2.color) ? atom2.color.getHexString() : '';
                let dist1_dist2_atom1_atom2 = labels2dist[labels].split('_');
                let dist1 = dist1_dist2_atom1_atom2[0];
                let dist2 = dist1_dist2_atom1_atom2[1];
                let atom1Name = dist1_dist2_atom1_atom2[2];
                let atom2Name = dist1_dist2_atom1_atom2[3];
                let contactCnt = dist1_dist2_atom1_atom2[4];
                tmpText += '<tr><td><span style="white-space:nowrap"><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'inter2_' +  cnt + 'a" resid="' + resid1 + '"/> ' + resid1 + '@' + atom1Name + colorText1 + color1 + colorText2 + '</span></td><td><span style="white-space:nowrap"><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'inter2_' +  cnt + 'b" resid="' + resid2 + '"/> ' + resid2 + '@' + atom2Name + colorText1 + color2 + colorText2 + '</span></td><td align="center">' + contactCnt + '</td><td align="center">' + dist1 + '</td><td align="center">' + dist2 + '</td>';
                tmpText += '<td align="center"><button class="' + ic.pre + 'selres" resid="' + resid1 + '|' + resid2 + '">Highlight</button></td>';
                tmpText += '</tr>';
                cnt += parseInt(contactCnt);
            }
        }
        return {html: tmpText, cnt: cnt}
    }

    //Export the list of residues in some chain interacting with residues in another chain.
    exportInteractions() {var ic = this.icn3d; ic.icn3dui;
       let text = '<html><body><div style="text-align:center"><br><b>Interacting residues</b>:<br/><table align=center border=1 cellpadding=10 cellspacing=0><tr><th>Base Chain: Residues</th><th>Interacting Chain</th></tr>';
       for(let fisrtChainid in ic.chainname2residues) {
           for(let name in ic.chainname2residues[fisrtChainid]) {
               let secondChainid = fisrtChainid.substr(0, fisrtChainid.indexOf('_')) + '_' + name.substr(0, name.indexOf(' '));
               text += '<tr><td>' + fisrtChainid + ': ';
               text += ic.resid2specCls.residueids2spec(ic.chainname2residues[fisrtChainid][name]);
               text += '</td><td>' + secondChainid + '</td></tr>';
           }
       }
       text += '</table><br/></div></body></html>';
       let file_pref =(ic.inputid) ? ic.inputid : "custom";
       ic.saveFileCls.saveFile(file_pref + '_interactions.html', 'html', text);
    }
    exportSsbondPairs() {var ic = this.icn3d; ic.icn3dui;
        let tmpText = '';
        let cnt = 0;
        for(let structure in ic.structures) {
            let ssbondArray = ic.ssbondpnts[structure];
            if(ssbondArray === undefined) {
                break;
            }
            for(let i = 0, il = ssbondArray.length; i < il; i = i + 2) {
                let resid1 = ssbondArray[i];
                let resid2 = ssbondArray[i+1];
                tmpText += '<tr><td>' + resid1 + ' Cys</td><td>' + resid2 + ' Cys</td></tr>';
                ++cnt;
            }
        }
        let text = '<html><body><div style="text-align:center"><br><b>' + cnt + ' disulfide pairs</b>:<br><br><table align=center border=1 cellpadding=10 cellspacing=0><tr><th>Residue ID 1</th><th>Residue ID 2</th></tr>';
        text += tmpText;
        text += '</table><br/></div></body></html>';
        let file_pref =(ic.inputid) ? ic.inputid : "custom";
        ic.saveFileCls.saveFile(file_pref + '_disulfide_pairs.html', 'html', text);
    }
    exportClbondPairs() {var ic = this.icn3d; ic.icn3dui;
        let tmpText = '';
        let cnt = 0;
        let residHash = {};
        for(let structure in ic.structures) {
            let clbondArray = ic.clbondpnts[structure];
            if(clbondArray === undefined) {
                break;
            }
            for(let i = 0, il = clbondArray.length; i < il; i = i + 2) {
                let resid1 = clbondArray[i];
                let resid2 = clbondArray[i+1];
                if(!residHash.hasOwnProperty(resid1 + '_' + resid2)) {
                    let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1]);
                    let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2]);
                    tmpText += '<tr><td>' + resid1 + ' ' + atom1.resn + '</td><td>' + resid2 + ' ' + atom2.resn + '</td></tr>';
                    ++cnt;
                }
                residHash[resid1 + '_' + resid2] = 1;
                residHash[resid2 + '_' + resid1] = 1;
            }
        }
        let text = '<html><body><div style="text-align:center"><br><b>' + cnt + ' cross-linkage pairs</b>:<br><br><table align=center border=1 cellpadding=10 cellspacing=0><tr><th>Residue ID 1</th><th>Residue ID 2</th></tr>';
        text += tmpText;
        text += '</table><br/></div></body></html>';
        let file_pref =(ic.inputid) ? ic.inputid : "custom";
        ic.saveFileCls.saveFile(file_pref + '_crosslinkage_pairs.html', 'html', text);
    }
    exportHbondPairs(type, labelType) {var ic = this.icn3d, me = ic.icn3dui;
        let tmpText = '';
        let cnt = 0;
        let colorText1 = ' <span style="background-color:#';
        let colorText2 = '">&nbsp;&nbsp;&nbsp;</span>';
        for(let resid1 in ic.resid2ResidhashHbond) {
            let resid1Real = ic.getGraphCls.convertLabel2Resid(resid1);
            let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1Real]);
            let color1 = (atom1.color) ? atom1.color.getHexString() : '';
            for(let resid2 in ic.resid2ResidhashHbond[resid1]) {
                let resid2Real = ic.getGraphCls.convertLabel2Resid(resid2);
                let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2Real]);
                let color2 = (atom2.color) ? atom2.color.getHexString() : '';
                let dist = Math.sqrt(ic.resid2ResidhashHbond[resid1][resid2]).toFixed(1);
                tmpText += '<tr><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'hbond_' +  cnt + 'a" resid="' + resid1 + '"/> ' + resid1 + colorText1 + color1 + colorText2 + '</td><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'hbond_' +  cnt + 'b" resid="' + resid2 + '"/> ' + resid2 + colorText1 + color2 + colorText2 + '</td><td align="center">' + dist + '</td>';
                if(type == 'view') tmpText += '<td align="center"><button class="' + ic.pre + 'selres" resid="' + resid1 + '|' + resid2 + '">Highlight</button></td>';
                tmpText += '</tr>';
                ++cnt;
            }
        }
        let text = '<div style="text-align:center"><br><b>' + cnt
          + ' hydrogen bond pairs</b> (backbone atoms: @CA, @N, @C, @O):</div><br>';
        if(cnt > 0) {
            text += '<br><table align=center border=1 cellpadding=10 cellspacing=0>'
            + '<tr><th>Atom 1</th><th>Atom 2</th><th>Distance(&#8491;)</th>';
            if(type == 'view') text += '<th align="center">Highlight in 3D</th>';
            text += '</tr>';
            text += tmpText;
            text += '</table><br/>';
        }
        if(type == 'graph' || type == 'linegraph' || type == 'scatterplot') {
            let hbondStr = ic.getGraphCls.getGraphLinks(ic.resid2ResidhashHbond, ic.resid2ResidhashHbond, me.htmlCls.hbondColor, labelType, me.htmlCls.hbondValue);
            return hbondStr;
        }
        else {
            return text;
        }
    }
    exportSaltbridgePairs(type, labelType) {var ic = this.icn3d, me = ic.icn3dui;
        let tmpText = '';
        let cnt = 0;
        let colorText1 = ' <span style="background-color:#';
        let colorText2 = '">&nbsp;&nbsp;&nbsp;</span>';
        for(let resid1 in ic.resid2ResidhashSaltbridge) {
            let resid1Real = ic.getGraphCls.convertLabel2Resid(resid1);
            let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1Real]);
            let color1 = (atom1.color) ? atom1.color.getHexString() : '';
            for(let resid2 in ic.resid2ResidhashSaltbridge[resid1]) {
                let resid2Real = ic.getGraphCls.convertLabel2Resid(resid2);
                let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2Real]);
                let color2 = (atom2.color) ? atom2.color.getHexString() : '';
                let dist = Math.sqrt(ic.resid2ResidhashSaltbridge[resid1][resid2]).toFixed(1);
                tmpText += '<tr><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'saltb_' +  cnt + 'a" resid="' + resid1 + '"/> ' + resid1 + colorText1 + color1 + colorText2 + '</td><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'saltb_' +  cnt + 'b" resid="' + resid2 + '"/> ' + resid2 + colorText1 + color2 + colorText2 + '</td><td align="center">' + dist + '</td>';
                if(type == 'view') tmpText += '<td align="center"><button class="' + ic.pre + 'selres" resid="' + resid1 + '|' + resid2 + '">Highlight</button></td>';
                tmpText += '</tr>';
                ++cnt;
            }
        }
        let text = '<div style="text-align:center"><br><b>' + cnt
          + ' salt bridge/ionic interaction pairs</b>:</div><br>';
        if(cnt > 0) {
            text += '<br><table align=center border=1 cellpadding=10 cellspacing=0>'
              + '<tr><th>Atom 1</th><th>Atom 2</th><th>Distance(&#8491;)</th>';
            if(type == 'view') text += '<th align="center">Highlight in 3D</th>';
            text += '</tr>';
            text += tmpText;
            text += '</table><br/>';
        }
        if(type == 'graph' || type == 'linegraph' || type == 'scatterplot') {
            let hbondStr = ic.getGraphCls.getGraphLinks(ic.resid2ResidhashSaltbridge, ic.resid2ResidhashSaltbridge, me.htmlCls.ionicColor, labelType, me.htmlCls.ionicValue);
            return hbondStr;
        }
        else {
            return text;
        }
    }
    exportHalogenPiPairs(type, labelType, interactionType) {var ic = this.icn3d, me = ic.icn3dui;
        let tmpText = '';
        let cnt = 0;
        let colorText1 = ' <span style="background-color:#';
        let colorText2 = '">&nbsp;&nbsp;&nbsp;</span>';
        let resid2Residhash, color, value;
        if(interactionType == 'halogen') {
            resid2Residhash = ic.resid2ResidhashHalogen;
            color = me.htmlCls.halogenColor;
            value = me.htmlCls.halogenValue;
        }
        else if(interactionType == 'pi-cation') {
            resid2Residhash = ic.resid2ResidhashPication;
            color = me.htmlCls.picationColor;
            value = me.htmlCls.picationValue;
        }
        else if(interactionType == 'pi-stacking') {
            resid2Residhash = ic.resid2ResidhashPistacking;
            color = me.htmlCls.pistackingColor;
            value = me.htmlCls.pistackingValue;
        }
        for(let resid1 in resid2Residhash) {
            let resid1Real = ic.getGraphCls.convertLabel2Resid(resid1);
            let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1Real]);
            let color1 = (atom1.color) ? atom1.color.getHexString() : '';
            for(let resid2 in resid2Residhash[resid1]) {
                let resid2Real = ic.getGraphCls.convertLabel2Resid(resid2);
                let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2Real]);
                let color2 = (atom2.color) ? atom2.color.getHexString() : '';
                let dist = Math.sqrt(resid2Residhash[resid1][resid2]).toFixed(1);
                tmpText += '<tr><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + interactionType + '_' +  cnt + 'a" resid="' + resid1 + '"/> ' + resid1 + colorText1 + color1 + colorText2 + '</td><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + interactionType + '_' +  cnt + 'b" resid="' + resid2 + '"/> ' + resid2 + colorText1 + color2 + colorText2 + '</td><td align="center">' + dist + '</td>';
                if(type == 'view') tmpText += '<td align="center"><button class="' + ic.pre + 'selres" resid="' + resid1 + '|' + resid2 + '">Highlight</button></td>';
                tmpText += '</tr>';
                ++cnt;
            }
        }
        let text = '<div style="text-align:center"><br><b>' + cnt
          + ' ' + interactionType + ' pairs</b>:</div><br>';
        if(cnt > 0) {
            text += '<br><table align=center border=1 cellpadding=10 cellspacing=0>'
              + '<tr><th>Atom 1</th><th>Atom 2</th><th>Distance(&#8491;)</th>';
            if(type == 'view') text += '<th align="center">Highlight in 3D</th>';
            text += '</tr>';
            text += tmpText;
            text += '</table><br/>';
        }
        if(type == 'graph' || type == 'linegraph' || type == 'scatterplot') {
            let hbondStr = ic.getGraphCls.getGraphLinks(resid2Residhash, resid2Residhash, color, labelType, value);
            return hbondStr;
        }
        else {
            return text;
        }
    }
    exportSpherePairs(bInteraction, type, labelType) {var ic = this.icn3d, me = ic.icn3dui;
        let tmpText = '';
        let cnt = 0;
        let residHash =(bInteraction) ? ic.resid2ResidhashInteractions : ic.resid2ResidhashSphere;
        let colorText1 = ' <span style="background-color:#';
        let colorText2 = '">&nbsp;&nbsp;&nbsp;</span>';
        for(let resid1 in residHash) { // e.g., resid1: TYR $1KQ2.A:42
            let resid1Real = ic.getGraphCls.convertLabel2Resid(resid1);
            let atom1 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid1Real]);
            let color1 = (atom1.color) ? atom1.color.getHexString() : '';
            for(let resid2 in residHash[resid1]) {
                let resid2Real = ic.getGraphCls.convertLabel2Resid(resid2);
                let atom2 = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid2Real]);
                let color2 = (atom2.color) ? atom2.color.getHexString() : '';
                let dist1_dist2_atom1_atom2 = residHash[resid1][resid2].split('_');
                let dist1 = dist1_dist2_atom1_atom2[0];
                let dist2 = dist1_dist2_atom1_atom2[1];
                atom1 = dist1_dist2_atom1_atom2[2];
                atom2 = dist1_dist2_atom1_atom2[3];
                let contactCnt = dist1_dist2_atom1_atom2[4];
                if(bInteraction) {
                    tmpText += '<tr><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'inter_' +  cnt + 'a" resid="' + resid1 + '"/> ' + resid1 + '@' + atom1 + colorText1 + color1 + colorText2 + '</td><td><input type="checkbox" class="' + ic.pre + 'seloneres" id="' + ic.pre + 'inter_' +  cnt + 'b" resid="' + resid2 + '"/> ' + resid2 + '@' + atom2 + colorText1 + color2 + colorText2 + '</td><td align="center">' + contactCnt + '</td><td align="center">' + dist1 + '</td><td align="center">' + dist2 + '</td>';
                    if(type == 'view') tmpText += '<td align="center"><button class="' + ic.pre + 'selres" resid="' + resid1 + '|' + resid2 + '">Highlight</button></td>';
                    tmpText += '</tr>';
                }
                else {
                    tmpText += '<tr><td>' + resid1 + '</td><td>' + resid2 + '</td><td align="center">' + contactCnt + '</td><td align="center">' + dist1 + '</td><td align="center">' + dist2 + '</td></tr>';
                }
                ++cnt;
            }
        }
        let nameStr =(bInteraction) ? "the contacts" : "sphere";
        let text = '<div style="text-align:center"><br><b>' + cnt
          + ' residue pairs in ' + nameStr + '</b>:</div><br>';
        if(cnt > 0) {
            if(bInteraction) {
                text += '<br><table align=center border=1 cellpadding=10 cellspacing=0>'
                  + '<tr><th>Residue 1</th><th>Residue 2</th><th align="center">Num Contacts</th><th align="center">Min Distance(&#8491;)</th><th align="center">C-alpha Distance(&#8491;)</th>';
                if(type == 'view') text += '<th align="center">Highlight in 3D</th>';
                text += '</tr>';
            }
            else {
                text += '<br><table align=center border=1 cellpadding=10 cellspacing=0>'
                  + '<tr><th>Residue 1</th><th>Residue 2</th><th align="center">Num Contacts</th><th align="center">Min Distance(&#8491;)</th><th align="center">C-alpha Distance(&#8491;)</th></tr>';
            }
            text += tmpText;
            text += '</table><br/>';
        }
        if(type == 'graph' || type == 'linegraph' || type == 'scatterplot'
          || type == 'calpha' || type == 'cbeta' || type == 'heavyatoms') {
            let interStr = ic.getGraphCls.getGraphLinks(residHash, residHash, me.htmlCls.contactColor, labelType, me.htmlCls.contactValue);
            return interStr;
        }
        else {
            return text;
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class DrawGraph {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    drawGraph(jsonStr, divid) { var ic = this.icn3d, me = ic.icn3dui;
        //function createV4SelectableForceDirectedGraph(svg, graph) {
        // if both d3v3 and d3v4 are loaded, we'll assume
        // that d3v4 is called d3v4, otherwise we'll assume
        // that d3v4 is the default (d3)
        if (typeof d3v4 == 'undefined')
            var d3v4 = d3;

        //if(ic.bRender !== true) return;

        var graph = JSON.parse(jsonStr);

        //var width = +svg.attr("width"),
        //    height = +svg.attr("height");

        var width = $("#" + divid).width();
        var height = $("#" + divid).height();

        var widthView = (!isNaN(width)) ? width * 1.0 : 300;
        var heightView = (!isNaN(height)) ? height * 1.0 : 300;

        var parentWidth = width;
        var parentHeight = height;

        //    var svg = d3v4.select('svg')
        //    .attr('width', parentWidth)
        //    .attr('height', parentHeight)

        var svg = d3.select("#" + me.svgid)
            .attr("width", width)
            .attr("height", height)
            .attr("viewBox", "0,0," + widthView + "," + heightView);

        // remove any previous graphs
        svg.selectAll('.g-main').remove();
        // added
        //$("#" + me.svgid).empty();

        var gMain = svg.append('g')
            .classed('g-main', true);

        var rect = gMain.append('rect')
            .attr('width', parentWidth)
            .attr('height', parentHeight)
            .style('fill', '#FFF');

        var gDraw = gMain.append('g');

        var zoom = d3v4.zoom()
            .on('zoom', zoomed);

        gMain.call(zoom);


        function zoomed() {
            gDraw.attr('transform', d3v4.event.transform);
        }

        //var color = d3v4.scaleOrdinal(d3v4.schemeCategory20);

        if (!(graph.links)) {
            console.log("Graph is missing links");
            return;
        }

        // clean graph.links
        var linkArray = [];

        var nodeHash = {};
        for (var i = 0, il = graph.nodes.length; i < il; ++i) {
            var node = graph.nodes[i];
            nodeHash[node.id] = 1;
        }

        var bError = false;
        for (var i = 0, il = graph.links.length; i < il; ++i) {
            var link = graph.links[i];

            if (nodeHash.hasOwnProperty(link.source) && nodeHash.hasOwnProperty(link.target)) {
                linkArray.push(link);
            } else {
                if (!nodeHash.hasOwnProperty(link.source)) {
                    console.log("The node " + link.source + " is not found... ");
                }
                if (!nodeHash.hasOwnProperty(link.target)) {
                    console.log("The node " + link.target + " is not found... ");
                }

                bError = true;
            }
        }

        if (bError) console.log(JSON.stringify(graph));

        graph.links = linkArray;

        var nodes = {};
        var i;
        for (i = 0; i < graph.nodes.length; i++) {
            // enlarge the distance when no force
            if (!me.htmlCls.force) {
                graph.nodes[i].x *= 10;
                graph.nodes[i].y *= 10;
            }
            nodes[graph.nodes[i].id] = graph.nodes[i];
            graph.nodes[i].weight = 1.01;
        }

        // remove the internal edges when no force
        if (me.htmlCls.hideedges && !me.htmlCls.force) {
            var links2 = [];
            for (i = 0; i < graph.links.length; i++) {
                if (graph.links[i].c != 'FFF') {
                    links2.push(graph.links[i]);
                }
            }

            graph.links = links2;
        }

        // the brush needs to go before the nodes so that it doesn't
        // get called when the mouse is over a node
        var gBrushHolder = gDraw.append('g');
        var gBrush = null;

        var link = gDraw.append("g")
            .attr("class", "link")
            .selectAll("line")
            .data(graph.links)
            .enter().append("line")
            //.attr("stroke", function(d) { return "#" + d.c; })
            .attr("stroke", function(d) {
                if (d.v == me.htmlCls.contactInsideValue) return "#" + me.htmlCls.contactInsideColor;
                else if (d.v == me.htmlCls.hbondInsideValue) return "#" + me.htmlCls.hbondInsideColor;
                else if (d.v == me.htmlCls.ionicInsideValue) return "#" + me.htmlCls.ionicInsideColor;
                else if (d.v == me.htmlCls.halogenInsideValue) return "#" + me.htmlCls.halogenInsideColor;
                else if (d.v == me.htmlCls.picationInsideValue) return "#" + me.htmlCls.picationInsideColor;
                else if (d.v == me.htmlCls.pistackingInsideValue) return "#" + me.htmlCls.pistackingInsideColor;
                else return "#" + d.c;
            })
            .attr("stroke-width", function(d) {
                if (d.v == me.htmlCls.contactValue || d.v == me.htmlCls.contactInsideValue ||
                    d.v == me.htmlCls.hbondInsideValue || d.v == me.htmlCls.ionicInsideValue ||
                    d.v == me.htmlCls.halogenInsideValue || d.v == me.htmlCls.picationInsideValue ||
                    d.v == me.htmlCls.pistackingInsideValue) return "1px";
                else if (d.v == me.htmlCls.hbondValue || d.v == me.htmlCls.ionicValue ||
                    d.v == me.htmlCls.halogenValue || d.v == me.htmlCls.picationValue ||
                    d.v == me.htmlCls.pistackingValue) return "2px";
                else if (d.v == me.htmlCls.ssbondValue || d.v == me.htmlCls.clbondValue) return "3px";
                else return d.v + "px";
            });

        var allNodes = gDraw.append("g")
            .attr("class", "node");

        var node = allNodes.selectAll("circle")
            .data(graph.nodes)
            //.attr("cx", function(d){return d.x})
            //.attr("cy", function(d){return d.y})
            .enter().append("circle")
            .attr("r", 3) //5)
            .attr("fill", function(d) { return "#" + d.c; })
            .attr("stroke", function(d) { return "#" + d.c; })
            .attr("res", function(d) { return d.r; })
            .attr("class", "icn3d-node")
            .call(d3v4.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended));

        var label = allNodes.selectAll("text")
            .data(graph.nodes)
            .enter().append("text")
            .text(function(d) {
                var idStr = d.id;
                var pos = idStr.indexOf('.');
                if (pos !== -1) idStr = idStr.substr(0, pos);
                return idStr;
            })
            //.style("stroke", function(d) { return "#" + d.c; })
            .attr("fill", function(d) { return "#" + d.c; })
            .attr("stroke", "none")
            .attr("class", "icn3d-node-text8");
        //.style("font-size", "8px")
        //.style("font-weight", "bold")
        //.attr("x", function(d){return d.x + 6})
        //.attr("y", function(d){return d.y + 3})

        // add titles for mouseover blurbs
        node.append("title")
            .text(function(d) { return d.id; });

        var dist_ss = parseInt($("#" + ic.pre + "dist_ss").val());
        var dist_coil = parseInt($("#" + ic.pre + "dist_coil").val());
        var dist_hbond = parseInt($("#" + ic.pre + "dist_hbond").val());
        var dist_inter = parseInt($("#" + ic.pre + "dist_inter").val());
        var dist_ssbond = parseInt($("#" + ic.pre + "dist_ssbond").val());
        var dist_ionic = parseInt($("#" + ic.pre + "dist_ionic").val());

        var dist_halogen = parseInt($("#" + ic.pre + "dist_halogen").val());
        var dist_pication = parseInt($("#" + ic.pre + "dist_pication").val());
        var dist_pistacking = parseInt($("#" + ic.pre + "dist_pistacking").val());

        me.htmlCls.simulation = d3v4.forceSimulation()
            .force("link", d3v4.forceLink()
                .id(function(d) { return d.id; })
                .distance(function(d) {
                    //var dist = 20 / d.value;
                    //return dist;

                    return 30;
                })
                .strength(function(d) {
                    if (!me.htmlCls.force) {
                        return 0;
                    } else {
                        //return 1 / Math.min(count(d.source), count(d.target));

                        // larger distance means more relaxed
                        if (d.v == me.htmlCls.ssValue) { // secondary
                            return !isNaN(dist_ss) ? dist_ss / 100.0 : 1;
                        } else if (d.v == me.htmlCls.coilValue || d.v == me.htmlCls.clbondValue) { // coil
                            return !isNaN(dist_coil) ? dist_coil / 100.0 : 0.5;
                        } else if (d.v == me.htmlCls.hbondValue || d.v == me.htmlCls.hbondInsideValue) { // hydrogen bonds
                            return !isNaN(dist_hbond) ? dist_hbond / 100.0 : 0.5;
                        } else if (d.v == me.htmlCls.contactValue || d.v == me.htmlCls.contactInsideValue) { // interactions
                            return !isNaN(dist_inter) ? dist_inter / 100.0 : 0.25;
                        } else if (d.v == me.htmlCls.ssbondValue) { // hydrogen bonds
                            return !isNaN(dist_ssbond) ? dist_ssbond / 100.0 : 0.5;
                        } else if (d.v == me.htmlCls.ionicValue || d.v == me.htmlCls.ionicInsideValue) { // ionic interaction
                            return !isNaN(dist_ionic) ? dist_ionic / 100.0 : 0.5;
                        } else if (d.v == me.htmlCls.halogenValue || d.v == me.htmlCls.halogenInsideValue) {
                            return !isNaN(dist_halogen) ? dist_halogen / 100.0 : 0.5;
                        } else if (d.v == me.htmlCls.picationValue || d.v == me.htmlCls.picationInsideValue) {
                            return !isNaN(dist_pication) ? dist_pication / 100.0 : 0.5;
                        } else if (d.v == me.htmlCls.pistackingValue || d.v == me.htmlCls.pistackingInsideValue) {
                            return !isNaN(dist_pistacking) ? dist_pistacking / 100.0 : 0.5;
                        } else {
                            return 0;
                        }
                    } // else
                })
            )
            .force("center", d3v4.forceCenter(parentWidth / 2, parentHeight / 2));

        if (me.htmlCls.force) {
            me.htmlCls.simulation.force("charge", d3v4.forceManyBody());
        }

        //me.htmlCls.simulation.force("x", d3v4.forceX(parentWidth/2))
        //    .force("y", d3v4.forceY(parentHeight/2));

        if (me.htmlCls.force == 1) { // x-axis
            me.htmlCls.simulation.force("x", d3v4.forceX(function(d) {
                    if (d.s == 'a') {
                        return parentWidth / 4;
                    } else {
                        return parentWidth * 0.75;
                    }
                }).strength(function(d) { return 0.4; }))
                .force("y", d3v4.forceY(parentHeight / 2).strength(function(d) { return 0.02; }));

        } else if (me.htmlCls.force == 2) { // y-axis
            me.htmlCls.simulation.force("y", d3v4.forceY(function(d) {
                    if (d.s == 'a') {
                        return parentHeight * 0.75;
                    } else {
                        return parentHeight / 4;
                    }
                }).strength(function(d) { return 0.4; }))
                .force("x", d3v4.forceX(parentWidth / 2).strength(function(d) { return 0.02; }));
        } else if (me.htmlCls.force == 3) { // circle
            me.htmlCls.simulation.force("r", d3v4.forceRadial(function(d) {
                if (d.s == 'a') {
                    return 200;
                } else {
                    return 100;
                }

            }, parentWidth / 2, parentHeight / 2).strength(function(d) { return 0.8; }));
        } else if (me.htmlCls.force == 4) ;

        me.htmlCls.simulation
            .nodes(graph.nodes)
            .on("tick", ticked);

        me.htmlCls.simulation.force("link")
            .links(graph.links);

        //    me.htmlCls.simulation.stop();
        //    me.htmlCls.simulation.restart();

        function ticked() {
            // update node and line positions at every step of
            // the force me.htmlCls.simulation
            link.attr("x1", function(d) { var ret = d.source.x; return !isNaN(ret) ? ret : 0; })
                .attr("y1", function(d) { var ret = parentHeight - d.source.y; return !isNaN(ret) ? ret : 0; })
                .attr("x2", function(d) { var ret = d.target.x; return !isNaN(ret) ? ret : 0; })
                .attr("y2", function(d) { var ret = parentHeight - d.target.y; return !isNaN(ret) ? ret : 0; });

            node.attr("cx", function(d) { var ret = d.x; return !isNaN(ret) ? ret : 0; })
                .attr("cy", function(d) { var ret = parentHeight - d.y; return !isNaN(ret) ? ret : 0; });

            label.attr("x", function(d) { var ret = d.x + 6; return !isNaN(ret) ? ret : 0; })
                .attr("y", function(d) { var ret = parentHeight - (d.y + 3); return !isNaN(ret) ? ret : 0; });

        }

        var brushMode = false;
        var brushing = false;

        var brush = d3v4.brush()
            .on("start", brushstarted)
            .on("brush", brushed)
            .on("end", brushended);

        function brushstarted() {
            // keep track of whether we're actively brushing so that we
            // don't remove the brush on keyup in the middle of a selection
            brushing = true;

            node.each(function(d) {
                d.previouslySelected = ctrlKey && d.selected;
            });
        }

        rect.on('click', function() {
            node.each(function(d) {
                d.selected = false;
                d.previouslySelected = false;
            });
            node.classed("selected", false);
        });

        function brushed() {
            if (!d3v4.event.sourceEvent) return;
            if (!d3v4.event.selection) return;

            var extent = d3v4.event.selection;

            node.classed("selected", function(d) {
                return d.selected = d.previouslySelected ^
                    (extent[0][0] <= d.x && d.x < extent[1][0] &&
                        extent[0][1] <= parentHeight - d.y && parentHeight - d.y < extent[1][1]);
            });
        }

        function brushended() {
            if (!d3v4.event.sourceEvent) return;
            if (!d3v4.event.selection) return;
            if (!gBrush) return;

            gBrush.call(brush.move, null);

            if (!brushMode) {
                // the shift key has been release before we ended our brushing
                gBrush.remove();
                gBrush = null;
            }

            brushing = false;
        }

        d3v4.select('body').on('keydown', keydown);
        d3v4.select('body').on('keyup', keyup);

        var ctrlKey;

        function keydown() {
            ctrlKey = d3v4.event.ctrlKey;

            if (ctrlKey) {
                // if we already have a brush, don't do anything
                if (gBrush)
                    return;

                brushMode = true;

                if (!gBrush) {
                    gBrush = gBrushHolder.append('g');
                    gBrush.call(brush);
                }
            }
        }

        function keyup() {
            ctrlKey = false;
            brushMode = false;

            if (!gBrush)
                return;

            if (!brushing) {
                // only remove the brush if we're not actively brushing
                // otherwise it'll be removed when the brushing ends
                gBrush.remove();
                gBrush = null;
            }
        }

        function dragstarted(d) {
            if (!d3v4.event.active) me.htmlCls.simulation.alphaTarget(0.9).restart();

            if (!d.selected && !ctrlKey) {
                // if this node isn't selected, then we have to unselect every other node
                node.classed("selected", function(p) {
                    return p.selected = p.previouslySelected = false;
                });
            }

            d3v4.select(this).classed("selected", function(p) { d.previouslySelected = d.selected; return d.selected = true; });

            node.filter(function(d) { return d.selected; })
                .each(function(d) { //d.fixed |= 2;
                    d.fx = d.x;
                    d.fy = d.y;
                });

        }

        function dragged(d) {
            //d.fx = d3v4.event.x;
            //d.fy = d3v4.event.y;
            node.filter(function(d) { return d.selected; })
                .each(function(d) {
                    d.fx += d3v4.event.dx;
                    d.fy -= d3v4.event.dy; // += d3v4.event.dy;
                });
        }

        function dragended(d) {
            if (!d3v4.event.active) me.htmlCls.simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
            node.filter(function(d) { return d.selected; })
                .each(function(d) { //d.fixed &= ~6;
                    d.fx = null;
                    d.fy = null;
                });
        }

        return graph;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ContactMap {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async contactMap(contactDist, type) { let ic = this.icn3d; ic.icn3dui;
       let nameArray = ['selected'];
       let nameArray2 = ['selected'];
       if(nameArray2.length == 0) {
           alert("Please select the first set");
       }
       else {
           ic.definedSetsCls.setMode('selection');
           let bHbond = false;
           let bSaltbridge = false;
           let bInteraction = true;
           let bHalogen = false;
           let bPication = false;
           let bPistacking = false;
           await ic.viewInterPairsCls.viewInteractionPairs(nameArray2, nameArray, false, type,
                bHbond, bSaltbridge, bInteraction, bHalogen, bPication, bPistacking, contactDist);
       }
    }

    async afErrorMap(afid, bFull) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        me.htmlCls.dialogCls.openDlg('dl_alignerrormap', 'Show Predicted Aligned Error (PAE) map');
    
        let url = "https://alphafold.ebi.ac.uk/files/AF-" + afid + "-F1-predicted_aligned_error_" + ic.AFUniprotVersion + ".json";

        let data = await me.getAjaxPromise(url, 'json', false, 'There are some problems in loading the PAE file...');

        thisClass.processAfErrorMap(data, bFull);
    }

    processAfErrorMap(dataJson, bFull) { let ic = this.icn3d, me = ic.icn3dui;
        // json format: [{"residue1": [1, ..., 1, ..., n, ..., n], "residue2": [1, 2, ..., n, ..., 1, 2, ..., n], 
        // "distance": [n*n matrix],"max_predicted_aligned_error":31.75}]
        //let distMatrix = dataJson[0].distance; // version 2, one dimension
        let distMatrix = dataJson[0].predicted_aligned_error; // version 3, two dimensions 
        let max = dataJson[0].max_predicted_aligned_error;
        if(!distMatrix || !max) {
            alert("The PAE file didn't have the right format...");
            return;
        }

        // generate lineGraphStr
        // e.g.,  {"nodes": [{"id":"A1.A","r":"1_1_1TOP_A_1","s":"ab","x":1,"y":21,"c":"FF00FF"}, ...],
        // "links": [{"source": "A1.A", "target": "S2.A", "v": 3, "c": "FF00FF"}, ...]}
        let nodeStr = '"nodes": [', linkStr = '"links": [';
        let bNode = false, bLink = false;
        let postA = '', postB = '.';

        // initialize some parameters if no structure wasloaded yet
        let bStruData;
        if(!ic.chains || Object.keys(ic.chains).length == 0) {
            bStruData = false;
            ic.init_base();
        }
        else {
            bStruData = true;
        }

        //let chainidArray = Object.keys(ic.chains);
        //let chainid = (chainidArray.length == 1) ? chainidArray[0] : 'stru_A';

        //let dim = parseInt(Math.sqrt(distMatrix.length));
        let dim = distMatrix.length;

        // map index with residue number when the structure has multiple chains
        let index = 0;
        let index2resObj = {};
        for(let chainid in ic.chains) {
            for(let j = 0, jl = ic.chainsSeq[chainid].length; j < jl; ++j) {
                index2resObj[index] = ic.chainsSeq[chainid][j];
                index2resObj[index].chainid = chainid;
                ++index;
            }
        }

        //for(let chainid in ic.chains) {
        //for(let i = 0, il = ic.chainsSeq[chainid].length; i < il; ++i) {
        index = 0;
        for(let i = 0; i < dim; ++i) {
            let resi = (bStruData) ? index2resObj[i].resi : i + 1;
            let resn = (bStruData) ? index2resObj[i].name : '*';
            let chainid = (bStruData) ? index2resObj[i].chainid : 'stru_A';

            let resid = chainid + '_' + resi;
            let atom = (ic.residues[resid]) ? ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]) 
                : {color: me.parasCls.thr(0x888888)};
            let chain = chainid.substr(chainid.indexOf('_') + 1);
            let color = atom.color.getHexString();

            if(bNode) nodeStr += ', ';
            let idStr = resn + resi + '.' + chain;
            nodeStr += '{"id":"' + idStr + postA + '","r":"1_1_' + resid + '","s":"a","c":"' + color + '"}\n';
            nodeStr += ', {"id":"' + idStr + postB + '","r":"1_1_' + resid + '","s":"b","c":"' + color + '"}';
            bNode = true;

            let start = (bFull) ? 0 : i; // full map, or half map

            //for(let j = 0, jl = ic.chainsSeq[chainid].length; j < jl; ++j) {
            //for(let j = 0; j < dim; ++j) {
            for(let j = start; j < dim; ++j) { 
                index = i * dim + j;
                let resi2 = (bStruData) ? index2resObj[j].resi : j + 1;
                let resn2 = (bStruData) ? index2resObj[j].name : '*';
                let chainid2 = (bStruData) ? index2resObj[j].chainid : 'stru_A';
                let chain2 = chainid2.substr(chainid2.indexOf('_') + 1);

                let idStr2 = resn2 + resi2 + '.' + chain2;
                
                // max dark green color 004d00, 0x4d = 77, 77/255 = 0.302
                // 0: 004d00, max: FFFFFF
                //let ratio = (distMatrix[index]) ? distMatrix[index] / max : 0;
                let ratio = (distMatrix[i][j]) ? distMatrix[i][j] / max : 0;
                let r = parseInt(ratio*255).toString(16);
                let g = parseInt(((1.0 - 0.302)*ratio + 0.302) * 255).toString(16);
                let rHex = (r.length == 1) ? '0' + r : r;
                let gHex = (g.length == 1) ? '0' + g : g;
                let bHex = rHex;
                let color2 = rHex + gHex + bHex;

                if(bLink) linkStr += ', ';
                linkStr += '{"source": "' + idStr + postA + '", "target": "' + idStr2 + postB + '", "v": 11, "c": "' + color2 + '"}\n';
                bLink = true;
            }
        }
        //}

        dataJson = {};

        let lineGraphStr = '{' + nodeStr + '], ' + linkStr + ']}';
        let bAfMap = true;
        this.drawContactMap(lineGraphStr, bAfMap, max);    
        
        /// if(ic.deferredAfmap !== undefined) ic.deferredAfmap.resolve();
    }

    drawContactMap(lineGraphStr, bAfMap, max) { let ic = this.icn3d, me = ic.icn3dui;
        let html, graph = JSON.parse(lineGraphStr);
        let linkArray = graph.links;

        let nodeArray1 = [], nodeArray2 = [];
        let name2node = {};
        for(let i = 0, il = graph.nodes.length; i < il; ++i) {
            let node = graph.nodes[i];
            if(!node) continue;

            name2node[node.id] = node;

            if(node.s == 'a') {
                nodeArray1.push(node);
            }
            else if(node.s == 'b') {
                nodeArray2.push(node);
            }
            else if(node.s == 'ab') {
                nodeArray1.push(node);
                nodeArray2.push(node);
            }
        }

        // sort array
        nodeArray1.sort(function(a,b) {
          return ic.getGraphCls.compNode(a, b);
        });
        nodeArray2.sort(function(a,b) {
          return ic.getGraphCls.compNode(a, b);
        });

        let graphStr = '{\n';

        let struc1 = (ic.structures.length > 0) ? ic.structures[0] : 'stru';
        let len1 = nodeArray1.length,
            len2 = nodeArray2.length;
        let factor = 1;
        let r = 3 * factor;
        let gap = 7 * factor;
        let width, heightAll;
        let marginX = 10,
            marginY = 10,
            legendWidth = 30;
        heightAll =(len1 + 2) *(r + gap) + 2 * marginY + legendWidth;
        width =(len2 + 2) *(r + gap) + 2 * marginX + legendWidth;

        let id, graphWidth;
        if(bAfMap) {
            ic.alignerrormapWidth = 2 * width;
            graphWidth = ic.alignerrormapWidth;
            id = me.alignerrormapid;
        }
        else {
            ic.contactmapWidth = 2 * width;
            graphWidth = ic.contactmapWidth;
            id = me.contactmapid;
        }

        html =(linkArray.length > 0) ? "" : "No interactions found for these two sets<br><br>";
        html += "<svg xmlns='http://www.w3.org/2000/svg' id='" + id + "' viewBox='0,0," + width + "," + heightAll + "' width='" + graphWidth + "px'>";
        let bContactMap = true;

        if(bAfMap) { // cleaned the code by using "use" in SVG, but didn't improve rendering

            ic.hex2id = {};
            let threshold = 29.0 / max;
            ic.hex2skip = {}; // do not display any error larger than 29 angstrom
            let nRef = 1000;
            for(let i = 0; i < nRef; ++i) {
                let ratio = 1.0 * i / nRef;
                let r = parseInt(ratio*255).toString(16);
                let g = parseInt(((1.0 - 0.302)*ratio + 0.302) * 255).toString(16);
                let rHex = (r.length == 1) ? '0' + r : r;
                let gHex = (g.length == 1) ? '0' + g : g;
                let bHex = rHex;
                let color = rHex + gHex + bHex;

                let idRect = me.pre + "afmap_" + i;

                ic.hex2id[color] = idRect;
                if(ratio > threshold) {
                    ic.hex2skip[color] = idRect;
                }
                
                //html += "<g id='" + id + "'>";
//                html += "<rect id='" + idRect + "' x='0' y='0' width='" + rectSize + "' height='" + rectSize + "' fill='" 
//                    + strokecolor + "' stroke-width='" + linestrokewidth + "' stroke='" + strokecolor + "' />";
                //html += "</g>"
            }
//            html += "</defs>";
        }

        html += ic.lineGraphCls.drawScatterplot_base(nodeArray1, nodeArray2, linkArray, name2node, 0, bContactMap, undefined, undefined, bAfMap);
        graphStr += ic.getGraphCls.updateGraphJson(struc1, 1, nodeArray1, nodeArray2, linkArray);
        html += "</svg>";

        graphStr += '}\n';
        if(bAfMap) {
            ic.alignerrormapStr = graphStr;
            $("#" + ic.pre + "alignerrormapDiv").html(html);
  
            let scale = $("#" + me.alignerrormapid + "_scale").val();
            $("#" + me.alignerrormapid).attr("width",(ic.alignerrormapWidth * parseFloat(scale)).toString() + "px");
        }
        else {
            ic.contactmapStr = graphStr;
            $("#" + ic.pre + "contactmapDiv").html(html);
        }

        return html;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class AlignParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Load the VAST+ structure alignment for the pair of structures "align", e.g., "align" could be "1HHO,4N7N".
    async downloadAlignment(align, bDiagramOnly) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        ic.opts['proteins'] = 'c alpha trace';

        let alignArray = align.split(',');
        //var ids_str =(alignArray.length === 2? 'uids=' : 'ids=') + align;
        let ids_str = 'ids=' + align;

    //    let url2 = me.htmlCls.baseUrl + 'vastplus/vastplus.cgi?v=2&cmd=c&b=1&s=1&w3d&' + ids_str;
    //    let url2 = me.htmlCls.baseUrl + 'vastplus/vastplus.cgi?v=2&cmd=c&b=1&s=1&w3d&' + ids_str;
    //    let url1 = me.htmlCls.baseUrl + 'vastplus/vastplus.cgi?v=2&cmd=c1&b=1&s=1&d=1&' + ids_str;

        // combined url1 and url2
        let url2 = me.htmlCls.baseUrl + 'vastplus/vastplus.cgi?v=3&cmd=c&b=1&s=1&w3d&' + ids_str;

        if(me.cfg.inpara !== undefined) {
          //url1 += me.cfg.inpara;
          url2 += me.cfg.inpara;
        }

        //ic.bCid = undefined;

        // define for 'align' only
        ic.pdbid_chain2title = {};

        if(ic.chainids2resids === undefined) ic.chainids2resids = {}; // ic.chainids2resids[chainid1][chainid2] = [resid, resid]

        let seqalign = {};

        let errMess = "These two MMDB IDs " + alignArray + " do not have 3D alignment data in the VAST+ database. You can try the VAST alignment by visiting the VAST+ page https://www.ncbi.nlm.nih.gov/Structure/vastplus/vastplus.cgi?uid=[PDB ID] (e.g., uid=1KQ2), and clicking \"Original VAST\"";

        let data = await me.getAjaxPromise(url2, 'jsonp', true, errMess);

        seqalign = data.seqalign;
        if(seqalign === undefined) {
            alert(errMess);
            return false;
        }

        // set ic.pdbid_molid2chain and ic.chainsColor
        ic.pdbid_molid2chain = {};
        ic.chainsColor = {};
        //ic.mmdbidArray = [];
        //for(let i in data) {

        for(let i = 0, il = 2; i < il; ++i) {
            //if(i === 'seqalign') continue;
            let mmdbTmp = data['alignedStructures'][0][i];

            //var pdbid =(data[i].pdbid !== undefined) ? data[i].pdbid : i;
            let pdbid =(mmdbTmp.pdbId !== undefined) ? mmdbTmp.pdbId : mmdbTmp.mmdbId;
            //ic.mmdbidArray.push(pdbid); // here two molecules are in alphabatic order, themaster molecule could not be the first one

            let chainNameHash = {}; // chain name may be the same in assembly
            //for(let molid in mmdbTmp.molecules) {
            for(let j = 0, jl = mmdbTmp.molecules.length; j < jl; ++j) {
                let molecule = mmdbTmp.molecules[j];
                let molid = molecule.moleculeId;
                let chainName = molecule.chain.trim();
                if(chainNameHash[chainName] === undefined) {
                    chainNameHash[chainName] = 1;
                }
                else {
                    ++chainNameHash[chainName];
                }

                let finalChain =(chainNameHash[chainName] === 1) ? chainName : chainName + chainNameHash[chainName].toString();

                ic.pdbid_molid2chain[pdbid + '_' + molid] = finalChain;

                if(molecule.kind === 'p' || molecule.kind === 'n') {
                    ic.chainsColor[pdbid + '_' + finalChain] = me.parasCls.thr(me.htmlCls.GREY8);
                }
            }
        }

        //var index = 0;
        //for(let mmdbid in data) {
        ic.mmdbidArray = [];
        for(let i = 0, il = 2; i < il; ++i) {
            //if(index < 2) {
                let mmdbTmp = data['alignedStructures'][0][i];

                let pdbid = mmdbTmp.pdbId;
                ic.mmdbidArray.push(pdbid);

                let molecule = mmdbTmp.molecules;
                for(let molname in molecule) {
                    let chain = molecule[molname].chain;
                    ic.pdbid_chain2title[pdbid + '_' + chain] = molecule[molname].name;
                }
            //}

            //++index;
        }

        // get the color for each aligned chain pair
        ic.alignmolid2color = [];
        //ic.alignmolid2color[0] = {}
        //ic.alignmolid2color[1] = {}
        me.parasCls.stdChainColors.length;

        for(let i = 0, il = seqalign.length; i < il; ++i) {
            let molid1 = seqalign[i][0].moleculeId;
            let molid2 = seqalign[i][1].moleculeId;

            //ic.alignmolid2color[0][molid1] =(i+1).toString();
            //ic.alignmolid2color[1][molid2] =(i+1).toString();

            let tmpHash = {};
            tmpHash[molid1] =(i+1).toString();
            ic.alignmolid2color.push(tmpHash);

            tmpHash = {};
            tmpHash[molid2] =(i+1).toString();
            ic.alignmolid2color.push(tmpHash);
        }

        if(!bDiagramOnly) {
            //var url3 = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&atomonly=1&uid=" + ic.mmdbidArray[0];
            //var url4 = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&atomonly=1&uid=" + ic.mmdbidArray[1];
            // need the parameter moleculeInfor
            let url3 = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&uid=" + ic.mmdbidArray[0];
            let url4 = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&uid=" + ic.mmdbidArray[1];

            let d3 = me.getAjaxPromise(url3, 'jsonp', true);
            let d4 = me.getAjaxPromise(url4, 'jsonp', true);

            let allPromise = Promise.allSettled([d3, d4]);

            let dataArray = await allPromise;

            let data2 = data;
            let data3 = dataArray[0].value; //v3[0];
            let data4 = dataArray[1].value; //v4[0];

            if(data3.atoms !== undefined && data4.atoms !== undefined) {
                // ic.deferredOpm = $.Deferred(function() {
                    //ic.mmdbidArray = [];
                    //for(let i = 0, il = data.alignedStructures[0].length; i < il; ++i) {
                    //    ic.mmdbidArray.push(data.alignedStructures[0][i].pdbId);
                    //}

                    ic.ParserUtilsCls.setYourNote((ic.mmdbidArray[0] + ',' + ic.mmdbidArray[1]).toUpperCase() + '(VAST+) in iCn3D');

                    // get transformation factors
                    let factor = 1; //10000;
                    //var scale = data2.transform.scale / factor;
                    let tMaster = data2.transform.translate.master;
                    let tMVector = new THREE.Vector3(tMaster[0] / factor, tMaster[1] / factor, tMaster[2] / factor);
                    let tSlave = data2.transform.translate.slave;
                    let tSVector = new THREE.Vector3(tSlave[0] / factor, tSlave[1] / factor, tSlave[2] / factor);
                    let rotation = data2.transform.rotate;
                    let rMatrix = [];
                    for(let i = 0, il = rotation.length; i < il; ++i) { // 9 elements
                        rMatrix.push(rotation[i] / factor);
                    }

                    // get sequence
                    ic.chainid2seq = {};
                    for(let chain in data3.sequences) {
                        let chainid = ic.mmdbidArray[0] + '_' + chain;
                        ic.chainid2seq[chainid] = data3.sequences[chain]; // ["0","D","ASP"],
                    }
                    for(let chain in data4.sequences) {
                        let chainid = ic.mmdbidArray[1] + '_' + chain;
                        ic.chainid2seq[chainid] = data4.sequences[chain]; // ["0","D","ASP"],
                    }

                    // atoms
                    let atomsM = data3.atoms;
                    let atomsS = data4.atoms;

                    // fix serialInterval
                    let nAtom1 = data3.atomCount;
                    let nAtom2 = data4.atomCount;

                    for(let i = 0, il = data2.alignedStructures[0].length; i < il; ++i) {
                    let structure = data2.alignedStructures[0][i];

                    structure.serialInterval = [];
                    if(i == 0) {
                        structure.serialInterval.push(1);
                        structure.serialInterval.push(nAtom1);
                    }
                    else if(i == 1) {
                        structure.serialInterval.push(nAtom1 + 1);
                        structure.serialInterval.push(nAtom1 + nAtom2);
                    }
                    }

                    let allAtoms = {};
                    for(let i in atomsM) {
                        let atm = atomsM[i];

                        atm.coord = new THREE.Vector3(atm.coord[0], atm.coord[1], atm.coord[2]);
                        atm.coord.add(tMVector);

                        let x = atm.coord.x * rMatrix[0] + atm.coord.y * rMatrix[1] + atm.coord.z * rMatrix[2];
                        let y = atm.coord.x * rMatrix[3] + atm.coord.y * rMatrix[4] + atm.coord.z * rMatrix[5];
                        let z = atm.coord.x * rMatrix[6] + atm.coord.y * rMatrix[7] + atm.coord.z * rMatrix[8];

                        atm.coord.x = x;
                        atm.coord.y = y;
                        atm.coord.z = z;

                        allAtoms[i] = atm;
                    }

                    for(let i in atomsS) {
                        let atm = atomsS[i];

                        atm.coord = new THREE.Vector3(atm.coord[0], atm.coord[1], atm.coord[2]);
                        atm.coord.add(tSVector);

                        // update the bonds
                        for(let j = 0, jl = atm.bonds.length; j < jl; ++j) {
                            atm.bonds[j] += nAtom1;
                        }

                        allAtoms[(parseInt(i) + nAtom1).toString()] = atm;
                    }

                    // combine data
                    let allData = {};
                    allData.alignedStructures = data2.alignedStructures;
                    allData.alignment = data2.alignment;
                    allData.atoms = allAtoms;

                    await thisClass.loadOpmDataForAlign(allData, seqalign, ic.mmdbidArray);
                // });
                // return ic.deferredOpm.promise();
            }
            else {
                alert('invalid atoms data.');
                return false;
            }
        }
    }

    async downloadAlignmentPart2(data, seqalign, chainresiCalphaHash2) { let ic = this.icn3d, me = ic.icn3dui;
        //ic.init();
        ic.loadAtomDataCls.loadAtomDataIn(data, undefined, 'align', seqalign);

        if(me.cfg.align === undefined && Object.keys(ic.structures).length == 1) {
            $("#" + ic.pre + "alternateWrapper").hide();
        }

        // show all
        let allAtoms = {};
        for(let i in ic.atoms) {
            allAtoms[i] = 1;
        }
        ic.dAtoms = allAtoms;
        ic.hAtoms = allAtoms;

        ic.setStyleCls.setAtomStyleByOptions(ic.opts);
        // change the default color to "Identity"
        ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

        // memebrane is determined by one structure. But transform both structures
        if(chainresiCalphaHash2 !== undefined) ic.ParserUtilsCls.transformToOpmOriForAlign(ic.selectedPdbid, chainresiCalphaHash2, true);

        await ic.ParserUtilsCls.renderStructure();

        if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

        ic.html2ddgm = '';

        // by default, open the seq alignment window
        //if(me.cfg.show2d !== undefined && me.cfg.show2d) me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');
        if(me.cfg.showalignseq) {
            me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');
        }

        if(me.cfg.show2d && ic.bFullUi) {
            await ic.ParserUtilsCls.set2DDiagramsForAlign(ic.mmdbidArray[0].toUpperCase(), ic.mmdbidArray[1].toUpperCase());
        }

        //if(me.deferred !== undefined) me.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
    }

    async loadOpmDataForAlign(data, seqalign, mmdbidArray) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        let url = "https://opm-assets.storage.googleapis.com/pdb/" + mmdbidArray[0].toLowerCase()+ ".pdb";
        let prms1 = me.getAjaxPromise(url, 'text');
        let url2 = "https://opm-assets.storage.googleapis.com/pdb/" + mmdbidArray[1].toLowerCase()+ ".pdb";
        let prms2 = me.getAjaxPromise(url2, 'text');

        let allPromise = Promise.allSettled([prms1, prms2]);
        try {
            let dataArray = await allPromise;
            
            let bFound = false;
            for(let i = 0, il = dataArray.length; i < il; ++i) {
                let opmdata = dataArray[i].value;

                if(!opmdata) continue;

                ic.selectedPdbid = mmdbidArray[i];

                ic.bOpm = true;
                let bVector = true;
                let chainresiCalphaHash = ic.loadPDBCls.loadPDB(opmdata, mmdbidArray[i], ic.bOpm, bVector); // defined in the core library

                $("#" + ic.pre + "selectplane_z1").val(ic.halfBilayerSize);
                $("#" + ic.pre + "selectplane_z2").val(-ic.halfBilayerSize);

                $("#" + ic.pre + "extra_mem_z").val(ic.halfBilayerSize);
                $("#" + ic.pre + "intra_mem_z").val(-ic.halfBilayerSize);

                ic.init(); // remove all previously loaded data
                await thisClass.downloadAlignmentPart2(data, seqalign, chainresiCalphaHash);

                bFound = true;

                /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();

                // use the first one with membrane
                break;
            }

            if(!bFound) {
                ic.init(); // remove all previously loaded data
                await thisClass.downloadAlignmentPart2(data, seqalign);
            }
        }
        catch(err) {
            ic.init(); // remove all previously loaded data
            await thisClass.downloadAlignmentPart2(data, seqalign);

            /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
            return;
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ChainalignParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async downloadChainalignmentPart2(data1, data2Array, chainresiCalphaHash2, chainidArray) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        let hAtoms = {}, hAtomsTmp = {};
        let mmdbid_t, mmdbid_q;
        mmdbid_t = chainidArray[0].substr(0, chainidArray[0].indexOf('_'));
        let bLastQuery = false;
        if(mmdbid_t.length > 5) { 
            let bAppend = false, bNoDssp = true;
            hAtoms = await ic.pdbParserCls.loadPdbData(data1, mmdbid_t, false, bAppend, 'target', bLastQuery, bNoDssp);
        }
        else {
            let bNoSeqalign = true;
            hAtoms = await ic.mmdbParserCls.parseMmdbData(data1, 'target', chainidArray[0], 0, bLastQuery, bNoSeqalign);
        }

        for(let i = 0, il = data2Array.length; i < il; ++i) {
            if(i == data2Array.length - 1) bLastQuery = true;
            // each alignment has a chainIndex i
            mmdbid_q = chainidArray[i + 1].substr(0, chainidArray[i + 1].indexOf('_'));
            //mmdbid_q = (mmdbid_q_tmp.length == 5) ? mmdbid_q_tmp.substr(0, 4) : mmdbid_q_tmp; // added postfixfor same PDB IDs

            //if(mmdbid_q.length > 4) {
            if(mmdbid_q.length > 5) {  // PDB ID plus postfix could be 5 
                let bAppend = true, bNoDssp = true;
                hAtomsTmp = await ic.pdbParserCls.loadPdbData(data2Array[i], mmdbid_q, false, bAppend, 'query', bLastQuery, bNoDssp);
            }
            else {
                let bNoSeqalign = true;
                hAtomsTmp = await ic.mmdbParserCls.parseMmdbData(data2Array[i], 'query', chainidArray[i + 1], i, bLastQuery, bNoSeqalign);
            }
            hAtoms = me.hashUtilsCls.unionHash(hAtoms, hAtomsTmp);
        }

        if(me.cfg.resnum) {
            await ic.realignParserCls.realignChainOnSeqAlign(chainresiCalphaHash2, chainidArray);
        }
        else if(me.cfg.resdef) {
            await ic.realignParserCls.realignChainOnSeqAlign(chainresiCalphaHash2, chainidArray, undefined, true);
        }
        else {
            // calculate secondary structures with applyCommandDssp
            //$.when(ic.pdbParserCls.applyCommandDssp(true)).then(function() {
                await ic.pdbParserCls.applyCommandDssp(true);

                // align PDB chains
                for(let index in ic.pdbChainIndexHash) {
                    let idArray = ic.pdbChainIndexHash[index].split('_');
                    mmdbid_q = idArray[0];
                    idArray[1];
                    mmdbid_t = idArray[2];
                    idArray[3];

                    thisClass.transformStructure(mmdbid_q, index-1, 'query');                
                }

                // dynamicly align pairs in ic.afChainIndexHash
                let ajaxArray = [], indexArray = [], struArray = [];
                let urlalign = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi";
                let urltmalign = me.htmlCls.baseUrl + "tmalign/tmalign.cgi";
                //let urltmalign = "https://test.ncbi.nlm.nih.gov/Structure/tmalign/tmalign.cgi";

                for(let index in ic.afChainIndexHash) {
                    let idArray = ic.afChainIndexHash[index].split('_');
                    mmdbid_q = idArray[0];
                    let chain_q = idArray[1];
                    mmdbid_t = idArray[2];
                    let chain_t = idArray[3];

                    let alignAjax;
                    if(me.cfg.aligntool != 'tmalign') {
                        let jsonStr_q = ic.domain3dCls.getDomainJsonForAlign(ic.chains[mmdbid_q + '_' + chain_q]);
                        let jsonStr_t = ic.domain3dCls.getDomainJsonForAlign(ic.chains[mmdbid_t + '_' + chain_t]);
                            
                        let dataObj = {'domains1': jsonStr_q, 'domains2': jsonStr_t};
                        alignAjax = me.getAjaxPostPromise(urlalign, dataObj);
                    }
                    else {
                        let pdb_query = ic.saveFileCls.getAtomPDB(ic.chains[mmdbid_q + '_' + chain_q]);
                        let pdb_target= ic.saveFileCls.getAtomPDB(ic.chains[mmdbid_t + '_' + chain_t]);

                        let dataObj = {'pdb_query': pdb_query, 'pdb_target': pdb_target};
                        alignAjax = me.getAjaxPostPromise(urltmalign, dataObj);                
                    }

                    ajaxArray.push(alignAjax);
                    indexArray.push(index - 1);
                    struArray.push(mmdbid_q);
                }

                let allPromise = Promise.allSettled(ajaxArray);
                try {
                    let dataArray = await allPromise;
                    await thisClass.downloadChainalignmentPart2b(chainresiCalphaHash2, chainidArray, hAtoms, dataArray, indexArray, mmdbid_t, struArray);
                }
                catch(err) {
                    if(ic.bRender) alert("These structures can NOT be aligned to each other...");
                }                  
            //});
        }
    }

    async downloadChainalignmentPart2b(chainresiCalphaHash2, chainidArray, hAtoms, dataArray, indexArray, mmdbid_t, struArray) { let ic = this.icn3d, me = ic.icn3dui;
        //let bTargetTransformed = (ic.qt_start_end[0]) ? true : false;

        // modify the previous trans and rotation matrix
        for(let i = 0, il = dataArray.length; i < il; ++i) {
            let align = dataArray[i].value;//[0];

            let mmdbid_q = struArray[i];
            let index = indexArray[i];

            let bEqualMmdbid = (mmdbid_q == mmdbid_t);
            let bEqualChain = false;

            let queryData = {}; // check whether undefined

            this.processAlign(align, index, queryData, bEqualMmdbid, bEqualChain);
        }
       
        // do not transform the target
        //if(!bTargetTransformed) {
        //    this.transformStructure(mmdbid_t, indexArray[0], 'target');
        //}

        // transform the rest
        for(let i = 0, il = dataArray.length; i < il; ++i) {
            let mmdbid_q = struArray[i];
            let index = indexArray[i];

            this.transformStructure(mmdbid_q, index, 'query');
        }

        let hAtomsAll = {};

        if(ic.bFullUi && ic.q_rotation !== undefined && !me.cfg.resnum && !me.cfg.resdef) {
            // set multiple seqeunce alignment from ic.qt_start_end
            hAtomsAll = this.setMsa(chainidArray);
        }

        // highlight all aligned atoms
        //ic.hAtoms = me.hashUtilsCls.cloneHash(hAtomsTmp);
        ic.hAtoms = me.hashUtilsCls.cloneHash(hAtomsAll);

        ic.transformCls.zoominSelection();

        // do the rest
        await this.downloadChainalignmentPart3(chainresiCalphaHash2, chainidArray, ic.hAtoms);
    }

    setMsa(chainidArray, bVastplus, bRealign) { let ic = this.icn3d, me = ic.icn3dui;
        // get aligned length for each pair
        let index_alignLen = [];
        for(let index = 1, indexl = chainidArray.length; index < indexl; ++index) {
            let alignLen = 0;
            if(ic.qt_start_end && ic.qt_start_end[index - 1]) {
                for(let i = 0, il = ic.qt_start_end[index - 1].length; i < il; ++i) { 
                    alignLen += parseInt(ic.qt_start_end[index - 1][i].q_end) - parseInt(ic.qt_start_end[index - 1][i].q_start) + 1;
                }
            }
            index_alignLen.push({index: index, alignLen: alignLen});
        }
        index_alignLen.sort(function(a,b){
            return b.alignLen - a.alignLen;
        });

        let hAtomsAll = ic.setSeqAlignCls.setSeqAlignChainForAll(chainidArray, index_alignLen, bRealign);

        if(bVastplus) {
            ic.opts['color'] = 'identity';
            ic.setColorCls.setColorByOptions(ic.opts, hAtomsAll);
        }

        let bReverse = false;
        let seqObj = me.htmlCls.alignSeqCls.getAlignSequencesAnnotations(Object.keys(ic.alnChains), undefined, undefined, false, undefined, bReverse);
        let oriHtml = $("#" + ic.pre + "dl_sequence2").html();

        $("#" + ic.pre + "dl_sequence2").html(oriHtml + seqObj.sequencesHtml);
        $("#" + ic.pre + "dl_sequence2").width(me.htmlCls.RESIDUE_WIDTH * seqObj.maxSeqCnt + 200);

        me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');

        return hAtomsAll;
    }

    downloadChainalignmentPart2bRealign(dataArray, chainidPairArray) { let ic = this.icn3d, me = ic.icn3dui;
        // set trans and rotation matrix
        ic.t_trans_add = [];
        ic.q_trans_sub = [];

        if(me.cfg.aligntool == 'tmalign') ic.q_trans_add = [];

        ic.q_rotation = [];
        ic.qt_start_end = [];

        let mmdbid2cnt = {}, mmdbidpairHash = {};
             
        let bFoundAlignment = false;
        for(let i = 0, il = dataArray.length; i < il; ++i) {
            let align = dataArray[i].value;//[0];

            let bEqualMmdbid = false;
            let bEqualChain = false;

            let queryData = {}; // check whether undefined

            let chainpair = chainidPairArray[i].split(',');
            let mmdbid1 = chainpair[0].substr(0, chainpair[0].indexOf('_'));
            let mmdbid2 = chainpair[1].substr(0, chainpair[1].indexOf('_'));
            if(mmdbidpairHash.hasOwnProperty(mmdbid1 + '_' + mmdbid2)) { // aligned already
                continue;
            }

            let bNoAlert = true;
            let bAligned = this.processAlign(align, i, queryData, bEqualMmdbid, bEqualChain, bNoAlert);

            if(bAligned) {
                bFoundAlignment = true;

                mmdbid2cnt[mmdbid1] = (mmdbid2cnt[mmdbid1] === undefined) ? 1 : ++mmdbid2cnt[mmdbid1];
                mmdbid2cnt[mmdbid2] = (mmdbid2cnt[mmdbid2] === undefined) ? 1 : ++mmdbid2cnt[mmdbid2];

                mmdbidpairHash[mmdbid1 + '_' + mmdbid2] = chainpair + ',' + i;
            }
        }

        if(!bFoundAlignment) {
            /// if(ic.deferredRealignByStruct !== undefined) ic.deferredRealignByStruct.resolve();
            if(ic.bRender) alert("These structures can NOT be aligned...");
            return;
        }

        // find the max aligned mmdbid as mmdbid_t
        let cnt = 0, mmdbid_t;
        for(let mmdbidpair in mmdbidpairHash) {
            let mmdbidArray = mmdbidpair.split('_');
            if(mmdbid2cnt[mmdbidArray[0]] > cnt) {
                cnt = mmdbid2cnt[mmdbidArray[0]];
                mmdbid_t = mmdbidArray[0];
            }
            if(mmdbid2cnt[mmdbidArray[1]] > cnt) {
                cnt = mmdbid2cnt[mmdbidArray[1]];
                mmdbid_t = mmdbidArray[1];
            }
        }

        let aligType;
        // transform all pairs 
        let allChainidHash = {}, hAtoms = {}, alignMMdbids = {}, mmdbidpairFinalHash = {};
        for(let mmdbidpair in mmdbidpairHash) {
            let mmdbidArray = mmdbidpair.split('_');
            let chainidArray = mmdbidpairHash[mmdbidpair].split(',');
            let index = chainidArray[2];

            let target, query;
            if(mmdbid_t == mmdbidArray[0]) {
                target = mmdbidArray[0];
                query = mmdbidArray[1];
            } 
            else if(mmdbid_t == mmdbidArray[1]) {
                target = mmdbidArray[1];
                query = mmdbidArray[0];               
            }
            else {
                target = mmdbidArray[0];
                query = mmdbidArray[1];               
            }

            // If all chains align to the same target, just check the query.
            // If there are different targets, also just check the query. The taget should not appear again in the query.
            alignMMdbids[target] = 1;
              
            if(alignMMdbids.hasOwnProperty(query)) continue;
            alignMMdbids[query] = 1;

            mmdbidpairFinalHash[mmdbidpair] = mmdbidpairHash[mmdbidpair];

            // chainid1 is target
            aligType = 'target';
            this.transformStructure(target, index, aligType);

            aligType = 'query';
            this.transformStructure(query, index, aligType);

            allChainidHash[chainidArray[0]] = 1;
            allChainidHash[chainidArray[1]] = 1;

            //hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.chains[chainidArray[0]]);
            //hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.chains[chainidArray[1]]);
        }

        // set up the view of sequence alignment for each pair
        for(let mmdbidpair in mmdbidpairFinalHash) {           
            if(ic.q_rotation !== undefined) {
                let chainidArrayTmp = mmdbidpairFinalHash[mmdbidpair].split(','); // chainid_chainid_index
                // switch these two chains
                let chainidArray = [chainidArrayTmp[1], chainidArrayTmp[0], chainidArrayTmp[2]];

                let hAtomsTmp = ic.setSeqAlignCls.setSeqAlignChain(undefined, undefined, chainidArray);
                hAtoms = me.hashUtilsCls.unionHash(hAtoms, hAtomsTmp);

                let bReverse = false;
                let seqObj = me.htmlCls.alignSeqCls.getAlignSequencesAnnotations(Object.keys(ic.alnChains), undefined, undefined, false, undefined, bReverse);
                let oriHtml = $("#" + ic.pre + "dl_sequence2").html();

                $("#" + ic.pre + "dl_sequence2").html(oriHtml + seqObj.sequencesHtml);
                $("#" + ic.pre + "dl_sequence2").width(me.htmlCls.RESIDUE_WIDTH * seqObj.maxSeqCnt + 200);
            }
        }

        //this.downloadChainalignmentPart3(undefined, Object.keys(allChainidHash), hAtoms);

        ic.dAtoms = me.hashUtilsCls.cloneHash(hAtoms);
        ic.hAtoms = me.hashUtilsCls.cloneHash(hAtoms);

        let name = 'protein_aligned';
        ic.selectionCls.saveSelection(name, name);

        ic.opts['color'] = 'identity';
        //ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);
        ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

        me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');

        ic.drawCls.draw();
        ic.transformCls.zoominSelection();
        
        ic.hlUpdateCls.updateHlAll();

        /// if(ic.deferredRealignByStruct !== undefined) ic.deferredRealignByStruct.resolve();
    }

    transformStructure(mmdbid, index, alignType, bForce) { let ic = this.icn3d, me = ic.icn3dui;
        let chainidArray = ic.structures[mmdbid];

        for(let i = 0, il = chainidArray.length; i < il; ++i) {
            for(let serial in ic.chains[chainidArray[i]]) {
                let atm = ic.atoms[serial];
                //if(ic.q_rotation !== undefined && ic.t_trans_add.length > 0 && !me.cfg.resnum && !me.cfg.resdef) {
                if(ic.q_rotation !== undefined && (bForce || (!me.cfg.resnum && !me.cfg.resdef)) ) {
                    atm = this.transformAtom(atm, index, alignType);
                }
            }
        }
    }

    transformAtom(atm, index, alignType) { let ic = this.icn3d, me = ic.icn3dui;
        if(alignType === 'target') ;
        else if(alignType === 'query') {
            if(me.cfg.aligntool != 'tmalign') {
                atm.coord.x -= ic.q_trans_sub[index].x;
                atm.coord.y -= ic.q_trans_sub[index].y;
                atm.coord.z -= ic.q_trans_sub[index].z;
            }

            let x = atm.coord.x * ic.q_rotation[index].x1 + atm.coord.y * ic.q_rotation[index].y1 + atm.coord.z * ic.q_rotation[index].z1;
            let y = atm.coord.x * ic.q_rotation[index].x2 + atm.coord.y * ic.q_rotation[index].y2 + atm.coord.z * ic.q_rotation[index].z2;
            let z = atm.coord.x * ic.q_rotation[index].x3 + atm.coord.y * ic.q_rotation[index].y3 + atm.coord.z * ic.q_rotation[index].z3;

            if(me.cfg.aligntool != 'tmalign') {
                x -= ic.t_trans_add[index].x;
                y -= ic.t_trans_add[index].y;
                z -= ic.t_trans_add[index].z;
            }
            else {
                x += ic.q_trans_add[index].x;
                y += ic.q_trans_add[index].y;
                z += ic.q_trans_add[index].z;
            }

            atm.coord.x = x;
            atm.coord.y = y;
            atm.coord.z = z;
        }

        return atm;
    }

    async downloadChainalignmentPart3(chainresiCalphaHash2, chainidArray, hAtoms) { let ic = this.icn3d, me = ic.icn3dui;
        // select all
        let allAtoms = {};
        for(let i in ic.atoms) {
            allAtoms[i] = 1;
        }
        ic.dAtoms = allAtoms;
        ic.hAtoms = allAtoms;

        ic.setStyleCls.setAtomStyleByOptions(ic.opts);
        // change the default color to "Identity"

        ic.opts['color'] = 'identity';
        ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

        // memebrane is determined by one structure. But transform both structures
        if(chainresiCalphaHash2 !== undefined) ic.ParserUtilsCls.transformToOpmOriForAlign(ic.selectedPdbid, chainresiCalphaHash2, true);

        //ic.dAtoms = hAtoms;
        //ic.hAtoms = hAtoms;
        ic.hAtoms = me.hashUtilsCls.cloneHash(hAtoms);
        ic.dAtoms = me.hashUtilsCls.cloneHash(hAtoms);
        
        await ic.ParserUtilsCls.renderStructure();

        //if(ic.chainidArray.length > 2) {
        if(chainidArray.length > 2) {
            let residuesHash = ic.firstAtomObjCls.getResiduesFromAtoms(hAtoms);

            let commandname = 'protein_aligned';
            let commanddescr = 'protein aligned';
            let select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residuesHash));

            ic.selectionCls.addCustomSelection(Object.keys(residuesHash), commandname, commanddescr, select, true);
        }

        ic.hlUpdateCls.updateHlAll();

        if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

        ic.html2ddgm = '';

        // by default, open the seq alignment window
         //if(me.cfg.showalignseq) {
//            me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');
        //}

        if(me.cfg.show2d && ic.bFullUi) {
            me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');
            if(ic.bFullUi) {
                if(!ic.bChainAlign) {
                    ic.ParserUtilsCls.download2Ddgm(ic.inputid.toUpperCase());
                }
                else {
                    //ic.ParserUtilsCls.set2DDiagramsForAlign(ic.inputid2.toUpperCase(), ic.inputid.toUpperCase());
                    await ic.ParserUtilsCls.set2DDiagramsForChainalign(chainidArray);
                }
            }
        }

        //if(me.deferred !== undefined) me.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
    }

    addPostfixForChainids(chainidArray) { let ic = this.icn3d; ic.icn3dui;
        let struct2cnt = {};
        for(let i = 0, il = chainidArray.length; i < il; ++i) {
            let chainid = chainidArray[i];
            let pos = chainid.indexOf('_');
            let struct = chainid.substr(0, pos); 
            if(struct != 'stru') struct = struct.toUpperCase();

            if(!struct2cnt.hasOwnProperty(struct)) {
                struct2cnt[struct] = 1;
            }
            else {
                ++struct2cnt[struct];
            }

            struct = (struct2cnt[struct] == 1) ? struct : struct + struct2cnt[struct];

            chainidArray[i] = struct + chainid.substr(pos);
        }

        return chainidArray;
    }

    async downloadChainalignment(chainalign, resnum, resdef) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        ic.opts['proteins'] = 'c alpha trace';

        let alignArray = chainalign.split(',');
        let domainArray = (me.cfg.domainids) ? me.cfg.domainids.split(',') : [];
        if(domainArray.length < alignArray.length) domainArray = [];

        ic.chainidArray = this.addPostfixForChainids(alignArray);

        let pos1 = alignArray[0].indexOf('_');
        ic.mmdbid_t = alignArray[0].substr(0, pos1).toUpperCase();
        ic.chain_t = alignArray[0].substr(pos1+1);

        let ajaxArray = [];
        let targetAjax;

        let url_t;
        if(ic.mmdbid_t.length > 5) {
            url_t = "https://alphafold.ebi.ac.uk/files/AF-" + ic.mmdbid_t + "-F1-model_" + ic.AFUniprotVersion + ".pdb";

            targetAjax = me.getAjaxPromise(url_t, 'text');
        }
        else {
            url_t = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&uid=" + ic.mmdbid_t;
            if(me.cfg.inpara !== undefined) url_t += me.cfg.inpara;

            targetAjax = me.getAjaxPromise(url_t, 'jsonp');
        }

        ajaxArray.push(targetAjax);

        ic.ParserUtilsCls.setYourNote(chainalign.toUpperCase() + ' in iCn3D');
        //ic.bCid = undefined;
        // define for 'align' only
        ic.pdbid_chain2title = {};
        if(ic.chainids2resids === undefined) ic.chainids2resids = {}; // ic.chainids2resids[chainid1][chainid2] = [resid, resid]

        ic.afChainIndexHash = {};
        ic.pdbChainIndexHash = {};
        for(let index = 1, indexLen = alignArray.length; index < indexLen; ++index) {
            let pos2 = alignArray[index].indexOf('_');
            let mmdbid_q_tmp = alignArray[index].substr(0, pos2).toUpperCase();
            ic.mmdbid_q = (mmdbid_q_tmp.length == 5) ? mmdbid_q_tmp.substr(0, 4) : mmdbid_q_tmp; // added postfix for same PDB IDs

            ic.chain_q = alignArray[index].substr(pos2+1);

            let url_q, queryAjax;
            if(ic.mmdbid_q.length > 5) {
                url_q = "https://alphafold.ebi.ac.uk/files/AF-" + ic.mmdbid_q + "-F1-model_" + ic.AFUniprotVersion + ".pdb";

                queryAjax = me.getAjaxPromise(url_q, 'text');
            }
            else {
                url_q = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&uid=" + ic.mmdbid_q;
                if(me.cfg.inpara !== undefined) url_q += me.cfg.inpara;

                queryAjax = me.getAjaxPromise(url_q, 'jsonp');
            }

            ajaxArray.push(queryAjax);
        }
        
        for(let index = 1, indexLen = alignArray.length; index < indexLen; ++index) {
            let pos2 = alignArray[index].indexOf('_');
            let mmdbid_q_tmp = alignArray[index].substr(0, pos2).toUpperCase();
            ic.mmdbid_q = (mmdbid_q_tmp.length == 5) ? mmdbid_q_tmp.substr(0, 4) : mmdbid_q_tmp; // added postfix for same PDB IDs

            ic.chain_q = alignArray[index].substr(pos2+1);

            if(!me.cfg.resnum && !me.cfg.resdef) {
                let chainalignFinal = ic.mmdbid_q + "_" + ic.chain_q + "," + ic.mmdbid_t + "_" + ic.chain_t;
                let domainalign = (domainArray.length > 0) ? domainArray[index] + "," + domainArray[0] : undefined;

                // TM-align (me.cfg.aligntool == 'tmalign') needs to input PDB
                if(me.cfg.aligntool != 'tmalign' && ic.mmdbid_t.length == 4 && ic.mmdbid_q.length == 4) {
                    let urlalign;
                    
                    if(domainArray.length > 0) {
                        urlalign = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?domainpairs=" + domainalign;
                    }
                    else {
                        urlalign = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?chainpairs=" + chainalignFinal;
                    }
                    
                    let alignAjax = me.getAjaxPromise(urlalign, 'jsonp');

                    ajaxArray.push(alignAjax);

                    ic.pdbChainIndexHash[index] = mmdbid_q_tmp + "_" + ic.chain_q + "_" + ic.mmdbid_t + "_" + ic.chain_t;
                }
                else {
                    // get the dynamic alignment after loading the structures
                    ic.afChainIndexHash[index] = ic.mmdbid_q + "_" + ic.chain_q + "_" + ic.mmdbid_t + "_" + ic.chain_t;
                }
            }
        }

        let allPromise = Promise.allSettled(ajaxArray);
        try {
            let dataArray = await allPromise;
            await thisClass.parseChainAlignData(dataArray, alignArray, ic.mmdbid_t, ic.chain_t);
        }
        catch(err) {
            let serverName = (me.cfg.aligntool == 'tmalign') ? 'TM-align' : 'VAST';
         
            if(ic.bRender) alert("These chains can not be aligned by " + serverName + ". You can specify the residue range and try it again...");
        }          
    }

    async parseChainAlignData(dataArray, chainidArray, mmdbid_t, chain_t) { let ic = this.icn3d, me = ic.icn3dui;

        //var dataArray =(chainidArray.length == 1) ? [data] : data;

        // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
        //var data2 = v2[0];
        // index = 0: the mmdb data of target
        let targetData = dataArray[0].value; //[0];
        let header = 'HEADER                                                        ' + mmdbid_t + '\n';
        if(isNaN(mmdbid_t) && mmdbid_t.length > 5) targetData = header + targetData;

        ic.t_trans_add = [];
        ic.q_trans_sub = [];

        if(me.cfg.aligntool == 'tmalign') ic.q_trans_add = [];

        ic.q_rotation = [];
        ic.qt_start_end = [];

        ic.mmdbidArray = [];
        ic.mmdbidArray.push(mmdbid_t);

        let queryDataArray = [];

        for(let index = 1, indexl = chainidArray.length; index < indexl; ++index) {
            let queryData = dataArray[index].value;//[0];

            let pos = chainidArray[index].indexOf('_');
            let mmdbid_q = chainidArray[index].substr(0, pos).toUpperCase();

            let header = 'HEADER                                                        ' + mmdbid_q + '\n';
            if(isNaN(mmdbid_q) && mmdbid_q.length > 5) queryData = header + queryData;

            if(queryData !== undefined && JSON.stringify(queryData).indexOf('Oops there was a problem') === -1
                ) {
                ic.mmdbidArray.push(mmdbid_q);
                queryDataArray.push(queryData);
            }
            else {
                alert("The coordinate data can NOT be retrieved for the structure " + mmdbid_q + "...");
                return;
            }
        }

        let missedChainCnt = 0;
        //for(let index = chainidArray.length, indexl = dataArray.length; index < indexl; index += step) {
        for(let index = 1, indexl = chainidArray.length; index < indexl; ++index) {
            let queryData = queryDataArray[index - 1]; 

            let pos = chainidArray[index].indexOf('_');
            let mmdbid_q = chainidArray[index].substr(0, pos).toUpperCase();
            let chain_q = chainidArray[index].substr(pos+1);

            if(!me.cfg.resnum && !me.cfg.resdef) {
                let index2 = chainidArray.length + index - 1;
                if(ic.afChainIndexHash.hasOwnProperty(index)) {
                    ++missedChainCnt;

                    // need to pass C-alpha coords and get transformation matrix from backend
                    ic.t_trans_add[index-1] = {"x":0, "y":0, "z":0};
                    ic.q_trans_sub[index-1] = {"x":0, "y":0, "z":0};

                    if(me.cfg.aligntool == 'tmalign') ic.q_trans_add[index-1] = {"x":0, "y":0, "z":0};

                    ic.q_rotation[index-1] = {"x1":1, "y1":0, "z1":0, "x2":0, "y2":1, "z2":0, "x3":0, "y3":0, "z3":1};
                    ic.qt_start_end[index-1] = undefined;
                }
                else {
                    let align = dataArray[index2 - missedChainCnt].value;//[0];

                    let bEqualMmdbid = (mmdbid_q == mmdbid_t);
                    let bEqualChain = (chain_q == chain_t);

                    this.processAlign(align, index-1, queryData, bEqualMmdbid, bEqualChain);
                }
            }
        }

        ic.mmdb_data_q = queryDataArray;

        await this.loadOpmDataForChainalign(targetData, queryDataArray, chainidArray, ic.mmdbidArray);
    }

    processAlign(align, index, queryData, bEqualMmdbid, bEqualChain, bNoAlert) { let ic = this.icn3d, me = ic.icn3dui;
        let bAligned = false;
        if((!align || align.length == 0) && !bNoAlert) {
            let serverName = (me.cfg.aligntool == 'tmalign') ? 'TM-align' : 'VAST';
        
            if(ic.bRender) alert("These chains can not be aligned by " + serverName + ".");
            return bAligned;
        }

        if(queryData !== undefined && JSON.stringify(queryData).indexOf('Oops there was a problem') === -1
            && align !== undefined && JSON.stringify(align).indexOf('Oops there was a problem') === -1
        ) {
            if((align === undefined || align.length == 0) && bEqualMmdbid && bEqualChain) {
                ic.t_trans_add[index] = {"x":0, "y":0, "z":0};
                ic.q_trans_sub[index] = {"x":0, "y":0, "z":0};
                ic.q_rotation[index] = {"x1":1, "y1":0, "z1":0, "x2":0, "y2":1, "z2":0, "x3":0, "y3":0, "z3":1};
                ic.qt_start_end[index] = undefined;
            }
            else if(align === undefined || align.length == 0) {
                if(!me.cfg.command && !bNoAlert) alert('These two chains can not align to each other. ' + 'Please select sequences from these two chains in the "Sequences & Annotations" window, ' + 'and click "Realign Selection" in the "File" menu to align your selection.');

                ic.t_trans_add[index] = {"x":0, "y":0, "z":0};
                ic.q_trans_sub[index] = {"x":0, "y":0, "z":0};
                ic.q_rotation[index] = {"x1":1, "y1":0, "z1":0, "x2":0, "y2":1, "z2":0, "x3":0, "y3":0, "z3":1};
                ic.qt_start_end[index] = undefined;

                me.cfg.showanno = 1;
                me.cfg.showalignseq = 0;
            }
            else {
                /*
                ic.t_trans_add.push(align[0].t_trans_add);
                ic.q_trans_sub.push(align[0].q_trans_sub);
                ic.q_rotation.push(align[0].q_rotation);
                ic.qt_start_end.push(align[0].segs);
                */
                ic.t_trans_add[index] = align[0].t_trans_add;
                ic.q_trans_sub[index] = align[0].q_trans_sub;

                if(me.cfg.aligntool == 'tmalign') ic.q_trans_add[index] = align[0].q_trans_add;

                ic.q_rotation[index] = align[0].q_rotation;
                ic.qt_start_end[index] = align[0].segs;

                let rmsd = align[0].super_rmsd;

                let logStr = "alignment RMSD: " + rmsd.toPrecision(4);
                if(me.cfg.aligntool == 'tmalign') logStr += "; TM-score: " + align[0].score.toPrecision(4);
                me.htmlCls.clickMenuCls.setLogCmd(logStr, false);
                let html = "<br><b>Alignment RMSD</b>: " + rmsd.toPrecision(4) + " &#8491;<br>";
                if(me.cfg.aligntool == 'tmalign') html += "<b>TM-score</b>: " + align[0].score.toPrecision(4) + "<br><br>";
                $("#" + ic.pre + "dl_rmsd").html(html);
                if(!me.cfg.bSidebyside) me.htmlCls.dialogCls.openDlg('dl_rmsd', 'RMSD of alignment');

                bAligned = true;
            }
        }

        return bAligned;
    }

    async loadOpmDataForChainalign(data1, data2, chainidArray, mmdbidArray) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        if(me.cfg.resnum || me.cfg.resdef) {
            if(!ic.bCommandLoad) ic.init(); // remove all previously loaded data
            await this.downloadChainalignmentPart2(data1, data2, undefined, chainidArray);

            /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
        }
        else {
            let url = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?mmdbids2opm=" + mmdbidArray.join("','");

            try {
                let data = await me.getAjaxPromise(url, 'jsonp');

                let mmdbid = data.mmdbid;
                ic.selectedPdbid = mmdbid;

                if(!mmdbid) {
                  if(!ic.bCommandLoad) ic.init(); // remove all previously loaded data
                  await thisClass.downloadChainalignmentPart2(data1, data2, undefined, chainidArray);

                  /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
                }
                else {
                    let url2 = "https://opm-assets.storage.googleapis.com/pdb/" + mmdbid.toLowerCase()+ ".pdb";

                    try {
                        let opmdata = await me.getAjaxPromise(url2, 'text');

                        ic.bOpm = true;
                        let bVector = true;
                        let chainresiCalphaHash = ic.loadPDBCls.loadPDB(opmdata, mmdbid, ic.bOpm, bVector); // defined in the core library

                        $("#" + ic.pre + "selectplane_z1").val(ic.halfBilayerSize);
                        $("#" + ic.pre + "selectplane_z2").val(-ic.halfBilayerSize);

                        $("#" + ic.pre + "extra_mem_z").val(ic.halfBilayerSize);
                        $("#" + ic.pre + "intra_mem_z").val(-ic.halfBilayerSize);

                        if(!ic.bCommandLoad) ic.init(); // remove all previously loaded data
                        await thisClass.downloadChainalignmentPart2(data1, data2, chainresiCalphaHash, chainidArray);

                        /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
                    }
                    catch(err) {
                        if(!ic.bCommandLoad) ic.init(); // remove all previously loaded data
                        await thisClass.downloadChainalignmentPart2(data1, data2, undefined, chainidArray);

                        /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
                        return;
                    }
                }
            }
            catch(err) {
                  if(!ic.bCommandLoad) ic.init(); // remove all previously loaded data
                  await thisClass.downloadChainalignmentPart2(data1, data2, undefined, chainidArray);

                  /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
                  return;
            }
        }
    }

    async downloadMmdbAf(idlist, bQuery, vastplusAtype) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        // ic.deferredMmdbaf = $.Deferred(function() {
        ic.structArray = idlist.split(',');

        let ajaxArray = [];

        for(let i = 0, il = ic.structArray.length; i < il; ++i) {
            let url_t, targetAjax;
            let structure = ic.structArray[i];

            if(isNaN(structure) && structure.length > 5) {
                url_t = "https://alphafold.ebi.ac.uk/files/AF-" + ic.structArray[i] + "-F1-model_" + ic.AFUniprotVersion + ".pdb";

                targetAjax = me.getAjaxPromise(url_t, 'text');
            }
            else {
                url_t = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&uid=" + structure;
                if(me.cfg.inpara !== undefined) url_t += me.cfg.inpara;

                targetAjax = me.getAjaxPromise(url_t, 'jsonp');
            }

            ajaxArray.push(targetAjax);
        }

        ic.ParserUtilsCls.setYourNote(ic.structArray + ' in iCn3D');
        //ic.bCid = undefined;

        ic.ParserUtilsCls.showLoading();

        let allPromise = Promise.allSettled(ajaxArray);
        try {
            let dataArray = await allPromise;
            await thisClass.parseMMdbAfData(dataArray, ic.structArray, bQuery, vastplusAtype);
            if(vastplusAtype === undefined) ic.ParserUtilsCls.hideLoading();
        }
        catch(err) {
            alert("There are some problems in retrieving the coordinates...");
        }          
    //   });
    
    //   return ic.deferredMmdbaf.promise();
    }

    async parseMMdbAfData(dataArray, structArray, bQuery, vastplusAtype) { let ic = this.icn3d, me = ic.icn3dui;

        let queryDataArray = [];
        for(let index = 0, indexl = structArray.length; index < indexl; ++index) {
            let queryData = dataArray[index].value;//[0];
            let header = 'HEADER                                                        ' + structArray[index] + '\n';
            if(isNaN(structArray[index]) && structArray[index].length > 5) queryData = header + queryData;

            if(queryData !== undefined && JSON.stringify(queryData).indexOf('Oops there was a problem') === -1
                ) {
                queryDataArray.push(queryData);
            }
            else {
                alert("The coordinate data can NOT be retrieved for the structure " + structArray[index] + "...");
                return;
            }
        }

        if(!ic.bCommandLoad && !bQuery) ic.init(); // remove all previously loaded data
        
        let hAtoms = {}, hAtomsTmp = {};
        let bLastQuery = false;

        ic.opts['color'] = (structArray.length > 1) ? 'structure' : ((structArray[0].length > 5) ? 'confidence' : 'chain');

        for(let i = 0, il = structArray.length; i < il; ++i) {
            if(i == structArray.length - 1) bLastQuery = true;

            let targetOrQuery, bAppend;
            if(i == 0 && !bQuery) {
                targetOrQuery = 'target';
                bAppend = false; 
            }
            else {
                targetOrQuery = 'query';
                bAppend = true; 
            }
            
            //if(structArray[i].length > 4) {
            if(isNaN(structArray[i]) && structArray[i].length > 5) {  // PDB ID plus postfix could be 5 
                //let bNoDssp = true;
                let bNoDssp = false; // get secondary structure info
                hAtomsTmp = await ic.pdbParserCls.loadPdbData(queryDataArray[i], structArray[i], false, bAppend, targetOrQuery, bLastQuery, bNoDssp);
            }
            else {
                let bNoSeqalign = true;
                hAtomsTmp = await ic.mmdbParserCls.parseMmdbData(queryDataArray[i], targetOrQuery, undefined, undefined, bLastQuery, bNoSeqalign);
            }
                    
            hAtoms = me.hashUtilsCls.unionHash(hAtoms, hAtomsTmp);
        }

        // parseMmdbData() didn't render structures for mmdbafid input
        if(structArray.length > 1) ic.opts['color'] = 'structure';
        ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);
        await ic.ParserUtilsCls.renderStructure();
        if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

        if(bQuery && me.cfg.matchedchains) {          
           // $.when(ic.pdbParserCls.applyCommandDssp(true)).then(function() {
                let bRealign = true, bPredefined = true;
                await ic.realignParserCls.realignChainOnSeqAlign(undefined, ic.chainidArray, bRealign, bPredefined);
                // reset annotations
                $("#" + ic.pre + "dl_annotations").html("");
                ic.bAnnoShown = false;
                if($('#' + ic.pre + 'dl_selectannotations').dialog( 'isOpen' )) {
                    $('#' + ic.pre + 'dl_selectannotations').dialog( 'close' );
                }
           //});
        }
        else if(vastplusAtype !== undefined) {
            // vastplusAtype: 0: VAST, global, 1: VAST, invarant core, 2: TM-align, global
            // VAST+ on the fly
            let structArray = Object.keys(ic.structures);
            await ic.vastplusCls.vastplusAlign(structArray, vastplusAtype);
        }

        // /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();

        // if(Object.keys(ic.structures).length == 1 && me.cfg.mmdbafid.length > 5) {
        //     ic.ParserUtilsCls.checkMemProtein(me.cfg.mmdbafid);
        // }
        // else {
        //     /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
        // }
    }
}

/**
 * @file Dsn6 Parser
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 * Modified by Jiyao Wang / https://github.com/ncbi/icn3d
 */

class Dsn6Parser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    dsn6Parser(pdbid, type, sigma) { let ic = this.icn3d; ic.icn3dui;
        // https://edmaps.rcsb.org/maps/1kq2_2fofc.dsn6
        // https://edmaps.rcsb.org/maps/1kq2_fofc.dsn6

        let url = "https://edmaps.rcsb.org/maps/" + pdbid.toLowerCase() + "_" + type + ".dsn6";
        this.dsn6ParserBase(url, type, sigma);
    }

    dsn6ParserBase(url, type, sigma) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;

        //https://stackoverflow.com/questions/33902299/using-jquery-ajax-to-download-a-binary-file
        if(type == '2fofc' && ic.bAjax2fofc) {
            ic.mapData.sigma2 = sigma;
            ic.setOptionCls.setOption('map', type);
        }
        else if(type == 'fofc' && ic.bAjaxfofc) {
            ic.mapData.sigma = sigma;
            ic.setOptionCls.setOption('map', type);
        }
        else {
            let oReq = new XMLHttpRequest();
            oReq.open("GET", url, true);
            oReq.responseType = "arraybuffer";

            oReq.onreadystatechange = function() {
                if(this.readyState == 4) {
                   if(this.status == 200) {
                       let arrayBuffer = oReq.response;
                       thisClass.loadDsn6Data(arrayBuffer, type, sigma);

                       if(type == '2fofc') {
                           ic.bAjax2fofc = true;
                       }
                       else if(type == 'fofc') {
                           ic.bAjaxfofc = true;
                       }

                       ic.setOptionCls.setOption('map', type);
                    }
                    else {
                        alert("RCSB server has no corresponding eletron density map for this structure.");
                    }

                    /// if(ic.deferredMap !== undefined) ic.deferredMap.resolve();
                }
                else {
                    ic.ParserUtilsCls.showLoading();
                }
            };

            oReq.send();
        }
    }

    loadDsn6Data(dsn6data, type, sigma) { let ic = this.icn3d; ic.icn3dui;
        // DSN6 http://www.uoxray.uoregon.edu/tnt/manual/node104.html
        // BRIX http://svn.cgl.ucsf.edu/svn/chimera/trunk/libs/VolumeData/dsn6/brix-1.html

        let voxelSize = 1;

        let header = {};
        let divisor, summand;

        let bin =(dsn6data.buffer && dsn6data.buffer instanceof ArrayBuffer) ? dsn6data.buffer : dsn6data;
        let intView = new Int16Array(bin);
        let byteView = new Uint8Array(bin);
        let brixStr = String.fromCharCode.apply(null, byteView.subarray(0, 512));

        if(brixStr.indexOf(':-)') == 0) {
          header.xStart = parseInt(brixStr.substr(10, 5)); // NXSTART
          header.yStart = parseInt(brixStr.substr(15, 5));
          header.zStart = parseInt(brixStr.substr(20, 5));

          header.xExtent = parseInt(brixStr.substr(32, 5)); // NX
          header.yExtent = parseInt(brixStr.substr(38, 5));
          header.zExtent = parseInt(brixStr.substr(42, 5));

          header.xRate = parseInt(brixStr.substr(52, 5)); // MX
          header.yRate = parseInt(brixStr.substr(58, 5));
          header.zRate = parseInt(brixStr.substr(62, 5));

          header.xlen = parseFloat(brixStr.substr(73, 10)) * voxelSize;
          header.ylen = parseFloat(brixStr.substr(83, 10)) * voxelSize;
          header.zlen = parseFloat(brixStr.substr(93, 10)) * voxelSize;

          header.alpha = parseFloat(brixStr.substr(103, 10));
          header.beta = parseFloat(brixStr.substr(113, 10));
          header.gamma = parseFloat(brixStr.substr(123, 10));

          divisor = parseFloat(brixStr.substr(138, 12)) / 100;
          summand = parseInt(brixStr.substr(155, 8));

          header.sigma = parseFloat(brixStr.substr(170, 12)) * 100;
        } else {
          // swap byte order when big endian
          if(intView[ 18 ] !== 100) { // true
            for(let i = 0, n = intView.length; i < n; ++i) {
              let val = intView[ i ];

              intView[ i ] =((val & 0xff) << 8) |((val >> 8) & 0xff);
            }
          }

          header.zStart = intView[ 2 ];
          header.xStart = intView[ 0 ]; // NXSTART
          header.yStart = intView[ 1 ];

          header.xExtent = intView[ 3 ]; // NX
          header.yExtent = intView[ 4 ];
          header.zExtent = intView[ 5 ];

          header.xRate = intView[ 6 ]; // MX
          header.yRate = intView[ 7 ];
          header.zRate = intView[ 8 ];

          let factor = 1 / intView[ 17 ];
          let scalingFactor = factor * voxelSize;

          header.xlen = intView[ 9 ] * scalingFactor;
          header.ylen = intView[ 10 ] * scalingFactor;
          header.zlen = intView[ 11 ] * scalingFactor;

          header.alpha = intView[ 12 ] * factor;
          header.beta = intView[ 13 ] * factor;
          header.gamma = intView[ 14 ] * factor;

          //divisor = intView[ 15 ] / 100;
          divisor = intView[ 15 ] / intView[ 18 ];
          summand = intView[ 16 ];
        }

        let data = new Float32Array(
          header.xExtent * header.yExtent * header.zExtent
        );

        let offset = 512;
        let xBlocks = Math.ceil(header.xExtent / 8);
        let yBlocks = Math.ceil(header.yExtent / 8);
        let zBlocks = Math.ceil(header.zExtent / 8);

        // loop over blocks
        for(let zz = 0; zz < zBlocks; ++zz) {
          for(let yy = 0; yy < yBlocks; ++yy) {
            for(let xx = 0; xx < xBlocks; ++xx) {
              // loop inside block
              for(let k = 0; k < 8; ++k) {
                let z = 8 * zz + k;
                for(let j = 0; j < 8; ++j) {
                  let y = 8 * yy + j;
                  for(let i = 0; i < 8; ++i) {
                    let x = 8 * xx + i;

                    // check if remaining slice-part contains data
                    if(x < header.xExtent && y < header.yExtent && z < header.zExtent) {
                      let idx =((((x * header.yExtent) + y) * header.zExtent) + z);
                      data[ idx ] =(byteView[ offset ] - summand) / divisor;
                      ++offset;
                    } else {
                      offset += 8 - i;
                      break;
                    }
                  }
                }
              }
            }
          }
        }

        if(type == '2fofc') {
            ic.mapData.header2 = header;
            ic.mapData.data2 = data;
            ic.mapData.matrix2 = this.getMatrix(header);
            ic.mapData.type2 = type;
            ic.mapData.sigma2 = sigma;
        }
        else {
            ic.mapData.header = header;
            ic.mapData.data = data;
            ic.mapData.matrix = this.getMatrix(header);
            ic.mapData.type = type;
            ic.mapData.sigma = sigma;
        }
    }

    getMatrix(header) { let ic = this.icn3d; ic.icn3dui;
        let h = header;

        let basisX = [
          h.xlen,
          0,
          0
        ];

        let basisY = [
          h.ylen * Math.cos(Math.PI / 180.0 * h.gamma),
          h.ylen * Math.sin(Math.PI / 180.0 * h.gamma),
          0
        ];

        let 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 ]
        );

        let basis = [ [], basisX, basisY, basisZ ];
        let nxyz = [ 0, h.xRate, h.yRate, h.zRate ];
        let mapcrs = [ 0, 1, 2, 3 ];

        let 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.multiply(new THREE.Matrix4().makeTranslation(
          h.xStart, h.yStart, h.zStart
        ));

        return matrix;
    }

    loadDsn6File(type) {var ic = this.icn3d, me = ic.icn3dui;
       let thisClass = this;

       let file = $("#" + ic.pre + "dsn6file" + type)[0].files[0];
       let sigma = $("#" + ic.pre + "dsn6sigma" + type).val();
       if(!file) {
         alert("Please select a file before clicking 'Load'");
       }
       else {
         me.utilsCls.checkFileAPI();
         let reader = new FileReader();
         reader.onload = function(e) { let ic = thisClass.icn3d;
           let arrayBuffer = e.target.result; // or = reader.result;
           thisClass.loadDsn6Data(arrayBuffer, type, sigma);
           if(type == '2fofc') {
               ic.bAjax2fofc = true;
           }
           else if(type == 'fofc') {
               ic.bAjaxfofc = true;
           }
           ic.setOptionCls.setOption('map', type);
           me.htmlCls.clickMenuCls.setLogCmd('load dsn6 file ' + $("#" + ic.pre + "dsn6file" + type).val(), false);
         };
         reader.readAsArrayBuffer(file);
       }
    }

    loadDsn6FileUrl(type) {var ic = this.icn3d, me = ic.icn3dui;
       let url = $("#" + ic.pre + "dsn6fileurl" + type).val();
       let sigma = $("#" + ic.pre + "dsn6sigmaurl" + type).val();
       if(!url) {
            alert("Please input the file URL before clicking 'Load'");
       }
       else {
           this.dsn6ParserBase(url, type, sigma);
           me.htmlCls.clickMenuCls.setLogCmd('set map ' + type + ' sigma ' + sigma + ' | ' + encodeURIComponent(url), true);
       }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class MmcifParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Ajax call was used to get the atom data from the "mmcifid". This function was deferred
    //so that it can be chained together with other deferred functions for sequential execution.
    async downloadMmcif(mmcifid) { let ic = this.icn3d, me = ic.icn3dui;
        //ic.bCid = undefined;

        ic.ParserUtilsCls.setYourNote(mmcifid.toUpperCase() + '(MMCIF) in iCn3D');

        let url = "https://files.rcsb.org/view/" + mmcifid + ".cif";
        let data = await me.getAjaxPromise(url, 'text', true);

        url = me.htmlCls.baseUrl + "mmcifparser/mmcifparser.cgi";
        let dataObj = {'mmciffile': data};
        let data2 = await me.getAjaxPostPromise(url, dataObj, true);

        await this.loadMmcifData(data2, mmcifid);
    }

    async downloadMmcifSymmetry(mmcifid, type) { let ic = this.icn3d, me = ic.icn3dui;
        let url = "https://files.rcsb.org/header/" + mmcifid + ".cif";

        //ic.bCid = undefined;
 
        let data1 = await me.getAjaxPromise(url, 'text', false, 'The structure " + mmcifid + " was not found...');

        url = me.htmlCls.baseUrl + "mmcifparser/mmcifparser.cgi";
        let dataObj = {'mmcifheader': data1};
        let data = await me.getAjaxPostPromise(url, dataObj, false, 'The mmCIF data of " + mmcifid + " can not be parsed...');

        if(data.emd !== undefined) ic.emd = data.emd;
        if(data.organism !== undefined) ic.organism = data.organism;

        if(ic.bAssemblyUseAsu) {
            for(let i = 0, il = data.assembly.length; i < il; ++i) {
                let mat4 = new THREE.Matrix4();
                mat4.fromArray(data.assembly[i]);
    
                ic.biomtMatrices[i] = mat4;
            }
    
            ic.asuCnt = ic.biomtMatrices.length;
    
            // show bioassembly 
            if(me.cfg.bu == 1 && Object.keys(ic.atoms).length * ic.asuCnt > ic.maxatomcnt) {
                ic.bAssembly = true;
            }
        }

        if(type === 'mmtfid' && data.missingseq !== undefined) {
            // adjust missing residues
            let maxMissingResi = 0, prevMissingChain = '';
            //let chainMissingResidueArray = {}
            for(let i = 0, il = data.missingseq.length; i < il; ++i) {
                let resn = data.missingseq[i].resn;
                let chain = data.missingseq[i].chain;
                let resi = data.missingseq[i].resi;

                let chainNum = mmcifid + '_' + chain;

                if(ic.chainMissingResidueArray[chainNum] === undefined) ic.chainMissingResidueArray[chainNum] = [];
                let resObject = {};
                resObject.resi = resi;
                resObject.name = me.utilsCls.residueName2Abbr(resn).toLowerCase();

                if(chain != prevMissingChain) {
                    maxMissingResi = 0;
                }

                // not all listed residues are considered missing, e.g., PDB ID 4OR2, only the firts four residues are considered missing
                if(!isNaN(resi) &&(prevMissingChain == '' ||(chain != prevMissingChain) ||(chain == prevMissingChain && resi > maxMissingResi)) ) {
                    ic.chainMissingResidueArray[chainNum].push(resObject);

                    maxMissingResi = resi;
                    prevMissingChain = chain;
                }
            }

            ic.loadPDBCls.adjustSeq(ic.chainMissingResidueArray);
        }

        ///// if(ic.deferredSymmetry !== undefined) ic.deferredSymmetry.resolve();
    }

    //Atom "data" from mmCIF file was parsed to set up parameters for the 3D viewer by calling the function
    //loadAtomDataIn. The deferred parameter was resolved after the parsing so that other javascript code can be executed.
    async loadMmcifData(data, mmcifid) { let ic = this.icn3d; ic.icn3dui;
        if(!mmcifid) mmcifid = data.mmcif;
        if(!mmcifid) mmcifid = 'stru';

        if(data.atoms !== undefined) {
            ic.init();

            if(data.emd !== undefined) ic.emd = data.emd;
            if(data.organism !== undefined) ic.organism = data.organism;

            if(ic.emd !== undefined) {
              $("#" + ic.pre + "mapWrapper1").hide();
              $("#" + ic.pre + "mapWrapper2").hide();
              $("#" + ic.pre + "mapWrapper3").hide();
            }
            else {
              $("#" + ic.pre + "emmapWrapper1").hide();
              $("#" + ic.pre + "emmapWrapper2").hide();
              $("#" + ic.pre + "emmapWrapper3").hide();
            }

            await ic.opmParserCls.loadOpmData(data, mmcifid, undefined, 'mmcif');
        }
        else {
            //alert('invalid atoms data.');
            return false;
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class MmdbParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Ajax call was used to get the atom data from the NCBI "mmdbid". This function was deferred so that
    //it can be chained together with other deferred functions for sequential execution. If the structure
    //is too large, a 3D dgm will show up. You can select your interested chains to see the details.

    //Atom "data" from MMDB file was parsed to set up parameters for the 3D viewer by calling the function
    //loadAtomDataIn. The deferred parameter was resolved after the parsing so that other javascript code can be executed.
    async downloadMmdb(mmdbid, bGi) { let ic = this.icn3d, me = ic.icn3dui;
        let data;
        
        try {
            data = await this.loadMmdbPrms(mmdbid, bGi);

            if(!data || data.error) {
                this.getNoData(mmdbid, bGi);
                return;
            }    
        }
        catch(err) {
            this.getNoData(mmdbid, bGi);
            return;
        }
        
        if(Object.keys(data.atoms).length == 0) { // for large structures such as 3J3Q
            // use mmtfid
            let pdbid = data.pdbId;
            ic.mmtfParserCls.downloadMmtf(pdbid);

            return;
        }

        let bCalphaOnly = me.utilsCls.isCalphaPhosOnly(data.atoms); //, 'CA');

        if(bCalphaOnly || data.atomCount <= ic.maxatomcnt) {
            await this.parseMmdbData(data);
        }
        else {
            let data2;
        
            try {
                data2 = await this.loadMmdbPrms(mmdbid, bGi, true);
            }
            catch(err) {
                this.getNoData(mmdbid, bGi);
                return;
            }

            await this.parseMmdbData(data2);
        }
    }

        //Ajax call was used to get the atom data from the NCBI "gi". This function was deferred so that
    //it can be chained together with other deferred functions for sequential execution. Note that
    //only one structure corresponding to the gi will be shown. If there is no structures available
    //for the gi, a warning message will be shown.
    async downloadGi(gi) { let ic = this.icn3d; ic.icn3dui;
        ic.bCid = undefined;
        let bGi = true;
        await this.downloadMmdb(gi, bGi);
    }

    //Ajax call was used to get the atom data from "sequence_id_comma_structure_id", comma-separated
    //NCBI protein accessions of a protein sequence and a chain of a 3D structure (e.g., 23491729,1TUP_A).
    //This function was deferred so that it can be chained together with other deferred functions for
    //sequential execution. Note that only one structure corresponding to the blast_rep_id will be shown.
    //If there is no structures available for the blast_rep_id, a warning message will be shown.
    async downloadBlast_rep_id(sequence_structure_ids) { let ic = this.icn3d, me = ic.icn3dui;
        //ic.bCid = undefined;

        let idArray = sequence_structure_ids.split(',');
        me.cfg.query_id = idArray[0];
        me.cfg.blast_rep_id = idArray[1];

        let mmdbid = me.cfg.blast_rep_id.split('_')[0];

        await this.downloadMmdb(mmdbid);
    }

    async downloadRefseq(refseqid) { let ic = this.icn3d, me = ic.icn3dui;
        let url = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?refseq2uniprot=" + refseqid;
 
        //ic.bCid = undefined;

        let data = await me.getAjaxPromise(url, 'jsonp', false, 'The protein accession ' + refseqid + ' can not be mapped to AlphaFold UniProt ID...');

        if(!data || !data.uniprot) {
            alert('The protein accession ' + refseqid + ' can not be mapped to AlphaFold UniProt ID...');
            return;
        }

        me.cfg.afid = data.uniprot;

        let bAf = true;

        await ic.pdbParserCls.downloadPdb(me.cfg.afid, bAf);
        
        await ic.loadScriptCls.loadScript(me.cfg.command, undefined, true);
    }

    getNoData(mmdbid, bGi) { let ic = this.icn3d, me = ic.icn3dui;
        if(bGi) {
            alert("This gi " + mmdbid + " has no corresponding 3D structure...");
        }
        else {
            alert("This mmdbid " + mmdbid + " with the parameters " + me.cfg.inpara + " may not have 3D structure data. Please visit the summary page for details: " + me.htmlCls.baseUrl + "pdb/" + mmdbid);
        }
    }

    async parseMmdbData(data, type, chainid, chainIndex, bLastQuery, bNoTransformNoSeqalign) { let ic = this.icn3d, me = ic.icn3dui;
        let hAtoms;
        let pdbid =(data.pdbId !== undefined) ? data.pdbId : data.mmdbId;

        this.parseMmdbDataPart1(data, type);

        if(type === undefined) { // default mmdbid input
            if(data.opm !== undefined && data.opm.rot !== undefined) {
                ic.bOpm = true;
                ic.opmParserCls.setOpmData(data);
            }

            hAtoms = ic.loadAtomDataCls.loadAtomDataIn(data, pdbid, 'mmdbid', undefined, type);
        }
        else { // multiple mmdbids, typically for alignment
            if(chainid) pdbid = chainid.substr(0, chainid.indexOf('_'));

            hAtoms = ic.loadAtomDataCls.loadAtomDataIn(data, pdbid, 'mmdbid', undefined, type, chainid, chainIndex, bLastQuery, bNoTransformNoSeqalign);
        }

        // set 3d domains
        let structure = data.pdbId;

        if(type === undefined) ic.ParserUtilsCls.setYourNote(structure.toUpperCase() + '(MMDB) in iCn3D');

        // let bNCBI = (me.cfg.mmdbid || me.cfg.gi || me.cfg.align || me.cfg.chainalign || me.cfg.mmdbafid || me.cfg.blast_rep_id);

        for(let molid in data.domains) {
            let chain = data.domains[molid].chain;
            let chainid = structure + '_' + chain;
            let domainArray = data.domains[molid].domains;

            for(let index = 0, indexl = domainArray.length; index < indexl; ++index) {
                let domainName = structure + '_' + chain + '_3d_domain_' +(index+1).toString();
                ic.tddomains[domainName] = {};

                let subdomainArray = domainArray[index].intervals;

                // remove duplicate, e.g., at https://www.ncbi.nlm.nih.gov/Structure/mmdb/mmdb_strview.cgi?v=2&program=icn3d&domain&molinfor&uid=1itw
                let domainFromHash = {}, domainToHash = {};

                //var fromArray = [], toArray = [];
                //var resCnt = 0
                for(let i = 0, il = subdomainArray.length; i < il; ++i) {
                    let domainFrom = Math.round(subdomainArray[i][0]) - 1; // 1-based
                    let domainTo = Math.round(subdomainArray[i][1]) - 1;

                    if(domainFromHash.hasOwnProperty(domainFrom) || domainToHash.hasOwnProperty(domainTo)) {
                        continue; // do nothing for duplicated "from" or "to", e.g, PDBID 1ITW, 5FWI
                    }
                    else {
                        domainFromHash[domainFrom] = 1;
                        domainToHash[domainTo] = 1;
                    }

                    //fromArray.push(domainFrom + ic.baseResi[chnid]);
                    //toArray.push(domainTo + ic.baseResi[chnid]);
                    //resCnt += domainTo - domainFrom + 1;

                    for(let j = domainFrom; j <= domainTo; ++j) {
                        let resid;
                        let residNCBI = chainid + '_' +(j+1).toString();

                        // if(bNCBI && ic.ncbi2resid[residNCBI]) {
                            resid = ic.ncbi2resid[residNCBI];
                        // }
                        // else {
                        //     resid = chainid + '_' +(j+1 + ic.chainid2offset[chainid]).toString();
                        // }

                        if(resid) ic.tddomains[domainName][resid] = 1;
                    }
                }
            } // for each domainArray
        } // for each molid

        // "asuAtomCount" is defined when: 1) atom count is over the threshold 2) bu=1 3) asu atom count is smaller than biological unit atom count
        ic.bAssemblyUseAsu =(data.asuAtomCount !== undefined) ? true : false;
        if(type !== undefined) {
            ic.bAssemblyUseAsu = false;
        }
        else {
            await ic.mmcifParserCls.downloadMmcifSymmetry(pdbid);
        }

        if(ic.bAssemblyUseAsu) { 
            $("#" + ic.pre + "assemblyWrapper").show();
            //ic.bAssembly = true;
        }

        if(ic.emd !== undefined) {
          $("#" + ic.pre + "mapWrapper1").hide();
          $("#" + ic.pre + "mapWrapper2").hide();
          $("#" + ic.pre + "mapWrapper3").hide();
        }
        else {
          $("#" + ic.pre + "emmapWrapper1").hide();
          $("#" + ic.pre + "emmapWrapper2").hide();
          $("#" + ic.pre + "emmapWrapper3").hide();
        }

        ic.setStyleCls.setAtomStyleByOptions(ic.opts);
        // use the original color from cgi output
        if(me.cfg.blast_rep_id !== undefined) {
          ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);
        }
        else {
          ic.setColorCls.setColorByOptions(ic.opts, ic.atoms, true);
        }

        if(type === undefined) {
            await ic.ParserUtilsCls.renderStructure();
            if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

            ic.html2ddgm = '';
            if(me.cfg.show2d) {
                me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');
                if(ic.bFullUi) {
                    //if(type === undefined) {
                        ic.ParserUtilsCls.download2Ddgm(ic.inputid.toUpperCase());
                    //}
                    //else {
                    //    ic.ParserUtilsCls.set2DDiagramsForAlign(ic.inputid2.toUpperCase(), ic.inputid.toUpperCase());
                        //ic.ParserUtilsCls.set2DDiagramsForChainalign(chainidArray);
                    //}
                }
            }
        }

        if((me.cfg.align === undefined || me.cfg.chainalign === undefined || me.cfg.mmdbafid === undefined) && Object.keys(ic.structures).length == 1) {
            if($("#" + ic.pre + "alternateWrapper") !== null) $("#" + ic.pre + "alternateWrapper").hide();
        }

        //if(me.deferred !== undefined) me.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();

        return hAtoms;
    }

    parseMmdbDataPart1(data, type) { let ic = this.icn3d, me = ic.icn3dui;
        // if type is defined, always process target before query
        if(data.atoms === undefined && data.molid2rescount === undefined) {
            alert('invalid MMDB data.');
            return false;
        }

        if(type === undefined || type === 'target') {
            // if a command contains "load...", the commands should not be cleared with init()
            let bKeepCmd = (ic.bCommandLoad) ? true : false;
            if(!ic.bStatefile) ic.init(bKeepCmd);

            ic.chainsColor = {};
            ic.chainsGene = {};
        }

        // used in download2Ddgm()
        if(type === 'query') ;
        else {
            ic.interactionData = {"moleculeInfor": data.moleculeInfor, "intrac": data.intrac, "intracResidues": data.intracResidues};
        }

        if(type === 'query') ;
        else {
            ic.mmdb_data = data;
        }

        let id =(data.pdbId !== undefined) ? data.pdbId : data.mmdbId;
        if(type === 'query') {
            ic.inputid2 = id;
        }
        else {
            ic.inputid = id;
        }

        let molid2rescount = data.moleculeInfor;
        let molid2color = {}, chain2molid = {}, molid2chain = {};
        let chainNameHash = {};       
        for(let i in molid2rescount) {
          if(Object.keys(molid2rescount[i]).length === 0) continue;

          let color =(molid2rescount[i].color === undefined) ? '#CCCCCC' : '#' +( '000000' + molid2rescount[i].color.toString( 16 ) ).slice( - 6 );
          let chainName =(molid2rescount[i].chain === undefined) ? '' : molid2rescount[i].chain.trim();
          if(chainNameHash[chainName] === undefined) {
              chainNameHash[chainName] = 1;
          }
          else {
              ++chainNameHash[chainName];
          }

          let chainNameFinal =(chainNameHash[chainName] === 1) ? chainName : chainName + chainNameHash[chainName].toString();
          let chain = id + '_' + chainNameFinal;

          molid2color[i] = color;
          chain2molid[chain] = i;
          molid2chain[i] = chain;

          ic.chainsColor[chain] = (type !== undefined && !me.cfg.mmdbafid) ? me.parasCls.thr(me.htmlCls.GREY8) : me.parasCls.thr(color);

          let geneId =(molid2rescount[i].geneId === undefined) ? '' : molid2rescount[i].geneId;
          let geneSymbol =(molid2rescount[i].geneSymbol === undefined) ? '' : molid2rescount[i].geneSymbol;
          let geneDesc =(molid2rescount[i].geneDesc === undefined) ? '' : molid2rescount[i].geneDesc;
          ic.chainsGene[chain] = {'geneId': geneId, 'geneSymbol': geneSymbol, 'geneDesc': geneDesc};
        }

        //ic.molid2color = molid2color;
        //ic.chain2molid = chain2molid;
        ic.molid2chain = molid2chain;

        // small structure with all atoms
        // show surface options
        $("#" + ic.pre + "accordion5").show();

        //ic.loadAtomDataCls.loadAtomDataIn(data, id, 'mmdbid', undefined, type);
    }

    loadMmdbPrms(mmdbid, bGi, bCalpha) { let ic = this.icn3d, me = ic.icn3dui;

        let url;

        // b: b-factor, s: water, ft: pdbsite
        //&ft=1
        if(bGi) {
            url = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&simple=1&gi=" + mmdbid;
        }
        else {
            url = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&b=1&s=1&ft=1&bu=" + me.cfg.bu + "&simple=1&uid=" + mmdbid;
        }

        // use asymmetric unit for BLAST search, e.g., https://www.ncbi.nlm.nih.gov/Structure/icn3d/full.html?from=blast&blast_rep_id=5XZC_B&query_id=1TUP_A&command=view+annotations;set+annotation+cdd;set+annotation+site;set+view+detailed+view;select+chain+5XZC_B;show+selection&log$=align&blast_rank=1&RID=EPUCYNVV014&bu=0
        if(me.cfg.blast_rep_id !== undefined) url += '&bu=0';

        //ic.bCid = undefined;

        if(me.cfg.inpara !== undefined) {
            url += me.cfg.inpara;
        }

        if(bCalpha) url += '&complexity=2';

        if(ic.chainids2resids === undefined) ic.chainids2resids = {}; // ic.chainids2resids[chainid1][chainid2] = [resid, resid]

        return me.getAjaxPromise(url, 'jsonp', true);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class MmtfParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // from the 2016 NCBI hackathon in Orlando: https://github.com/NCBI-Hackathons/iCN3D-MMTF
    // Contributors: Jiyao Wang, Alexander Rose, Peter Rose
    // requires the library mmtf.js

    //MMTF is a new binary data format besides the previous PDB and mmCIF formats for 3D structures.
    //The JavaScript Methods at http://mmtf.rcsb.org/ was used to load and parse the data.
    downloadMmtf(mmtfid) { let ic = this.icn3d; ic.icn3dui;
        ic.ParserUtilsCls.setYourNote(mmtfid.toUpperCase() + '(MMTF) in iCn3D');
        //ic.bCid = undefined;

        MMTF.fetchReduced(
            mmtfid,
            // onLoad callback
            async function( mmtfData ){
                if(mmtfData.numAtoms * 10 > ic.maxatomcnt) {
                    let bFull = false;
                    if(Object.keys(mmtfData).length == 0) {
                        alert('This PDB structure is not found at RCSB...');
                        return;
                    }

                    await ic.opmParserCls.loadOpmData(mmtfData, mmtfid, bFull, 'mmtf');
                }
                else {
                    mmtfData = null;

                    MMTF.fetch(
                        mmtfid,
                        // onLoad callback
                        async function( mmtfData2 ){
                            let bFull = true;
                            if(Object.keys(mmtfData2).length == 0) {
                                alert('This PDB structure is not found at RCSB...');
                                return;
                            }
                            // ic.deferredOpm = $.Deferred(function() {
                            //     ic.opmParserCls.loadOpmData(mmtfData2, mmtfid, bFull, 'mmtf');
                            // });

                            // return ic.deferredOpm.promise();

                            await ic.opmParserCls.loadOpmData(mmtfData2, mmtfid, bFull, 'mmtf');
                        },
                        // onError callback
                        function( error ){
                            //alert('This PDB structure is not found at RCSB...');
                            //console.error( error )
                        }
                    );
                }
            },
            // onError callback
            function( error ){
                //alert('This PDB structure is not found at RCSB...');
                //console.error( error )
            }
        );
    }

    async parseMmtfData(mmtfData, mmtfid, bFull) { let ic = this.icn3d, me = ic.icn3dui;
        mmtfData.numAtoms;

        ic.init();

        let pmin = new THREE.Vector3( 9999, 9999, 9999);
        let pmax = new THREE.Vector3(-9999,-9999,-9999);
        let psum = new THREE.Vector3();

        let id = mmtfData.structureId;

        ic.molTitle = mmtfData.title;

        // bioAsembly
        if(mmtfData.bioAssemblyList !== undefined && mmtfData.bioAssemblyList[0]!== undefined && mmtfData.bioAssemblyList[0].transformList.length > 1) {
            ic.biomtMatrices = [];

            for(let i = 0, il = mmtfData.bioAssemblyList[0].transformList.length; i < il; ++i) {
                //var biomt = new THREE.Matrix4().identity();

                //for(let j = 0, jl = mmtfData.bioAssemblyList[0].transformList[i].matrix.length; j < jl; ++j) {
                    //biomt.elements[j] = mmtfData.bioAssemblyList[0].transformList[i].matrix[j];
                //}

                let biomt = new THREE.Matrix4().fromArray(mmtfData.bioAssemblyList[0].transformList[i].matrix).transpose();

                ic.biomtMatrices.push(biomt);
            }
        }

        if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1) {
            $("#" + ic.pre + "assemblyWrapper").show();

            ic.asuCnt = ic.biomtMatrices.length;
        }

        let oriindex2serial = {};

        // save SG atoms in CYS residues
        let SGAtomSerialArray = [];

        let prevSS = 'coil';
        let prevChain = '';
        let prevResi = 0;

        let serial = 0;

        let structure, chain, resn, resi, ss, ssbegin, ssend;
        let het, bProtein, bNucleotide;
        let elem, atomName, coord, b, alt;
        let CSerial, prevCSerial, OSerial, prevOSerial;

        let callbackDict = {
            onModel: function( modelData ){
                structure =(modelData.modelIndex === 0) ? id : id +(modelData.modelIndex + 1).toString();
            },
            onChain: function( chainData ){

                chain = chainData.chainName; // or chainData.chainId
                let chainid = structure + '_' + chain;

                if(ic.structures[structure] === undefined) ic.structures[structure] = [];
                ic.structures[structure].push(chainid);
            },
            onGroup: function( groupData ){
                resn = groupData.groupName;
                resi = groupData.groupId;

                let resid = structure + '_' + chain + '_' + resi;

                if(groupData.secStruct === 0 || groupData.secStruct === 2 || groupData.secStruct === 4) {
                    ss = 'helix';
                }
                else if(groupData.secStruct === 3) {
                    ss = 'sheet';
                }
                else if(groupData.secStruct === -1) {
                    ss = 'other';
                }
                else {
                    ss = 'coil';
                }

                // no residue can be both ssbegin and ssend in DSSP calculated secondary structures
                let bSetPrevSsend = false;

                if(chain !== prevChain) {
                    prevCSerial = undefined;
                    prevOSerial = undefined;

                    // new chain
                    if(ss !== 'coil' && ss !== 'other') {
                        ssbegin = true;
                        ssend = false;
                    }
                    else {
                        ssbegin = false;
                        ssend = false;
                    }

                    // set up the end of previous chain
                    if(prevSS !== 'coil' && prevSS !== 'other') {
                        let prevResid = structure + '_' + prevChain + '_' + prevResi.toString();

                        for(let i in ic.residues[prevResid]) {
                            ic.atoms[i].ssbegin = false;
                            ic.atoms[i].ssend = true;
                        }
                    }
                }
                else {
                    prevCSerial = CSerial;
                    prevOSerial = OSerial;

                    if(ss !== prevSS) {
                        if(prevSS === 'coil' || prevSS === 'other') {
                            ssbegin = true;
                            ssend = false;
                        }
                        else if(ss === 'coil' || ss === 'other') {
                            bSetPrevSsend = true;
                            ssbegin = false;
                            ssend = false;
                        }
                        else if((prevSS === 'sheet' && ss === 'helix') ||(prevSS === 'helix' && ss === 'sheet')) {
                            bSetPrevSsend = true;
                            ssbegin = true;
                            ssend = false;
                        }
                    }
                    else {
                            ssbegin = false;
                            ssend = false;
                    }
                }

                if(bSetPrevSsend && !isNaN(resi)) {
                    let prevResid = structure + '_' + chain + '_' +(resi - 1).toString();
                    for(let i in ic.residues[prevResid]) {
                        ic.atoms[i].ssbegin = false;
                        ic.atoms[i].ssend = true;
                    }
                }

                prevSS = ss;
                prevChain = chain;
                prevResi = resi;

                het = false;
                bProtein = false;
                bNucleotide = false;
                if(groupData.chemCompType.toLowerCase() === 'non-polymer' || groupData.chemCompType.toLowerCase() === 'other' || groupData.chemCompType.toLowerCase().indexOf('saccharide') !== -1) {
                    het = true;
                }
                else if(groupData.chemCompType.toLowerCase().indexOf('peptide') !== -1) {
                    bProtein = true;
                }
                else if(groupData.chemCompType.toLowerCase().indexOf('dna') !== -1 || groupData.chemCompType.toLowerCase().indexOf('rna') !== -1) {
                    bNucleotide = true;
                }
                else {
                    bProtein = true;
                }

                  // add sequence information
                  let chainid = structure + '_' + chain;

                  let resObject = {};
                  resObject.resi = resi;
                  resObject.name = me.utilsCls.residueName2Abbr(resn);

                  ic.residueId2Name[resid] = resObject.name;
                  if(resObject.resi % 10 === 0) resObject.resi.toString();

                  let secondaries = '-';
                  if(ss === 'helix') {
                      secondaries = 'H';
                  }
                  else if(ss === 'sheet') {
                      secondaries = 'E';
                  }
                  else if(ss === 'coil') {
                      secondaries = 'c';
                  }
                  else if(ss === 'other') {
                      secondaries = 'o';
                  }

                  if(ic.chainsSeq[chainid] === undefined) ic.chainsSeq[chainid] = [];
                  if(ic.bFullUi) ic.chainsSeq[chainid].push(resObject);

                  ic.secondaries[resid] = secondaries;
            },
            onAtom: function( atomData ){
                elem = atomData.element;
                atomName = atomData.atomName;
                coord = new THREE.Vector3(atomData.xCoord, atomData.yCoord, atomData.zCoord);
                b = atomData.bFactor;

                alt = atomData.altLoc;
                if(atomData.altLoc === '\u0000') { // a temp value, should be ''
                    alt = '';
                }

                // skip the atoms where alt is not '' or 'A'
                if(alt === '' || alt === 'A') {
                    ++serial;

                    if(atomName === 'SG') SGAtomSerialArray.push(serial);

                    oriindex2serial[atomData.atomIndex] = serial;

                    let atomDetails = {
                        het: het, // optional, used to determine chemicals, water, ions, etc
                        serial: serial,         // required, unique atom id
                        name: atomName,             // required, atom name
                        alt: alt,               // optional, some alternative coordinates
                        resn: resn,             // optional, used to determine protein or nucleotide
                        structure: structure,   // optional, used to identify structure
                        chain: chain,           // optional, used to identify chain
                        resi: resi,             // optional, used to identify residue ID
                        //insc: line.substr(26, 1),
                        coord: coord,           // required, used to draw 3D shape
                        b: b,         // optional, used to draw B-factor tube
                        elem: elem,             // optional, used to determine hydrogen bond
                        bonds: [],              // required, used to connect atoms
                        bondOrder: [],
                        ss: ss,             // optional, used to show secondary structures
                        ssbegin: ssbegin,         // optional, used to show the beginning of secondary structures
                        ssend: ssend            // optional, used to show the end of secondary structures
                    };

                    if(!atomDetails.het && atomDetails.name === 'C') {
                        CSerial = serial;
                    }
                    if(!atomDetails.het && atomDetails.name === 'O') {
                        OSerial = serial;
                    }

                    // from DSSP C++ code
                    if(!atomDetails.het && atomDetails.name === 'N' && prevCSerial !== undefined && prevOSerial !== undefined) {
                        let dist = ic.atoms[prevCSerial].coord.distanceTo(ic.atoms[prevOSerial].coord);

                        let x2 = atomDetails.coord.x +(ic.atoms[prevCSerial].coord.x - ic.atoms[prevOSerial].coord.x) / dist;
                        let y2 = atomDetails.coord.y +(ic.atoms[prevCSerial].coord.y - ic.atoms[prevOSerial].coord.y) / dist;
                        let z2 = atomDetails.coord.z +(ic.atoms[prevCSerial].coord.z - ic.atoms[prevOSerial].coord.z) / dist;

                        atomDetails.hcoord = new THREE.Vector3(x2, y2, z2);
                    }

                    ic.atoms[serial] = atomDetails;

                    pmin.min(coord);
                    pmax.max(coord);
                    psum.add(coord);

                    let chainid = structure + '_' + chain;
                    let resid = chainid + '_' + resi;

                    if(ic.chains[chainid] === undefined) ic.chains[chainid] = {};
                    ic.chains[chainid][serial] = 1;

                    if(ic.residues[resid] === undefined) ic.residues[resid] = {};
                    ic.residues[resid][serial] = 1;

                    if(bProtein) {
                      ic.proteins[serial] = 1;

                      if(atomName === 'CA') ic.calphas[serial] = 1;
                      if(atomName !== 'N' && atomName !== 'H' && atomName !== 'CA' && atomName !== 'HA' && atomName !== 'C' && atomName !== 'O') ic.sidec[serial] = 1;
                    }
                    else if(bNucleotide) {
                      ic.nucleotides[serial] = 1;

                      if(bFull &&(atomName == "O3'" || atomName == "O3*")) {
                          ic.nucleotidesO3[serial] = 1;
                      }
                      else if(!bFull && atomName == 'P') {
                          ic.nucleotidesO3[serial] = 1;
                      }

                      if(me.parasCls.nuclMainArray.indexOf(atomName) === -1) {
                        ic.ntbase[serial] = 1;
                      }
                    }
                    else {
                      if(elem.toLowerCase() === resn.toLowerCase()) {
                          ic.ions[serial] = 1;
                      }
                      else if(resn === 'HOH' || resn === 'WAT' || resn === 'SQL' || resn === 'H2O' || resn === 'W' || resn === 'DOD' || resn === 'D3O') {
                          ic.water[serial] = 1;
                      }
                      else {
                          ic.chemicals[serial] = 1;
                      }
                    }

                    ic.dAtoms[serial] = 1;
                    ic.hAtoms[serial] = 1;
                }
            },
            onBond: function( bondData ){
                let from = oriindex2serial[bondData.atomIndex1];
                let to = oriindex2serial[bondData.atomIndex2];

                if(oriindex2serial.hasOwnProperty(bondData.atomIndex1) && oriindex2serial.hasOwnProperty(bondData.atomIndex2)) { // some alt atoms were skipped
                    ic.atoms[from].bonds.push(to);
                    ic.atoms[to].bonds.push(from);

                    if(het) {
                        let order = bondData.bondOrder;

                        ic.atoms[from].bondOrder.push(order);
                        ic.atoms[to].bondOrder.push(order);

                        if(order === 2) {
                            ic.doublebonds[from + '_' + to] = 1;
                            ic.doublebonds[to + '_' + from] = 1;
                        }
                        else if(order === 3) {
                            ic.triplebonds[from + '_' + to] = 1;
                            ic.triplebonds[to + '_' + from] = 1;
                        }
                    }
                }
            }
        };

        // traverse
        MMTF.traverse( mmtfData, callbackDict );

        ic.loadPDBCls.setResidMapping();

        // set up disulfide bonds
        let sgLength = SGAtomSerialArray.length;
        for(let i = 0, il = sgLength; i < il; ++i) {
            for(let j = i+1; j < il; ++j) {

                let serial1 = SGAtomSerialArray[i];
                let serial2 = SGAtomSerialArray[j];

                let atom1 = ic.atoms[serial1];
                let atom2 = ic.atoms[serial2];

                if($.inArray(serial2, atom1.bonds) !== -1) {
                    let resid1 = atom1.structure + '_' + atom1.chain + '_' + atom1.resi;
                    let resid2 = atom2.structure + '_' + atom2.chain + '_' + atom2.resi;

                    if(ic.ssbondpnts[atom1.structure] === undefined) ic.ssbondpnts[atom1.structure] = [];

                    ic.ssbondpnts[atom1.structure].push(resid1);
                    ic.ssbondpnts[atom1.structure].push(resid2);
                }
            }
        }

        ic.cnt = serial;

        if(ic.cnt > ic.maxatomcnt ||(ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) ) {
            ic.opts['proteins'] = 'c alpha trace'; //ribbon, strand, cylinder and plate, schematic, c alpha trace, b factor tube, lines, stick, ball and stick, sphere, nothing
            ic.opts['nucleotides'] = 'o3 trace'; //nucleotide cartoon, o3 trace, schematic, lines, stick,
        }

        ic.pmin = pmin;
        ic.pmax = pmax;
        //ic.maxD = pmax.distanceTo(pmin);
        //ic.center = psum.multiplyScalar(1.0 / ic.cnt);
        ic.center = ic.ParserUtilsCls.getGeoCenter(ic.pmin, ic.pmax);
        ic.maxD = ic.ParserUtilsCls.getStructureSize(ic.atoms, ic.pmin, ic.pmax, ic.center);

        if(ic.maxD < 5) ic.maxD = 5;
        ic.oriMaxD = ic.maxD;
        ic.oriCenter = ic.center.clone();

        ic.ParserUtilsCls.transformToOpmOri(mmtfid);

        if(me.cfg.align === undefined && Object.keys(ic.structures).length == 1) {
            $("#" + ic.pre + "alternateWrapper").hide();
        }

        ic.setStyleCls.setAtomStyleByOptions(ic.opts);
        ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

        await ic.ParserUtilsCls.renderStructure();

        ic.saveFileCls.showTitle();

        if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

        //if(me.deferred !== undefined) me.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Mol2Parser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async loadMol2Data(data) { let ic = this.icn3d, me = ic.icn3dui;
        let bResult = this.loadMol2AtomData(data);

        if(me.cfg.align === undefined && Object.keys(ic.structures).length == 1) {
            $("#" + ic.pre + "alternateWrapper").hide();
        }

        if(!bResult) {
          alert('The Mol2 file has the wrong format...');
        }
        else {
          ic.setStyleCls.setAtomStyleByOptions(ic.opts);
          ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

          await ic.ParserUtilsCls.renderStructure();

          if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

          //if(me.deferred !== undefined) me.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
        }
    }

    loadMol2AtomData(data) { let ic = this.icn3d; ic.icn3dui;
        let lines = data.split(/\r?\n|\r/);
        if(lines.length < 4) return false;

        ic.init();

        let structure = 1;
        let chain = 'A';
        let resn = 'LIG';
        let resi = 1;

        let AtomHash = {};
        let moleculeNum = 1, chainNum = '1_A', residueNum = '1_A_1';
        let atomCount, bondCount, atomIndex = 0, bondIndex = 0;
        let serial=1;

        let bAtomSection = false, bBondSection = false;

        let atomid2serial = {};
        let skipAtomids = {};

        for(let i = 0, il = lines.length; i < il; ++i) {
            let line = lines[i].trim();
            if(line === '') continue;
            if(line.substr(0, 1) === '#') continue;

            if(line == '@<TRIPOS>MOLECULE') {
                ic.molTitle = lines[i + 1].trim();
                let atomCnt_bondCnt = lines[i + 2].trim().replace(/\s+/g, " ").split(" ");
                atomCount = atomCnt_bondCnt[0];
                bondCount = atomCnt_bondCnt[1];
                i = i + 4;
            }
            else if(line == '@<TRIPOS>ATOM') { // 1    C1    1.207    2.091    0.000    C.ar    1    BENZENE    0.000
                serial = 1;

                bAtomSection = true;

                ++i;
            }
            else if(line == '@<TRIPOS>BOND') { // 1    1    2    ar
                bBondSection = true;
                bAtomSection = false;

                ++i;
            }
            else if(line == '@<TRIPOS>SUBSTRUCTURE') { // 1    1    2    ar
                bBondSection = false;

                ++i;
            }

            line = lines[i].trim();
            if(line === '') continue;
            if(line.substr(0, 1) === '#') continue;

            if(bAtomSection && atomIndex < atomCount) {
                // 1    C1    1.207    2.091    0.000    C.ar    1    BENZENE    0.000
                let atomArray = line.replace(/\s+/g, " ").split(" ");

                let atomid = parseInt(atomArray[0]);
                atomid2serial[atomid] = serial;

                let name = atomArray[1];
                let x = parseFloat(atomArray[2]);
                let y = parseFloat(atomArray[3]);
                let z = parseFloat(atomArray[4]);
                let coord = new THREE.Vector3(x, y, z);

                let elemFull = atomArray[5];
                let pos = elemFull.indexOf('.');

                let elem;
                if(pos === -1) {
                    elem = elemFull;
                }
                else {
                    elem = elemFull.substr(0, pos);
                }

                // skip H, but keep H.spc, H.t3p, etc
                if(elem === 'H' && elem === elemFull) {
                    skipAtomids[atomid] = 1;
                }
                else {
                    let atomDetails = {
                        het: true,              // optional, used to determine chemicals, water, ions, etc
                        serial: serial,         // required, unique atom id
                        name: name,             // required, atom name
                        resn: resn,             // optional, used to determine protein or nucleotide
                        structure: structure,   // optional, used to identify structure
                        chain: chain,           // optional, used to identify chain
                        resi: resi,             // optional, used to identify residue ID
                        coord: coord,           // required, used to draw 3D shape
                        b: 0,                   // optional, used to draw B-factor tube
                        elem: elem,             // optional, used to determine hydrogen bond
                        bonds: [],              // required, used to connect atoms
                        ss: 'coil',             // optional, used to show secondary structures
                        ssbegin: false,         // optional, used to show the beginning of secondary structures
                        ssend: false,           // optional, used to show the end of secondary structures

                        bondOrder: []           // optional, specific for chemicals
                    };

                    ic.atoms[serial] = atomDetails;
                    AtomHash[serial] = 1;

                    ++serial;
                }

                ++atomIndex;
            }

            if(bBondSection && bondIndex < bondCount) {
                // 1    1    2    ar
                let bondArray = line.replace(/\s+/g, " ").split(" ");
                let fromAtomid = parseInt(bondArray[1]);
                let toAtomid = parseInt(bondArray[2]);
                let bondType = bondArray[3];
                let finalBondType = bondType;

                //� 1 = single � 2 = double � 3 = triple � am = amide � ar = aromatic � du = dummy � un = unknown(cannot be determined from the parameter tables) � nc = not connected
                if(bondType === 'am') {
                    finalBondType = '1';
                }

                if(bondType === 'ar') {
                    finalBondType = '1.5';
                }

                if(!skipAtomids.hasOwnProperty(fromAtomid) && !skipAtomids.hasOwnProperty(toAtomid) &&(finalBondType === '1' || finalBondType === '2' || finalBondType === '3' || finalBondType === '1.5') ) {
                    let order = finalBondType;
                    let from = atomid2serial[fromAtomid];
                    let to = atomid2serial[toAtomid];

                    // skip all bonds between H and C
                    //if( !(ic.atoms[from].elem === 'H' && ic.atoms[to].elem === 'C') && !(ic.atoms[from].elem === 'C' && ic.atoms[to].elem === 'H') ) {
                        ic.atoms[from].bonds.push(to);
                        ic.atoms[from].bondOrder.push(order);
                        ic.atoms[to].bonds.push(from);
                        ic.atoms[to].bondOrder.push(order);

                        if(order == '2') {
                            ic.doublebonds[from + '_' + to] = 1;
                            ic.doublebonds[to + '_' + from] = 1;
                        }
                        else if(order == '3') {
                            ic.triplebonds[from + '_' + to] = 1;
                            ic.triplebonds[to + '_' + from] = 1;
                        }
                        else if(order == '1.5') {
                            ic.aromaticbonds[from + '_' + to] = 1;
                            ic.aromaticbonds[to + '_' + from] = 1;
                        }
                    //}
                }

                ++bondIndex;
            }
        }

        ic.dAtoms = AtomHash;
        ic.hAtoms= AtomHash;
        ic.structures[moleculeNum] = [chainNum]; //AtomHash;
        ic.chains[chainNum] = AtomHash;
        ic.residues[residueNum] = AtomHash;

        ic.residueId2Name[residueNum] = resn;

        if(ic.chainsSeq[chainNum] === undefined) ic.chainsSeq[chainNum] = [];

        let resObject = {};
        resObject.resi = resi;
        resObject.name = resn;

        ic.chainsSeq[chainNum].push(resObject);

        ic.ParserUtilsCls.setMaxD();

        ic.saveFileCls.showTitle();

        return true;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class OpmParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async downloadOpm(opmid) { let ic = this.icn3d, me = ic.icn3dui;
        ic.ParserUtilsCls.setYourNote(opmid.toUpperCase() + '(OPM) in iCn3D');
 
        //ic.bCid = undefined;
        // no rotation
        ic.bStopRotate = true;

        let url = "https://opm-assets.storage.googleapis.com/pdb/" + opmid.toLowerCase()+ ".pdb";
        let data = await me.getAjaxPromise(url, 'text', true, 'This is probably not a transmembrane protein. It has no data in Orientations of Proteins in Membranes(OPM) database.');

        ic.bOpm = true;
        await ic.pdbParserCls.loadPdbData(data, opmid, ic.bOpm);

        $("#" + ic.pre + "selectplane_z1").val(ic.halfBilayerSize);
        $("#" + ic.pre + "selectplane_z2").val(-ic.halfBilayerSize);

        $("#" + ic.pre + "extra_mem_z").val(ic.halfBilayerSize);
        $("#" + ic.pre + "intra_mem_z").val(-ic.halfBilayerSize);
    }


    async loadOpmData(data, pdbid, bFull, type, pdbid2) { let ic = this.icn3d, me = ic.icn3dui;
        try {
             if(!pdbid) pdbid = 'stru';
            let url = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&opm&uid=" + pdbid.toLowerCase();
    
            let opmdata = await me.getAjaxPromise(url, 'jsonp', false);
    
            this.setOpmData(opmdata); // set ic.bOpm
            await this.parseAtomData(data, pdbid, bFull, type, pdbid2);
        }
        catch(err) {
            await this.parseAtomData(data, pdbid, bFull, type, pdbid2);
        }
    }

    setOpmData(data) { let ic = this.icn3d; ic.icn3dui;
        if(data.opm !== undefined && data.opm.rot !== undefined) {
            ic.bOpm = true;

            ic.halfBilayerSize = data.opm.thickness;
            ic.rmsd_supr = {};
            ic.rmsd_supr.rot = data.opm.rot;
            ic.rmsd_supr.trans1 = new THREE.Vector3(data.opm.trans1[0], data.opm.trans1[1], data.opm.trans1[2]);
            ic.rmsd_supr.trans2 = new THREE.Vector3(data.opm.trans2[0], data.opm.trans2[1], data.opm.trans2[2]);
            ic.rmsd_supr.rmsd = data.opm.rmsd;

          $("#" + ic.pre + "selectplane_z1").val(ic.halfBilayerSize);
          $("#" + ic.pre + "selectplane_z2").val(-ic.halfBilayerSize);

          $("#" + ic.pre + "extra_mem_z").val(ic.halfBilayerSize);
          $("#" + ic.pre + "intra_mem_z").val(-ic.halfBilayerSize);
        }
        else {
            ic.bOpm = false;
        }
    }

    async parseAtomData(data, pdbid, bFull, type, pdbid2) { let ic = this.icn3d, me = ic.icn3dui;
        if(type === 'mmtf') {
            await ic.mmtfParserCls.parseMmtfData(data, pdbid, bFull);

            /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
        }
        else if(type === 'mmcif') {
            ic.loadAtomDataCls.loadAtomDataIn(data, data.mmcif, 'mmcifid', undefined, undefined);

            if(Object.keys(ic.structures).length == 1) {
                $("#" + ic.pre + "alternateWrapper").hide();
            }
    
            // load assembly info
            let assembly =(data.assembly !== undefined) ? data.assembly : [];
            for(let i = 0, il = assembly.length; i < il; ++i) {
                if(ic.biomtMatrices[i] == undefined) ic.biomtMatrices[i] = new THREE.Matrix4().identity();
    
                for(let j = 0, jl = assembly[i].length; j < jl; ++j) {
                ic.biomtMatrices[i].elements[j] = assembly[i][j];
                }
            }
    
            if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1) {
                $("#" + ic.pre + "assemblyWrapper").show();
    
                ic.asuCnt = ic.biomtMatrices.length;
            }
    
            ic.setStyleCls.setAtomStyleByOptions(ic.opts);
            ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);
    
            await ic.ParserUtilsCls.renderStructure();
    
            if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

            /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
        }
        else if(type === 'pdb') {
            await ic.pdbParserCls.loadPdbData(data, pdbid);
        }
        else if(type === 'align') {
            if(ic.bOpm) {
                await ic.alignParserCls.downloadAlignmentPart2(pdbid);
                /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
            }
            else {
                if(pdbid2 !== undefined) {
                    await this.loadOpmData(data, pdbid2, bFull, type);
                }
                else {
                    await ic.alignParserCls.downloadAlignmentPart2(pdbid);
                    /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
                }
            }
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class PdbParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Ajax call was used to get the atom data from the "pdbid". This function was deferred so that
    //it can be chained together with other deferred functions for sequential execution. A wrapper
    //was added to support both http and https.
    async downloadPdb(pdbid, bAf) { let ic = this.icn3d, me = ic.icn3dui;
        let url;

        if(bAf) {
            url = "https://alphafold.ebi.ac.uk/files/AF-" + pdbid + "-F1-model_" + ic.AFUniprotVersion + ".pdb";
            if(me.cfg.refseqid) {
                ic.ParserUtilsCls.setYourNote(me.cfg.refseqid.toUpperCase() + '(NCBI Protein Acc.) in iCn3D');
            }
            else {
                ic.ParserUtilsCls.setYourNote(pdbid.toUpperCase() + '(AlphaFold) in iCn3D');
            }
        }
        else {
            url = "https://files.rcsb.org/view/" + pdbid + ".pdb";
            ic.ParserUtilsCls.setYourNote(pdbid.toUpperCase() + '(PDB) in iCn3D');
        }

        //ic.bCid = undefined;

        let data = await me.getAjaxPromise(url, 'text', true, 'The ID ' + pdbid + ' can not be found in the server ' + url + '...');

        if(bAf) {
            // add UniProt ID into the header
            let header = 'HEADER                                                        ' + pdbid + '\n';
            data = header + data;          
            await ic.opmParserCls.parseAtomData(data, pdbid, undefined, 'pdb', undefined);
        }
        else {
            await ic.opmParserCls.loadOpmData(data, pdbid, undefined, 'pdb');
        }
    }

    //Load structures from a "URL". Due to the same domain policy of Ajax call, the URL should be in the same
    //domain. "type" could be "pdb", "mol2", "sdf", "xyz", "icn3dpng", or "pae" 
    //for pdb file, mol2file, sdf file, xyz file, iCn3D PNG image, and ALphaFold PAE file, respectively.
    async downloadUrl(url, type, command) { let ic = this.icn3d, me = ic.icn3dui;
        let pos = url.lastIndexOf('/');
        if(pos != -1) {
            let posDot = url.lastIndexOf('.');
            ic.filename = url.substr(pos + 1, posDot - pos - 1);
        }
        else {
            let posDot = url.lastIndexOf('.');
            ic.filename = url.substr(0, posDot);
        }

        //ic.bCid = undefined;

        let data = await me.getAjaxPromise(url, 'text', true);

        ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + data : data;
        ic.InputfileType = type;

        if(type === 'pdb') {
            await this.loadPdbData(data);
            await ic.loadScriptCls.loadScript(command, undefined, true);
        }
        else if(type === 'mmcif') {
            let url = me.htmlCls.baseUrl + "mmcifparser/mmcifparser.cgi";
            let dataObj = {'mmciffile': data};
            let data2 = await me.getAjaxPostPromise(url, dataObj, true);
            await ic.mmcifParserCls.loadMmcifData(data2, undefined);
        }
        else if(type === 'mol2') {
            await ic.mol2ParserCls.loadMol2Data(data);
        }
        else if(type === 'sdf') {
            await ic.sdfParserCls.loadSdfData(data);
        }
        else if(type === 'xyz') {
            await ic.xyzParserCls.loadXyzData(data);
        }
        else if(type === 'mmcif') {
            await ic.mmcifParserCls.loadMmcifData(data);
        }
        else if(type === 'icn3dpng') {
            await me.htmlCls.setHtmlCls.loadPng(data, command);
        }
        else if(type === 'pae') {
            me.htmlCls.dialogCls.openDlg('dl_alignerrormap', 'Show Predicted Aligned Error (PAE) map');
            let bFull = true;
            ic.contactMapCls.processAfErrorMap(JSON.parse(data), bFull);
        }
    }

    //Atom "data" from PDB file was parsed to set up parameters for the 3D viewer. The deferred parameter
    //was resolved after the parsing so that other javascript code can be executed.
    async loadPdbData(data, pdbid, bOpm, bAppend, type, bLastQuery, bNoDssp) { let ic = this.icn3d, me = ic.icn3dui;
        if(!bAppend && (type === undefined || type === 'target')) {
            // if a command contains "load...", the commands should not be cleared with init()
            let bKeepCmd = (ic.bCommandLoad) ? true : false;
            if(!ic.bStatefile) ic.init(bKeepCmd);
        }

        let hAtoms = ic.loadPDBCls.loadPDB(data, pdbid, bOpm, undefined, undefined, bAppend, type, bLastQuery); // defined in the core library

        if(me.cfg.opmid === undefined) ic.ParserUtilsCls.transformToOpmOri(pdbid);

        if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1) {
          $("#" + ic.pre + "assemblyWrapper").show();

          ic.asuCnt = ic.biomtMatrices.length;
        }

        if(ic.emd !== undefined) {
            $("#" + ic.pre + "mapWrapper1").hide();
            $("#" + ic.pre + "mapWrapper2").hide();
            $("#" + ic.pre + "mapWrapper3").hide();
        }
        else {
            $("#" + ic.pre + "emmapWrapper1").hide();
            $("#" + ic.pre + "emmapWrapper2").hide();
            $("#" + ic.pre + "emmapWrapper3").hide();
        }

        // calculate secondary structures if not available
        // DSSP only works for structures with all atoms. The Calpha only strucutres didn't work
        //if(!ic.bSecondaryStructure && !bCalphaOnly) {
        let bCalcSecondary = false;
        if(ic.bSecondaryStructure && Object.keys(ic.structures).length == 1) {
            bCalcSecondary = false;
        }
        else if(!me.cfg.mmtfid && !me.cfg.pdbid && !me.cfg.opmid && !me.cfg.mmdbid && !me.cfg.gi && !me.cfg.uniprotid && !me.cfg.blast_rep_id && !me.cfg.cid && !me.cfg.mmcifid && !me.cfg.align && !me.cfg.chainalign) {
            bCalcSecondary = true;
        }

//        if(!ic.bSecondaryStructure && Object.keys(ic.proteins).length > 0) {
        if((!ic.bSecondaryStructure || bCalcSecondary) && Object.keys(ic.proteins).length > 0 && !bNoDssp) {
            this.applyCommandDssp(bAppend);
        }
        else {
            await this.loadPdbDataRender(bAppend);

            await ic.ParserUtilsCls.checkMemProteinAndRotate();

            /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
        }

        return hAtoms;
    }

    async applyCommandDssp(bAppend) { let ic = this.icn3d, me = ic.icn3dui;
        // ic.deferredSecondary = $.Deferred(function() {
        //     let bCalphaOnly = me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(ic.proteins, ic.atoms));//, 'CA');
        //     ic.dsspCls.applyDssp(bCalphaOnly, bAppend);
        // }); // end of me.deferred = $.Deferred(function() {

        // return ic.deferredSecondary.promise();

        let bCalphaOnly = me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(ic.proteins, ic.atoms));//, 'CA');
        await ic.dsspCls.applyDssp(bCalphaOnly, bAppend);
    }

    async loadPdbDataRender(bAppend) { let ic = this.icn3d, me = ic.icn3dui;
        //ic.pmid = ic.pmid;

        if(me.cfg.align === undefined && Object.keys(ic.structures).length == 1) {
            $("#" + ic.pre + "alternateWrapper").hide();
        }

        if(me.cfg.afid && !ic.bAfMem) {
            ic.opts['color'] = 'confidence';
        }

        ic.setStyleCls.setAtomStyleByOptions(ic.opts);
//        ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);
        ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

        await ic.ParserUtilsCls.renderStructure();

        ic.saveFileCls.showTitle();

        if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

        if(bAppend) {
            // show all
            ic.definedSetsCls.setModeAndDisplay('all');
        }

    //    if(me.deferred !== undefined) me.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SdfParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Ajax call was used to get the atom data from the PubChem "cid". This function was
    //deferred so that it can be chained together with other deferred functions for sequential execution.
    async downloadCid(cid) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        ic.ParserUtilsCls.setYourNote('PubChem CID ' + cid + ' in iCn3D');

        ic.bCid = true;

        // get parent CID
        let urlParent = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/" + ic.inputid + "/cids/JSONP?cids_type=parent";
        let dataParent = await me.getAjaxPromise(urlParent, 'jsonp', true, "Can not retrieve the parant CID...");

        let cidParent = dataParent.IdentifierList.CID[0];

        let url = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/" + cidParent + "/record/SDF/?record_type=3d&response_type=display";
        let data = await me.getAjaxPromise(url, 'text', true, "This CID may not have 3D structure...");

        let bResult = thisClass.loadSdfAtomData(data, cid);

        if(me.cfg.align === undefined && Object.keys(ic.structures).length == 1) {
            $("#" + ic.pre + "alternateWrapper").hide();
        }

        if(!bResult) {
            alert('The SDF of CID ' + cid + ' has the wrong format...');
        }
        else {
            ic.setStyleCls.setAtomStyleByOptions(ic.opts);
            ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

            await ic.ParserUtilsCls.renderStructure();

            if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);
        }
    }

    async loadSdfData(data) { let ic = this.icn3d, me = ic.icn3dui;
        let bResult = this.loadSdfAtomData(data);

        if(me.cfg.align === undefined && Object.keys(ic.structures).length == 1) {
            $("#" + ic.pre + "alternateWrapper").hide();
        }

        if(!bResult) {
          alert('The SDF file has the wrong format...');
        }
        else {
          ic.setStyleCls.setAtomStyleByOptions(ic.opts);
          ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

          await ic.ParserUtilsCls.renderStructure();

          if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

          //if(ic.deferred !== undefined) ic.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
        }
    }

    //Atom "data" from SDF file was parsed to set up parameters for the 3D viewer.
    //The deferred parameter was resolved after the parsing so that other javascript code can be executed.
    loadSdfAtomData(data, cid) { let ic = this.icn3d; ic.icn3dui;
        let lines = data.split(/\r?\n|\r/);
        if(lines.length < 4) return false;

        ic.init();

        let structure = cid ? cid : 1;
        let chain = 'A';
        let resi = 1;
        let resn = 'LIG';

        let moleculeNum = structure;
        let chainNum = structure + '_' + chain;
        let residueNum = chainNum + '_' + resi;

        let atomCount = parseInt(lines[3].substr(0, 3));
        if(isNaN(atomCount) || atomCount <= 0) return false;

        let bondCount = parseInt(lines[3].substr(3, 3));
        let offset = 4;
        if(lines.length < offset + atomCount + bondCount) return false;

        let start = 0;
        let end = atomCount;
        let i, line;

        let atomid2serial = {};
        let HAtomids = {};

        let AtomHash = {};
        let serial = 1;
        for(i = start; i < end; i++) {
            line = lines[offset];
            offset++;

            //var name = line.substr(31, 3).replace(/ /g, "");
            let name = line.substr(31, 3).trim();

            //if(name !== 'H') {
                let x = parseFloat(line.substr(0, 10));
                let y = parseFloat(line.substr(10, 10));
                let z = parseFloat(line.substr(20, 10));
                let coord = new THREE.Vector3(x, y, z);

                let atomDetails = {
                    het: true,              // optional, used to determine chemicals, water, ions, etc
                    serial: serial,         // required, unique atom id
                    name: name,             // required, atom name
                    resn: resn,             // optional, used to determine protein or nucleotide
                    structure: structure,   // optional, used to identify structure
                    chain: chain,           // optional, used to identify chain
                    resi: resi,             // optional, used to identify residue ID
                    coord: coord,           // required, used to draw 3D shape
                    b: 0,                   // optional, used to draw B-factor tube
                    elem: name,             // optional, used to determine hydrogen bond
                    bonds: [],              // required, used to connect atoms
                    ss: 'coil',             // optional, used to show secondary structures
                    ssbegin: false,         // optional, used to show the beginning of secondary structures
                    ssend: false,           // optional, used to show the end of secondary structures

                    bondOrder: []           // optional, specific for chemicals
                };

                ic.atoms[serial] = atomDetails;
                AtomHash[serial] = 1;

                atomid2serial[i] = serial;

                ++serial;
            //}
            //else {
                if(name == 'H') HAtomids[i] = 1;
            //}
        }

        ic.dAtoms = AtomHash;
        ic.hAtoms= AtomHash;
        ic.structures[moleculeNum] = [chainNum]; //AtomHash;
        ic.chains[chainNum] = AtomHash;
        ic.residues[residueNum] = AtomHash;

        ic.residueId2Name[residueNum] = resn;

        if(ic.chainsSeq[chainNum] === undefined) ic.chainsSeq[chainNum] = [];

        let resObject = {};
        resObject.resi = resi;
        resObject.name = resn;

        ic.chainsSeq[chainNum].push(resObject);

        for(i = 0; i < bondCount; i++) {
            line = lines[offset];
            offset++;
            let fromAtomid = parseInt(line.substr(0, 3)) - 1 + start;
            let toAtomid = parseInt(line.substr(3, 3)) - 1 + start;
            //var order = parseInt(line.substr(6, 3));
            let order = line.substr(6, 3).trim();

            //if(!HAtomids.hasOwnProperty(fromAtomid) && !HAtomids.hasOwnProperty(toAtomid)) {
                let from = atomid2serial[fromAtomid];
                let to = atomid2serial[toAtomid];

                ic.atoms[from].bonds.push(to);
                ic.atoms[from].bondOrder.push(order);
                ic.atoms[to].bonds.push(from);
                ic.atoms[to].bondOrder.push(order);

                if(!HAtomids.hasOwnProperty(fromAtomid) && !HAtomids.hasOwnProperty(toAtomid)) {
                    if(order == '2') {
                        ic.doublebonds[from + '_' + to] = 1;
                        ic.doublebonds[to + '_' + from] = 1;
                    }
                    else if(order == '3') {
                        ic.triplebonds[from + '_' + to] = 1;
                        ic.triplebonds[to + '_' + from] = 1;
                    }
                }
        }

        // read partial charge
        let bCrg = false;
        for(let il = lines.length; offset < il; ++offset) {
            if(lines[offset].indexOf('PARTIAL_CHARGES') != -1) {
                bCrg = true;
                break;
            }
            else {
                continue;
            }
        }

        if(bCrg) {
            ++offset;
            let crgCnt = parseInt(lines[offset]);

            ++offset;
            for(i = 0; i < crgCnt; ++i, ++offset) {
                line = lines[offset];
                let serial_charge = line.split(' ');
                let sTmp = parseInt(serial_charge[0]);
                let crg = parseFloat(serial_charge[1]);
                ic.atoms[sTmp].crg = crg;
            }
        }

        // backup bonds
        for(i in ic.atoms) {
            if(ic.atoms[i].name !== 'H') { // only need to deal with non-hydrogen atoms
                ic.atoms[i].bonds2 = ic.atoms[i].bonds.concat();
                ic.atoms[i].bondOrder2 = ic.atoms[i].bondOrder.concat();
            }
        }

        ic.ParserUtilsCls.setMaxD();

        ic.saveFileCls.showTitle();

        return true;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class XyzParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async loadXyzData(data) { let ic = this.icn3d, me = ic.icn3dui;
        let bResult = this.loadXyzAtomData(data);

        if(me.cfg.align === undefined && Object.keys(ic.structures).length == 1) {
            $("#" + ic.pre + "alternateWrapper").hide();
        }

        if(!bResult) {
          alert('The XYZ file has the wrong format...');
        }
        else {
          ic.setStyleCls.setAtomStyleByOptions(ic.opts);
          ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

          await ic.ParserUtilsCls.renderStructure();

          if(me.cfg.rotate !== undefined) ic.resizeCanvasCls.rotStruc(me.cfg.rotate, true);

          //if(me.deferred !== undefined) me.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
        }
    }

    setXyzAtomSeq(AtomHash, moleculeNum, chainNum, residueNum) { let ic = this.icn3d, me = ic.icn3dui;
        ic.dAtoms = me.hashUtilsCls.unionHash(ic.dAtoms, AtomHash);
        ic.hAtoms= me.hashUtilsCls.unionHash(ic.hAtoms, AtomHash);

        ic.structures[moleculeNum] = [chainNum]; //AtomHash;
        ic.chains[chainNum] = AtomHash;
        ic.residues[residueNum] = AtomHash;

        ic.residueId2Name[residueNum] = 'LIG';

        if(ic.chainsSeq[chainNum] === undefined) ic.chainsSeq[chainNum] = [];

        let resObject = {};
        resObject.resi = 1;
        resObject.name = 'LIG';

        ic.chainsSeq[chainNum].push(resObject);

        // determine bonds
        let serialArray = Object.keys(AtomHash);
        for(let j = 0, jl = serialArray.length; j < jl; ++j) {
            let atom0 = ic.atoms[serialArray[j]];

            for(let k = j + 1, kl = serialArray.length; k < kl; ++k) {
                let atom1 = ic.atoms[serialArray[k]];
                let maxR = 1.2 *(me.parasCls.covalentRadii[atom0.elem.toUpperCase()] + me.parasCls.covalentRadii[atom1.elem.toUpperCase()]);
                if(Math.abs(atom0.coord.x - atom1.coord.x) > maxR) continue;
                if(Math.abs(atom0.coord.y - atom1.coord.y) > maxR) continue;
                if(Math.abs(atom0.coord.z - atom1.coord.z) > maxR) continue;

                if(me.utilsCls.hasCovalentBond(atom0, atom1)) {
                    ic.atoms[serialArray[j]].bonds.push(serialArray[k]);
                    ic.atoms[serialArray[k]].bonds.push(serialArray[j]);
                }
            }
        }
    }

    loadXyzAtomData(data) { let ic = this.icn3d; ic.icn3dui;
        let lines = data.split(/\r?\n|\r/);
        if(lines.length < 3) return false;

        ic.init();

        let chain = 'A';
        let resn = 'LIG';
        let resi = 1;

        let AtomHash = {};
        let moleculeNum = 0, chainNum, residueNum;
        let structure, serial=1, offset = 2;

        ic.molTitle = "";

        for(let i = 0, il = lines.length; i < il; ++i) {
            let line = lines[i].trim();
            if(line === '') continue;

            if(line !== '' && !isNaN(line)) { // start a new molecule
                if(i !== 0) {
                    this.setXyzAtomSeq(AtomHash, moleculeNum, chainNum, residueNum);
                }

                ++moleculeNum;
                AtomHash = {};

                structure = moleculeNum;
                chainNum = structure + '_' + chain;
                residueNum = chainNum + '_' + resi;
                if(moleculeNum > 1) {
                    ic.molTitle += "; ";
                }
                ic.molTitle += lines[i+1].trim();

                i = i + offset;
            }

            line = lines[i].trim();
            if(line === '') continue;

            let name_x_y_z = line.replace(/,/, " ").replace(/\s+/g, " ").split(" ");

            let name = name_x_y_z[0];
            let x = parseFloat(name_x_y_z[1]);
            let y = parseFloat(name_x_y_z[2]);
            let z = parseFloat(name_x_y_z[3]);
            let coord = new THREE.Vector3(x, y, z);

            let atomDetails = {
                het: true,              // optional, used to determine chemicals, water, ions, etc
                serial: serial,         // required, unique atom id
                name: name,             // required, atom name
                resn: resn,             // optional, used to determine protein or nucleotide
                structure: structure,   // optional, used to identify structure
                chain: chain,           // optional, used to identify chain
                resi: resi,             // optional, used to identify residue ID
                coord: coord,           // required, used to draw 3D shape
                b: 0,                   // optional, used to draw B-factor tube
                elem: name,             // optional, used to determine hydrogen bond
                bonds: [],              // required, used to connect atoms
                ss: 'coil',             // optional, used to show secondary structures
                ssbegin: false,         // optional, used to show the beginning of secondary structures
                ssend: false,           // optional, used to show the end of secondary structures

                bondOrder: []           // optional, specific for chemicals
            };

            ic.atoms[serial] = atomDetails;
            AtomHash[serial] = 1;

            ++serial;
        }

        this.setXyzAtomSeq(AtomHash, moleculeNum, chainNum, residueNum);

        ic.ParserUtilsCls.setMaxD();

        ic.saveFileCls.showTitle();

        return true;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class RealignParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // realign, residue by residue
    realign() { let ic = this.icn3d, me = ic.icn3dui;
        ic.selectionCls.saveSelectionPrep();

        let index = Object.keys(ic.defNames2Atoms).length;
        let name = 'alseq_' + index;

        ic.selectionCls.saveSelection(name, name);

        me.htmlCls.clickMenuCls.setLogCmd("realign", true);

        let structHash = {}, struct2chain = {};
        ic.realignResid = {};
        let lastStruResi = '';
        for(let serial in ic.hAtoms) {
            let atom = ic.atoms[serial];
            let chainid = atom.structure + '_' + atom.chain;
            if((ic.proteins.hasOwnProperty(serial) && atom.name == "CA")
              ||(ic.nucleotides.hasOwnProperty(serial) &&(atom.name == "O3'" || atom.name == "O3*")) ) {
                if(atom.structure + '_' + atom.resi == lastStruResi) continue; // e.g., Alt A and B

                if(!structHash.hasOwnProperty(atom.structure)) {
                    structHash[atom.structure] = [];
                }
                structHash[atom.structure].push(atom.coord.clone());

                if(!ic.realignResid.hasOwnProperty(chainid)) {
                    ic.realignResid[chainid] = [];
                }

                ic.realignResid[chainid].push({'resid': chainid + '_' + atom.resi, 'resn': me.utilsCls.residueName2Abbr(atom.resn.substr(0, 3)).substr(0, 1)});

                struct2chain[atom.structure] = atom.structure + '_' + atom.chain;

                lastStruResi = atom.structure + '_' + atom.resi;
            }
        }

        let structArray = Object.keys(structHash);

        let toStruct = structArray[0];

        let chainidArray = [];
        ic.qt_start_end = []; // reset the alignment

        chainidArray.push(struct2chain[toStruct]);
        for(let i = 1, il = structArray.length; i < il; ++i) {
            let fromStruct = structArray[i];

            // transform from the second structure to the first structure
            let coordsFrom = structHash[fromStruct];
            let coordsTo = structHash[toStruct];

            let bKeepSeq = true;
            //ic.ParserUtilsCls.alignCoords(coordsFrom, coordsTo, fromStruct, bKeepSeq);
            ic.ParserUtilsCls.alignCoords(coordsFrom, coordsTo, fromStruct, bKeepSeq, struct2chain[toStruct], struct2chain[fromStruct]);
            chainidArray.push(struct2chain[fromStruct]);
        }

        // align seq
        //ic.hAtoms = ic.chainalignParserCls.setMsa(chainidArray, undefined, true);
        ic.hAtoms = ic.chainalignParserCls.setMsa(chainidArray);

        name = 'protein_aligned';
        ic.selectionCls.saveSelection(name, name);
      
        ic.transformCls.zoominSelection();

        ic.hlUpdateCls.updateHlAll();
    }

    async parseChainRealignPredefined(chainidArray, struct2SeqHash, struct2CoorHash, struct2resid) { let ic = this.icn3d, me = ic.icn3dui;

      let toStruct = chainidArray[0].substr(0, chainidArray[0].indexOf('_')); //.toUpperCase();

      let hAtoms = {}, rmsd;

      ic.realignResid = {};

      ic.opts['color'] = 'grey';
      ic.setColorCls.setColorByOptions(ic.opts, ic.dAtoms);
      
      // reinitialize
      ic.qt_start_end = [];

      for(let index = 0, indexl = chainidArray.length - 1; index < indexl; ++index) {         
          let fromStruct = chainidArray[index + 1].substr(0, chainidArray[index + 1].indexOf('_')); //.toUpperCase();

          //if(toStruct == fromStruct) fromStruct += me.htmlCls.postfix;

          let chainTo = toStruct + chainidArray[0].substr(chainidArray[0].indexOf('_'));
          let chainFrom = fromStruct + chainidArray[index + 1].substr(chainidArray[index + 1].indexOf('_'));

          chainidArray[0] = chainTo;
          chainidArray[index + 1] = chainFrom;

          let chainpair =  chainTo + ',' + chainFrom;

          if(!struct2SeqHash[chainpair]) continue;

          let seq1 = struct2SeqHash[chainpair][toStruct];
          let seq2 = struct2SeqHash[chainpair][fromStruct];

          let coord1 = struct2CoorHash[chainpair][toStruct];
          let coord2 = struct2CoorHash[chainpair][fromStruct];

          let residArray1 = struct2resid[chainpair][toStruct];
          let residArray2 = struct2resid[chainpair][fromStruct];

          ic.realignResid[chainTo] = [];
          ic.realignResid[chainFrom] = [];

          for(let i = 0, il = seq1.length; i < il; ++i) {
              ic.realignResid[chainTo].push({'resid':residArray1[i], 'resn':seq1[i]});
              ic.realignResid[chainFrom].push({'resid':residArray2[i], 'resn':seq2[i]});
          }

          let bChainAlign = true;
          // set ic.qt_start_end in alignCoords()

          let result = ic.ParserUtilsCls.alignCoords(coord2, coord1, fromStruct, undefined, chainTo, chainFrom, index + 1, bChainAlign);
          hAtoms = me.hashUtilsCls.unionHash(hAtoms, result.hAtoms);
          rmsd = parseFloat(result.rmsd);
      }

      // If rmsd from vastsrv is too large, realign the chains
      if(me.cfg.chainalign && !me.cfg.usepdbnum && me.cfg.resdef && rmsd > 5) {      
        let nameArray = me.cfg.chainalign.split(',');
        if(nameArray.length > 0) {
            ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
        }

        me.cfg.aligntool = 'tmalign';

        await ic.realignParserCls.realignOnStructAlign();
        // if(nameArray.length > 0) {
        //     me.htmlCls.clickMenuCls.setLogCmd("realign on tmalign | " + nameArray, true);
        // }
        // else {
        //     me.htmlCls.clickMenuCls.setLogCmd("realign on tmalign", true);
        // }
      }
      else {
        // align seq
        //ic.hAtoms = ic.chainalignParserCls.setMsa(chainidArray, undefined, true);
        ic.hAtoms = ic.chainalignParserCls.setMsa(chainidArray);

        ic.transformCls.zoominSelection();

        await ic.chainalignParserCls.downloadChainalignmentPart3(undefined, chainidArray, ic.hAtoms);
      }
    }

    async parseChainRealignData(dataArray, chainresiCalphaHash2, chainidArray, struct2SeqHash, struct2CoorHash, struct2resid, bRealign) { let ic = this.icn3d, me = ic.icn3dui;
      //var dataArray =(chainidArray.length == 2) ? [ajaxData] : ajaxData;

      let toStruct = chainidArray[0].substr(0, chainidArray[0].indexOf('_')); //.toUpperCase();
      if(!bRealign) toStruct = toStruct.toUpperCase();


      let hAtoms = {};

      ic.realignResid = {};

      ic.opts['color'] = 'grey';
      ic.setColorCls.setColorByOptions(ic.opts, ic.dAtoms);

      // reinitialize
      ic.qt_start_end = [];

      // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
      //var data2 = v2[0];
      for(let index = 0, indexl = dataArray.length; index < indexl; ++index) {
    //  for(let index = 1, indexl = dataArray.length; index < indexl; ++index) {
          let data = dataArray[index].value;//[0];
          if(!data) continue;

          let fromStruct = chainidArray[index + 1].substr(0, chainidArray[index + 1].indexOf('_')); //.toUpperCase();
          if(!bRealign) fromStruct = fromStruct.toUpperCase();

          //if(toStruct == fromStruct) fromStruct += me.htmlCls.postfix;

          let chainTo = toStruct + chainidArray[0].substr(chainidArray[0].indexOf('_'));
          let chainFrom = fromStruct + chainidArray[index + 1].substr(chainidArray[index + 1].indexOf('_'));

          chainidArray[0] = chainTo;
          chainidArray[index + 1] = chainFrom;

          let seq1 = struct2SeqHash[chainTo];
          let seq2 = struct2SeqHash[chainFrom];

          let coord1 = struct2CoorHash[chainTo];
          let coord2 = struct2CoorHash[chainFrom];

          let residArray1 = struct2resid[chainTo];
          let residArray2 = struct2resid[chainFrom];

          let query, target;

          if(data.data !== undefined) {
              query = data.data[0].query;
              let targetName = Object.keys(data.data[0].targets)[0];
              target = data.data[0].targets[targetName];

              target = target.hsps[0];
          }

          if(query !== undefined && target !== undefined) {
              // transform from the second structure to the first structure
              let coordsTo = [];
              let coordsFrom = [];

              let seqto = '', seqfrom = '';

              ic.realignResid[chainTo] = [];
              ic.realignResid[chainFrom] = [];

              let segArray = target.segs;
              for(let i = 0, il = segArray.length; i < il; ++i) {
                  let seg = segArray[i];
                  let prevChain1 = '', prevChain2 = '';
                  for(let j = 0; j <= seg.orito - seg.orifrom; ++j) {
                      let chainid1 = residArray1[j + seg.orifrom].substr(0, residArray1[j + seg.orifrom].lastIndexOf('_'));
                      let chainid2 = residArray2[j + seg.from].substr(0, residArray2[j + seg.from].lastIndexOf('_'));

                      if(!coord1[j + seg.orifrom] || !coord2[j + seg.from]) continue;

                      coordsTo.push(coord1[j + seg.orifrom]);
                      coordsFrom.push(coord2[j + seg.from]);

                      seqto += seq1[j + seg.orifrom];
                      seqfrom += seq2[j + seg.from];

                      // one chaincould be longer than the other
                      if(j == 0 ||(prevChain1 == chainid1 && prevChain2 == chainid2) ||(prevChain1 != chainid1 && prevChain2 != chainid2)) {
                          ic.realignResid[chainTo].push({'resid':residArray1[j + seg.orifrom], 'resn':seq1[j + seg.orifrom]});
                          ic.realignResid[chainFrom].push({'resid':residArray2[j + seg.from], 'resn':seq2[j + seg.from]});
                      }

                      prevChain1 = chainid1;
                      prevChain2 = chainid2;
                  }
              }

              //let chainTo = chainidArray[0];
              //let chainFrom = chainidArray[index + 1];

              let bChainAlign = true;


              let result = ic.ParserUtilsCls.alignCoords(coordsFrom, coordsTo, fromStruct, undefined, chainTo, chainFrom, index + 1, bChainAlign);
              hAtoms = me.hashUtilsCls.unionHash(hAtoms, result.hAtoms);

    //          ic.opts['color'] = 'identity';
    //          ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

              //ic.hlUpdateCls.updateHlAll();
          }
          else {
              if(fromStruct === undefined && !me.cfg.command) {
                if(ic.bRender) alert('Please do not align residues in the same structure');
              }
              else if(seq1 && seq2) {
                if((seq1.length < 6 || seq2.length < 6) && !me.cfg.command) {
                    if(ic.bRender) alert('These sequences are too short for alignment');
                }
                else if(seq1.length >= 6 && seq2.length >= 6 && !me.cfg.command) {
                    if(ic.bRender) alert('These sequences can not be aligned to each other');
                }
              }
          }

          // update all residue color

          ///// if(ic.deferredRealign !== undefined) ic.deferredRealign.resolve();
      }

      if(bRealign) {
        // align seq
        //ic.hAtoms = ic.chainalignParserCls.setMsa(chainidArray, undefined, bRealign);
        ic.hAtoms = ic.chainalignParserCls.setMsa(chainidArray);
        let name = 'protein_aligned';
        ic.selectionCls.saveSelection(name, name);

        if(ic.bAfMem) {
            ic.selectionCls.selectAll_base();

            ic.opts['chemicals'] = 'stick';  
            ic.opts['color'] = 'confidence'; //'structure';

            ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);
        }
        else {
            ic.transformCls.zoominSelection();

            ic.dAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms); //hAtoms;
    
            ic.opts['color'] = 'identity';

            ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);
        }

        ic.drawCls.draw();
        ic.hlUpdateCls.updateHlAll();

        if(ic.bAfMem) {
            let axis = new THREE.Vector3(1,0,0);
            let angle = -90 / 180.0 * Math.PI;

            ic.transformCls.setRotation(axis, angle);
        }
               
        /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
        /// if(ic.deferredRealign !== undefined) ic.deferredRealign.resolve();
      }
      else {
        // align seq
        ic.hAtoms = ic.chainalignParserCls.setMsa(chainidArray);
        
        ic.transformCls.zoominSelection();

        await ic.chainalignParserCls.downloadChainalignmentPart3(chainresiCalphaHash2, chainidArray, ic.hAtoms);
      }
    }

    async realignOnSeqAlign(pdbidTemplate) { let ic = this.icn3d; ic.icn3dui;
        let chainidHash = ic.firstAtomObjCls.getChainsFromAtoms(ic.hAtoms);

        let chainidArrayTmp = Object.keys(chainidHash);
        let chainidArray = [];

        let prevChainid = '';
        for(let i = 0, il = chainidArrayTmp.length; i < il; ++i) {
            if(chainidArrayTmp[i] != prevChainid) chainidArray.push(chainidArrayTmp[i]);
            prevChainid = chainidArrayTmp[i];
        }
        
        // use the model from Membranome as template
        if(ic.bAfMem && chainidArray.length == 2) {
            if(chainidArray[1].split('_')[0] == pdbidTemplate) {
                let tmp = chainidArray[0];
                chainidArray[0] = chainidArray[1]; 
                chainidArray[1] = tmp;
            }
        }
        
        let bRealign = true;
        ic.qt_start_end = []; // reset the alignment

        await this.realignChainOnSeqAlign(undefined, chainidArray, bRealign);
    }

    async realignOnStructAlign() { let ic = this.icn3d, me = ic.icn3dui;
        // each 3D domain should have at least 3 secondary structures
        let minSseCnt = 3;
        let struct2domain = {};

        for(let struct in ic.structures) {
            struct2domain[struct] = {};
            let chainidArray = ic.structures[struct];
            for(let i = 0, il = chainidArray.length; i < il; ++i) {
                let chainid = chainidArray[i];
                let atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.chains[chainid]);               
                let sseCnt = 0;
                for(let serial in atoms) {
                    if(ic.atoms[serial].ssbegin) ++sseCnt;
                    if(sseCnt == minSseCnt) {
                        struct2domain[struct][chainid] = atoms;
                        break;
                    }
                }
            }
        }

        let ajaxArray = [], chainidPairArray = [];
        let urlalign = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi";
        let urltmalign = me.htmlCls.baseUrl + "tmalign/tmalign.cgi";
        let structArray = Object.keys(struct2domain);
        for(let s = 0, sl = structArray.length; s < sl; ++s) {
            let struct1 = structArray[s];
            let chainidArray1 = Object.keys(struct2domain[struct1]);
            if(chainidArray1.length == 0) continue;
            for(let t = s+1, tl = structArray.length; t < tl; ++t) {
                let struct2 = structArray[t];
                let chainidArray2 = Object.keys(struct2domain[struct2]);
                if(chainidArray2.length == 0) continue;

                for(let i = 0, il = chainidArray1.length; i < il; ++i) {
                    let chainid1 = chainidArray1[i];
                    for(let j = 0, jl = chainidArray2.length; j < jl; ++j) {
                        let chainid2 = chainidArray2[j];

                        let alignAjax;

                        if(me.cfg.aligntool != 'tmalign') {
                            let jsonStr_t = ic.domain3dCls.getDomainJsonForAlign(struct2domain[struct1][chainid1]);
                            let jsonStr_q = ic.domain3dCls.getDomainJsonForAlign(struct2domain[struct2][chainid2]);
                        
                            let dataObj = {'domains1': jsonStr_q, 'domains2': jsonStr_t};
                            alignAjax = me.getAjaxPostPromise(urlalign, dataObj);
                        }
                        else {
                            let pdb_target = ic.saveFileCls.getAtomPDB(struct2domain[struct1][chainid1], undefined, undefined, undefined, undefined, struct1);
                            let pdb_query = ic.saveFileCls.getAtomPDB(struct2domain[struct2][chainid2], undefined, undefined, undefined, undefined, struct2);
  
                            // let pdb_target = ic.saveFileCls.getAtomPDB(ic.chains[chainid1], undefined, undefined, undefined, undefined, struct1);
                            // let pdb_query = ic.saveFileCls.getAtomPDB(ic.chains[chainid2], undefined, undefined, undefined, undefined, struct2);
    
                            let dataObj = {'pdb_query': pdb_query, 'pdb_target': pdb_target};
                            alignAjax = me.getAjaxPostPromise(urltmalign, dataObj);                    
                        }

                        ajaxArray.push(alignAjax);
                        chainidPairArray.push(chainid1 + ',' + chainid2); // chainid2 is target
                    }
                }
            }
        }

        let allPromise = Promise.allSettled(ajaxArray);
        try {
            let dataArray = await allPromise;
            ic.qt_start_end = []; // reset the alignment
            await ic.chainalignParserCls.downloadChainalignmentPart2bRealign(dataArray, chainidPairArray);  
        }
        catch(err) {
            if(ic.bRender) alert("These structures can NOT be aligned to each other...");
        }                   
    }

    async realignChainOnSeqAlign(chainresiCalphaHash2, chainidArray, bRealign, bPredefined) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        //bRealign: realign based on seq alignment
        //bPredefined: chain alignment with predefined matching residues

        let struct2SeqHash = {};
        let struct2CoorHash = {};
        let struct2resid = {};

        let mmdbid_t, chainid_t;
        let ajaxArray = [];
        let url = me.htmlCls.baseUrl + 'pwaln/pwaln.fcgi?from=chainalign';

        let predefinedResArray, predefinedResPair;

        if(bPredefined) {
            predefinedResArray = me.cfg.resdef.trim().replace(/\+/gi, ' ').split('; ');
            
            if(predefinedResArray.length != chainidArray.length - 1) {
               alert("Please make sure the number of chains and the lines of predefined residues are the same...");
               return;
            }
        }

        let result, resiArray;
        for(let i = 0, il = chainidArray.length; i < il; ++i) {
            //if(bPredefined) predefinedRes = predefinedResArray[i].trim();

            let pos = chainidArray[i].indexOf('_');
            let mmdbid = chainidArray[i].substr(0, pos); //.toUpperCase();

            if(!bRealign) mmdbid =  mmdbid.toUpperCase();

            if(i == 0) {
                mmdbid_t = mmdbid;
            }

            let chainid = mmdbid + chainidArray[i].substr(pos);
            if(i == 0) chainid_t = chainid;
            
            if(!ic.chainsSeq || !ic.chainsSeq[chainid]) {
                //alert("Please select one chain per structure and try it again...");
                //return;
                continue;
            }

            if(!struct2SeqHash.hasOwnProperty(chainid) && !bPredefined) {
                struct2SeqHash[chainid] = '';
                struct2CoorHash[chainid] = [];
                struct2resid[chainid] = [];
            }
 
            if(bPredefined) {
                //base = parseInt(ic.chainsSeq[chainid][0].resi);

                if(i == 0) ;
                else {
                    let hAtoms = {};

                    predefinedResPair = predefinedResArray[i - 1].split(' | ');

                    let chainidpair = chainid_t + ',' + chainid;
                    if(!struct2SeqHash[chainidpair]) struct2SeqHash[chainidpair] = {};
                    if(!struct2CoorHash[chainidpair]) struct2CoorHash[chainidpair] = {};
                    if(!struct2resid[chainidpair]) struct2resid[chainidpair] = {};

                    // master
                    resiArray = predefinedResPair[0].split(",");        
                    result = thisClass.getSeqCoorResid(resiArray, chainid_t);

                    hAtoms = me.hashUtilsCls.unionHash(hAtoms, result.hAtoms);

                    if(!struct2SeqHash[chainidpair][mmdbid_t]) struct2SeqHash[chainidpair][mmdbid_t] = '';
                    if(!struct2CoorHash[chainidpair][mmdbid_t]) struct2CoorHash[chainidpair][mmdbid_t] = [];
                    if(!struct2resid[chainidpair][mmdbid_t]) struct2resid[chainidpair][mmdbid_t] = [];

                    struct2SeqHash[chainidpair][mmdbid_t] += result.seq;
                    struct2CoorHash[chainidpair][mmdbid_t] = struct2CoorHash[chainidpair][mmdbid_t].concat(result.coor);
                    struct2resid[chainidpair][mmdbid_t] = struct2resid[chainidpair][mmdbid_t].concat(result.resid);

                    // slave
                    resiArray = predefinedResPair[1].split(",");
                    result = thisClass.getSeqCoorResid(resiArray, chainid); 
                    hAtoms = me.hashUtilsCls.unionHash(hAtoms, result.hAtoms);

                    if(!struct2SeqHash[chainidpair][mmdbid]) struct2SeqHash[chainidpair][mmdbid] = '';
                    if(!struct2CoorHash[chainidpair][mmdbid]) struct2CoorHash[chainidpair][mmdbid] = [];
                    if(!struct2resid[chainidpair][mmdbid]) struct2resid[chainidpair][mmdbid] = [];

                    struct2SeqHash[chainidpair][mmdbid] += result.seq;
                    struct2CoorHash[chainidpair][mmdbid] = struct2CoorHash[chainidpair][mmdbid].concat(result.coor);
                    struct2resid[chainidpair][mmdbid] = struct2resid[chainidpair][mmdbid].concat(result.resid);

                    // let residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(hAtoms);
                    // let residueArray = Object.keys(residueHash);
        
                    // let commandname = chainidpair;
                    // let commanddescr = 'aligned ' + chainidpair;
                    // let select = "select " + ic.resid2specCls.residueids2spec(residueArray);
        
                    // ic.selectionCls.addCustomSelection(residueArray, commandname, commanddescr, select, true);
        
                    // me.htmlCls.clickMenuCls.setLogCmd(select + " | name " + commandname, true);
                    // me.htmlCls.clickMenuCls.setLogCmd("realign", true);
                }
            }
            else {           
                if(i == 0) { // master
                    //base = parseInt(ic.chainsSeq[chainid][0].resi);

                    resiArray = [];
                    if(bRealign) {
                        //resiArray = [resRange];
                        let residHash = ic.firstAtomObjCls.getResiduesFromAtoms(ic.hAtoms);

                        for(var resid in residHash) {
                            let resi = resid.substr(resid.lastIndexOf('_') + 1);

                            let chainidTmp = resid.substr(0, resid.lastIndexOf('_'));
                            if(chainidTmp == chainid) resiArray.push(resi);
                        }
                    }
                    else if(me.cfg.resnum) {
                        resiArray = me.cfg.resnum.split(",");
                    }
                    
                    //if(!bPredefined) {
                        result = thisClass.getSeqCoorResid(resiArray, chainid);   
                        struct2SeqHash[chainid] += result.seq;

                        struct2CoorHash[chainid] = struct2CoorHash[chainid].concat(result.coor);
                        struct2resid[chainid] = struct2resid[chainid].concat(result.resid);
                    //}
                }
                else {
                    // if selected both chains
                    let bSelectedBoth = false;
                    if(bRealign) {
                        //resiArray = [resRange];
                        let residHash = ic.firstAtomObjCls.getResiduesFromAtoms(ic.hAtoms);
                        for(var resid in residHash) {
                            //let resi = resid.substr(resid.lastIndexOf('_') + 1);
                            let chainidTmp = resid.substr(0, resid.lastIndexOf('_'));
                            if(chainidTmp == chainid) {
                                bSelectedBoth = true;

                                let resn = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]).resn;
                                struct2SeqHash[chainid] += me.utilsCls.residueName2Abbr(resn);

                                struct2CoorHash[chainid] = struct2CoorHash[chainid].concat(this.getResCoorArray(resid));

                                struct2resid[chainid].push(resid);
                            }
                        }
                    }

                    if(!bSelectedBoth) {
                        for(let j = 0, jl = ic.chainsSeq[chainid].length; j < jl; ++j) {
                            struct2SeqHash[chainid] += ic.chainsSeq[chainid][j].name;
                            let resid = chainid + '_' + ic.chainsSeq[chainid][j].resi;

                            struct2CoorHash[chainid] = struct2CoorHash[chainid].concat(this.getResCoorArray(resid));

                            struct2resid[chainid].push(resid);
                        }
                    }

                    let seq1 = struct2SeqHash[chainid_t];
                    let seq2 = struct2SeqHash[chainid];
                    
                    let dataObj = {'targets': seq1, 'queries': seq2};
                    let queryAjax = me.getAjaxPostPromise(url, dataObj);

                    ajaxArray.push(queryAjax);
                }  
            }        
        } // for

        if(bPredefined) {
            await thisClass.parseChainRealignPredefined(chainidArray, struct2SeqHash, struct2CoorHash, struct2resid);
        }
        else {
            let allPromise = Promise.allSettled(ajaxArray);
            try {
                let dataArray = await allPromise;
                //thisClass.parseChainRealignData(Array.from(dataArray), chainresiCalphaHash2, chainidArray, struct2SeqHash, struct2CoorHash, struct2resid, bRealign);
                await thisClass.parseChainRealignData(dataArray, chainresiCalphaHash2, chainidArray, struct2SeqHash, struct2CoorHash, struct2resid, bRealign);

                ///// if(ic.deferredAfMem !== undefined) ic.deferredAfMem.resolve();
                /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
                /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
            }
            catch(err) {
                alert("The realignment did not work...");
                ///// if(ic.deferredAfMem !== undefined) ic.deferredAfMem.resolve();
                /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
                /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();

                return;
            }              
        }
    }

    getSeqCoorResid(resiArray, chainid) { let ic = this.icn3d, me = ic.icn3dui;
        let seq = '', coorArray = [], residArray = [];
        let hAtoms = {};

        for(let j = 0, jl = resiArray.length; j < jl; ++j) {
            if(resiArray[j].indexOf('-') != -1) {
                let startEnd = resiArray[j].split('-');

                for(let k = parseInt(startEnd[0]); k <= parseInt(startEnd[1]); ++k) {
                    // from VAST neighbor page, use NCBI residue number
                    //if(me.cfg.usepdbnum === false) k += base - 1;

                    //let seqIndex = k - base;
                    let seqIndex = ic.setSeqAlignCls.getPosFromResi(chainid, k);

                    // if(ic.bNCBI) {
                    //     let atom = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[chainid + '_' + k]);
                    //     if(atom && atom.resiNCBI) seqIndex = atom.resiNCBI - 1;
                    // }

                    // don't align solvent or chemicals
                    if(!ic.chainsSeq[chainid] || !ic.chainsSeq[chainid][seqIndex] || me.parasCls.b62ResArray.indexOf(ic.chainsSeq[chainid][seqIndex].name.toUpperCase()) == -1) continue;

                    seq += ic.chainsSeq[chainid][seqIndex].name.toUpperCase();

                    coorArray = coorArray.concat(this.getResCoorArray(chainid + '_' + k));

                    residArray.push(chainid + '_' + k);
                }            
            }
            else { // one residue
                //let k = parseInt(resiArray[j]);
                let k = resiArray[j];

                // from VAST neighbor page, use NCBI residue number
                //if(me.cfg.usepdbnum === false) k += base - 1;

                //let seqIndex = k - base;
                let seqIndex = ic.setSeqAlignCls.getPosFromResi(chainid, k);

                // if(ic.bNCBI) {
                //     let atom = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[chainid + '_' + k]);
                //     if(atom && atom.resiNCBI) seqIndex = atom.resiNCBI - 1;
                // }

                if(!ic.chainsSeq[chainid][seqIndex]) continue;

                let resCoorArray = this.getResCoorArray(chainid + '_' + k);
                //if(resCoorArray.length == 1 && resCoorArray[0] === undefined) continue;

                seq += ic.chainsSeq[chainid][seqIndex].name.toUpperCase();

                coorArray = coorArray.concat(resCoorArray);

                residArray.push(chainid + '_' + k);
            }
        }

        for(let i = 0, il = residArray.length; i < il; ++i) {
            hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.residues[residArray[i]]);
        }

        return {seq: seq, coor: coorArray, resid: residArray, hAtoms: hAtoms};
    }

    getResCoorArray(resid) { let ic = this.icn3d; ic.icn3dui;
        let struct2CoorArray = [];

        let bFound = false;
        for(let serial in ic.residues[resid]) {
            let atom = ic.atoms[serial];

            //if((ic.proteins.hasOwnProperty(serial) && atom.name == "CA" && atom.elem == "C")
            //  ||(ic.nucleotides.hasOwnProperty(serial) &&(atom.name == "O3'" || atom.name == "O3*") && atom.elem == "O") ) {
            if((atom.name == "CA" && atom.elem == "C")
              ||((atom.name == "O3'" || atom.name == "O3*") && atom.elem == "O") ) {
                struct2CoorArray.push(atom.coord.clone());
                bFound = true;
                break;
            }
        }
        if(!bFound) struct2CoorArray.push(undefined);

        return struct2CoorArray;
    }
}

/**
 * @file Density Cif Parser
 * @author David Sehnal dsehnal <alexander.rose@weirdbyte.de>
 * Modified by Jiyao Wang / https://github.com/ncbi/icn3d
 */

class DensityCifParser {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    densityCifParser(pdbid, type, sigma, emd) { let ic = this.icn3d, me = ic.icn3dui;
       let thisClass = this;

       let url;
       let detail = (me.utilsCls.isMobile() || me.cfg.notebook) ? 0 : 4; //4;

       //https://www.ebi.ac.uk/pdbe/densities/doc.html
       if(type == '2fofc' || type == 'fofc') {
           url = "https://www.ebi.ac.uk/pdbe/densities/x-ray/" + pdbid.toLowerCase() + "/cell?detail=" + detail;
       }
       else if(type == 'em') {
           url = "https://www.ebi.ac.uk/pdbe/densities/emd/" + emd.toLowerCase() + "/cell?detail=" + detail;
       }

       //var bCid = undefined;

        //https://stackoverflow.com/questions/33902299/using-jquery-ajax-to-download-a-binary-file
        if(type == '2fofc' && ic.bAjax2fofc) {
            ic.mapData.sigma2 = sigma;
            ic.setOptionCls.setOption('map', type);
        }
        else if(type == 'fofc' && ic.bAjaxfofc) {
            ic.mapData.sigma = sigma;
            ic.setOptionCls.setOption('map', type);
        }
        else if(type == 'em' && ic.bAjaxEm) {
            ic.mapData.sigmaEm = sigma;
            ic.setOptionCls.setOption('emmap', type);
        }
        else {
            let oReq = new XMLHttpRequest();
            oReq.open("GET", url, true);
            oReq.responseType = "arraybuffer";

            oReq.onreadystatechange = function() {
                if (this.readyState == 4) {
                   if(this.status == 200) {
                       let arrayBuffer = oReq.response;

                       thisClass.parseChannels(arrayBuffer, type, sigma);

                       if(type == '2fofc' || type == 'fofc') {
                           ic.bAjax2fofc = true;
                           ic.bAjaxfofc = true;

                           ic.setOptionCls.setOption('map', type);
                       }
                       else if(type == 'em') {
                           ic.bAjaxEm = true;

                           ic.setOptionCls.setOption('emmap', type);
                       }
                    }
                    else {
                       if(type == '2fofc' || type == 'fofc') {
                           alert("Density server at EBI has no corresponding electron density map for this structure.");
                       }
                       else if(type == 'em') {
                           alert("Density server at EBI has no corresponding EM density map for this structure.");
                       }
                    }

                    /// if(ic.deferredEmmap !== undefined) ic.deferredEmmap.resolve();
                }
                else {
                    ic.ParserUtilsCls.showLoading();
                }
            };

            oReq.send();
        }
    }

    parseChannels(densitydata, type, sigma) { let ic = this.icn3d; ic.icn3dui;
        let cif = this.BinaryParse(densitydata);

        if(type == '2fofc' || type == 'fofc') {
            let twoDensity = this.getChannel(cif, '2FO-FC');
            let oneDensity = this.getChannel(cif, 'FO-FC');

            // '2fofc'
            let density = twoDensity;
            let sampleCount = density.box.sampleCount;
            let header = {xExtent: sampleCount[0], yExtent: sampleCount[1], zExtent: sampleCount[2], mean: density.valuesInfo.mean, sigma: density.valuesInfo.sigma};
            ic.mapData.header2 = header;

            ic.mapData.data2 = density.data;

            let origin = density.box.origin;
            let dimensions = density.box.dimensions;
            let basis = density.spacegroup.basis;
            let scale = new THREE.Matrix4().makeScale(
                dimensions[0] / (sampleCount[0] ),
                dimensions[1] / (sampleCount[1] ),
                dimensions[2] / (sampleCount[2] ));
            let translate = new THREE.Matrix4().makeTranslation(origin[0], origin[1], origin[2]);
            let fromFrac = new THREE.Matrix4().set(
                basis.x[0], basis.y[0], basis.z[0], 0.0,
                0.0, basis.y[1], basis.z[1], 0.0,
                0.0, 0.0, basis.z[2], 0.0,
                0.0, 0.0, 0.0, 1.0);

            //var toFrac = new LiteMol.Visualization.THREE.Matrix4().getInverse(fromFrac);
            let matrix = fromFrac.multiply(translate).multiply(scale);

            ic.mapData.matrix2 = matrix;

            ic.mapData.type2 = type;
            ic.mapData.sigma2 = sigma;

            // 'fofc'
            density = oneDensity;
            sampleCount = density.box.sampleCount;
            header = {xExtent: sampleCount[0], yExtent: sampleCount[1], zExtent: sampleCount[2], mean: density.valuesInfo.mean, sigma: density.valuesInfo.sigma};
            ic.mapData.header = header;

            ic.mapData.data = density.data;

            origin = density.box.origin;
            dimensions = density.box.dimensions;
            basis = density.spacegroup.basis;
            scale = new THREE.Matrix4().makeScale(
                dimensions[0] / (sampleCount[0] ),
                dimensions[1] / (sampleCount[1] ),
                dimensions[2] / (sampleCount[2] ));
            translate = new THREE.Matrix4().makeTranslation(origin[0], origin[1], origin[2]);
            fromFrac = new THREE.Matrix4().set(
                basis.x[0], basis.y[0], basis.z[0], 0.0,
                0.0, basis.y[1], basis.z[1], 0.0,
                0.0, 0.0, basis.z[2], 0.0,
                0.0, 0.0, 0.0, 1.0);
            //var toFrac = new LiteMol.Visualization.THREE.Matrix4().getInverse(fromFrac);
            matrix = fromFrac.multiply(translate).multiply(scale);
            ic.mapData.matrix = matrix;

            ic.mapData.type = type;
            ic.mapData.sigma = sigma;
        }
        else if(type == 'em') {
            let density = this.getChannel(cif, 'EM');

            let sampleCount = density.box.sampleCount;
            let header = {xExtent: sampleCount[0], yExtent: sampleCount[1], zExtent: sampleCount[2], max: density.valuesInfo.max, min: density.valuesInfo.min};
            ic.mapData.headerEm = header;

            ic.mapData.dataEm = density.data;

            let origin = density.box.origin;
            let dimensions = density.box.dimensions;
            let basis = density.spacegroup.basis;
            let scale = new THREE.Matrix4().makeScale(
                dimensions[0] / (sampleCount[0] ),
                dimensions[1] / (sampleCount[1] ),
                dimensions[2] / (sampleCount[2] ));
            let translate = new THREE.Matrix4().makeTranslation(origin[0], origin[1], origin[2]);
            let fromFrac = new THREE.Matrix4().set(
                basis.x[0], basis.y[0], basis.z[0], 0.0,
                0.0, basis.y[1], basis.z[1], 0.0,
                0.0, 0.0, basis.z[2], 0.0,
                0.0, 0.0, 0.0, 1.0);
            //var toFrac = new LiteMol.Visualization.THREE.Matrix4().getInverse(fromFrac);
            let matrix = fromFrac.multiply(translate).multiply(scale);
            ic.mapData.matrixEm = matrix;

            ic.mapData.typeEm = type;
            ic.mapData.sigmaEm = sigma;
        }
    }

    getChannel(data, name) { let ic = this.icn3d; ic.icn3dui;
        //var block = data.dataBlocks.filter(b => b.header === name)[0];
        //var block = data.dataBlocks.filter(b => b.id === name)[0];

        let jsonData = data.toJSON();

        let block;
        for(let i = 0, il = jsonData.length; i < il; ++i) {
            if(jsonData[i].id == name) block = data.dataBlocks[i];
        }

        let density = this.CIFParse(block);

        return density;
    }

    CIFParse(block) { let ic = this.icn3d; ic.icn3dui;
        let info = block.getCategory('_volume_data_3d_info');

        if (!info) {
            conole.log('_volume_data_3d_info category is missing.');
            return undefined;
        }
        if (!block.getCategory('_volume_data_3d')) {
            conole.log('_volume_data_3d category is missing.');
            return undefined;
        }

        function getVector3(name) {
            let ret = [0, 0, 0];
            for (let i = 0; i < 3; i++) {
                ret[i] = info.getColumn(name + '[' + i + ']').getFloat(0);
            }
            return ret;
        }

        function getNum(name) { return info.getColumn(name).getFloat(0); }

        let header = {
            name: info.getColumn('name').getString(0),
            axisOrder: getVector3('axis_order'),

            origin: getVector3('origin'),
            dimensions: getVector3('dimensions'),

            sampleCount: getVector3('sample_count'),

            spacegroupNumber: getNum('spacegroup_number') | 0,
            cellSize: getVector3('spacegroup_cell_size'),
            cellAngles: getVector3('spacegroup_cell_angles'),

            mean: getNum('mean_sampled'),
            sigma: getNum('sigma_sampled')
        };

        let indices = [0, 0, 0];
        indices[header.axisOrder[0]] = 0;
        indices[header.axisOrder[1]] = 1;
        indices[header.axisOrder[2]] = 2;

        function normalizeOrder(xs) {
            return [xs[indices[0]], xs[indices[1]], xs[indices[2]]];
        }

        function readValues(col, xyzSampleCount, sampleCount, axisIndices) {
            let data = new Float32Array(xyzSampleCount[0] * xyzSampleCount[1] * xyzSampleCount[2]);
            let coord = [0, 0, 0];
            let iX = axisIndices[0], iY = axisIndices[1], iZ = axisIndices[2];
            let mX = sampleCount[0], mY = sampleCount[1], mZ = sampleCount[2];


            xyzSampleCount[0];
            xyzSampleCount[0] * xyzSampleCount[1];

            let zSize = xyzSampleCount[2];
            let yzSize = xyzSampleCount[1] * xyzSampleCount[2];

            let offset = 0;
            let min = col.getFloat(0), max = min;

            for (let cZ = 0; cZ < mZ; cZ++) {
                coord[2] = cZ;
                for (let cY = 0; cY < mY; cY++) {
                    coord[1] = cY;
                    for (let cX = 0; cX < mX; cX++) {
                        coord[0] = cX;
                        let v = col.getFloat(offset);
                        offset += 1;
                        //data[coord[iX] + coord[iY] * xSize + coord[iZ] * xySize] = v;
                        data[coord[iZ] + coord[iY] * zSize + coord[iX] * yzSize] = v;
                        if (v < min) min = v;
                        else if (v > max) max = v;
                    }
                }
            }

            return { data: data, min: min, max: max };
        }

        function createSpacegroup(number, size, angles) {
            let alpha = (Math.PI / 180.0) * angles[0], beta = (Math.PI / 180.0) * angles[1], gamma = (Math.PI / 180.0) * angles[2];
            let xScale = size[0], yScale = size[1], zScale = size[2];

            let z1 = Math.cos(beta),
                  z2 = (Math.cos(alpha) - Math.cos(beta) * Math.cos(gamma)) / Math.sin(gamma),
                  z3 = Math.sqrt(1.0 - z1 * z1 - z2 * z2);

            let x = [xScale, 0.0, 0.0];
            let y = [Math.cos(gamma) * yScale, Math.sin(gamma) * yScale, 0.0];
            let z = [z1 * zScale, z2 * zScale, z3 * zScale];

            return {
                number: number,
                size: size,
                angles: angles,
                basis: { x: x, y: y, z: z }
            };
        }

        let sampleCount = normalizeOrder(header.sampleCount);

        let rawData = readValues(block.getCategory('_volume_data_3d').getColumn('values'), sampleCount, header.sampleCount, indices);
        //var field = new Field3DZYX(rawData.data, sampleCount);

        let data = {
            name: header.name,
            spacegroup: createSpacegroup(header.spacegroupNumber, header.cellSize, header.cellAngles),
            box: {
                origin: normalizeOrder(header.origin),
                dimensions: normalizeOrder(header.dimensions),
                sampleCount: sampleCount
            },
            //data: field,
            data: rawData.data,
            valuesInfo: { min: rawData.min, max: rawData.max, mean: header.mean, sigma: header.sigma }
        };

        return data;
    }

    BinaryParse(data) { let ic = this.icn3d; ic.icn3dui;
    //    let minVersion = [0, 3];
    //    try {
            let array = new Uint8Array(data);

            let unpacked = this.MessagePackParse({
                        buffer: array,
                        offset: 0,
                        dataView: new DataView(array.buffer)
            });

            let DataBlock = (function () {
                function DataBlock(data) {
                    this.additionalData = {};
                    this.header = data.header;
                    this.categoryList = data.categories.map(function (c) { return new Category(c); });
                    this.categoryMap = new Map();
                    for (let _i = 0, _a = this.categoryList; _i < _a.length; _i++) {
                        let c = _a[_i];
                        this.categoryMap.set(c.name, c);
                    }
                }
                Object.defineProperty(DataBlock.prototype, "categories", {
                    get: function () { return this.categoryList; },
                    enumerable: true,
                    configurable: true
                });
                DataBlock.prototype.getCategory = function (name) { return this.categoryMap.get(name); };
                DataBlock.prototype.toJSON = function () {
                    return {
                        id: this.header,
                        categories: this.categoryList.map(function (c) { return c.toJSON(); }),
                        additionalData: this.additionalData
                    };
                };
                return DataBlock;
            }());

            let Category = (function () {
                function Category(data) {
                    this.name = data.name;
                    this.columnCount = data.columns.length;
                    this.rowCount = data.rowCount;
                    this.columnNameList = [];
                    this.encodedColumns = new Map();
                    for (let _i = 0, _a = data.columns; _i < _a.length; _i++) {
                        let c = _a[_i];
                        this.encodedColumns.set(c.name, c);
                        this.columnNameList.push(c.name);
                    }
                }
                Object.defineProperty(Category.prototype, "columnNames", {
                    get: function () { return this.columnNameList; },
                    enumerable: true,
                    configurable: true
                });

                let _UndefinedColumn = (function () {
                    function _UndefinedColumn() {
                        this.isDefined = false;
                    }
                    _UndefinedColumn.prototype.getString = function (row) { return null; };
                    _UndefinedColumn.prototype.getInteger = function (row) { return 0; };
                    _UndefinedColumn.prototype.getFloat = function (row) { return 0.0; };
                    _UndefinedColumn.prototype.getValuePresence = function (row) { return 1 /* NotSpecified */; };
                    _UndefinedColumn.prototype.areValuesEqual = function (rowA, rowB) { return true; };
                    _UndefinedColumn.prototype.stringEquals = function (row, value) { return value === null; };
                    return _UndefinedColumn;
                }());

                Category.prototype.getColumn = function (name) {
                    let w = this.encodedColumns.get(name);
                    if (w)
                        return wrapColumn(w);
                    return _UndefinedColumn;
                };
                Category.prototype.toJSON = function () {
                    let _this = this;
                    let rows = [];
                    let columns = this.columnNameList.map(function (name) { return ({ name: name, column: _this.getColumn(name) }); });
                    for (let i = 0; i < this.rowCount; i++) {
                        let item = {};
                        for (let _i = 0, columns_1 = columns; _i < columns_1.length; _i++) {
                            let c = columns_1[_i];
                            let d = c.column.getValuePresence(i);
                            if (d === 0 /* Present */)
                                item[c.name] = c.column.getString(i);
                            else if (d === 1 /* NotSpecified */)
                                item[c.name] = '.';
                            else
                                item[c.name] = '?';
                        }
                        rows[i] = item;
                    }
                    return { name: this.name, columns: this.columnNames, rows: rows };
                };
                return Category;
            }());

            function getIntArray(type, size) {
                switch (type) {
                    case 1 /* Int8 */: return new Int8Array(size);
                    case 2 /* Int16 */: return new Int16Array(size);
                    case 3 /* Int32 */: return new Int32Array(size);
                    case 4 /* Uint8 */: return new Uint8Array(size);
                    case 5 /* Uint16 */: return new Uint16Array(size);
                    case 6 /* Uint32 */: return new Uint32Array(size);
                    default: throw new Error('Unsupported integer data type.');
                }
            }
            function getFloatArray(type, size) {
                switch (type) {
                    case 32 /* Float32 */: return new Float32Array(size);
                    case 33 /* Float64 */: return new Float64Array(size);
                    default: throw new Error('Unsupported floating data type.');
                }
            }
            // http://stackoverflow.com/questions/7869752/javascript-typed-arrays-and-endianness
            let isLittleEndian = (function () {
                let arrayBuffer = new ArrayBuffer(2);
                let uint8Array = new Uint8Array(arrayBuffer);
                let uint16array = new Uint16Array(arrayBuffer);
                uint8Array[0] = 0xAA;
                uint8Array[1] = 0xBB;
                if (uint16array[0] === 0xBBAA)
                    return true;
                return false;
            })();
            function int8(data) { return new Int8Array(data.buffer, data.byteOffset); }
            function flipByteOrder(data, bytes) {
                let buffer = new ArrayBuffer(data.length);
                let ret = new Uint8Array(buffer);
                for (let i = 0, n = data.length; i < n; i += bytes) {
                    for (let j = 0; j < bytes; j++) {
                        ret[i + bytes - j - 1] = data[i + j];
                    }
                }
                return buffer;
            }
            function view(data, byteSize, c) {
                if (isLittleEndian)
                    return new c(data.buffer);
                return new c(flipByteOrder(data, byteSize));
            }
            function int16(data) { return view(data, 2, Int16Array); }
            function uint16(data) { return view(data, 2, Uint16Array); }
            function int32(data) { return view(data, 4, Int32Array); }
            function uint32(data) { return view(data, 4, Uint32Array); }
            function float32(data) { return view(data, 4, Float32Array); }
            function float64(data) { return view(data, 8, Float64Array); }
            function fixedPoint(data, encoding) {
                let n = data.length;
                let output = getFloatArray(encoding.srcType, n);
                let f = 1 / encoding.factor;
                for (let i = 0; i < n; i++) {
                    output[i] = f * data[i];
                }
                return output;
            }
            function intervalQuantization(data, encoding) {
                let n = data.length;
                let output = getFloatArray(encoding.srcType, n);
                let delta = (encoding.max - encoding.min) / (encoding.numSteps - 1);
                let min = encoding.min;
                for (let i = 0; i < n; i++) {
                    output[i] = min + delta * data[i];
                }
                return output;
            }
            function runLength(data, encoding) {
                let output = getIntArray(encoding.srcType, encoding.srcSize);
                let dataOffset = 0;
                for (let i = 0, il = data.length; i < il; i += 2) {
                    let value = data[i]; // value to be repeated
                    let length_7 = data[i + 1]; // number of repeats
                    for (let j = 0; j < length_7; ++j) {
                        output[dataOffset++] = value;
                    }
                }
                return output;
            }
            function delta(data, encoding) {
                let n = data.length;
                let output = getIntArray(encoding.srcType, n);
                if (!n)
                    return output;
                output[0] = data[0] + (encoding.origin | 0);
                for (let i = 1; i < n; ++i) {
                    output[i] = data[i] + output[i - 1];
                }
                return output;
            }
            function integerPackingSigned(data, encoding) {
                let upperLimit = encoding.byteCount === 1 ? 0x7F : 0x7FFF;
                let lowerLimit = -upperLimit - 1;
                let n = data.length;
                let output = new Int32Array(encoding.srcSize);
                let i = 0;
                let j = 0;
                while (i < n) {
                    let value = 0, t = data[i];
                    while (t === upperLimit || t === lowerLimit) {
                        value += t;
                        i++;
                        t = data[i];
                    }
                    value += t;
                    output[j] = value;
                    i++;
                    j++;
                }
                return output;
            }
            function integerPackingUnsigned(data, encoding) {
                let upperLimit = encoding.byteCount === 1 ? 0xFF : 0xFFFF;
                let n = data.length;
                let output = new Int32Array(encoding.srcSize);
                let i = 0;
                let j = 0;
                while (i < n) {
                    let value = 0, t = data[i];
                    while (t === upperLimit) {
                        value += t;
                        i++;
                        t = data[i];
                    }
                    value += t;
                    output[j] = value;
                    i++;
                    j++;
                }
                return output;
            }
            function integerPacking(data, encoding) {
                return encoding.isUnsigned ? integerPackingUnsigned(data, encoding) : integerPackingSigned(data, encoding);
            }
            function stringArray(data, encoding) {
                let str = encoding.stringData;
                let offsets = decode({ encoding: encoding.offsetEncoding, data: encoding.offsets });
                let indices = decode({ encoding: encoding.dataEncoding, data: data });
                let cache = Object.create(null);
                let result = new Array(indices.length);
                let offset = 0;
                for (let _i = 0, indices_1 = indices; _i < indices_1.length; _i++) {
                    let i = indices_1[_i];
                    if (i < 0) {
                        result[offset++] = null;
                        continue;
                    }
                    let v = cache[i];
                    if (v === void 0) {
                        v = str.substring(offsets[i], offsets[i + 1]);
                        cache[i] = v;
                    }
                    result[offset++] = v;
                }
                return result;
            }

            function decodeStep(data, encoding) {
                switch (encoding.kind) {
                    case 'ByteArray': {
                        switch (encoding.type) {
                            case 4 /* Uint8 */: return data;
                            case 1 /* Int8 */: return int8(data);
                            case 2 /* Int16 */: return int16(data);
                            case 5 /* Uint16 */: return uint16(data);
                            case 3 /* Int32 */: return int32(data);
                            case 6 /* Uint32 */: return uint32(data);
                            case 32 /* Float32 */: return float32(data);
                            case 33 /* Float64 */: return float64(data);
                            default: throw new Error('Unsupported ByteArray type.');
                        }
                    }
                    case 'FixedPoint': return fixedPoint(data, encoding);
                    case 'IntervalQuantization': return intervalQuantization(data, encoding);
                    case 'RunLength': return runLength(data, encoding);
                    case 'Delta': return delta(data, encoding);
                    case 'IntegerPacking': return integerPacking(data, encoding);
                    case 'StringArray': return stringArray(data, encoding);
                }
            }

            function decode(data) {
                let current = data.data;
                for (let i = data.encoding.length - 1; i >= 0; i--) {
                    current = decodeStep(current, data.encoding[i]);
                }
                return current;
            }

            function wrapColumn(column) {
                if (!column.data.data)
                    return _UndefinedColumn;
                let data = decode(column.data);
                let mask = void 0;
                if (column.mask)
                    mask = decode(column.mask);
                if (data.buffer && data.byteLength && data.BYTES_PER_ELEMENT) {
                    return mask ? new MaskedNumericColumn(data, mask) : new NumericColumn(data);
                }
                return mask ? new MaskedStringColumn(data, mask) : new StringColumn(data);
            }
            //var fastParseInt = CIFTools.me.utilsCls.FastNumberParsers.parseInt;
            function fastParseInt(str, start, end) {
                let ret = 0, neg = 1;
                if (str.charCodeAt(start) === 45 /* - */) {
                    neg = -1;
                    start++;
                }
                for (; start < end; start++) {
                    let c = str.charCodeAt(start) - 48;
                    if (c > 9 || c < 0)
                        return (neg * ret) | 0;
                    else
                        ret = (10 * ret + c) | 0;
                }
                return neg * ret;
            }
            //var fastParseFloat = CIFTools.me.utilsCls.FastNumberParsers.parseFloat;
            function fastParseFloat(str, start, end) {
                let neg = 1.0, ret = 0.0, point = 0.0, div = 1.0;
                if (str.charCodeAt(start) === 45) {
                    neg = -1.0;
                    ++start;
                }
                while (start < end) {
                    let c = str.charCodeAt(start) - 48;
                    if (c >= 0 && c < 10) {
                        ret = ret * 10 + c;
                        ++start;
                    }
                    else if (c === -2) {
                        ++start;
                        while (start < end) {
                            c = str.charCodeAt(start) - 48;
                            if (c >= 0 && c < 10) {
                                point = 10.0 * point + c;
                                div = 10.0 * div;
                                ++start;
                            }
                            else if (c === 53 || c === 21) {
                                return parseScientific(neg * (ret + point / div), str, start + 1, end);
                            }
                            else {
                                return neg * (ret + point / div);
                            }
                        }
                        return neg * (ret + point / div);
                    }
                    else if (c === 53 || c === 21) {
                        return parseScientific(neg * ret, str, start + 1, end);
                    }
                    else
                        break;
                }
                return neg * ret;
            }

            let NumericColumn = (function () {
                function NumericColumn(data) {
                    this.data = data;
                    this.isDefined = true;
                }
                NumericColumn.prototype.getString = function (row) { return "" + this.data[row]; };
                NumericColumn.prototype.getInteger = function (row) { return this.data[row] | 0; };
                NumericColumn.prototype.getFloat = function (row) { return 1.0 * this.data[row]; };
                NumericColumn.prototype.stringEquals = function (row, value) { return this.data[row] === fastParseFloat(value, 0, value.length); };
                NumericColumn.prototype.areValuesEqual = function (rowA, rowB) { return this.data[rowA] === this.data[rowB]; };
                NumericColumn.prototype.getValuePresence = function (row) { return 0 /* Present */; };
                return NumericColumn;
            }());
            let MaskedNumericColumn = (function () {
                function MaskedNumericColumn(data, mask) {
                    this.data = data;
                    this.mask = mask;
                    this.isDefined = true;
                }
                MaskedNumericColumn.prototype.getString = function (row) { return this.mask[row] === 0 /* Present */ ? "" + this.data[row] : null; };
                MaskedNumericColumn.prototype.getInteger = function (row) { return this.mask[row] === 0 /* Present */ ? this.data[row] : 0; };
                MaskedNumericColumn.prototype.getFloat = function (row) { return this.mask[row] === 0 /* Present */ ? this.data[row] : 0; };
                MaskedNumericColumn.prototype.stringEquals = function (row, value) { return this.mask[row] === 0 /* Present */ ? this.data[row] === fastParseFloat(value, 0, value.length) : value === null || value === void 0; };
                MaskedNumericColumn.prototype.areValuesEqual = function (rowA, rowB) { return this.data[rowA] === this.data[rowB]; };
                MaskedNumericColumn.prototype.getValuePresence = function (row) { return this.mask[row]; };
                return MaskedNumericColumn;
            }());
            let StringColumn = (function () {
                function StringColumn(data) {
                    this.data = data;
                    this.isDefined = true;
                }
                StringColumn.prototype.getString = function (row) { return this.data[row]; };
                StringColumn.prototype.getInteger = function (row) { let v = this.data[row]; return fastParseInt(v, 0, v.length); };
                StringColumn.prototype.getFloat = function (row) { let v = this.data[row]; return fastParseFloat(v, 0, v.length); };
                StringColumn.prototype.stringEquals = function (row, value) { return this.data[row] === value; };
                StringColumn.prototype.areValuesEqual = function (rowA, rowB) { return this.data[rowA] === this.data[rowB]; };
                StringColumn.prototype.getValuePresence = function (row) { return 0 /* Present */; };
                return StringColumn;
            }());
            let MaskedStringColumn = (function () {
                function MaskedStringColumn(data, mask) {
                    this.data = data;
                    this.mask = mask;
                    this.isDefined = true;
                }
                MaskedStringColumn.prototype.getString = function (row) { return this.mask[row] === 0 /* Present */ ? this.data[row] : null; };
                MaskedStringColumn.prototype.getInteger = function (row) { if (this.mask[row] !== 0 /* Present */)
                    return 0; let v = this.data[row]; return fastParseInt(v || '', 0, (v || '').length); };
                MaskedStringColumn.prototype.getFloat = function (row) { if (this.mask[row] !== 0 /* Present */)
                    return 0; let v = this.data[row]; return fastParseFloat(v || '', 0, (v || '').length); };
                MaskedStringColumn.prototype.stringEquals = function (row, value) { return this.data[row] === value; };
                MaskedStringColumn.prototype.areValuesEqual = function (rowA, rowB) { return this.data[rowA] === this.data[rowB]; };
                MaskedStringColumn.prototype.getValuePresence = function (row) { return this.mask[row]; };
                return MaskedStringColumn;
            }());

            let File = (function () {
                        function File(data) {
                            this.dataBlocks = data.dataBlocks.map(function (b) { return new DataBlock(b); });
                        }
                        File.prototype.toJSON = function () {
                            return this.dataBlocks.map(function (b) { return b.toJSON(); });
                        };
                        return File;
            }());

            let file = new File(unpacked);
            return file;

    //    }
    //    catch (e) {
    //        return CIFTools.ParserResult.error('' + e);
    //    }
    }

    MessagePackParse(state) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;

        /*
         * Adapted from https://github.com/rcsb/mmtf-javascript
         * by Alexander Rose <alexander.rose@weirdbyte.de>, MIT License, Copyright (c) 2016
         */
        /**
         * decode all key-value pairs of a map into an object
         * @param  {Integer} length - number of key-value pairs
         * @return {Object} decoded map
         */
        function map(state, length) {
            let value = {};
            for (let i = 0; i < length; i++) {
                let key = thisClass.MessagePackParse(state);
                value[key] = thisClass.MessagePackParse(state);
            }
            return value;
        }
        /**
         * decode binary array
         * @param  {Integer} length - number of elements in the array
         * @return {Uint8Array} decoded array
         */
        function bin(state, length) {
            // This approach to binary parsing wastes a bit of memory to trade for speed compared to:
            //
            //   let value = buffer.subarray(offset, offset + length); //new Uint8Array(buffer.buffer, offset, length);
            //
            // It turns out that using the view created by subarray probably uses DataView
            // in the background, which causes the element access to be several times slower
            // than creating the new byte array.
            let value = new Uint8Array(length);
            let o = state.offset;
            for (let i = 0; i < length; i++)
                value[i] = state.buffer[i + o];
            state.offset += length;
            return value;
        }
        /**
             * decode array
             * @param  {Integer} length - number of array elements
             * @return {Array} decoded array
             */
        function array(state, length) {
            let value = new Array(length);
            for (let i = 0; i < length; i++) {
                value[i] = thisClass.MessagePackParse(state);
            }
            return value;
        }

        /**
         * decode string
         * @param  {Integer} length - number string characters
         * @return {String} decoded string
         */
        function str(state, length) {
            let value = utf8Read(state.buffer, state.offset, length);
            state.offset += length;
            return value;
        }

        let __chars = function () {
            let data = [];
            for (let i = 0; i < 1024; i++)
                data[i] = String.fromCharCode(i);
            return data;
        }();

        function utf8Read(data, offset, length) {
            let chars = __chars;
            let str = void 0, chunk = [], chunkSize = 512, chunkOffset = 0;
            for (let i = offset, end = offset + length; i < end; i++) {
                let byte = data[i];
                // One byte character
                if ((byte & 0x80) === 0x00) {
                    chunk[chunkOffset++] = chars[byte];
                }
                else if ((byte & 0xe0) === 0xc0) {
                    chunk[chunkOffset++] = chars[((byte & 0x0f) << 6) | (data[++i] & 0x3f)];
                }
                else if ((byte & 0xf0) === 0xe0) {
                    chunk[chunkOffset++] = String.fromCharCode(((byte & 0x0f) << 12) |
                        ((data[++i] & 0x3f) << 6) |
                        ((data[++i] & 0x3f) << 0));
                }
                else if ((byte & 0xf8) === 0xf0) {
                    chunk[chunkOffset++] = String.fromCharCode(((byte & 0x07) << 18) |
                        ((data[++i] & 0x3f) << 12) |
                        ((data[++i] & 0x3f) << 6) |
                        ((data[++i] & 0x3f) << 0));
                }
                else
                    throwError("Invalid byte " + byte.toString(16));
                if (chunkOffset === chunkSize) {
                    str = str || [];
                    str[str.length] = chunk.join('');
                    chunkOffset = 0;
                }
            }
            if (!str)
                return chunk.slice(0, chunkOffset).join('');
            if (chunkOffset > 0) {
                str[str.length] = chunk.slice(0, chunkOffset).join('');
            }
            return str.join('');
        }

        let type = state.buffer[state.offset];

        let value, length;
        // Positive FixInt
        if ((type & 0x80) === 0x00) {
            state.offset++;
            return type;
        }
        // FixMap
        if ((type & 0xf0) === 0x80) {
            length = type & 0x0f;
            state.offset++;
            return map(state, length);
        }
        // FixArray
        if ((type & 0xf0) === 0x90) {
            length = type & 0x0f;
            state.offset++;
            return array(state, length);
        }
        // FixStr
        if ((type & 0xe0) === 0xa0) {
            length = type & 0x1f;
            state.offset++;
            return str(state, length);
        }
        // Negative FixInt
        if ((type & 0xe0) === 0xe0) {
            value = state.dataView.getInt8(state.offset);
            state.offset++;
            return value;
        }
        switch (type) {
            // nil
            case 0xc0:
                state.offset++;
                return null;
            // false
            case 0xc2:
                state.offset++;
                return false;
            // true
            case 0xc3:
                state.offset++;
                return true;
            // bin 8
            case 0xc4:
                length = state.dataView.getUint8(state.offset + 1);
                state.offset += 2;
                return bin(state, length);
            // bin 16
            case 0xc5:
                length = state.dataView.getUint16(state.offset + 1);
                state.offset += 3;
                return bin(state, length);
            // bin 32
            case 0xc6:
                length = state.dataView.getUint32(state.offset + 1);
                state.offset += 5;
                return bin(state, length);
            // float 32
            case 0xca:
                value = state.dataView.getFloat32(state.offset + 1);
                state.offset += 5;
                return value;
            // float 64
            case 0xcb:
                value = state.dataView.getFloat64(state.offset + 1);
                state.offset += 9;
                return value;
            // uint8
            case 0xcc:
                value = state.buffer[state.offset + 1];
                state.offset += 2;
                return value;
            // uint 16
            case 0xcd:
                value = state.dataView.getUint16(state.offset + 1);
                state.offset += 3;
                return value;
            // uint 32
            case 0xce:
                value = state.dataView.getUint32(state.offset + 1);
                state.offset += 5;
                return value;
            // int 8
            case 0xd0:
                value = state.dataView.getInt8(state.offset + 1);
                state.offset += 2;
                return value;
            // int 16
            case 0xd1:
                value = state.dataView.getInt16(state.offset + 1);
                state.offset += 3;
                return value;
            // int 32
            case 0xd2:
                value = state.dataView.getInt32(state.offset + 1);
                state.offset += 5;
                return value;
            // str 8
            case 0xd9:
                length = state.dataView.getUint8(state.offset + 1);
                state.offset += 2;
                return str(state, length);
            // str 16
            case 0xda:
                length = state.dataView.getUint16(state.offset + 1);
                state.offset += 3;
                return str(state, length);
            // str 32
            case 0xdb:
                length = state.dataView.getUint32(state.offset + 1);
                state.offset += 5;
                return str(state, length);
            // array 16
            case 0xdc:
                length = state.dataView.getUint16(state.offset + 1);
                state.offset += 3;
                return array(state, length);
            // array 32
            case 0xdd:
                length = state.dataView.getUint32(state.offset + 1);
                state.offset += 5;
                return array(state, length);
            // map 16:
            case 0xde:
                length = state.dataView.getUint16(state.offset + 1);
                state.offset += 3;
                return map(state, length);
            // map 32
            case 0xdf:
                length = state.dataView.getUint32(state.offset + 1);
                state.offset += 5;
                return map(state, length);
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ParserUtils {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    alignCoords(coordsFrom, coordsTo, secondStruct, bKeepSeq, chainid_t, chainid, chainIndex, bChainAlign) { let ic = this.icn3d, me = ic.icn3dui;
      //var n = coordsFrom.length;
      let n =(coordsFrom.length < coordsTo.length) ? coordsFrom.length : coordsTo.length;

      let hAtoms = {}, rmsd;

      if(n < 4) alert("Please select at least four residues in each structure...");
      if(n >= 4) {
          ic.rmsd_suprTmp = me.rmsdSuprCls.getRmsdSuprCls(coordsFrom, coordsTo, n);

          // apply matrix for each atom
          if(ic.rmsd_suprTmp.rot !== undefined) {
              let rot = ic.rmsd_suprTmp.rot;
              if(rot[0] === null) alert("Please select more residues in each structure...");

              let centerFrom = ic.rmsd_suprTmp.trans1;
              let centerTo = ic.rmsd_suprTmp.trans2;
              rmsd = ic.rmsd_suprTmp.rmsd;

              if(rmsd) {
                  me.htmlCls.clickMenuCls.setLogCmd("realignment RMSD: " + rmsd.toPrecision(4), false);
                  let html = "<br><b>Realignment RMSD</b>: " + rmsd.toPrecision(4) + " &#8491;<br><br>";
                  if(ic.bAfMem) {
                    //if(window.dialog) window.dialog.dialog( "close" );
                    html += me.utilsCls.getMemDesc();
                  }
                  $("#" + ic.pre + "dl_rmsd").html(html);
                  if(!me.cfg.bSidebyside) me.htmlCls.dialogCls.openDlg('dl_rmsd', 'Realignment RMSD');
              }

              let chainDone = {};
              for(let i = 0, il = ic.structures[secondStruct].length; i < il; ++i) {
                  let chainidTmp = ic.structures[secondStruct][i];
                  // some chains were pushed twice in some cases
                  if(chainDone.hasOwnProperty(chainidTmp)) continue;

                  for(let j in ic.chains[chainidTmp]) {
                    let atom = ic.atoms[j];
                    atom.coord = ic.surfaceCls.transformMemPro(atom.coord, rot, centerFrom, centerTo);
                  }

                  chainDone[chainidTmp] = 1;
              }

              ic.bRealign = true;

              if(!bChainAlign) {
                ic.opts['color'] = 'identity';
                ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);
              }

/*
              //if(!bKeepSeq) ic.setSeqAlignCls.setSeqAlignForRealign(chainid_t, chainid, chainIndex);
              ic.setSeqAlignCls.setSeqAlignForRealign(chainid_t, chainid, chainIndex);
         
              let bShowHighlight = false;
              let seqObj = me.htmlCls.alignSeqCls.getAlignSequencesAnnotations(Object.keys(ic.alnChains), undefined, undefined, bShowHighlight);

              let oriHtml =(chainIndex === 1) ? '' : $("#" + ic.pre + "dl_sequence2").html();
              $("#" + ic.pre + "dl_sequence2").html(oriHtml + seqObj.sequencesHtml);
              $("#" + ic.pre + "dl_sequence2").width(me.htmlCls.RESIDUE_WIDTH * seqObj.maxSeqCnt + 200);

              me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');
*/
              // assign ic.qt_start_end
              if(!ic.qt_start_end) ic.qt_start_end = [];

              let curr_qt_start_end = this.getQtStartEndFromRealignResid(chainid_t, chainid);
              ic.qt_start_end.push(curr_qt_start_end);

              hAtoms = ic.hAtoms;
          }
      }

      return {hAtoms: hAtoms, rmsd: rmsd};
    }

    getQtStartEndFromRealignResid(chainid_t, chainid_q) { let ic = this.icn3d; ic.icn3dui;
        chainid_t.substr(0, chainid_t.indexOf('_')); 
        chainid_q.substr(0, chainid_q.indexOf('_')); 

        let qt_start_end = [];

        let resi2pos_t = {};
        for(let i = 0, il = ic.chainsSeq[chainid_t].length; i < il; ++i) {
            let resi = ic.chainsSeq[chainid_t][i].resi;
            resi2pos_t[resi] = i + 1;
        }

        let resi2pos_q = {};
        for(let i = 0, il = ic.chainsSeq[chainid_q].length; i < il; ++i) {
            let resi = ic.chainsSeq[chainid_q][i].resi;
            resi2pos_q[resi] = i + 1;
        }

        for(let i = 0, il = ic.realignResid[chainid_t].length; i < il && i < ic.realignResid[chainid_q].length; ++i) {
            let resid_t = ic.realignResid[chainid_t][i].resid;
            let pos_t = resid_t.lastIndexOf('_');
            let resi_t = parseInt(resid_t.substr(pos_t + 1));
            let resid_q = ic.realignResid[chainid_q][i].resid;
            let pos_q = resid_q.lastIndexOf('_');
            let resi_q = parseInt(resid_q.substr(pos_q + 1));

            let resiPos_t = resi2pos_t[resi_t];
            let resiPos_q = resi2pos_q[resi_q];

            qt_start_end.push({"q_start": resiPos_q, "q_end": resiPos_q, "t_start": resiPos_t, "t_end": resiPos_t}); 
        }

        return qt_start_end;
    }

    getMissingResidues(seqArray, type, chainid) { let ic = this.icn3d, me = ic.icn3dui;
        ic.chainsSeq[chainid] = [];

        // find the offset of MMDB sequence
        let offset = 0;
        if(type === 'mmdbid' || type === 'align') {
            for(let i = 0, il = seqArray.length; i < il; ++i) {
                if(seqArray[i][0] != 0) {
                    offset = seqArray[i][0] - (i + 1);
                    break;
                }
            }
        }

        let prevResi = 0;
        for(let i = 0, il = seqArray.length; i < il; ++i) {
            let seqName, resiPos;
            // mmdbid: ["0","R","ARG"],["502","V","VAL"]; mmcifid: [1, "ARG"]; align: ["0","R","ARG"] //align: [1, "0","R","ARG"]
            if(type === 'mmdbid') {
                seqName = seqArray[i][1];
                resiPos = 0;
            }
            else if(type === 'mmcifid') {
                seqName = seqArray[i][1];
                seqName = me.utilsCls.residueName2Abbr(seqName);
                resiPos = 0;
            }
            else if(type === 'align') {
                seqName = seqArray[i][1];
                resiPos = 0;
            }

            // fixe some missing residue names such as residue 6 in 5C1M_A
            if(seqName === '') {
                seqName = 'x';
            }

            let resObject = {};

            if(!ic.bUsePdbNum) {
                resObject.resi = i + 1;
            }
            else {
                //let offset =(ic.chainid2offset[chainid]) ? ic.chainid2offset[chainid] : 0;
                
                if(type === 'mmdbid' || type === 'align') {
                    resObject.resi =(seqArray[i][resiPos] == '0') ? i + 1 + offset : seqArray[i][resiPos];
                }
                else {
                    resObject.resi =(seqArray[i][resiPos] == '0') ? parseInt(prevResi) + 1 : seqArray[i][resiPos];
                }
            }

            //resObject.resi =(seqArray[i][resiPos] == '0') ? i + 1 + offset : seqArray[i][resiPos];

            resObject.name = (type === 'align') ? seqName.toLowerCase() : seqName;

            ic.chainsSeq[chainid].push(resObject);

            prevResi = resObject.resi;
        }
    }

    //Generate the 2D interaction diagram for the structure "mmdbid", which could be PDB ID. The 2D
    //interaction diagram is only available when the input is NCBI MMDB ID, i.e., the URL is something like "&mmdbid=...".
    async set2DDiagramsForAlign(mmdbid1, mmdbid2) { let ic = this.icn3d, me = ic.icn3dui;
        me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');

    ///       mmdbid1 = mmdbid1.substr(0, 4);
    ///       mmdbid2 = mmdbid2.substr(0, 4);

        let url1 = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&uid="+mmdbid1+"&intrac=1";
        let url2 = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&uid="+mmdbid2+"&intrac=1";

        if(me.cfg.inpara !== undefined) {
            url1 += me.cfg.inpara;
            url2 += me.cfg.inpara;
        }

        let prms1 = me.getAjaxPromise(url1, 'jsonp');
        let prms2 = me.getAjaxPromise(url2, 'jsonp');

        let allPromise = Promise.allSettled([prms1, prms2]);
        let dataArray = await allPromise;
        
        ic.interactionData1 = dataArray[0].value;
        ic.html2ddgm = '';
        ic.diagram2dCls.draw2Ddgm(ic.interactionData1, mmdbid1, 0);
        if(me.cfg.show2d) me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');


        ic.interactionData2 = dataArray[1].value;
        ic.diagram2dCls.draw2Ddgm(ic.interactionData2, mmdbid2, 1);

        ic.html2ddgm += "<br>" + ic.diagram2dCls.set2DdgmNote(true);
        $("#" + ic.pre + "dl_2ddgm").html(ic.html2ddgm);

        ic.b2DShown = true;

        /// if(ic.deferredViewinteraction !== undefined) ic.deferredViewinteraction.resolve();
    }

    async set2DDiagramsForChainalign(chainidArray) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');

        let ajaxArray = [];
        for(let index = 0, indexLen = chainidArray.length; index < indexLen; ++index) {
           let pos = chainidArray[index].indexOf('_');
           let mmdbid = chainidArray[index].substr(0, pos).toUpperCase();

           let url = me.htmlCls.baseUrl + "mmdb/mmdb_strview.cgi?v=2&program=icn3d&uid="+mmdbid+"&intrac=1";

           if(me.cfg.inpara !== undefined) url += me.cfg.inpara;

           let twodAjax = me.getAjaxPromise(url, 'jsonp');

           ajaxArray.push(twodAjax);
        }

        let allPromise = Promise.allSettled(ajaxArray);
        try {
            let dataArray = await allPromise;
            thisClass.parse2DDiagramsData(dataArray, chainidArray);
        }
        catch(err) {
            
        }          
    }

    parse2DDiagramsData(dataArray, chainidArray) { let ic = this.icn3d, me = ic.icn3dui;
        //var dataArray =(chainidArray.length == 1) ? [dataInput] : dataInput;

        ic.html2ddgm = '';

        // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
        //var data2 = v2[0];
        for(let index = 0, indexl = chainidArray.length; index < indexl; ++index) {
            let data = dataArray[index].value;//[0];
            let mmdbid = chainidArray[index].substr(0, chainidArray[index].indexOf('_'));

            ic.diagram2dCls.draw2Ddgm(data, mmdbid, 0);
        }

        ic.html2ddgm += "<br>" + ic.diagram2dCls.set2DdgmNote(true);

        ic.b2DShown = true;
        $("#" + ic.pre + "dl_2ddgm").html(ic.html2ddgm);
        if(me.cfg.show2d) me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');

        /// if(ic.deferredViewinteraction !== undefined) ic.deferredViewinteraction.resolve();
    }

    download2Ddgm(mmdbid, structureIndex) {        this.set2DDiagrams(mmdbid);
    }

    set2DDiagrams(mmdbid) { let ic = this.icn3d, me = ic.icn3dui;
        me.htmlCls.dialogCls.openDlg('dl_2ddgm', 'Interactions');

        if(ic.b2DShown === undefined || !ic.b2DShown) {
            ic.html2ddgm = '';

            ic.diagram2dCls.draw2Ddgm(ic.interactionData, mmdbid);

            ic.html2ddgm += "<br>" + ic.diagram2dCls.set2DdgmNote();
            $("#" + ic.pre + "dl_2ddgm").html(ic.html2ddgm);
        }

        ic.b2DShown = true;
    }

    showLoading() { let ic = this.icn3d; ic.icn3dui;
          if($("#" + ic.pre + "wait")) $("#" + ic.pre + "wait").show();
          if($("#" + ic.pre + "canvas")) $("#" + ic.pre + "canvas").hide();
          if($("#" + ic.pre + "cmdlog")) $("#" + ic.pre + "cmdlog").hide();
    }

    hideLoading() { let ic = this.icn3d; ic.icn3dui;
        //if(ic.bCommandLoad === undefined || !ic.bCommandLoad) {
          if($("#" + ic.pre + "wait")) $("#" + ic.pre + "wait").hide();
          if($("#" + ic.pre + "canvas")) $("#" + ic.pre + "canvas").show();
          if($("#" + ic.pre + "cmdlog")) $("#" + ic.pre + "cmdlog").show();
        //}
    }

    setYourNote(yournote) { let ic = this.icn3d, me = ic.icn3dui;
        ic.yournote = yournote;
        $("#" + ic.pre + "yournote").val(ic.yournote);
        if(me.cfg.shownote) document.title = ic.yournote;
    }

    transformToOpmOri(pdbid) { let ic = this.icn3d; ic.icn3dui;
      // apply matrix for each atom
      if(ic.rmsd_supr !== undefined && ic.rmsd_supr.rot !== undefined) {
          let rot = ic.rmsd_supr.rot;
          let centerFrom = ic.rmsd_supr.trans1;
          let centerTo = ic.rmsd_supr.trans2;
          ic.rmsd_supr.rmsd;

          let dxymaxsq = 0;
          for(let i in ic.atoms) {
            let atom = ic.atoms[i];

            atom.coord = ic.surfaceCls.transformMemPro(atom.coord, rot, centerFrom, centerTo);
            let xysq = atom.coord.x * atom.coord.x + atom.coord.y * atom.coord.y;
            if(Math.abs(atom.coord.z) <= 25 && xysq > dxymaxsq) {
                dxymaxsq = xysq;
            }
          }

          //ic.center = chainresiCalphaHash2.center;
          //ic.oriCenter = ic.center.clone();

          // add membranes
          // the membrane atoms belongs to the structure "pdbid"
          this.addMemAtoms(ic.halfBilayerSize, pdbid, Math.sqrt(dxymaxsq));

          // no rotation
          ic.bStopRotate = true;

          ic.bOpm = true;

          // show transmembrane features
          $("#" + ic.pre + "togglememli").show();
          $("#" + ic.pre + "adjustmemli").show();
          $("#" + ic.pre + "selectplaneli").show();
          //$("#" + ic.pre + "anno_transmemli").show();
      }
      else {
          ic.bOpm = false;
      }
    }

    transformToOpmOriForAlign(pdbid, chainresiCalphaHash2, bResi_ori) { let ic = this.icn3d, me = ic.icn3dui;
      if(chainresiCalphaHash2 !== undefined) {
          let chainresiCalphaHash1 = ic.loadPDBCls.getChainCalpha(ic.chains, ic.atoms, bResi_ori, pdbid);

          let bOneChain =(Object.keys(chainresiCalphaHash1.chainresiCalphaHash).length == 1 || Object.keys(chainresiCalphaHash2.chainresiCalphaHash).length == 1) ? true : false;

          let coordsFrom = [], coordsTo = [];
          for(let chain in chainresiCalphaHash1.chainresiCalphaHash) {
              if(chainresiCalphaHash2.chainresiCalphaHash.hasOwnProperty(chain)) {
                  let coord1 = chainresiCalphaHash1.chainresiCalphaHash[chain];
                  let coord2 = chainresiCalphaHash2.chainresiCalphaHash[chain];

                  if(coord1.length == coord2.length || bOneChain) {
                      coordsFrom = coordsFrom.concat(coord1);
                      coordsTo = coordsTo.concat(coord2);
                  }

                  if(coordsFrom.length > 500) break; // no need to use all c-alpha
              }
          }

          //var n = coordsFrom.length;
          let n =(coordsFrom.length < coordsTo.length) ? coordsFrom.length : coordsTo.length;

          if(n >= 4) {
              ic.rmsd_supr = me.rmsdSuprCls.getRmsdSuprCls(coordsFrom, coordsTo, n);

              // apply matrix for each atom
              if(ic.rmsd_supr.rot !== undefined && ic.rmsd_supr.rmsd < 0.1) {
                  let rot = ic.rmsd_supr.rot;
                  let centerFrom = ic.rmsd_supr.trans1;
                  let centerTo = ic.rmsd_supr.trans2;
                  let rmsd = ic.rmsd_supr.rmsd;

                  me.htmlCls.clickMenuCls.setLogCmd("RMSD of alignment to OPM: " + rmsd.toPrecision(4), false);
                  //$("#" + ic.pre + "dl_rmsd").html("<br><b>RMSD of alignment to OPM</b>: " + rmsd.toPrecision(4) + " &#8491;<br><br>");
                  //if(!me.cfg.bSidebyside) me.htmlCls.dialogCls.openDlg('dl_rmsd', 'RMSD of alignment to OPM');

                  let dxymaxsq = 0;
                  for(let i in ic.atoms) {
                    let atom = ic.atoms[i];

                    atom.coord = ic.surfaceCls.transformMemPro(atom.coord, rot, centerFrom, centerTo);
                    let xysq = atom.coord.x * atom.coord.x + atom.coord.y * atom.coord.y;
                    if(Math.abs(atom.coord.z) <= 25 && xysq > dxymaxsq) {
                        dxymaxsq = xysq;
                    }
                  }

                  ic.center = chainresiCalphaHash2.center;
                  ic.oriCenter = ic.center.clone();

                  // add membranes
                  this.addMemAtoms(ic.halfBilayerSize, pdbid, Math.sqrt(dxymaxsq));

                  // no rotation
                  ic.bStopRotate = true;

                  ic.bOpm = true;

                  // show transmembrane features
                  $("#" + ic.pre + "togglememli").show();
                  $("#" + ic.pre + "adjustmemli").show();
                  $("#" + ic.pre + "selectplaneli").show();
                  //$("#" + ic.pre + "anno_transmemli").show();
              }
              else {
                  ic.bOpm = false;
              }
          }
          else {
              ic.bOpm = false;
          }
      }
    }

    addOneDumAtom(pdbid, atomName, x, y, z, lastSerial) { let ic = this.icn3d, me = ic.icn3dui;
      let resn = 'DUM';
      let chain = 'MEM';
      let resi = 1;
      let coord = new THREE.Vector3(x, y, z);

      let atomDetails = {
          het: true, // optional, used to determine chemicals, water, ions, etc
          serial: ++lastSerial,         // required, unique atom id
          name: atomName,             // required, atom name
          alt: undefined,               // optional, some alternative coordinates
          resn: resn,             // optional, used to determine protein or nucleotide
          structure: pdbid,   // optional, used to identify structure
          chain: chain,           // optional, used to identify chain
          resi: resi,             // optional, used to identify residue ID
          coord: coord,           // required, used to draw 3D shape
          b: undefined, // optional, used to draw B-factor tube
          elem: atomName,             // optional, used to determine hydrogen bond
          bonds: [],              // required, used to connect atoms
          ss: '',             // optional, used to show secondary structures
          ssbegin: false,         // optional, used to show the beginning of secondary structures
          ssend: false,            // optional, used to show the end of secondary structures
          color: me.parasCls.atomColors[atomName]
      };
      ic.atoms[lastSerial] = atomDetails;

      ic.chains[pdbid + '_MEM'][lastSerial] = 1;
      ic.residues[pdbid + '_MEM_1'][lastSerial] = 1;

      ic.chemicals[lastSerial] = 1;

      ic.dAtoms[lastSerial] = 1;
      ic.hAtoms[lastSerial] = 1;

      return lastSerial;
    }

    addMemAtoms(dmem, pdbid, dxymax) { let ic = this.icn3d; ic.icn3dui;
      if(!pdbid) return;

      let npoint=40; // points in radius
      let step = 2;
      let maxpnt=2*npoint+1; // points in diameter
      let fn=step*npoint; // center point

      //var dxymax = npoint / 2.0 * step;

      pdbid =(pdbid) ? pdbid.toUpperCase() : 'stru';

      ic.structures[pdbid].push(pdbid + '_MEM');
      ic.chains[pdbid + '_MEM'] = {};
      ic.residues[pdbid + '_MEM_1'] = {};

      ic.chainsSeq[pdbid + '_MEM'] = [{'name':'DUM', 'resi': 1}];
      let lastSerial = Object.keys(ic.atoms).length;
      for(let i = 0; i < 1000; ++i) {
          if(!ic.atoms.hasOwnProperty(lastSerial + i)) {
              lastSerial = lastSerial + i - 1;
              break;
          }
      }

      for(let i=0; i < maxpnt; ++i) {
         for(let j=0; j < maxpnt; ++j) {
            let a=step*i-fn;
            let b=step*j-fn;
            let dxy=Math.sqrt(a*a+b*b);
            if(dxy < dxymax) {
                  let c=-dmem-0.4;
                  // Resn: DUM, name: N, a,b,c
                  lastSerial = this.addOneDumAtom(pdbid, 'N', a, b, c, lastSerial);

                  c=dmem+0.4;
                  // Resn: DUM, name: O, a,b,c
                  lastSerial = this.addOneDumAtom(pdbid, 'O', a, b, c, lastSerial);
            }
         }
      }
    }

    setMaxD() { let ic = this.icn3d; ic.icn3dui;
        let pmin = new THREE.Vector3( 9999, 9999, 9999);
        let pmax = new THREE.Vector3(-9999,-9999,-9999);
        let psum = new THREE.Vector3();
        let cnt = 0;
        // assign atoms
        for(let i in ic.atoms) {
            let atom = ic.atoms[i];
            let coord = atom.coord;
            psum.add(coord);
            pmin.min(coord);
            pmax.max(coord);
            ++cnt;

            if(atom.het) {
              //if($.inArray(atom.elem, me.parasCls.ionsArray) !== -1) {
              if(atom.bonds.length == 0) {
                ic.ions[atom.serial] = 1;
              }
              else {
                ic.chemicals[atom.serial] = 1;
              }
            }
        } // end of for


        ic.pmin = pmin;
        ic.pmax = pmax;

        ic.cnt = cnt;

        //ic.maxD = ic.pmax.distanceTo(ic.pmin);
        //ic.center = psum.multiplyScalar(1.0 / ic.cnt);
        ic.center = this.getGeoCenter(ic.pmin, ic.pmax);

        ic.maxD = this.getStructureSize(ic.atoms, ic.pmin, ic.pmax, ic.center);

        if(ic.maxD < 5) ic.maxD = 5;
        ic.oriMaxD = ic.maxD;
        ic.oriCenter = ic.center.clone();
    }

    //Update the dropdown menu and show the structure by calling the function "draw()".
    async renderStructure() { let ic = this.icn3d, me = ic.icn3dui;
      if(ic.bInitial) {
          //$.extend(ic.opts, ic.opts);
          if(ic.bOpm &&(me.cfg.align !== undefined || me.cfg.chainalign !== undefined)) { // show membrane
              let resid = ic.selectedPdbid + '_MEM_1';
              for(let i in ic.residues[resid]) {
                  let atom = ic.atoms[i];
                  atom.style = 'stick';
                  atom.color = me.parasCls.atomColors[atom.name];
                  ic.atomPrevColors[i] = atom.color;
                  ic.dAtoms[i] = 1;
              }
          }
          if(me.cfg.command !== undefined && me.cfg.command !== '') {
              ic.bRender = false;
              ic.drawCls.draw();
          }
          else {
              ic.selectionCls.oneStructurePerWindow(); // for alignment
              ic.drawCls.draw();
          }
          if(ic.bOpm) {
              let axis = new THREE.Vector3(1,0,0);
              let angle = -0.5 * Math.PI;
              ic.transformCls.setRotation(axis, angle);
          }
          //if(Object.keys(ic.structures).length > 1) {
          //    $("#" + ic.pre + "alternate").show();
          //}
          //else {
          //    $("#" + ic.pre + "alternate").hide();
          //}

          $("#" + ic.pre + "alternate").show();
      }
      else {
          ic.selectionCls.saveSelectionIfSelected();
          ic.drawCls.draw();
      }
      
      // set defined sets before loadScript
      if(ic.bInitial) {
        if(me.cfg.showsets) {
             ic.definedSetsCls.showSets();
        }
      }

//      if(ic.bInitial && me.cfg.command !== undefined && me.cfg.command !== '') {
      if(!ic.bCommandLoad && ic.bInitial && me.cfg.command !== undefined && me.cfg.command !== '') {
          if(Object.keys(ic.structures).length == 1) {
              let id = Object.keys(ic.structures)[0];
              me.cfg.command = me.cfg.command.replace(new RegExp('!','g'), id + '_');
          }
          // final step resolved ic.deferred
          await ic.loadScriptCls.loadScript(me.cfg.command, undefined, true);
          //ic.loadScriptCls.loadScript(me.cfg.command);
      }
      //if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined || ic.bRealign ||( ic.bInputfile && ic.InputfileType == 'pdb' && Object.keys(ic.structures).length >= 2) ) {
      if(Object.keys(ic.structures).length >= 2) {
          $("#" + ic.pre + "mn2_alternateWrap").show();
          //$("#" + ic.pre + "mn2_realignWrap").show();
      }
      else {
          $("#" + ic.pre + "mn2_alternateWrap").hide();
          //$("#" + ic.pre + "mn2_realignWrap").hide();
      }
 
      // display the structure right away. load the mns and sequences later
      setTimeout(async function(){
          if(ic.bInitial) {
            //   if(me.cfg.showsets) {
            //        ic.definedSetsCls.showSets();
            //   }
              if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined) {
                  // expand the toolbar
                  let id = ic.pre + 'selection';
                  $("#" + id).show();
                  $("#" + id + "_expand").hide();
                  $("#" + id + "_shrink").show();

                  if(me.cfg.align !== undefined && me.cfg.atype != 2) { // atype = 2: dynamic VAST+
                      let bShowHighlight = false;                  
                      let seqObj = me.htmlCls.alignSeqCls.getAlignSequencesAnnotations(Object.keys(ic.alnChains), undefined, undefined, bShowHighlight);
                      $("#" + ic.pre + "dl_sequence2").html(seqObj.sequencesHtml);
                      $("#" + ic.pre + "dl_sequence2").width(me.htmlCls.RESIDUE_WIDTH * seqObj.maxSeqCnt + 200);
                  }
              }
              //ic.definedSetsCls.setProtNuclLigInMenu();
              if(me.cfg.showanno) {
                   let cmd = "view annotations";
                   me.htmlCls.clickMenuCls.setLogCmd(cmd, true);
                   await ic.showAnnoCls.showAnnotations(); 
              }
              if(me.cfg.closepopup) {
                  ic.resizeCanvasCls.closeDialogs();
              }
          }
          else {
              ic.hlUpdateCls.updateHlAll();
          }
          if($("#" + ic.pre + "atomsCustom").length > 0) $("#" + ic.pre + "atomsCustom")[0].blur();
          ic.bInitial = false;
      }, 0);
    }

    getMassCenter(psum, cnt) { let ic = this.icn3d; ic.icn3dui;
        return psum.multiplyScalar(1.0 / cnt);
    }

    getGeoCenter(pmin, pmax) { let ic = this.icn3d; ic.icn3dui;
        return pmin.clone().add(pmax).multiplyScalar(0.5);
    }

    getStructureSize(atoms, pmin, pmax, center) { let ic = this.icn3d; ic.icn3dui;
        let maxD = 0;
        for(let i in atoms) {
            let coord = ic.atoms[i].coord;
            if(Math.round(pmin.x) == Math.round(coord.x) || Math.round(pmin.y) == Math.round(coord.y)
              || Math.round(pmin.z) == Math.round(coord.z) || Math.round(pmax.x) == Math.round(coord.x)
              || Math.round(pmax.y) == Math.round(coord.y) || Math.round(pmax.z) == Math.round(coord.z)) {
                let dist = coord.distanceTo(center) * 2;
                if(dist > maxD) {
                    maxD = dist;
                }
            }
        }

        return maxD;
    }

    async checkMemProteinAndRotate() { let ic = this.icn3d, me = ic.icn3dui;
        if(!ic.bCheckMemProtein) {
            let afid = (me.cfg.afid) ? me.cfg.afid : me.cfg.mmdbafid;

            await ic.ParserUtilsCls.checkMemProtein(afid);
            ic.bCheckMemProtein = true;
        }

        // rotate for links from Membranome
        if(me.cfg.url && me.cfg.url.indexOf('membranome') != -1) {
            let axis = new THREE.Vector3(1,0,0);
            let angle = -90 / 180.0 * Math.PI;

            ic.transformCls.setRotation(axis, angle);
        }
    }

    async checkMemProtein(afid) { let ic = this.icn3d, me = ic.icn3dui;
      //ic.deferredAfMem = $.Deferred(function() {
        try {
            let url = me.htmlCls.baseUrl + "vastdyn/vastdyn.cgi?afid2mem=" + afid;
            let data = await me.getAjaxPromise(url, 'jsonp');

            if(data && data.pdbid) {
              let question = "This is a single-spanning (bitopic) transmembrane protein according to the Membranome database. Do you want to align the protein with the model from Membranome? If you click \"OK\", you can press the letter \"a\" to alternate the structures.";
             
              if (me.cfg.afmem == 'off') {
                // do nothing
                /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
              }
              else if (me.cfg.afmem == 'on' || confirm(question)) {         
                try {  
                    let url2 = "https://storage.googleapis.com/membranome-assets/pdb_files/proteins/" + data.pdbid + ".pdb";
                    let afMemdata = await me.getAjaxPromise(url2, 'text');

                    ic.bAfMem = true;
                    $("#" + me.pre + "togglememli").show(); // show the menu "View > Toggle Membrane"

                    // append the PDB
                    let pdbid = data.pdbid.substr(0, data.pdbid.indexOf('_'));
                    let bOpm = true, bAppend = true;
                    await ic.pdbParserCls.loadPdbData(afMemdata, pdbid, bOpm, bAppend);

                    if(bAppend) {
                        if(ic.bSetChainsAdvancedMenu) ic.definedSetsCls.showSets();
                        if(ic.bAnnoShown) await ic.showAnnoCls.showAnnotations();
                    }

                    // Realign by seqeunce alignment with the residues in "segment", i.e., transmembrane helix
                    let segment = data.segment;   // e.g., " 361- 379 ( 359- 384)", the first range is trnasmembrane range, 
                                                //the second range is the range of the helix
                    let range = segment.replace(/ /gi, '').split('(')[0]; //361-379
                    ic.afmem_start_end = range.split('-');

                    ic.hAtoms = {};
                    ic.dAtoms = {};

                    // get the AlphaFold structure
                    for(let i in ic.atoms) {
                    if(ic.atoms[i].structure != pdbid) {
                        ic.hAtoms[i] = 1;
                    }
                    ic.dAtoms[i] = 1;
                    }

                    // get the transmembrane from the model of Membranome
                    for(let i = parseInt(ic.afmem_start_end[0]); i <= parseInt(ic.afmem_start_end[1]); ++i) {
                    ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[pdbid + '_A_' + i]);
                    }

                    await ic.realignParserCls.realignOnSeqAlign(pdbid);
                }
                catch(err) {
                      console.log("Error in retrieving matched PDB from Membranome...");
                      ///// if(ic.deferredAfMem !== undefined) ic.deferredAfMem.resolve();
                      /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
                      /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
                      return;
                }
              }
            }
            else {
                /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
                /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
            }
        }
        catch(err) {
              console.log("Error in finding matched PDB in Membranome...");
              ///// if(ic.deferredAfMem !== undefined) ic.deferredAfMem.resolve();
              /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
              /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
              return;
        }
      //});

      //return ic.deferredAfMem.promise();
    }

    getResi(chainid, resiPos) { let ic = this.icn3d; ic.icn3dui;
        // let resi;

        // if(bRealign) {
        //     resi = resiPos;
        // }
        // else {
        //     if(!ic.chainsSeq[chainid] || !ic.chainsSeq[chainid][resiPos]) {
        //         resi = '';
        //     }
        //     else {
        //         resi = ic.chainsSeq[chainid][resiPos].resi;
        //     }
        // }
        let resid = ic.ncbi2resid[chainid + '_' + (resiPos+1).toString()];
        let resi = (resid) ? resid.substr(resid.lastIndexOf('_') + 1) : '';

        return resi;
    }

    getResiNCBI(chainid, resi) { let ic = this.icn3d; ic.icn3dui;
        let residNCBI = ic.resid2ncbi[chainid + '_' + resi];
        let resiNCBI = (residNCBI) ? parseInt(residNCBI.substr(residNCBI.lastIndexOf('_') + 1)) : 0;

        return resiNCBI;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class LoadAtomData {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //type: "mmdbid", "mmcifid", "align"
    //alignType: "query", "target" for chain to chain 3D alignment

    //This function was used to parse atom "data" to set up parameters for the 3D viewer. "type" is mmcifid or mmdbid.
    //"id" is the MMDB ID or mmCIF ID.
    loadAtomDataIn(data, id, type, seqalign, alignType, chainidInput, chainIndex, bLastQuery, bNoSeqalign) { let ic = this.icn3d, me = ic.icn3dui;
        //ic.init();
        ic.pmin = new THREE.Vector3( 9999, 9999, 9999);
        ic.pmax = new THREE.Vector3(-9999,-9999,-9999);
        ic.psum = new THREE.Vector3();

        let atoms = data.atoms;

        //let serialBase =(alignType === undefined || alignType === 'target') ? 0 : ic.lastTargetSerial;
        let serialBase = (ic.atoms) ? Object.keys(ic.atoms).length : 0;

        let serial = serialBase;

        let serial2structure = {}; // for "align" only
        let mmdbid2pdbid = {}; // for "align" only
/*
        if(alignType === undefined || alignType === 'target') {
            ic.pmid = data.pubmedId;

            ic.chainid2title = {};
            ic.chainid2sid = {};
        }
        else {
            ic.pmid2 = data.pubmedId;
        }
*/

        ic.pmid = data.pubmedId;
        if(ic.chainid2title === undefined) ic.chainid2title = {};
        if(ic.chainid2sid === undefined) ic.chainid2sid = {};

        let chainid2kind = {}, chainid2color = {};

        if(type === 'align') {
          //serial2structure
          ic.pmid = "";
          ic.molTitle = "";
          if(me.cfg.inpara && me.cfg.inpara.indexOf('atype=1') !== -1) {
            ic.molTitle = 'Invariant Core Structure Alignment (VAST) of ';
          }
          else if(me.cfg.inpara && me.cfg.inpara.indexOf('atype=2') !== -1) {
            ic.molTitle = 'Structure Alignment (TM-align) of ';
          }
          else {
            ic.molTitle = 'Structure Alignment (VAST) of ';
          }
          

          let bTitle = false;
          for(let i = 0, il = data.alignedStructures[0].length; i < il; ++i) {
              let structure = data.alignedStructures[0][i];

              if(i === 1) {
                  ic.secondId = structure.pdbId; // set the second pdbid to add indent in the structure and chain mns
              }

              let pdbidTmp = structure.pdbId;
              let mmdbidTmp = structure.mmdbId;

              for(let j = structure.serialInterval[0], jl = structure.serialInterval[1]; j <= jl; ++j) {
                  serial2structure[j] = pdbidTmp.toString();
                  mmdbid2pdbid[mmdbidTmp] = pdbidTmp;
              }

              for(let j = 0, jl = structure.molecules.length; j < jl; ++j) {
                  let chain = structure.molecules[j].chain;
                  let kind = structure.molecules[j].kind;
                  let title = structure.molecules[j].name;
                  //var seq = structure.molecules[j].sequence;
                  let sid = structure.molecules[j].sid;

                  let chainid = pdbidTmp + '_' + chain;

                  //if(ic.bFullUi) chainid2seq[chainid] = seq;
                  chainid2kind[chainid] = kind;

                  ic.chainid2title[chainid] = title;
                  if(sid !== undefined) ic.chainid2sid[chainid] = sid;
              }

              ic.molTitle +=  "<a href=\"" + me.htmlCls.baseUrl + "mmdb/mmdbsrv.cgi?uid=" + structure.pdbId.toUpperCase() + "\" target=\"_blank\">" + structure.pdbId.toUpperCase() + "</a>";

              if(structure.descr !== undefined) ic.pmid += structure.descr.pubmedid;
              if(i === 0) {
                  ic.molTitle += " and ";
                  if(structure.descr !== undefined) ic.pmid += "_";
              }

              bTitle = true;
          }

          ic.molTitle += ' from VAST+';

          if(!bTitle) ic.molTitle = '';
        }
        else { // mmdbid or mmcifid
            if(data.descr !== undefined) ic.molTitle += data.descr.name;
            if(type === 'mmdbid') {
              let pdbidTmp = (isNaN(id)) ? id : data.pdbId;
              let chainHash = {};

              if(ic.alignmolid2color === undefined) ic.alignmolid2color = [];

              let molidCnt = 1;
           
              for(let molid in data.moleculeInfor) {
                  if(Object.keys(data.moleculeInfor[molid]).length === 0) continue;

                  let chain = data.moleculeInfor[molid].chain.trim();
                  let chainid = pdbidTmp + '_' + chain;

                  if(chainHash.hasOwnProperty(chain)) {
                      ++chainHash[chain];
                      chainid += chainHash[chain];
                  }
                  else {
                      chainHash[chain] = 1;
                  }

                  if(((ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t)) && alignType === 'query') ;

                  //if(chainidInput && chainidInput.substr(chainidInput.indexOf('_') + 1) == chain) chainid = chainidInput;
 
                  let kind = data.moleculeInfor[molid].kind;
                  let color = data.moleculeInfor[molid].color;
                  let sid = data.moleculeInfor[molid].sid;

                  chainid2kind[chainid] = kind;
                  chainid2color[chainid] = color;

                  if(kind == 'protein') ic.organism = data.moleculeInfor[molid].taxonomyName.toLowerCase();

                  if(sid !== undefined) ic.chainid2sid[chainid] = sid;

                  if(ic.pdbid_chain2title === undefined) ic.pdbid_chain2title = {};
                  ic.pdbid_chain2title[chainid] = data.moleculeInfor[molid].name;

                  //if(alignType == 'query' && chain == ic.chain_q) {
                  //    ic.alignmolid2color[0][molid] = molidCnt.toString();
                  //}
                  //else if(alignType == 'target' && chain == ic.chain_t) {
                  //    ic.alignmolid2color[1][molid] = molidCnt.toString();
                  //}

                  if(chain == chainid.substr(chainid.lastIndexOf('_')) ) {
                      let tmpHash = {};
                      tmpHash[molid] = molidCnt.toString();
                      ic.alignmolid2color.push(tmpHash);
                  }

                  ++molidCnt;
              }
            }
        }
        
        let atomid2serial = {};
        let prevStructureNum = '', prevChainNum = '', prevResidueNum = '';
        let structureNum = '', chainNum = '', residueNum = '';
        let prevResi = 0, prevResiOri = 0, prevResn = ''; // continuous from 1 for each chain
        let bChainSeqSet = true;
        let bAddedNewSeq = false;
        let molid, prevMolid = '';

        let bPhosphorusOnly = me.utilsCls.isCalphaPhosOnly(atoms); //, "O3'", "O3*") || me.utilsCls.isCalphaPhosOnly(atoms, "P");
        let miscCnt = 0;
        let CSerial, prevCSerial, OSerial, prevOSerial;

        let biopolymerChainsHash = {};
        for(let i in atoms) {
            ++serial;

            atomid2serial[i] = serial;

            let atm = atoms[i];
            atm.serial = serial;

            let mmdbId;

            if(type === 'mmdbid' || type === 'mmcifid') {
              mmdbId = id; // here mmdbId is pdbid or mmcif id
            }
            else if(type === 'align') {
              mmdbId = serial2structure[serial]; // here mmdbId is pdbid
            }

            let bSetResi = false;

            //if(mmdbId !== prevmmdbId) resiArray = [];
            if(atm.chain === undefined && (type === 'mmdbid' || type === 'align')) {
                if(type === 'mmdbid') {
                  molid = atm.ids.m;

                  if(ic.molid2chain[molid] !== undefined) {
                      let pos = ic.molid2chain[molid].indexOf('_');
                      atm.chain = ic.molid2chain[molid].substr(pos + 1);
                  }
                  else {
                      let miscName = 'Misc';

                      //if(atm.resn != prevResn || chainid2kind[chainNum] === 'solvent' || atm.resn === 'HOH' || atm.name == atm.elem) {
                      if((chainid2kind[chainNum] === 'protein' && chainid2kind[chainNum] === 'nucleotide' && atm.resi != prevResiOri)
                        ||(chainid2kind[chainNum] !== 'protein' && chainid2kind[chainNum] !== 'nucleotide'
                        &&(atm.resn.substr(0,3) != prevResn.substr(0,3) || atm.resi != prevResiOri || chainid2kind[chainNum] === 'solvent' || atm.resn === 'HOH')) ) {
                          ++miscCnt;
                      }

                          atm.resi_ori = atm.resi;
                          atm.resi = miscCnt;
                          bSetResi = true;

                      //if all are defined in the chain section, no "Misc" should appear
                      atm.chain = miscName;
                  }

                  //if(ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t && alignType === 'query') {
                      //atm.chain += me.htmlCls.postfix;
                  //}
                }
                else if(type === 'align') {
                  molid = atm.ids.m;

                  if(ic.pdbid_molid2chain[mmdbId + '_' + molid] !== undefined) {
                      atm.chain = ic.pdbid_molid2chain[mmdbId + '_' + molid];
                  }
                  else {
                      let miscName = 'Misc';

                      //if(atm.resn != prevResn || chainid2kind[chainNum] === 'solvent' || atm.resn === 'HOH' || atm.name == atm.elem) {
                      if((chainid2kind[chainNum] === 'protein' && chainid2kind[chainNum] === 'nucleotide' && atm.resi != prevResiOri)
                        ||(chainid2kind[chainNum] !== 'protein' && chainid2kind[chainNum] !== 'nucleotide'
                        &&(atm.resn.substr(0,3) != prevResn.substr(0,3) || atm.resi != prevResiOri || chainid2kind[chainNum] === 'solvent' || atm.resn === 'HOH')) ) {
                          ++miscCnt;

                          atm.resi_ori = atm.resi;
                          atm.resi = miscCnt;
                          bSetResi = true;
                      }

                      // chemicals do not have assigned chains.
                      atm.chain = miscName;
                  }
                }
            }
            else {
              atm.chain =(atm.chain === '') ? 'Misc' : atm.chain;
            }

            atm.chain = atm.chain.trim(); //.replace(/_/g, '');

            // mmcif has pre-assigned structure in mmcifparser.cgi output
            if(type === 'mmdbid' || type === 'align') {
                atm.structure = mmdbId;

                if(type === 'mmdbid' &&((ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t))
                  && alignType === 'query') ;
            }

            structureNum = atm.structure;

            chainNum = structureNum + '_' + atm.chain;

            //if(ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t && alignType === 'query') chainNum += me.htmlCls.postfix;

            //var resiCorrection = 0;
            if(type === 'mmdbid' || type === 'align') {
                if(!bSetResi) {
                    atm.resi_ori = atm.resi; //parseInt(atm.resi); // original PDB residue number, has to be integer
                    if(!ic.bUsePdbNum) {
                        atm.resi = atm.ids.r; // corrected for residue insertion code
                    }
                    else {
                        // make MMDB residue number consistent with PDB residue number
                        atm.resi = atm.resi_ori; // corrected for residue insertion code
                        //if(ic.chainid2offset && !ic.chainid2offset[chainNum]) ic.chainid2offset[chainNum] = atm.resi_ori - atm.ids.r;
                    }
                }

                //resiCorrection = atm.resi - atm.resi_ori;

                let pos = atm.resn.indexOf(' ');
                if(pos !== -1 && pos != 0) atm.resn = atm.resn.substr(0, pos);

                // remember NCBI residue number
                // atm.resiNCBI = atm.ids.r;
                // ic.ncbi2resid[chainNum + '_' + atm.resiNCBI] = chainNum + '_' + atm.resi;
                // ic.resid2ncbi[chainNum + '_' + atm.resi] = chainNum + '_' + atm.resiNCBI;
            }
            
            if(chainNum !== prevChainNum) {
                prevResi = 0;
            }

            if(atm.resi !== prevResi) {
                if(chainNum !== prevChainNum) {
                    prevCSerial = undefined;
                    prevOSerial = undefined;
                }
                else {
                    prevCSerial = CSerial;
                    prevOSerial = OSerial;
                }
            }

            if(type === 'mmdbid') {
                atm.coord = new THREE.Vector3(atm.coord[0], atm.coord[1], atm.coord[2]);
                //if(ic.q_rotation !== undefined && ic.t_trans_add.length > 0 && !me.cfg.resnum && !me.cfg.resdef && chainIndex) {
                //    atm = ic.chainalignParserCls.transformAtom(atm, chainIndex, alignType);
                //}
            }
            else {
                atm.coord = new THREE.Vector3(atm.coord.x, atm.coord.y, atm.coord.z);
            }

            let oneLetterRes = me.utilsCls.residueName2Abbr(atm.resn.substr(0, 3));

            if((type === 'mmdbid' || type === 'align') && ic.bFullUi ) {
                // set ic.mmdbMolidResid2mmdbChainResi
                if(ic.mmdbMolidResid2mmdbChainResi === undefined) ic.mmdbMolidResid2mmdbChainResi = {};
                ic.mmdbMolidResid2mmdbChainResi[mmdbId + '_' + atm.ids.m + '_' + atm.ids.r] = mmdbId + '_' + atm.chain + '_' + atm.resi;
            }

            ic.pmin.min(atm.coord);
            ic.pmax.max(atm.coord);
            ic.psum.add(atm.coord);

            let bProtein =(me.cfg.mmcifid === undefined && ic.InputfileType != 'mmcif') ? chainid2kind[chainNum] === 'protein' : atm.mt === 'p';
            let bNucleotide =(me.cfg.mmcifid === undefined && ic.InputfileType != 'mmcif') ? chainid2kind[chainNum] === 'nucleotide' : atm.mt === 'n';
            let bSolvent =(me.cfg.mmcifid === undefined && ic.InputfileType != 'mmcif') ? chainid2kind[chainNum] === 'solvent' : atm.mt === 's';
            // in vastplus.cgi, ions arenotlisted in alignedStructures...molecules, thus chainid2kind[chainNum] === undefined is used.
            // ions will be separated from chemicals later.
            // here "ligand" is used in the cgi output
            //var bChemicalIons =(me.cfg.mmcifid === undefined) ?(chainid2kind[chainNum] === 'ligand' || chainid2kind[chainNum] === 'otherPolymer' || chainid2kind[chainNum] === undefined) : atm.mt === 'l';
            // kind: other, otherPolymer, etc
            let bChemicalIons =(me.cfg.mmcifid === undefined && ic.InputfileType != 'mmcif') ?(chainid2kind[chainNum] === 'ligand' ||(chainid2kind[chainNum] !== undefined && chainid2kind[chainNum].indexOf('other') !== -1) || chainid2kind[chainNum] === undefined) : atm.mt === 'l';

            if((atm.chain === 'Misc' || chainid2kind[chainNum] === 'other') && biopolymerChainsHash[chainNum] !== 'protein' && biopolymerChainsHash[chainNum] !== 'nucleotide') { // biopolymer, could be protein or nucleotide
                if(atm.name === 'CA' && atm.elem === 'C') {
                    biopolymerChainsHash[chainNum] = 'protein';
                }
                else if(atm.name === 'P' && atm.elem === 'P') {
                    biopolymerChainsHash[chainNum] = 'nucleotide';
                }
                else {
                    biopolymerChainsHash[chainNum] = 'chemical';
                }
            }

            if(bProtein || bNucleotide) {
                if(bProtein) {
                  ic.proteins[serial] = 1;

                  if(atm.name === 'CA') ic.calphas[serial] = 1;
                  if(atm.name !== 'N' && atm.name !== 'H' && atm.name !== 'CA' && atm.name !== 'HA' && atm.name !== 'C' && atm.name !== 'O') ic.sidec[serial] = 1;
                }
                else if(bNucleotide) {
                  ic.nucleotides[serial] = 1;

                  //if(atm.name == 'P') ic.nucleotidesO3[serial] = 1;
                  if(atm.name == "O3'" || atm.name == "O3*" ||(bPhosphorusOnly && atm.name == 'P') ) {
                      ic.nucleotidesO3[serial] = 1;
                  }

                  if(me.parasCls.nuclMainArray.indexOf(atm.name) === -1) {
                      ic.ntbase[serial] = 1;
                  }
                }

                atm.het = false;
            }
            else if(bSolvent) { // solvent
              ic.water[serial] = 1;

              atm.het = true;
            }
            else if(bChemicalIons) { // chemicals and ions
              //if(atm.bonds.length === 0) ic.ions[serial] = 1;
              if(atm.resn === 'HOH' || atm.resn === 'O') {
                  ic.water[serial] = 1;
              }
              else if(atm.elem === atm.resn) {
                  ic.ions[serial] = 1;
              }
              else {
                  ic.chemicals[serial] = 1;
              }

              atm.het = true;
            }

            if(type === 'mmdbid') {
                if(!atm.het) {
                    atm.color =(chainid2color[chainNum] !== undefined) ? me.parasCls.thr(chainid2color[chainNum]) : me.parasCls.chargeColors[atm.resn];
                }
                else {
                    atm.color = me.parasCls.atomColors[atm.elem] || me.parasCls.defaultAtomColor;
                }
            }
            else {
                if(atm.color !== undefined) atm.color = me.parasCls.thr(atm.color);
            }

            if(atm.resn.charAt(0) !== ' ' && atm.resn.charAt(1) === ' ') {
              atm.resn = atm.resn.charAt(0);
            }

            if(!atm.het && atm.name === 'C') {
                CSerial = serial;
            }
            if(!atm.het && atm.name === 'O') {
                OSerial = serial;
            }

            // from DSSP C++ code
            if(!atm.het && atm.name === 'N' && prevCSerial !== undefined && prevOSerial !== undefined) {
                let dist = ic.atoms[prevCSerial].coord.distanceTo(ic.atoms[prevOSerial].coord);

                let x2 = atm.coord.x +(ic.atoms[prevCSerial].coord.x - ic.atoms[prevOSerial].coord.x) / dist;
                let y2 = atm.coord.y +(ic.atoms[prevCSerial].coord.y - ic.atoms[prevOSerial].coord.y) / dist;
                let z2 = atm.coord.z +(ic.atoms[prevCSerial].coord.z - ic.atoms[prevOSerial].coord.z) / dist;

                atm.hcoord = new THREE.Vector3(x2, y2, z2);
            }

            // double check
            if(atm.resn == 'HOH') ic.water[serial] = 1;

            ic.atoms[serial] = atm;
            ic.dAtoms[serial] = 1;
            ic.hAtoms[serial] = 1;

            // chain level
            let chainid = atm.structure + '_' + atm.chain;
            //if(ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t && alignType === 'query') chainid += me.htmlCls.postfix;

            if(ic.chains[chainid] === undefined) ic.chains[chainid] = {};
            ic.chains[chainid][serial] = 1;

            // residue level
            let residueid = chainid + '_' + atm.resi;
            if(ic.residues[residueid] === undefined) ic.residues[residueid] = {};
            ic.residues[residueid][serial] = 1;

            residueNum = chainNum + '_' + atm.resi;

            // different residue
            if(residueNum !== prevResidueNum) {
                // different chain
                if(chainNum !== prevChainNum) {
                    bChainSeqSet = true;

                    //if(serial !== 1) {
                    if(prevStructureNum !== '') {
                        if(ic.structures[prevStructureNum] === undefined) ic.structures[prevStructureNum] = [];
                        ic.structures[prevStructureNum].push(prevChainNum);
                    }
                }
            }

            ic.residueId2Name[residueid] = oneLetterRes;

            let secondaries = '-';
            if(atm.ss === 'helix') {
                secondaries = 'H';
            }
            else if(atm.ss === 'sheet') {
                secondaries = 'E';
            }
            else if(atm.het || bNucleotide ) {
                secondaries = 'o';
            }
            else if(!atm.het && me.parasCls.residueColors.hasOwnProperty(atm.resn.toUpperCase()) ) {
                secondaries = 'c';
            }
            else if(atm.ss === 'coil') {
                secondaries = 'c';
            }

            ic.secondaries[atm.structure + '_' + atm.chain + '_' + atm.resi] = secondaries;

            if((atm.resi != prevResi || molid != prevMolid) && ic.bFullUi) { // mmdbid 1tup has different molid, same resi
              if(ic.chainsSeq[chainid] === undefined) {
                  ic.chainsSeq[chainid] = [];
                  bChainSeqSet = false;
              }

              // ic.chainsSeq[chainid][atm.resi - 1] should have been defined for major chains
              if(!isNaN(atm.resi)) {
                  if( bChainSeqSet && !bAddedNewSeq && ic.chainsSeq[chainid][atm.resi - 1] !== undefined) {
                      ic.chainsSeq[chainid][atm.resi - 1].name = oneLetterRes;
                  }
                  else if(!bChainSeqSet || !ic.chainsSeq[chainid].hasOwnProperty(atm.resi - 1)) {
                      let resObject = {};
                      resObject.resi = atm.resi;
                      resObject.name = oneLetterRes;
                      if(atm.resi % 10 === 0) atm.resi.toString();

                      ic.chainsSeq[chainid].push(resObject);

                      bAddedNewSeq = true;
                  }
              }
            }

            prevResi = atm.resi;
            prevResiOri = atm.resi_ori;
            prevResn = atm.resn;

            prevStructureNum = structureNum;
            prevChainNum = chainNum;
            prevResidueNum = residueNum;

            prevMolid = molid;
        }

        //ic.lastTargetSerial = serial;

        // remove P-P bonds in PDB 3FGU
        for(let i in ic.chemicals) {
            let atom = ic.atoms[i];
            if(atom.elem == 'P' && atom.bonds.length >= 4) {
                // remove the bonds with another 'P'
                for(let j = atom.bonds.length - 1; j >= 0; --j) {
                    let atom2 = ic.atoms[atom.bonds[j]];
                    if(atom2.elem == 'P') {
                        atom.bonds.splice(j, 1);
                    }
                }
            }
        }

        // adjust biopolymer type
        for(let chainid in biopolymerChainsHash) {
            if(Object.keys(ic.chains[chainid]).length < 10) continue;

            if(biopolymerChainsHash[chainid] === 'chemical') continue;

            for(let serial in ic.chains[chainid]) {
                let atm = ic.atoms[serial];

                delete ic.chemicals[serial];
                atm.het = false;

                if(biopolymerChainsHash[chainid] === 'protein') {
                  ic.proteins[serial] = 1;

                  if(atm.name === 'CA') ic.calphas[serial] = 1;
                  if(atm.name !== 'N' && atm.name !== 'H' && atm.name !== 'CA' && atm.name !== 'HA' && atm.name !== 'C' && atm.name !== 'O') ic.sidec[serial] = 1;
                }
                else if(biopolymerChainsHash[chainid] === 'nucleotide') {
                  ic.nucleotides[serial] = 1;
                  //atm.style = 'nucleotide cartoon';

                  if(atm.name == "O3'" || atm.name == "O3*" ||(bPhosphorusOnly && atm.name == 'P') ) {
                      ic.nucleotidesO3[serial] = 1;
                  }

                  if(me.parasCls.nuclMainArray.indexOf(atm.name) === -1) {
                    ic.ntbase[serial] = 1;
                  }
                }
            }
        }

        // ic.loadPDBCls.adjustSeq(ic.chainMissingResidueArray);

        // add the last residue set
        if(ic.structures[structureNum] === undefined) ic.structures[structureNum] = [];
        ic.structures[structureNum].push(chainNum);

        //ic.countNextresiArray = {}
        //ic.chainMissingResidueArray = {}
        if(ic.bFullUi) {
            if(type === 'mmdbid' || type === 'mmcifid') {
                for(let chain in data.sequences) {
                    let seqArray = data.sequences[chain];
                    let chainid = id + '_' + chain;

                    if(((ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t)) && alignType === 'query') ;

                    ic.ParserUtilsCls.getMissingResidues(seqArray, type, chainid); // assign ic.chainsSeq
                }
            }
            else if(type === 'align') {
                //for(let chainid in chainid2seq) {
                for(let chainid in ic.chainid2seq) {
                    let seqArray = ic.chainid2seq[chainid];

                    ic.ParserUtilsCls.getMissingResidues(seqArray, type, chainid);
                }
            }
        }

        // set ResidMapping after ic.chainsSeq is assigned in the above paragraph
        ic.loadPDBCls.setResidMapping();

        // update bonds info
        if(type !== 'mmcifid') {
        //for(let i in ic.atoms) {
        for(let i in atoms) {
            let currSerial = atomid2serial[i];

            let bondLength =(ic.atoms[currSerial].bonds === undefined) ? 0 : ic.atoms[currSerial].bonds.length;

            for(let j = 0; j < bondLength; ++j) {
                ic.atoms[currSerial].bonds[j] = atomid2serial[ic.atoms[currSerial].bonds[j]];
            }
        }
        }

        // remove the reference
        data.atoms = {};

        //ic.cnt =(alignType === undefined || alignType === 'target') ? serial : serial - ic.lastTargetSerial;
        ic.cnt = serial;

        if(ic.cnt > ic.maxatomcnt ||(ic.biomtMatrices !== undefined && ic.biomtMatrices.length * ic.cnt > 10 * ic.maxatomcnt) ) {
            ic.opts['proteins'] = 'c alpha trace'; //ribbon, strand, cylinder and plate, schematic, c alpha trace, b factor tube, lines, stick, ball and stick, sphere, nothing
            ic.opts['nucleotides'] = 'o3 trace'; //nucleotide cartoon, o3 trace, schematic, lines, stick,
        }

        //ic.maxD = ic.pmax.distanceTo(ic.pmin);
        //ic.center = ic.psum.multiplyScalar(1.0 / ic.cnt);
        ic.center = ic.ParserUtilsCls.getGeoCenter(ic.pmin, ic.pmax);
        ic.maxD = ic.ParserUtilsCls.getStructureSize(ic.atoms, ic.pmin, ic.pmax, ic.center);

        if(ic.maxD < 5) ic.maxD = 5;

        ic.oriMaxD = ic.maxD;

        // set up disulfide bonds
        if(type === 'align' || bLastQuery) { // calculate disulfide bonds
            ic.ssbondpnts = {};

            ic.loadPDBCls.setSsbond();
        }
        
        if(type === 'mmdbid' && Object.keys(ic.structures).length == 1) {
            let disulfideArray = data.disulfides;

            if(disulfideArray !== undefined) {
                for(let i = 0, il = disulfideArray.length; i < il; ++i) {
                    let serial1 = disulfideArray[i][0].ca;
                    let serial2 = disulfideArray[i][1].ca;

                    let atom1 = ic.atoms[serial1];
                    let atom2 = ic.atoms[serial2];

                    let chain1 = atom1.chain;
                    let chain2 = atom2.chain;

                    let resid1 = atom1.structure + '_' + chain1 + '_' + atom1.resi;
                    let resid2 = atom2.structure + '_' + chain2 + '_' + atom2.resi;

                    if(ic.ssbondpnts[atom1.structure] === undefined) ic.ssbondpnts[atom1.structure] = [];

                    ic.ssbondpnts[atom1.structure].push(resid1);
                    ic.ssbondpnts[atom1.structure].push(resid2);
                }
            }
        }
        else if(type === 'mmcifid' && Object.keys(ic.structures).length == 1) {
            let disulfideArray = data.disulfides;

            if(disulfideArray !== undefined) {
                if(ic.ssbondpnts[id] === undefined) ic.ssbondpnts[id] = [];

                for(let i = 0, il = disulfideArray.length; i < il; ++i) {
                    let resid1 = disulfideArray[i][0];
                    let resid2 = disulfideArray[i][1];
                  
                    ic.ssbondpnts[id].push(resid1);
                    ic.ssbondpnts[id].push(resid2);
                }

                // copy disulfide bonds
                let structureArray = Object.keys(ic.structures);
                for(let s = 0, sl = structureArray.length; s < sl; ++s) {
                    let structure = structureArray[s];

                    if(structure == id) continue;

                    if(ic.ssbondpnts[structure] === undefined) ic.ssbondpnts[structure] = [];

                    for(let j = 0, jl = ic.ssbondpnts[id].length; j < jl; ++j) {
                        let ori_resid = ic.ssbondpnts[id][j];
                        let pos = ori_resid.indexOf('_');
                        let resid = structure + ori_resid.substr(pos);
                        ic.ssbondpnts[structure].push(resid);
                    }
                }
            }
        }

        if(type === 'mmcifid') {
            ic.ParserUtilsCls.transformToOpmOri(id);
        }
        else if(type === 'mmdbid' && alignType === undefined) {
            ic.ParserUtilsCls.transformToOpmOri(id);
        }

        // set up sequence alignment
        // display the structure right away. load the mns and sequences later
    //        setTimeout(function(){
        let hAtoms = {};
    
        if(type === 'align' && seqalign !== undefined && ic.bFullUi) {
            ic.setSeqAlignCls.setSeqAlign(seqalign, data.alignedStructures);
        } // if(align
        else if(type === 'mmdbid' && alignType === 'query' && ic.bFullUi && ic.q_rotation !== undefined 
            && !me.cfg.resnum && !me.cfg.resdef && !bNoSeqalign) {

            if(chainIndex) {
                ic.setSeqAlignCls.setSeqAlignChain(chainidInput, chainIndex);

                let bReverse = false;
                let seqObj = me.htmlCls.alignSeqCls.getAlignSequencesAnnotations(Object.keys(ic.alnChains), undefined, undefined, false, undefined, bReverse);
                let oriHtml = $("#" + ic.pre + "dl_sequence2").html();

                hAtoms = ic.hAtoms;

                $("#" + ic.pre + "dl_sequence2").html(oriHtml + seqObj.sequencesHtml);
                $("#" + ic.pre + "dl_sequence2").width(me.htmlCls.RESIDUE_WIDTH * seqObj.maxSeqCnt + 200);
            }
            else {            
                hAtoms = ic.hAtoms;
            }
        }
        else if(type === 'mmdbid' && alignType === 'target') {
            hAtoms = ic.hAtoms;
        }

        if(!me.cfg.mmdbafid && type === 'mmdbid' && (alignType === 'target' || alignType === 'query') && ic.q_rotation === undefined) {
            if(alignType === 'target' || alignType === 'query') {
                for(let i in atoms) {
                    let atom = atoms[i];
                    atom.coord.x -= ic.center.x;
                    atom.coord.y -= ic.center.y;
                    atom.coord.z -= ic.center.z;
                }
            }

            if(alignType === 'target') {
                //ic.maxD1 = ic.maxD;
                ic.oriMaxD = ic.maxD;
                ic.center1 = ic.center;
            }
            else if(alignType === 'query') {
                //ic.maxD2 = ic.maxD;
                //if(ic.maxD2 < ic.maxD1) ic.maxD = ic.maxD1;
                if(ic.oriMaxD < ic.maxD) ic.oriMaxD = ic.maxD;

                ic.center2 = ic.center;
                ic.center = new THREE.Vector3(0,0,0);
            }
        }

        //ic.oriMaxD = ic.maxD;
        ic.oriCenter = ic.center.clone();

        ic.saveFileCls.showTitle();

        data = {};

        return hAtoms;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SetSeqAlign {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    setSeqAlign(seqalign, alignedStructures) { let ic = this.icn3d, me = ic.icn3dui;
          let mmdbid1 = alignedStructures[0][0].pdbId;
          let mmdbid2 = alignedStructures[0][1].pdbId;

          ic.conservedName1 = mmdbid1 + '_cons';
          ic.nonConservedName1 = mmdbid1 + '_ncons';
          ic.notAlignedName1 = mmdbid1 + '_nalign';

          ic.conservedName2 = mmdbid2 + '_cons';
          ic.nonConservedName2 = mmdbid2 + '_ncons';
          ic.notAlignedName2 = mmdbid2 + '_nalign';

          ic.consHash1 = {};
          ic.nconsHash1 = {};
          ic.nalignHash1 = {};

          ic.consHash2 = {};
          ic.nconsHash2 = {};
          ic.nalignHash2 = {};

          for(let i = 0, il = seqalign.length; i < il; ++i) {
              // first sequence
              let alignData = seqalign[i][0];
              let molid1 = alignData.moleculeId;

              let chain1 = ic.pdbid_molid2chain[mmdbid1 + '_' + molid1];
              let chainid1 = mmdbid1 + '_' + chain1;

              let id2aligninfo = {};
              let start = alignData.sequence.length, end = -1;
              let bStart = false;
              for(let j = 0, jl = alignData.sequence.length; j < jl; ++j) {
                  // 0: internal resi id, 1: pdb resi id, 2: resn, 3: aligned or not
                  //let offset =(ic.chainid2offset[chainid1]) ? ic.chainid2offset[chainid1] : 0;
                  //let resi =(ic.bUsePdbNum) ? alignData.sequence[j][0] + offset : alignData.sequence[j][0];
                  let resi =(ic.bUsePdbNum) ? ic.ParserUtilsCls.getResi(chainid1, alignData.sequence[j][0] - 1) : alignData.sequence[j][0];
                  let resn =(alignData.sequence[j][2] === '~') ? '-' : alignData.sequence[j][2];
                  resn =(resn === ' ' || resn === '') ? 'X' : resn;
                  //resn = resn.toUpperCase();

                  let aligned =(alignData.sequence[j][3]) ? 1 : 0; // alignData.sequence[j][3]: 0, false, 1, true

                  if(aligned == 1) {
                      if(j < start && !bStart) {
                          start = j;
                          bStart = true; // set start just once
                      }
                      if(j > end) end = j;
                  }

                  id2aligninfo[j] = {"resi": resi, "resn": resn, "aligned": aligned};
              }

              // second sequence
              alignData = seqalign[i][1];
              let molid2 = alignData.moleculeId;

              let chain2 = ic.pdbid_molid2chain[mmdbid2 + '_' + molid2];
              let chainid2 = mmdbid2 + '_' + chain2;

              // annoation title for the master seq only
              if(ic.alnChainsAnTtl[chainid1] === undefined ) ic.alnChainsAnTtl[chainid1] = [];
              if(ic.alnChainsAnTtl[chainid1][0] === undefined ) ic.alnChainsAnTtl[chainid1][0] = [];
              if(ic.alnChainsAnTtl[chainid1][1] === undefined ) ic.alnChainsAnTtl[chainid1][1] = [];
              if(ic.alnChainsAnTtl[chainid1][2] === undefined ) ic.alnChainsAnTtl[chainid1][2] = [];
              if(ic.alnChainsAnTtl[chainid1][3] === undefined ) ic.alnChainsAnTtl[chainid1][3] = [];
              if(ic.alnChainsAnTtl[chainid1][4] === undefined ) ic.alnChainsAnTtl[chainid1][4] = [];
              if(ic.alnChainsAnTtl[chainid1][5] === undefined ) ic.alnChainsAnTtl[chainid1][5] = [];
              if(ic.alnChainsAnTtl[chainid1][6] === undefined ) ic.alnChainsAnTtl[chainid1][6] = [];

              // two annotations without titles
              ic.alnChainsAnTtl[chainid1][0].push(chainid2);
              ic.alnChainsAnTtl[chainid1][1].push(chainid1);
              ic.alnChainsAnTtl[chainid1][2].push("");
              ic.alnChainsAnTtl[chainid1][3].push("");

              // 2nd chain title
              ic.alnChainsAnTtl[chainid1][4].push(chainid2);
              // master chain title
              ic.alnChainsAnTtl[chainid1][5].push(chainid1);
              // empty line
              ic.alnChainsAnTtl[chainid1][6].push("");

              let alignIndex = 1;
              if(!ic.chainsMapping[chainid1]) ic.chainsMapping[chainid1] = {};
              if(!ic.chainsMapping[chainid2]) ic.chainsMapping[chainid2] = {};
              //for(let j = 0, jl = alignData.sseq.length; j < jl; ++j) {
              for(let j = start; j <= end; ++j) {
                  // 0: internal resi id, 1: pdb resi id, 2: resn, 3: aligned or not
                  //let offset =(ic.chainid2offset[chainid2]) ? ic.chainid2offset[chainid2] : 0;
                  //let resi =(ic.bUsePdbNum) ? alignData.sequence[j][0] + offset : alignData.sequence[j][0];
                  let resi =(ic.bUsePdbNum) ? ic.ParserUtilsCls.getResi(chainid2, alignData.sequence[j][0] - 1) : alignData.sequence[j][0];
                  let resn =(alignData.sequence[j][2] === '~') ? '-' : alignData.sequence[j][2];
                  //resn = resn.toUpperCase();

                  let alignedTmp =(alignData.sequence[j][3]) ? 1 : 0; // alignData.sequence[j][3]: 0, false, 1, true

                  let aligned = id2aligninfo[j].aligned + alignedTmp; // 0 or 2

                  let color, color2, classname;
                  if(aligned === 2) { // aligned
                      if(id2aligninfo[j].resn === resn) {
                          color = '#FF0000';
                          classname = 'icn3d-cons';

                          ic.consHash1[chainid1 + '_' + id2aligninfo[j].resi] = 1;
                          ic.consHash2[chainid2 + '_' + resi] = 1;
                      }
                      else {
                          color = '#0000FF';
                          classname = 'icn3d-ncons';

                          ic.nconsHash1[chainid1 + '_' + id2aligninfo[j].resi] = 1;
                          ic.nconsHash2[chainid2 + '_' + resi] = 1;
                      }

                      // mapping, use the firstsequence as the reference structure
                      ic.chainsMapping[chainid1][chainid1 + '_' + id2aligninfo[j].resi] = id2aligninfo[j].resn + id2aligninfo[j].resi;
                      ic.chainsMapping[chainid2][chainid2 + '_' + resi] = id2aligninfo[j].resn + id2aligninfo[j].resi;

                      color2 = '#' + ic.showAnnoCls.getColorhexFromBlosum62(id2aligninfo[j].resn, resn);

                      // expensive and thus remove
                      //alignedAtoms = me.hashUtilsCls.unionHash(alignedAtoms, ic.residues[chainid1 + '_' + id2aligninfo[j].resi]);
                      //alignedAtoms = me.hashUtilsCls.unionHash(alignedAtoms, ic.residues[chainid2 + '_' + resi]);
                  }
                  else {
                      color = me.htmlCls.GREY8;
                      classname = 'icn3d-nalign';

                      ic.nalignHash1[chainid1 + '_' + id2aligninfo[j].resi] = 1;
                      ic.nalignHash2[chainid2 + '_' + resi] = 1;
                  }

                  // chain1
                  if(ic.alnChainsSeq[chainid1] === undefined) ic.alnChainsSeq[chainid1] = [];

                  let resObject = {};
                  resObject.mmdbid = mmdbid1;
                  resObject.chain = chain1;
                  resObject.resi = id2aligninfo[j].resi;
                  // resi will be empty if there is no coordinates
                  resObject.resn =(resObject.resi === '' || classname === 'icn3d-nalign') ? id2aligninfo[j].resn.toLowerCase() : id2aligninfo[j].resn;
                  resObject.aligned = aligned;
                  // resi will be empty if there is no coordinates
                  resObject.color =(resObject.resi === '') ? me.htmlCls.GREYC : color; // color by identity
                  resObject.color2 =(resObject.resi === '') ? me.htmlCls.GREYC : color2; // color by conservation
                  resObject.class = classname;

                  ic.alnChainsSeq[chainid1].push(resObject);

                  if(id2aligninfo[j].resi !== '') {
                      if(ic.alnChains[chainid1] === undefined) ic.alnChains[chainid1] = {};
                      $.extend(ic.alnChains[chainid1], ic.residues[chainid1 + '_' + id2aligninfo[j].resi] );
                  }

                  // chain2
                  if(ic.alnChainsSeq[chainid2] === undefined) ic.alnChainsSeq[chainid2] = [];

                  resObject = {};
                  resObject.mmdbid = mmdbid2;
                  resObject.chain = chain2;
                  resObject.resi = resi;
                  // resi will be empty if there is no coordinates
                  resObject.resn =(resObject.resi === '' || classname === 'icn3d-nalign') ? resn.toLowerCase() : resn;
                  resObject.aligned = aligned;
                  // resi will be empty if there is no coordinates
                  resObject.color =(resObject.resi === '') ? me.htmlCls.GREYC : color; // color by identity
                  resObject.color2 =(resObject.resi === '') ? me.htmlCls.GREYC : color2; // color by conservation
                  resObject.class = classname;

                  ic.alnChainsSeq[chainid2].push(resObject);

                  if(resObject.resi !== '') {
                      if(ic.alnChains[chainid2] === undefined) ic.alnChains[chainid2] = {};
                      $.extend(ic.alnChains[chainid2], ic.residues[chainid2 + '_' + resi] );
                  }

                  // annotation is for the master seq only
                  if(ic.alnChainsAnno[chainid1] === undefined ) ic.alnChainsAnno[chainid1] = [];
                  if(ic.alnChainsAnno[chainid1][0] === undefined ) ic.alnChainsAnno[chainid1][0] = [];
                  if(ic.alnChainsAnno[chainid1][1] === undefined ) ic.alnChainsAnno[chainid1][1] = [];
                  if(ic.alnChainsAnno[chainid1][2] === undefined ) ic.alnChainsAnno[chainid1][2] = [];
                  if(ic.alnChainsAnno[chainid1][3] === undefined ) ic.alnChainsAnno[chainid1][3] = [];
                  if(j === start) {
                      // empty line
                      // 2nd chain title
                      if(ic.alnChainsAnno[chainid1][4] === undefined ) ic.alnChainsAnno[chainid1][4] = [];
                      // master chain title
                      if(ic.alnChainsAnno[chainid1][5] === undefined ) ic.alnChainsAnno[chainid1][5] = [];
                      // empty line
                      if(ic.alnChainsAnno[chainid1][6] === undefined ) ic.alnChainsAnno[chainid1][6] = [];

                      ic.alnChainsAnno[chainid1][4].push(ic.pdbid_chain2title[chainid2]);
                      ic.alnChainsAnno[chainid1][5].push(ic.pdbid_chain2title[chainid1]);
                      ic.alnChainsAnno[chainid1][6].push('');
                  }

                  let residueid1 = chainid1 + '_' + id2aligninfo[j].resi;
                  let residueid2 = chainid2 + '_' + resi;
                  let ss1 = ic.secondaries[residueid1];
                  let ss2 = ic.secondaries[residueid2];
                  if(ss2) {
                      ic.alnChainsAnno[chainid1][0].push(ss2);
                  }
                  else {
                      ic.alnChainsAnno[chainid1][0].push('-');
                  }

                  if(ss1) {
                      ic.alnChainsAnno[chainid1][1].push(ss1);
                  }
                  else {
                      ic.alnChainsAnno[chainid1][1].push('-');
                  }

                  let symbol = '.';
                  if(alignIndex % 5 === 0) symbol = '*';
                  if(alignIndex % 10 === 0) symbol = '|';
                  ic.alnChainsAnno[chainid1][2].push(symbol); // symbol: | for 10th, * for 5th, . for rest

                  let numberStr = '';
                  if(alignIndex % 10 === 0) numberStr = alignIndex.toString();
                  ic.alnChainsAnno[chainid1][3].push(numberStr); // symbol: 10, 20, etc, empty for rest

                  ++alignIndex;
              } // end for(let j           
          } // end for(let i

          seqalign = {};
    }

    getPosFromResi(chainid, resi) { let ic = this.icn3d; ic.icn3dui;
/*        
        let pos = undefined; //parseInt(resi);

        for(let i = 0, il = ic.chainsSeq[chainid].length; i < il; ++i) {
            if(ic.chainsSeq[chainid][i].resi == resi) {
                pos = i;
                break;
            }
        }
*/
        let residNCBI = ic.resid2ncbi[chainid + '_' + resi];
        let pos = undefined;
        if(residNCBI) {
            let resiNCBI = residNCBI.substr(residNCBI.lastIndexOf('_') + 1);
            pos = resiNCBI - 1;
        }

        return pos;
    }

    getResnFromResi(chainid, resi) { let ic = this.icn3d, me = ic.icn3dui;
        let pos = this.getPosFromResi(chainid, resi);
        if(!pos) return '?';

        let resid = chainid + '_' + resi;
        let resn = '';

        if(ic.residues[resid] === undefined) {
            resn = (ic.chainsSeq[chainid][pos]) ? ic.chainsSeq[chainid][pos].name : '?';
        }
        else {
            resn = me.utilsCls.residueName2Abbr(ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]).resn.substr(0, 3));
        }

        return resn;
    }

    setSeqAlignChain(chainid, chainIndex, chainidArray) { let ic = this.icn3d, me = ic.icn3dui;
        let hAtoms = {};

          let bRealign = (chainidArray) ? true : false;
          let mmdbid1, mmdbid2, chain1, chain2, chainid1, chainid2, pos1, pos2;

          if(bRealign) { 
            // originally chainid2 is target,chainid1 is query
            // switch them so that chainid1 is the target
            chainid1 = chainidArray[1];
            chainid2 = chainidArray[0];

            chainIndex = chainidArray[2];

            pos1 = chainid1.indexOf('_');
            pos2 = chainid2.indexOf('_');

            mmdbid1 = chainid1.substr(0, pos1).toUpperCase();
            mmdbid2 = chainid2.substr(0, pos2).toUpperCase();

            chain1 = chainid1.substr(pos1 + 1);
            chain2 = chainid2.substr(pos1 + 1);

            if(mmdbid1 == mmdbid2 && chain1 == chain2) {
                let chainLen = ic.chainsSeq[mmdbid2 + '_' + chain2].length;
                ic.qt_start_end[chainIndex] =  {"q_start":1, "q_end": chainLen, "t_start":1, "t_end": chainLen};
            }
          }
          else {
            //var chainidArray = me.cfg.chainalign.split(',');
            let pos1 = chainidArray[0].indexOf('_');
            let pos2 = chainid.indexOf('_');

            mmdbid1 = ic.mmdbid_t; //ic.chainidArray[0].substr(0, pos1).toUpperCase();
            mmdbid2 = chainid.substr(0, pos2).toUpperCase();

            chain1 = chainidArray[0].substr(pos1 + 1);
            chain2 = chainid.substr(pos2 + 1);

            if(mmdbid1 == mmdbid2 && chain1 == chain2) {
                let chainLen = ic.chainsSeq[ic.mmdbid_q + '_' + ic.chain_q].length;
                ic.qt_start_end[chainIndex] =  {"q_start":1, "q_end": chainLen, "t_start":1, "t_end": chainLen};
            }

            chainid1 = mmdbid1 + "_" + chain1;
            chainid2 = mmdbid2 + "_" + chain2;

            if(mmdbid2 !== undefined && mmdbid2 === ic.mmdbid_t) ;
         }

          ic.conservedName1 = chainid1 + '_cons';
          ic.nonConservedName1 = chainid1 + '_ncons';
          ic.notAlignedName1 = chainid1 + '_nalign';

          ic.conservedName2 = chainid2 + '_cons';
          ic.nonConservedName2 = chainid2 + '_ncons';
          ic.notAlignedName2 = chainid2 + '_nalign';

          ic.consHash1 = {};
          ic.nconsHash1 = {};
          ic.nalignHash1 = {};

          ic.consHash2 = {};
          ic.nconsHash2 = {};
          ic.nalignHash2 = {};

          ic.alnChains = {};

          ic.alnChainsSeq[chainid1] = [];
          ic.alnChains[chainid1] = {};

          ic.alnChainsSeq[chainid2] = [];
          ic.alnChains[chainid2] = {};
          
          ic.alnChainsAnno[chainid1] = [];
          ic.alnChainsAnTtl[chainid1] = [];

          if(ic.alnChainsAnTtl[chainid1] === undefined ) ic.alnChainsAnTtl[chainid1] = [];
          for(let i = 0; i < 7; ++i) {
              if(ic.alnChainsAnTtl[chainid1][i] === undefined ) ic.alnChainsAnTtl[chainid1][i] = [];
          }

          // two annotations without titles
          ic.alnChainsAnTtl[chainid1][0].push(chainid2);
          ic.alnChainsAnTtl[chainid1][1].push(chainid1);
          ic.alnChainsAnTtl[chainid1][2].push("");
          ic.alnChainsAnTtl[chainid1][3].push("");

          // 2nd chain title
          ic.alnChainsAnTtl[chainid1][4].push(chainid2);
          // master chain title
          ic.alnChainsAnTtl[chainid1][5].push(chainid1);
          // empty line
          ic.alnChainsAnTtl[chainid1][6].push("");

          let color, color2, classname;
          let prevIndex1 = 0, prevIndex2 = 0;

          if(ic.qt_start_end[chainIndex] === undefined) return;

          let alignIndex = 1; // number of residues displayed in seq alignment
          if(!ic.chainsMapping[chainid1]) ic.chainsMapping[chainid1] = {};
          if(!ic.chainsMapping[chainid2]) ic.chainsMapping[chainid2] = {};

          let posChain1 = {}, posChain2 = {};
         
          for(let i = 0, il = ic.qt_start_end[chainIndex].length; i < il; ++i) {
            let start1, start2, end1, end2;
            if(bRealign) { // real residue numbers are stored
              start1 = parseInt(ic.qt_start_end[chainIndex][i].t_start);
              start2 = parseInt(ic.qt_start_end[chainIndex][i].q_start);
              end1 = parseInt(ic.qt_start_end[chainIndex][i].t_end);
              end2 = parseInt(ic.qt_start_end[chainIndex][i].q_end); 
            }
            else {
              start1 = parseInt(ic.qt_start_end[chainIndex][i].t_start - 1);
              start2 = parseInt(ic.qt_start_end[chainIndex][i].q_start - 1);
              end1 = parseInt(ic.qt_start_end[chainIndex][i].t_end - 1);
              end2 = parseInt(ic.qt_start_end[chainIndex][i].q_end - 1);  
            }

            posChain1[start1] = 1;
            posChain1[end1] = 1;

            posChain2[start2] = 1;
            posChain2[end2] = 1;
          }

          for(let i = 0, il = ic.qt_start_end[chainIndex].length; i < il; ++i) {
              let start1, start2, end1, end2;
              if(bRealign) { // real residue numbers are stored
                start1 = parseInt(ic.qt_start_end[chainIndex][i].t_start);
                start2 = parseInt(ic.qt_start_end[chainIndex][i].q_start);
                end1 = parseInt(ic.qt_start_end[chainIndex][i].t_end);
                end2 = parseInt(ic.qt_start_end[chainIndex][i].q_end);
              }
              else {
                start1 = parseInt(ic.qt_start_end[chainIndex][i].t_start - 1);
                start2 = parseInt(ic.qt_start_end[chainIndex][i].q_start - 1);
                end1 = parseInt(ic.qt_start_end[chainIndex][i].t_end - 1);
                end2 = parseInt(ic.qt_start_end[chainIndex][i].q_end - 1);  
              }

              if(i > 0) {
                  let index1 = alignIndex;

                  for(let j = prevIndex1 + 1, jl = start1; j < jl; ++j) {
                      if(posChain1[j]) continue;
                      posChain1[j] = 1;

                      if(ic.chainsSeq[chainid1] === undefined || ic.chainsSeq[chainid1][j] === undefined) break;

                      let resi = (bRealign) ? j : ic.chainsSeq[chainid1][j].resi;
                      let resn = (bRealign) ? this.getResnFromResi(chainid1, j).toLowerCase() : ic.chainsSeq[chainid1][j].name.toLowerCase();
                      
                      if(resn == '?') continue;

                      color = me.htmlCls.GREY8;
                      classname = 'icn3d-nalign';

                      ic.nalignHash1[chainid1 + '_' + resi] = 1;
                      this.setSeqPerResi(chainid1, chainid1, chainid2, resi, resn, false, color, undefined, classname, true, false, index1);
                      ++index1;
                  }

                  let index2 = alignIndex;
                  for(let j = prevIndex2 + 1, jl = start2; j < jl; ++j) {
                      if(posChain2[j]) continue;
                      posChain2[j] = 1;

                      if(ic.chainsSeq[chainid2] === undefined || ic.chainsSeq[chainid2] === undefined) break;
                      
                      //let resi = ic.chainsSeq[chainid2][j].resi;
                      //let resn = ic.chainsSeq[chainid2][j].name.toLowerCase();
                      let resi = (bRealign) ? j : ic.chainsSeq[chainid2][j].resi;
                      let resn = (bRealign) ? this.getResnFromResi(chainid2, j).toLowerCase() : ic.chainsSeq[chainid2][j].name.toLowerCase();

                      if(resn == '?') continue;

                      color = me.htmlCls.GREY8;
                      classname = 'icn3d-nalign';

                      ic.nalignHash2[chainid2 + '_' + resi] = 1;
                      this.setSeqPerResi(chainid2, chainid1, chainid2, resi, resn, false, color, undefined, classname, false, false, index2);
                      ++index2; // count just once
                  }

                  if(index1 < index2) {
                      alignIndex = index2;

                      for(let j = 0; j < index2 - index1; ++j) {
                          let resi = '';
                          let resn = '-';

                          color = me.htmlCls.GREY8;
                          classname = 'icn3d-nalign';

                          this.setSeqPerResi(chainid1, chainid1, chainid2, resi, resn, false, color, undefined, classname, true, false, index1 + j);
                      }
                  }
                  else {
                      alignIndex = index1;

                      for(let j = 0; j < index1 - index2; ++j) {
                          let resi = '';
                          let resn = '-';

                          color = me.htmlCls.GREY8;
                          classname = 'icn3d-nalign';

                          this.setSeqPerResi(chainid2, chainid1, chainid2, resi, resn, false, color, undefined, classname, false, false, index2 + j);
                      }
                  }
              }
            
              for(let j = 0; j <= end1 - start1; ++j) {
                  if(ic.chainsSeq[chainid1] === undefined || ic.chainsSeq[chainid2] === undefined) break;

                  let resi1, resi2, resn1, resn2;
                  if(bRealign) {
                    resi1 = j + start1;
                    resi2 = j + start2;

                    resn1 = this.getResnFromResi(chainid1, resi1).toUpperCase();
                    resn2 = this.getResnFromResi(chainid2, resi2).toUpperCase();

                    if(resn1 == '?' || resn2 == '?') continue;
                  }
                  else {
                    if(ic.chainsSeq[chainid1][j + start1] === undefined || ic.chainsSeq[chainid2][j + start2] === undefined) continue;

                    resi1 = ic.chainsSeq[chainid1][j + start1].resi;
                    resi2 = ic.chainsSeq[chainid2][j + start2].resi;
                    resn1 = ic.chainsSeq[chainid1][j + start1].name.toUpperCase();
                    resn2 = ic.chainsSeq[chainid2][j + start2].name.toUpperCase();
                  }

                  if(resn1 === resn2) {
                      color = '#FF0000';
                      classname = 'icn3d-cons';

                      ic.consHash1[chainid1 + '_' + resi1] = 1;
                      ic.consHash2[chainid2 + '_' + resi2] = 1;
                  }
                  else {
                      color = '#0000FF';
                      classname = 'icn3d-ncons';

                      ic.nconsHash1[chainid1 + '_' + resi1] = 1;
                      ic.nconsHash2[chainid2 + '_' + resi2] = 1;
                  }

                  hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.residues[chainid1 + '_' + resi1]);
                  hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.residues[chainid2 + '_' + resi2]);

                  // mapping, use the firstsequence as the reference structure
                  ic.chainsMapping[chainid1][chainid1 + '_' + resi1] = resn1 + resi1;
                  ic.chainsMapping[chainid2][chainid2 + '_' + resi2] = resn1 + resi1;

                  color2 = '#' + ic.showAnnoCls.getColorhexFromBlosum62(resn1, resn2);

                  let bFirstResi =(i === 0 && j === 0) ? true : false;
                  this.setSeqPerResi(chainid1, chainid1, chainid2, resi1, resn1, true, color, color2, classname, true, bFirstResi, alignIndex);
                  this.setSeqPerResi(chainid2, chainid1, chainid2, resi2, resn2, true, color, color2, classname, false, bFirstResi, alignIndex);

                  ++alignIndex;
              } // end for(let j

              prevIndex1 = end1;
              prevIndex2 = end2;
          } // end for(let i

          return hAtoms;
    }

    setSeqAlignChainForAll(chainidArray, index_alignLen, bRealign) { let ic = this.icn3d, me = ic.icn3dui;
        let hAtoms = {};

        let chainid1 = chainidArray[0];

        ic.alnChainsAnno[chainid1] = [];

        // 1. assign ic.alnChainsAnTtl
        ic.alnChainsAnTtl[chainid1] = [];

        let n = chainidArray.length;

        // Title
        if(ic.alnChainsAnTtl[chainid1] === undefined ) ic.alnChainsAnTtl[chainid1] = [];
        for(let i = 0; i < 3 + 2*n; ++i) {
            if(ic.alnChainsAnTtl[chainid1][i] === undefined ) ic.alnChainsAnTtl[chainid1][i] = [];
        }

        for(let i = 0; i < n; ++i) {
            ic.alnChainsAnTtl[chainid1][i].push(chainidArray[n-1 - i]);
        }

        // two annotations without titles
        ic.alnChainsAnTtl[chainid1][n].push("");
        ic.alnChainsAnTtl[chainid1][n + 1].push("");

        for(let i = n + 2; i < 2*n + 2; ++i) {
            ic.alnChainsAnTtl[chainid1][i].push(chainidArray[2*n + 1 - i]);
        }

        // empty line
        ic.alnChainsAnTtl[chainid1][2*n + 2].push("");

        // 2. assign ic.alnChainsSeq and ic.alnChains for all chains
        ic.alnChainsSeq[chainid1] = [];

        ic.alnChains = {};
        ic.alnChains[chainid1] = {};      

        let resi2range_t = {}; // aaccumulative aligned residues in the template chain
        // start and end of MSA
        let start_t = 9999, end_t = -1;

        let baseResi = ic.chainsSeq[chainid1][0].resi - 1;
        for(let index = 1, indexl = chainidArray.length; index < indexl; ++index) { 
            let chainIndex = index - 1;
            if(!ic.qt_start_end[chainIndex]) continue;

            for(let i = 0, il = ic.qt_start_end[chainIndex].length; i < il; ++i) {
                let start1, end1;
                // if(bRealign) { // real residue numbers are stored
                //     start1 = ic.qt_start_end[chainIndex][i].t_start;
                //     end1 = ic.qt_start_end[chainIndex][i].t_end;
                // }
                // else {
                    start1 = parseInt(ic.qt_start_end[chainIndex][i].t_start) - 1;
                    end1 = parseInt(ic.qt_start_end[chainIndex][i].t_end) - 1;
                // }
                for(let j = start1; j <= end1; ++j) {
                    let resiPos = (bRealign || me.cfg.aligntool != 'tmalign') ? j : j - baseResi;
                    let resi = ic.ParserUtilsCls.getResi(chainidArray[0], resiPos);
                    resi2range_t[resi] = 1;
                    if(j < start_t) start_t = j;
                    if(j > end_t) end_t = j;
                }
            }
        }
        
        // TM-align should use "start1 = ic.qt_start_end[chainIndex][i].t_start - 1", but the rest are the same as ""bRealign"
        if(me.cfg.aligntool == 'tmalign') bRealign = true; // real residue numbers are stored

        let resi2rangeArray = Object.keys(resi2range_t);
        resi2rangeArray.sort(function(a, b) {
            return parseInt(a) - parseInt(b);
        });

        // assign range to each resi
        let prevResi = -999, start = 0, end = 0, resiArray = [], prevEnd = 0;
        for(let i = 0, il = resi2rangeArray.length; i < il; ++i) {
            let resi = resi2rangeArray[i];
            
            if(i == 0) {
                start = resi;
            }
            else if(i > 0 && ic.resid2ncbi[resi] != ic.resid2ncbi[prevResi] + 1 && ic.resid2ncbi[resi] != ic.resid2ncbi[prevResi]) { // new start
                end = prevResi;
                for(let j = 0, jl = resiArray.length; j < jl; ++j) {
                    resi2range_t[resiArray[j]] = {resiStart: start, resiEnd: end, prevResiEnd: prevEnd};
                }

                resiArray = [];
                start = resi;
                prevEnd = end;
            }

            resiArray.push(resi);

            prevResi = resi;
        }

        end = prevResi;
        for(let j = 0, jl = resiArray.length; j < jl; ++j) {
            resi2range_t[resiArray[j]] = {resiStart: start, resiEnd: end, prevResiEnd: prevEnd};
        }

        for(let i = 0, il = chainidArray.length; i < il; ++i) { 
            let chainid = chainidArray[i];
            ic.alnChainsSeq[chainid] = [];
            ic.alnChains[chainid] = {}; 

            ic.alnChainsAnno[chainid] = []; 
        }

        // fill the template ic.alnChainsSeq[chainid1]
        for(let j = 0, jl = ic.chainsSeq[chainid1].length; j < jl; ++j) { 
            let resi = ic.chainsSeq[chainid1][j].resi;

            let jAdjusted = (me.cfg.aligntool != 'tmalign') ? j : j + baseResi;

            //if(j + baseResi < start_t || j + baseResi > end_t) {
            if(jAdjusted < start_t || jAdjusted > end_t) {    
                continue;
            }

            let resObject = {};
            let pos = chainid1.indexOf('_');
            resObject.mmdbid = chainid1.substr(0, pos);
            resObject.chain = chainid1.substr(pos+1);
            resObject.resi = resi;
            resObject.resn = (resi2range_t[resi]) ? ic.chainsSeq[chainid1][j].name.toUpperCase() : ic.chainsSeq[chainid1][j].name.toLowerCase();
            resObject.aligned = (resi2range_t[resi]) ? true : false;
            resObject.color = (resi2range_t[resi]) ? '#FF0000' : me.htmlCls.GREYC; // color by identity
            resObject.color2 = (resi2range_t[resi]) ? '#FF0000' : me.htmlCls.GREYC; // color by conservation
            resObject.class = (resi2range_t[resi]) ? 'icn3d-align' : 'icn3d-nalign';
    
            ic.alnChainsSeq[chainid1].push(resObject);

            if(resi2range_t[resi]) {
                $.extend(ic.alnChains[chainid1], ic.residues[chainid1 + '_' + resObject.resi] );
                hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.residues[chainid1 + '_' + resObject.resi]);
            }
        }

        // progressively merge sequences, starting from most similar to least similar
        // assign ic.alnChainsSeq
        let alignedChainIndice = [0];
        for(let arrayIndex = 0, arrayIndexl = index_alignLen.length; arrayIndex < arrayIndexl; ++arrayIndex) { 
            let index = index_alignLen[arrayIndex].index;
            alignedChainIndice.push(index);
            let hAtomsTmp = this.mergeTwoSeqForAll(chainidArray, index, alignedChainIndice, resi2range_t, start_t, end_t, bRealign);

            hAtoms = me.hashUtilsCls.unionHash(hAtoms, hAtomsTmp);
        }      
          
        // 3. assign the varaible ic.alnChainsAnno
        for(let i = 0; i < 3 + 2*n; ++i) {
            if(ic.alnChainsAnno[chainid1][i] === undefined ) ic.alnChainsAnno[chainid1][i] = [];
        }

        // secondary structures
        for(let i = 0; i < n; ++i) {
            let chainid = chainidArray[i];

            for(let j = 0, jl = ic.alnChainsSeq[chainid].length; j < jl; ++j) {
                let resn = ic.alnChainsSeq[chainid][j].resn;
                if(resn == '-') {
                    ic.alnChainsAnno[chainid1][n - 1 - i].push('-');  
                }
                else {
                    let resi = ic.alnChainsSeq[chainid][j].resi;
                    let residueid = chainid + '_' + resi;
                    let ss = ic.secondaries[residueid];

                    // push the annotations to the template chain
                    if(ss !== undefined) {
                        ic.alnChainsAnno[chainid1][n - 1 - i].push(ss);
                    }
                    else {
                        ic.alnChainsAnno[chainid1][n - 1 - i].push('-');
                    }
                }
            }
        }

        // residue number 
        for(let alignIndex = 0, alignIndexl = ic.alnChainsSeq[chainid1].length; alignIndex < alignIndexl; ++alignIndex) {
            let symbol = '.';
            if(alignIndex % 5 === 0) symbol = '*';
            if(alignIndex % 10 === 0) symbol = '|';
            ic.alnChainsAnno[chainid1][n].push(symbol); // symbol: | for 10th, * for 5th, . for rest

            let numberStr = '';
            if(alignIndex % 10 === 0) numberStr = alignIndex.toString();
            ic.alnChainsAnno[chainid1][n + 1].push(numberStr); // symbol: 10, 20, etc, empty for rest
        }

        // title
        for(let i = n + 2; i < 2*n + 2; ++i) { // reverse order
            let title = ic.pdbid_chain2title && ic.pdbid_chain2title.hasOwnProperty(chainidArray[2*n + 1 - i]) ? ic.pdbid_chain2title[chainidArray[2*n + 1 - i]] : "";
            ic.alnChainsAnno[chainid1][i].push(title);
        }

        // empty line
        ic.alnChainsAnno[chainid1][2*n + 2].push("");    
        
        return hAtoms;
    }

    getResObject(chainid, bGap, bAligned, resi, resn, resn_t) { let ic = this.icn3d, me = ic.icn3dui;
        let resObject = {};
        let pos = chainid.indexOf('_');
        resObject.mmdbid = chainid.substr(0, pos);
        resObject.chain = chainid.substr(pos+1);
        resObject.resi = (bGap) ? '' : resi; // resi will be empty if there is no coordinates
        resObject.resn = (bGap) ? '-' : ((bAligned) ? resn.toUpperCase() : resn.toLowerCase());
        resObject.aligned = (bGap) ? false : bAligned;
        resObject.color = (bGap || !bAligned) ? me.htmlCls.GREYC : ((resn == resn_t) ? "#FF0000" : "#0000FF"); // color by identity
        resObject.color2 = (bGap || !bAligned) ? me.htmlCls.GREYC : '#' + ic.showAnnoCls.getColorhexFromBlosum62(resn, resn_t); // color by conservation
        resObject.class = (bGap || !bAligned) ? 'icn3d-nalign' : 'icn3d-align';

        return resObject;
    }

    getResn(chainid, resiPos) { let ic = this.icn3d; ic.icn3dui;
        let resn;
  
        // if(bRealign) {
        //     let resid = chainid + '_' + resiPos;

        //     if(ic.residues[resid] === undefined) {
        //         resn = '';
        //     }
        //     else {
        //         resn = me.utilsCls.residueName2Abbr(ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]).resn.substr(0, 3));
        //     }
        // }
        // else {
            if(!ic.chainsSeq[chainid] || !ic.chainsSeq[chainid][resiPos]) {
                resn = '';
            }
            else {
                resn = ic.chainsSeq[chainid][resiPos].name;
            }
        // }

        return resn;
    }

    getResiPosInTemplate(chainid1, resi_t) { let ic = this.icn3d; ic.icn3dui;
        // check the number of gaps before resiStart1 (nGap), and insert 'notAlnLen2 - notAlnLen1 - nGap' gaps
        let nGap = 0;

        let pos_t; // position to add gap

        if(ic.alnChainsSeq[chainid1]) {
            for(let j = 0, jl = ic.alnChainsSeq[chainid1].length; j < jl; ++j) {
                //add gap before the mapping region       
                if(parseInt(ic.alnChainsSeq[chainid1][j].resi) == parseInt(resi_t)) {
                    pos_t = j;
                    break;
                }

                if(ic.alnChainsSeq[chainid1][j].resn == '-') {
                    ++nGap;
                }
                else {
                    nGap = 0;
                }
            }
        }

        return {"pos": pos_t, "ngap": nGap};
    }

    addGapAllAlnChains(chainidArray, alignedChainIndice, chainid1, resi_t, len) { let ic = this.icn3d; ic.icn3dui;    
        let result = this.getResiPosInTemplate(chainid1, resi_t);
        result.ngap; let pos_t = result.pos;

        // add gaps for all previously aligned sequences, not the current sequence, which is the last one
        for(let j = 0, jl = alignedChainIndice.length - 1; j < jl; ++j) {
            let chainidTmp = chainidArray[alignedChainIndice[j]];
            let gapResObject = this.getResObject(chainidTmp, true);
            //for(let k = 0, kl = len - nGap; k < kl; ++k) {
            for(let k = 0, kl = len; k < kl; ++k) {
                ic.alnChainsSeq[chainidTmp].splice(pos_t, 0, gapResObject);
            }
        }

        //return len - nGap;
    }

    insertNotAlignRes(chainid, start, len, bRealign) { let ic = this.icn3d; ic.icn3dui;
        // insert non-aligned residues in query seq
        for(let j = 0, jl = len; j < jl; ++j) {
            let resi2 = ic.ParserUtilsCls.getResi(chainid, start + j);
            let resn2 = this.getResn(chainid, start + j);
            let resn1 = '-';
            let bAlign = false;
            let resObject = this.getResObject(chainid, false, bAlign, resi2, resn2, resn1);
            ic.alnChainsSeq[chainid].push(resObject);
        }
    }

    getTemplatePosFromOriPos(chainid1, start, end, bRealign) { let ic = this.icn3d; ic.icn3dui;
        let startResi = ic.ParserUtilsCls.getResi(chainid1, start);
        let endResi = ic.ParserUtilsCls.getResi(chainid1, end);
            
        let result1 = this.getResiPosInTemplate(chainid1, startResi);
        let result2 = this.getResiPosInTemplate(chainid1, endResi);
        
        return {"pos1": result1.pos, "pos2": result2.pos};
    }

    mergeTwoSeqForAll(chainidArray, index, alignedChainIndice, resi2range_t, start_t, end_t, bRealign) { let ic = this.icn3d, me = ic.icn3dui;
        let hAtoms = {};

        let chainid = chainidArray[index];
        let chainIndex = index - 1;

        //loadSeqAlignment
        let mmdbid1, mmdbid2, chain1, chain2, chainid1, chainid2;
        let pos1, pos2;

        pos1 = chainidArray[0].indexOf('_');
        pos2 = chainid.indexOf('_');

        //mmdbid1 = ic.mmdbid_t; 
        mmdbid1 = chainidArray[0].substr(0, pos1); //.toUpperCase();
        mmdbid2 = chainid.substr(0, pos2); //.toUpperCase();

        chain1 = chainidArray[0].substr(pos1 + 1);
        chain2 = chainid.substr(pos2 + 1);

        if(mmdbid1 == mmdbid2 && chain1 == chain2) {
            let chainLen = ic.chainsSeq[ic.mmdbid_q + '_' + ic.chain_q].length;
            ic.qt_start_end[chainIndex] =  {"q_start":1, "q_end": chainLen, "t_start":1, "t_end": chainLen};
        }

        chainid1 = mmdbid1 + "_" + chain1;
        chainid2 = mmdbid2 + "_" + chain2;

        if(mmdbid2 !== undefined && mmdbid2 === ic.mmdbid_t) ;

        //ic.alnChainsSeq[chainid2] = [];
        ic.alnChains[chainid2] = {};

        //ic.conservedName1 = chainid1 + '_cons';
        //ic.nonConservedName1 = chainid1 + '_ncons';
        //ic.notAlignedName1 = chainid1 + '_nalign';

        ic.conservedName2 = chainid2 + '_cons';
        ic.nonConservedName2 = chainid2 + '_ncons';
        ic.notAlignedName2 = chainid2 + '_nalign';

        //ic.consHash1 = {};
        //ic.nconsHash1 = {};
        //ic.nalignHash1 = {};

        ic.consHash2 = {};
        ic.nconsHash2 = {};
        ic.nalignHash2 = {};
        let prevIndex1, prevIndex2;

        if(ic.qt_start_end[chainIndex] === undefined) return;

        this.getResObject(chainid1, true);
        let gapResObject2 = this.getResObject(chainid2, true);
        // ic.chainsMapping is used for reference number
        if(!ic.chainsMapping[chainid1]) ic.chainsMapping[chainid1] = {};
        if(!ic.chainsMapping[chainid2]) ic.chainsMapping[chainid2] = {};

        let result;

        for(let i = 0, il = ic.qt_start_end[chainIndex].length; i < il; ++i) {
            let start1, start2, end1, end2;
            if(bRealign) { // real residue numbers are stored
                start1 = parseInt(ic.qt_start_end[chainIndex][i].t_start);
                start2 = parseInt(ic.qt_start_end[chainIndex][i].q_start);
                end1 = parseInt(ic.qt_start_end[chainIndex][i].t_end);
                end2 = parseInt(ic.qt_start_end[chainIndex][i].q_end);  

                // start1 = this.getPosFromResi(chainid1, ic.qt_start_end[chainIndex][i].t_start);
                // start2 = this.getPosFromResi(chainid2, ic.qt_start_end[chainIndex][i].q_start);
                // end1 = this.getPosFromResi(chainid1, ic.qt_start_end[chainIndex][i].t_end);
                // end2 = this.getPosFromResi(chainid2, ic.qt_start_end[chainIndex][i].q_end);
            }
            else {
                start1 = parseInt(ic.qt_start_end[chainIndex][i].t_start - 1);
                start2 = parseInt(ic.qt_start_end[chainIndex][i].q_start - 1);
                end1 = parseInt(ic.qt_start_end[chainIndex][i].t_end - 1);
                end2 = parseInt(ic.qt_start_end[chainIndex][i].q_end - 1);  
            }

            // 1. before the mapped residues
            let resiStart1 = ic.ParserUtilsCls.getResi(chainid1, start1);

            //let range = resi2range_t[resiStart1];
  
            // if the mapping does not start from start_t, add gaps to the query seq
            if(i == 0) {
                result = this.getTemplatePosFromOriPos(chainid1, start_t, start1, bRealign);
                pos1 = result.pos1;
                pos2 = result.pos2;

                if(start1 > start_t) {
                    for(let j = 0, jl = pos2 - pos1; j < jl; ++j) {
                        ic.alnChainsSeq[chainid2].push(gapResObject2);
                    }
                }
            }
            else {
                //let notAlnLen1 = start1 - (prevIndex1 + 1);
                result = this.getTemplatePosFromOriPos(chainid1, prevIndex1, start1, bRealign);
                pos1 = result.pos1;
                pos2 = result.pos2;
                let notAlnLen1 = pos2 - (pos1 + 1);
                let notAlnLen2 = start2 - (prevIndex2 + 1);
                
                // insert non-aligned residues in query seq
                this.insertNotAlignRes(chainid2, prevIndex2+1, notAlnLen2, bRealign);

                if(notAlnLen1 >= notAlnLen2) {
                    // add gaps before the query sequence
                    for(let j = 0, jl = notAlnLen1 - notAlnLen2; j < jl; ++j) {
                        ic.alnChainsSeq[chainid2].push(gapResObject2);
                    }                       
                }
                else {
                    // check the number of gaps before resiStart1 (n), and insert 'notAlnLen2 - notAlnLen1 - n' gaps
                    this.addGapAllAlnChains(chainidArray, alignedChainIndice, chainid1, resiStart1, notAlnLen2 - notAlnLen1);
                }                           
            }

            // 2. In the mapped residues
            result = this.getTemplatePosFromOriPos(chainid1, start1, end1, bRealign);
            pos1 = result.pos1;
            pos2 = result.pos2;

            let k = 0;    
            for(let j = pos1; j <= pos2; ++j) {
                // inherit the gaps from the template
                if(ic.alnChainsSeq[chainid1][j].resn == '-') {
                    ic.alnChainsSeq[chainid2].push(gapResObject2);
                }
                else {                   
                    let resi2 = ic.ParserUtilsCls.getResi(chainid2, start2 + k);
                    let resn2 = this.getResn(chainid2, start2 + k);
                    let resn1 = this.getResn(chainid1, start1 + k);
                    let bAlign = true;
                    let resObject = this.getResObject(chainid2, false, bAlign, resi2, resn2, resn1);
                    ic.alnChainsSeq[chainid2].push(resObject);
                    // update color in the template
                    ic.alnChainsSeq[chainid1][j].color = resObject.color;

                    //if(ic.alnChains[chainid2] === undefined) ic.alnChains[chainid2] = {}
                    $.extend(ic.alnChains[chainid2], ic.residues[chainid2 + '_' + resi2] );
                    hAtoms = me.hashUtilsCls.unionHash(hAtoms, ic.residues[chainid2 + '_' + resi2]);

                    ++k;
                }
            }
            
            prevIndex1 = end1;
            prevIndex2 = end2;  
        }  

        // add gaps at the end
        result = this.getTemplatePosFromOriPos(chainid1, prevIndex1, end_t, bRealign);
        pos1 = result.pos1;
        pos2 = result.pos2;
        for(let i = pos1; i < pos2; ++i) {
            ic.alnChainsSeq[chainid2].push(gapResObject2);           
        }     

        return hAtoms;
    }

    setSeqAlignForRealign(chainid_t, chainid, chainIndex) { let ic = this.icn3d, me = ic.icn3dui;
          //var chainid_t = ic.chainidArray[0];

    //      let structureArray = Object.keys(ic.structures);
        //   let structure1 = chainid_t.substr(0, chainid_t.indexOf('_')); //structureArray[0];
        //   let structure2 = chainid.substr(0, chainid.indexOf('_')); //structureArray[1];

        //   if(structure1 == structure2) structure2 += me.htmlCls.postfix;

          ic.conservedName1 = chainid_t + '_cons';
          ic.conservedName2 = chainid + '_cons';

          ic.consHash1 = {};
          ic.consHash2 = {};

          ic.alnChainsAnTtl = {};
          ic.alnChainsAnno = {};

          if(ic.alnChainsSeq === undefined) ic.alnChainsSeq = {};
          ic.alnChains = {};

          ic.alnChainsSeq[chainid_t] = [];
          ic.alnChains[chainid_t] = {};
          ic.alnChainsAnno[chainid_t] = [];
          ic.alnChainsAnTtl[chainid_t] = [];

          ic.alnChainsSeq[chainid] = [];
          ic.alnChains[chainid] = {};

    //      let emptyResObject = {resid: '', resn:'', resi: 0, aligned: false}

    //      let prevChainid1 = '', prevChainid2 = '', cnt1 = 0, cnt2 = 0;

          let residuesHash = {};
          if(!ic.chainsMapping[chainid_t]) ic.chainsMapping[chainid_t] = {};
          if(!ic.chainsMapping[chainid]) ic.chainsMapping[chainid] = {};

          for(let i = 0, il = ic.realignResid[chainid_t].length; i < il; ++i) {
              let resObject1 = ic.realignResid[chainid_t][i];
              let pos1 = resObject1.resid.lastIndexOf('_');
              let chainid1 = resObject1.resid.substr(0, pos1);
              let resi1 = resObject1.resid.substr(pos1 + 1);
              resObject1.resi = resi1;
              resObject1.aligned = true;

              let resObject2 = ic.realignResid[chainid][i];
              let pos2 = resObject2.resid.lastIndexOf('_');
              let chainid2 = resObject2.resid.substr(0, pos2);
              let resi2 = resObject2.resid.substr(pos2 + 1);
              resObject2.resi = resi2;
              resObject2.aligned = true;

              residuesHash[resObject1.resid] = 1;
              residuesHash[resObject2.resid] = 1;

              let color;
              if(resObject1.resn.toUpperCase() == resObject2.resn.toUpperCase()) {
                  color = "#FF0000";
              }
              else {
                  color = "#0000FF";
              }

              // mapping, use the firstsequence as the reference structure
              ic.chainsMapping[chainid_t][chainid_t + '_' + resObject1.resi] = resObject1.resn + resObject1.resi;
              ic.chainsMapping[chainid][chainid + '_' + resObject2.resi] = resObject1.resn + resObject1.resi;

              let color2 = '#' + ic.showAnnoCls.getColorhexFromBlosum62(resObject1.resn, resObject2.resn);

              resObject1.color = color;
              resObject2.color = color;

              resObject1.color2 = color2;
              resObject2.color2 = color2;

              for(let j in ic.residues[resObject1.resid]) {
                  ic.atoms[j].color = me.parasCls.thr(color);
              }
              for(let j in ic.residues[resObject2.resid]) {
                  ic.atoms[j].color = me.parasCls.thr(color);
              }

              // annoation title for the master seq only
              if(ic.alnChainsAnTtl[chainid1] === undefined ) ic.alnChainsAnTtl[chainid1] = [];

              for(let j = 0; j < 3; ++j) {
                  if(ic.alnChainsAnTtl[chainid1][j] === undefined ) ic.alnChainsAnTtl[chainid1][j] = [];
              }

              // two annotations without titles
              for(let j = 0; j < 3; ++j) {
                  ic.alnChainsAnTtl[chainid1][j].push("");
              }

              if(ic.alnChainsSeq[chainid1] === undefined) ic.alnChainsSeq[chainid1] = [];
              if(ic.alnChainsSeq[chainid2] === undefined) ic.alnChainsSeq[chainid2] = [];

              ic.alnChainsSeq[chainid1].push(resObject1);
              ic.alnChainsSeq[chainid2].push(resObject2);

              if(ic.alnChains[chainid1] === undefined) ic.alnChains[chainid1] = {};
              if(ic.alnChains[chainid2] === undefined) ic.alnChains[chainid2] = {};
              $.extend(ic.alnChains[chainid1], ic.residues[chainid1 + '_' + resObject1.resi] );
              $.extend(ic.alnChains[chainid2], ic.residues[chainid2 + '_' + resObject2.resi] );

              ic.consHash1[chainid1 + '_' + resObject1.resi] = 1;
              ic.consHash2[chainid2 + '_' + resObject2.resi] = 1;

              // annotation is for the master seq only
              if(ic.alnChainsAnno[chainid1] === undefined ) ic.alnChainsAnno[chainid1] = [];
              //if(ic.alnChainsAnno[chainid2] === undefined ) ic.alnChainsAnno[chainid2] = [];

              for(let j = 0; j < 3; ++j) {
                  if(ic.alnChainsAnno[chainid1][j] === undefined ) ic.alnChainsAnno[chainid1][j] = [];
              }

              let symbol = '.';
              if(i % 5 === 0) symbol = '*';
              if(i % 10 === 0) symbol = '|';
              ic.alnChainsAnno[chainid1][0].push(symbol); // symbol: | for 10th, * for 5th, . for rest

              let numberStr = '';
              if(i % 10 === 0) numberStr = i.toString();
              ic.alnChainsAnno[chainid1][1].push(numberStr); // symbol: 10, 20, etc, empty for rest
          }

            let commandname = 'protein_aligned';
            let commanddescr = 'protein aligned';
            let select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residuesHash));
            ic.selectionCls.addCustomSelection(Object.keys(residuesHash), commandname, commanddescr, select, true);
    }

    setSeqPerResi(chainid, chainid1, chainid2, resi, resn, bAligned, color, color2, classname, bFirstChain, bFirstResi, alignIndex) { let ic = this.icn3d, me = ic.icn3dui;
        if(ic.alnChainsSeq[chainid] === undefined) ic.alnChainsSeq[chainid] = [];

        let resObject = {};
        let pos = chainid.indexOf('_');
        resObject.mmdbid = chainid.substr(0, pos);
        resObject.chain = chainid.substr(pos+1);
        resObject.resi = resi;
        // resi will be empty if there is no coordinates
        resObject.resn =(resObject.resi === '' || classname === 'icn3d-nalign') ? resn.toLowerCase() : resn;
        resObject.aligned = bAligned;
        // resi will be empty if there is no coordinates
        resObject.color =(resObject.resi === '') ? me.htmlCls.GREYC : color; // color by identity
        resObject.color2 =(resObject.resi === '') ? me.htmlCls.GREYC : color2; // color by conservation
        resObject.class = classname;

        ic.alnChainsSeq[chainid].push(resObject);

        if(resObject.resi !== '') {
            if(ic.alnChains[chainid] === undefined) ic.alnChains[chainid] = {};
            $.extend(ic.alnChains[chainid], ic.residues[chainid + '_' + resObject.resi] );
        }

        if(bFirstChain) {
            // annotation is for the master seq only
            if(ic.alnChainsAnno[chainid] === undefined ) ic.alnChainsAnno[chainid] = [];
            if(ic.alnChainsAnno[chainid][0] === undefined ) ic.alnChainsAnno[chainid][0] = [];
            if(ic.alnChainsAnno[chainid][1] === undefined ) ic.alnChainsAnno[chainid][1] = [];
            if(ic.alnChainsAnno[chainid][2] === undefined ) ic.alnChainsAnno[chainid][2] = [];
            if(ic.alnChainsAnno[chainid][3] === undefined ) ic.alnChainsAnno[chainid][3] = [];
            if(bFirstResi) {
                // empty line
                // 2nd chain title
                if(ic.alnChainsAnno[chainid][4] === undefined ) ic.alnChainsAnno[chainid][4] = [];
                // master chain title
                if(ic.alnChainsAnno[chainid][5] === undefined ) ic.alnChainsAnno[chainid][5] = [];
                // empty line
                if(ic.alnChainsAnno[chainid][6] === undefined ) ic.alnChainsAnno[chainid][6] = [];

                let title1 = ic.pdbid_chain2title && ic.pdbid_chain2title.hasOwnProperty(chainid2) ? ic.pdbid_chain2title[chainid2] : "";
                let title2 = ic.pdbid_chain2title && ic.pdbid_chain2title.hasOwnProperty(chainid) ? ic.pdbid_chain2title[chainid] : "";
                ic.alnChainsAnno[chainid][4].push(title1);
                ic.alnChainsAnno[chainid][5].push(title2);
                ic.alnChainsAnno[chainid][6].push('');
            }

            let symbol = '.';
            if(alignIndex % 5 === 0) symbol = '*';
            if(alignIndex % 10 === 0) symbol = '|';
            ic.alnChainsAnno[chainid][2].push(symbol); // symbol: | for 10th, * for 5th, . for rest

            let numberStr = '';
            if(alignIndex % 10 === 0) numberStr = alignIndex.toString();
            ic.alnChainsAnno[chainid][3].push(numberStr); // symbol: 10, 20, etc, empty for rest

            let residueid = chainid + '_' + resi;
            let ss = ic.secondaries[residueid];

            if(ss !== undefined) {
                ic.alnChainsAnno[chainid][1].push(ss);
            }
            else {
                ic.alnChainsAnno[chainid][1].push('-');
            }
        }
        else {
            let residueid = chainid + '_' + resi;
            let ss = ic.secondaries[residueid];

            if(ic.alnChainsAnno.hasOwnProperty(chainid1) && ic.alnChainsAnno[chainid1].length > 0) {
                if(ss !== undefined) {
                    ic.alnChainsAnno[chainid1][0].push(ss);
                }
                else {
                    ic.alnChainsAnno[chainid1][0].push('-');
                }
            }
            else {
                console.log("Error: ic.alnChainsAnno[chainid1] is undefined");
            }
        }
    }   
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class LoadPDB {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // modified from iview (http://istar.cse.cuhk.edu.hk/iview/)
    //This PDB parser feeds the viewer with the content of a PDB file, pdbData.
    loadPDB(src, pdbid, bOpm, bVector, bMutation, bAppend, type, bLastQuery) { let ic = this.icn3d, me = ic.icn3dui;
        let hAtoms = {};

        let bNMR = false;
        let lines = src.split('\n');

        let chainsTmp = {}; // serial -> atom
        let residuesTmp = {}; // serial -> atom

        if(!ic.atoms) bAppend = false;

        let serial, moleculeNum;
        if(!bMutation && !bAppend) {
            ic.init();
            moleculeNum = 1;
            serial = 0;
        }
        else {
            // remove the last structure
            // if(ic.alertAlt) {
            //     let nStru = ic.oriNStru + 1; //Object.keys(ic.structures).length;
            //     let  chainArray = ic.structures[nStru - 1];
            //     for(let i = 0, il = (chainArray) ? chainArray.length : 0; i < il; ++i) {
            //         for(let j in ic.chains[chainArray[i]]) {
            //             delete ic.atoms[j];
            //             delete ic.hAtoms[j];
            //             delete ic.dAtoms[j];
            //         }
            //         delete ic.chains[chainArray[i]];
            //     }

            //     delete ic.structures[nStru - 1];
            // }
            // else {
                ic.oriNStru = (ic.structures) ? Object.keys(ic.structures).length : 0;
            // }

            moleculeNum = ic.oriNStru + 1; //Object.keys(ic.structures).length + 1;
            // Concatenation of two pdbs will have several atoms for the same serial
            serial = (ic.atoms) ? Object.keys(ic.atoms).length : 0;
        }

        //let helices = [], sheets = [];
        let sheetArray = [], sheetStart = [], sheetEnd = [], helixArray = [], helixStart = [], helixEnd = [];

        let chainNum, residueNum, oriResidueNum;
        let prevChainNum = '', prevResidueNum = '', prevOriResidueNum = '';

        let oriSerial2NewSerial = {};

        //let chainMissingResidueArray = {}

        let id = (pdbid) ? pdbid : 'stru';
        let structure = id;

        let prevMissingChain = '';
        let CSerial, prevCSerial, OSerial, prevOSerial;
        
        let bHeader = false;

        for (let i in lines) {
            let line = lines[i];
            let record = line.substr(0, 6);

            if (record === 'HEADER' && !bHeader && !pdbid) {              
                // if(bOpm === undefined || !bOpm) ic.bSecondaryStructure = true;

                ///id = line.substr(62, 4).trim();
                id = line.substr(62).trim();

                if(id == '') {
                    if(bAppend) {
                        id = "stru";
                    }
                    else {
                        //if(!ic.inputid) ic.inputid = 'stru';
                        id = (ic.inputid && ic.inputid.indexOf('/') == -1) ? ic.inputid.substr(0, 10) : "stru"; //ic.filename.substr(0, 4);
                    }
                }

                structure = id;

                if(id == 'stru' || bMutation) { // bMutation: side chain prediction
                //if(id == 'stru') {
                        structure = (moleculeNum === 1) ? id : id + moleculeNum.toString();
                }

                ic.molTitle = '';

                bHeader = true; // read the first header if there are multiple
            } else if (record === 'TITLE ') {
                let name = line.substr(10).replace(/ALPHAFOLD MONOMER V2.0 PREDICTION FOR /gi, '');
                ic.molTitle += name.trim() + " ";

            } else if (record === 'HELIX ') {
                ic.bSecondaryStructure = true;

                //let startChain = (line.substr(19, 1) == ' ') ? 'A' : line.substr(19, 1);
                let startChain = (line.substr(18, 2).trim() == '') ? 'A' : line.substr(18, 2).trim();
                let startResi = parseInt(line.substr(21, 4));
                let endResi = parseInt(line.substr(33, 4));

                for(let j = startResi; j <= endResi; ++j) {
                  let resid = structure + "_" + startChain + "_" + j;
                  helixArray.push(resid);

                  if(j === startResi) helixStart.push(resid);
                  if(j === endResi) helixEnd.push(resid);
                }    
            } else if (record === 'SHEET ') {
                //ic.bSecondaryStructure = true;
                if(bOpm === undefined || !bOpm) ic.bSecondaryStructure = true;

                //let startChain = (line.substr(21, 1) == ' ') ? 'A' : line.substr(21, 1);
                let startChain = (line.substr(20, 2).trim() == '') ? 'A' : line.substr(20, 2).trim();
                let startResi = parseInt(line.substr(22, 4));
                let endResi = parseInt(line.substr(33, 4));

                for(let j = startResi; j <= endResi; ++j) {
                  let resid = structure + "_" + startChain + "_" + j;
                  sheetArray.push(resid);

                  if(j === startResi) sheetStart.push(resid);
                  if(j === endResi) sheetEnd.push(resid);
                }           
            } else if (record === 'HBOND ') {
                if(bOpm === undefined || !bOpm) ic.bSecondaryStructure = true;
            } else if (record === 'SSBOND') {
                ic.bSsbondProvided = true;
                //SSBOND   1 CYS E   48    CYS E   51                          2555
                let chain1 = (line.substr(15, 1) == ' ') ? 'A' : line.substr(15, 1);
                let resi1 = line.substr(17, 4).trim();
                let resid1 = id + '_' + chain1 + '_' + resi1;

                let chain2 = (line.substr(29, 1) == ' ') ? 'A' : line.substr(29, 1);
                let resi2 = line.substr(31, 4).trim();
                let resid2 = id + '_' + chain2 + '_' + resi2;

                if(ic.ssbondpnts[id] === undefined) ic.ssbondpnts[id] = [];

                ic.ssbondpnts[id].push(resid1);
                ic.ssbondpnts[id].push(resid2);
            } else if (record === 'REMARK') {
                 let remarkType = parseInt(line.substr(7, 3));

                 if(line.indexOf('1/2 of bilayer thickness:') !== -1) { // OPM transmembrane protein
                    ic.halfBilayerSize = parseFloat(line.substr(line.indexOf(':') + 1).trim());
                 }
                 else if (remarkType == 210) {
                     if((line.substr(11, 32).trim() == 'EXPERIMENT TYPE') && line.substr(45).trim() == 'NMR') {
                        bNMR = true;
                     }
                 }
                 else if (remarkType == 350 && line.substr(13, 5) == 'BIOMT') {
                    let n = parseInt(line[18]) - 1;
                    //var m = parseInt(line.substr(21, 2));
                    let m = parseInt(line.substr(21, 2)) - 1; // start from 1
                    if (ic.biomtMatrices[m] == undefined) ic.biomtMatrices[m] = new THREE.Matrix4().identity();
                    ic.biomtMatrices[m].elements[n] = parseFloat(line.substr(24, 9));
                    ic.biomtMatrices[m].elements[n + 4] = parseFloat(line.substr(34, 9));
                    ic.biomtMatrices[m].elements[n + 8] = parseFloat(line.substr(44, 9));
                    //ic.biomtMatrices[m].elements[n + 12] = parseFloat(line.substr(54, 10));
                    ic.biomtMatrices[m].elements[n + 12] = parseFloat(line.substr(54, 14));
                 }
                 // missing residues
                 else if (remarkType == 465 && line.substr(18, 1) == ' ' && line.substr(20, 1) == ' ' && line.substr(21, 1) != 'S') {
                    let resn = line.substr(15, 3);
                    //let chain = line.substr(19, 1);
                    let chain = line.substr(18, 2).trim();
                    //let resi = parseInt(line.substr(21, 5));
                    let resi = line.substr(21, 5).trim();

                    //var chainNum = structure + '_' + chain;
                    let chainNum = id + '_' + chain;

                    if(ic.chainMissingResidueArray[chainNum] === undefined) ic.chainMissingResidueArray[chainNum] = [];
                    let resObject = {};
                    resObject.resi = resi;
                    resObject.name = me.utilsCls.residueName2Abbr(resn).toLowerCase();

                    // not all listed residues are considered missing, e.g., PDB ID 4OR2, only the firts four residues are considered missing
                    //if(!isNaN(resi) && (prevMissingChain == '' || (chain != prevMissingChain) || (chain == prevMissingChain && resi > maxMissingResi)) ) {
                    if(prevMissingChain == '' || (chain != prevMissingChain) || (chain == prevMissingChain) ) {
                        ic.chainMissingResidueArray[chainNum].push(resObject);
                        prevMissingChain = chain;
                    }

                 }
                 else if (remarkType == 900 && ic.emd === undefined && line.substr(34).trim() == 'RELATED DB: EMDB') {
                     //REMARK 900 RELATED ID: EMD-3906   RELATED DB: EMDB
                     ic.emd = line.substr(23, 11).trim();
                 }
            } else if (record === 'SOURCE' && ic.organism === undefined && line.substr(11, 15).trim() == 'ORGANISM_COMMON') {
                ic.organism = line.substr(28).toLowerCase().trim();

                ic.organism = ic.organism.substr(0, ic.organism.length - 1);
            } else if (record === 'ENDMDL') {
                ++moleculeNum;
                id = 'stru';

                structure = id;
                
                if(id == 'stru' || bMutation) { // bMutation: side chain prediction
                //if(id == 'stru') {
                        structure = (moleculeNum === 1) ? id : id + moleculeNum.toString();
                }

                //helices = [];
                //sheets = [];
                if(!bNMR) {
                    sheetArray = [];
                    sheetStart = [];
                    sheetEnd = [];
                    helixArray = [];
                    helixStart = [];
                    helixEnd = [];
                }

                bHeader = false; // reinitialize to read structure name from the header
            } else if (record === 'JRNL  ') {
                if(line.substr(12, 4) === 'PMID') {
                    ic.pmid = line.substr(19).trim();
                }
            } else if (record === 'ATOM  ' || record === 'HETATM') {
                structure = id;
                
                if(id == 'stru' || bMutation) { // bMutation: side chain prediction
                //if(id == 'stru') {
                        structure = (moleculeNum === 1) ? id : id + moleculeNum.toString();
                }

                let alt = line.substr(16, 1);
                //if (alt !== " " && alt !== "A") continue;

                // "CA" has to appear before "O". Otherwise the cartoon of secondary structure will have breaks
                // Concatenation of two pdbs will have several atoms for the same serial
                ++serial;

                let serial2 = parseInt(line.substr(6, 5));
                oriSerial2NewSerial[serial2] = serial;

                let elem = line.substr(76, 2).trim();
                if (elem === '') { // for some incorrect PDB files, important to use substr(12,2), not (12,4)
                   elem = line.substr(12, 2).trim();
                }
                let atom = line.substr(12, 4).trim();
                let resn = line.substr(17, 3);

                //let chain = line.substr(21, 1);
                //if(chain === ' ') chain = 'A';
                let chain = line.substr(20, 2).trim();
                if(chain === '') chain = 'A';

                //var oriResi = line.substr(22, 4).trim();
                let oriResi = line.substr(22, 5).trim();

                let resi = oriResi; //parseInt(oriResi);

                if(bOpm && resn === 'DUM') {
                    elem = atom;
                    chain = 'MEM';
                    resi = 1;
                    oriResi = 1;
                }

                if(bVector && resn === 'DUM') break; // just need to get the vector of the largest chain

                chainNum = structure + "_" + chain;
                oriResidueNum = chainNum + "_" + oriResi;

                residueNum = chainNum + "_" + resi;

                //let chain_resi = chain + "_" + resi;

                let x = parseFloat(line.substr(30, 8));
                let y = parseFloat(line.substr(38, 8));
                let z = parseFloat(line.substr(46, 8));
                let coord = new THREE.Vector3(x, y, z);

                let atomDetails = {
                    het: record[0] === 'H', // optional, used to determine chemicals, water, ions, etc
                    serial: serial,         // required, unique atom id
                    name: atom,             // required, atom name
                    alt: alt,               // optional, some alternative coordinates
                    resn: resn,             // optional, used to determine protein or nucleotide
                    structure: structure,   // optional, used to identify structure
                    chain: chain,           // optional, used to identify chain
                    resi: resi,             // optional, used to identify residue ID
                    //insc: line.substr(26, 1),
                    coord: coord,           // required, used to draw 3D shape
                    b: parseFloat(line.substr(60, 8)), // optional, used to draw B-factor tube
                    elem: elem,             // optional, used to determine hydrogen bond
                    bonds: [],              // required, used to connect atoms
                    ss: 'coil',             // optional, used to show secondary structures
                    ssbegin: false,         // optional, used to show the beginning of secondary structures
                    ssend: false            // optional, used to show the end of secondary structures
                };

                if(!atomDetails.het && atomDetails.name === 'C') {
                    CSerial = serial;
                }
                if(!atomDetails.het && atomDetails.name === 'O') {
                    OSerial = serial;
                }

                // from DSSP C++ code
                if(!atomDetails.het && atomDetails.name === 'N' && prevCSerial !== undefined && prevOSerial !== undefined) {
                    let dist = ic.atoms[prevCSerial].coord.distanceTo(ic.atoms[prevOSerial].coord);

                    let x2 = atomDetails.coord.x + (ic.atoms[prevCSerial].coord.x - ic.atoms[prevOSerial].coord.x) / dist;
                    let y2 = atomDetails.coord.y + (ic.atoms[prevCSerial].coord.y - ic.atoms[prevOSerial].coord.y) / dist;
                    let z2 = atomDetails.coord.z + (ic.atoms[prevCSerial].coord.z - ic.atoms[prevOSerial].coord.z) / dist;

                    atomDetails.hcoord = new THREE.Vector3(x2, y2, z2);
                }

                ic.atoms[serial] = atomDetails;

                ic.dAtoms[serial] = 1;
                ic.hAtoms[serial] = 1;
                hAtoms[serial] = 1;

                // Assign secondary structures from the input
                // if a residue is assigned both sheet and helix, it is assigned as sheet
                if(this.isSecondary(residueNum, sheetArray, bNMR)) {
                  ic.atoms[serial].ss = 'sheet';
                  if(this.isSecondary(residueNum, sheetStart, bNMR)) {
                    ic.atoms[serial].ssbegin = true;
                  }

                  // do not use else if. Some residues are both start and end of secondary structure
                  if(this.isSecondary(residueNum, sheetEnd, bNMR)) {
                    ic.atoms[serial].ssend = true;
                  }
                }
                else if(this.isSecondary(residueNum, helixArray, bNMR)) {
                  ic.atoms[serial].ss = 'helix';

                  if(this.isSecondary(residueNum, helixStart, bNMR)) {
                    ic.atoms[serial].ssbegin = true;
                  }

                  // do not use else if. Some residues are both start and end of secondary structure
                  if(this.isSecondary(residueNum, helixEnd, bNMR)) {
                    ic.atoms[serial].ssend = true;
                  }
                }

                let secondaries = '-';
                if(ic.atoms[serial].ss === 'helix') {
                    secondaries = 'H';
                }
                else if(ic.atoms[serial].ss === 'sheet') {
                    secondaries = 'E';
                }
                //else if(ic.atoms[serial].ss === 'coil') {
                //    secondaries = 'c';
                //}
                else if(!ic.atoms[serial].het && me.parasCls.residueColors.hasOwnProperty(ic.atoms[serial].resn.toUpperCase()) ) {
                    secondaries = 'c';
                }
                else {
                    secondaries = 'o';
                }

                ic.secondaries[residueNum] = secondaries;

                // different residue
                //if(residueNum !== prevResidueNum) {
                if(oriResidueNum !== prevOriResidueNum) {
                    let residue = me.utilsCls.residueName2Abbr(resn);
                    ic.residueId2Name[residueNum] = residue;

                    if(serial !== 1 && prevResidueNum !== '') ic.residues[prevResidueNum] = residuesTmp;

                    if(residueNum !== prevResidueNum) {
                        residuesTmp = {};
                    }

                    // different chain
                    if(chainNum !== prevChainNum) {
                        prevCSerial = undefined;
                        prevOSerial = undefined;

                        // a chain could be separated in two sections
                        if(serial !== 1 && prevChainNum !== '') {
                            if(ic.chains[prevChainNum] === undefined) ic.chains[prevChainNum] = {};
                            ic.chains[prevChainNum] = me.hashUtilsCls.unionHash(ic.chains[prevChainNum], chainsTmp);
                        }

                        chainsTmp = {};

                        if(ic.structures[structure.toString()] === undefined) ic.structures[structure.toString()] = [];
                        if(!ic.structures[structure.toString()].includes(chainNum)) ic.structures[structure.toString()].push(chainNum);

                        if(ic.chainsSeq[chainNum] === undefined) ic.chainsSeq[chainNum] = [];

                        let resObject = {};
                        resObject.resi = resi;
                        resObject.name = residue;

                        ic.chainsSeq[chainNum].push(resObject);
                    }
                    else {
                        prevCSerial = CSerial;
                        prevOSerial = OSerial;

                        let resObject = {};
                        resObject.resi = resi;
                        resObject.name = residue;

                        ic.chainsSeq[chainNum].push(resObject);
                    }
                }

                chainsTmp[serial] = 1;
                residuesTmp[serial] = 1;

                prevChainNum = chainNum;
                prevResidueNum = residueNum;
                prevOriResidueNum = oriResidueNum;

            } else if (record === 'CONECT') {
                let from = parseInt(line.substr(6, 5));
                for (let j = 0; j < 4; ++j) {
                    let to = parseInt(line.substr([11, 16, 21, 26][j], 5));
                    if (isNaN(to)) continue;

                    if(ic.atoms[oriSerial2NewSerial[from]] !== undefined) ic.atoms[oriSerial2NewSerial[from]].bonds.push(oriSerial2NewSerial[to]);
                }
            } else if (record.substr(0,3) === 'TER') {
                // Concatenation of two pdbs will have several atoms for the same serial
                ++serial;
            }
        }

        // add the last residue set
        ic.residues[residueNum] = residuesTmp;
        if(ic.chains[chainNum] === undefined) ic.chains[chainNum] = {};
        ic.chains[chainNum] = me.hashUtilsCls.unionHash2Atoms(ic.chains[chainNum], chainsTmp, ic.atoms);

        if(!bMutation) this.adjustSeq(ic.chainMissingResidueArray);

    //    ic.missingResidues = [];
    //    for(let chainid in chainMissingResidueArray) {
    //        let resArray = chainMissingResidueArray[chainid];
    //        for(let i = 0; i < resArray.length; ++i) {
    //            ic.missingResidues.push(chainid + '_' + resArray[i].resi);
    //        }
    //    }

        // copy disulfide bonds
        let structureArray = Object.keys(ic.structures);
        for(let s = 0, sl = structureArray.length; s < sl; ++s) {
            let structure = structureArray[s];

            if(structure == id) continue;

            if(ic.ssbondpnts[structure] === undefined) ic.ssbondpnts[structure] = [];

            if(ic.ssbondpnts[id] !== undefined) {
                for(let j = 0, jl = ic.ssbondpnts[id].length; j < jl; ++j) {
                    let ori_resid = ic.ssbondpnts[id][j];
                    let pos = ori_resid.indexOf('_');
                    let resid = structure + ori_resid.substr(pos);

                    ic.ssbondpnts[structure].push(resid);
                }
            }
        }

        // calculate disulfide bonds for PDB files
        if(!ic.bSsbondProvided) {
            this.setSsbond();
        }

        // remove the reference
        lines = null;

        let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.hAtoms);
        let curChain = firstAtom.chain, curResi = firstAtom.resi, curResAtoms = [];
      
        let pmin = new THREE.Vector3( 9999, 9999, 9999);
        let pmax = new THREE.Vector3(-9999,-9999,-9999);
        let psum = new THREE.Vector3();
        let cnt = 0;

        // lipids may be considered as protein if "ATOM" instead of "HETATM" was used
        let lipidResidHash = {};

        // assign atoms
        let prevCarbonArray = [firstAtom]; // add a dummy atom
        //for (let i in ic.atoms) {
        for (let i in ic.hAtoms) {    
            let atom = ic.atoms[i];
            let coord = atom.coord;
            psum.add(coord);
            pmin.min(coord);
            pmax.max(coord);
            ++cnt;

            if(!atom.het) {
              if($.inArray(atom.resn, me.parasCls.nucleotidesArray) !== -1) {
                ic.nucleotides[atom.serial] = 1;
                //if (atom.name === 'P') {
                if (atom.name === "O3'" || atom.name === "O3*") {
                    ic.nucleotidesO3[atom.serial] = 1;

                    ic.secondaries[atom.structure + '_' + atom.chain + '_' + atom.resi] = 'o'; // nucleotide
                }

                if(me.parasCls.nuclMainArray.indexOf(atom.name) === -1) {
                    ic.ntbase[atom.serial] = 1;
                }
              }
              else {
                if (atom.elem === 'P') {
                    lipidResidHash[atom.structure + '_' + atom.chain + '_' + atom.resi] = 1;
                }

                ic.proteins[atom.serial] = 1;
                if (atom.name === 'CA') ic.calphas[atom.serial] = 1;
                if (atom.name !== 'N' && atom.name !== 'H' && atom.name !== 'CA' && atom.name !== 'HA' && atom.name !== 'C' && atom.name !== 'O') ic.sidec[atom.serial] = 1;
              }
            }
            else if(atom.het) {
              if(atom.resn === 'HOH' || atom.resn === 'WAT' || atom.resn === 'SOL') {
                ic.water[atom.serial] = 1;
              }
              //else if(bOpm && atom.resn === 'DUM') {
              //  ic.mem[atom.serial] = 1;
              //}
              else if($.inArray(atom.resn, me.parasCls.ionsArray) !== -1 || atom.elem.trim() === atom.resn.trim()) {
                ic.ions[atom.serial] = 1;
              }
              else {
                ic.chemicals[atom.serial] = 1;
              }

              atom.color = me.parasCls.atomColors[atom.elem];
            }

            if(!(curChain === atom.chain && curResi === atom.resi)) {
                // a new residue, add the residue-residue bond besides the regular bonds               
                this.refreshBonds(curResAtoms, prevCarbonArray[0]);

                prevCarbonArray.splice(0, 1); // remove the first carbon

                curChain = atom.chain;
                curResi = atom.resi;
                //curInsc = atom.insc;
                curResAtoms.length = 0;
            }
            curResAtoms.push(atom);

            if(atom.name === 'C' || atom.name === 'O3\'') {
                prevCarbonArray.push(atom);
            }
        } // end of for

        // last residue
        //refreshBonds();
        this.refreshBonds(curResAtoms, prevCarbonArray[0]);

        // reset lipid
        for(let resid in lipidResidHash) {
            let atomHash = ic.residues[resid];
            for(serial in atomHash) {
                let atom = ic.atoms[serial];

                atom.het = true;
                ic.chemicals[atom.serial] = 1;
                ic.secondaries[resid] = 'o'; // nucleotide

                delete ic.proteins[atom.serial];
                if (atom.name === 'CA') delete ic.calphas[atom.serial];
                if (atom.name !== 'N' && atom.name !== 'H' && atom.name !== 'CA' && atom.name !== 'HA' && atom.name !== 'C' && atom.name !== 'O') delete ic.sidec[atom.serial];
            }
        }

        ic.pmin = pmin;
        ic.pmax = pmax;

        ic.cnt = cnt;

        //ic.maxD = ic.pmax.distanceTo(ic.pmin);
        //ic.center = psum.multiplyScalar(1.0 / ic.cnt);
        ic.center = ic.ParserUtilsCls.getGeoCenter(ic.pmin, ic.pmax);

        ic.maxD = ic.ParserUtilsCls.getStructureSize(ic.atoms, ic.pmin, ic.pmax, ic.center);

        if (ic.maxD < 5) ic.maxD = 5;

        ic.oriMaxD = ic.maxD;
        ic.oriCenter = ic.center.clone();

        if(type === 'target') {
            ic.oriMaxD = ic.maxD;
            ic.center1 = ic.center;
        }
        else if(type === 'query') {
            if(ic.oriMaxD < ic.maxD) ic.oriMaxD = ic.maxD;

            ic.center2 = ic.center;
            ic.center = new THREE.Vector3(0,0,0);
        }

        if(bVector) { // just need to get the vector of the largest chain
            return this.getChainCalpha(ic.chains, ic.atoms);
        }
        else {
            return hAtoms;
        }
    }

    // refresh for atoms in each residue
    refreshBonds(curResAtoms, prevCarbon) { let ic = this.icn3d, me = ic.icn3dui;
        let n = curResAtoms.length;
        for (let j = 0; j < n; ++j) {
            let atom0 = curResAtoms[j];
            for (let k = j + 1; k < n; ++k) {
                let atom1 = curResAtoms[k];
                if (atom0.alt === atom1.alt && me.utilsCls.hasCovalentBond(atom0, atom1)) {
                //if (me.utilsCls.hasCovalentBond(atom0, atom1)) {
                    atom0.bonds.push(atom1.serial);
                    atom1.bonds.push(atom0.serial);
                }
            }

            //f && f(atom0);
            if (prevCarbon && (prevCarbon.name === 'C' || prevCarbon.name === 'O3\'') && (atom0.name === 'N' || atom0.name === 'P') && me.utilsCls.hasCovalentBond(atom0, prevCarbon)) {
                atom0.bonds.push(prevCarbon.serial);
                prevCarbon.bonds.push(atom0.serial);
            }
        }
    }

    adjustSeq(chainMissingResidueArray) { let ic = this.icn3d; ic.icn3dui;
        // adjust sequences
        for(let chainNum in ic.chainsSeq) {
            if(chainMissingResidueArray[chainNum] === undefined) continue;

            ic.chainsSeq[chainNum] = this.mergeTwoSequences(chainMissingResidueArray[chainNum], ic.chainsSeq[chainNum]);     
        }

        this.setResidMapping();
    }

    mergeTwoSequences(A, B) {
        let m = A.length; // missing residues
        let n = B.length; // residues with coord

        // inserted domain such as PRK150 in the R chain of PDB 6WW2
        let lastResiA = parseInt(A[m - 1].resi);
        let lastResiB = parseInt(B[n - 1].resi);
        let lastResi = (lastResiA >= lastResiB) ? lastResiA : lastResiB;

        let C = new Array(m + n);
        // http://www.algolist.net/Algorithms/Merge/Sorted_arrays
        // m - size of A
        // n - size of B
        // size of C array must be equal or greater than m + n
          let i = 0, j = 0, k = 0;
          let bInsertion = false;

          while (i < m && j < n) {
                let aResi = parseInt(A[i].resi), bResi = parseInt(B[j].resi);
                if(aResi > lastResi && bResi > lastResi) bInsertion = true;

                if(aResi <= lastResi &&  bResi > lastResi) {
                    if (aResi > bResi || bInsertion) {
                        C[k] = B[j];
                        j++;
                    }
                    else  {
                        C[k] = A[i];
                        i++;
                    }
                }
                else if(aResi > lastResi &&  bResi <= lastResi) {
                    if (aResi <= bResi || bInsertion) {
                        C[k] = A[i];
                        i++;
                    }
                    else  {
                        C[k] = B[j];
                        j++;
                    }
                }
                else {
                    if (aResi <= bResi) {
                        C[k] = A[i];
                        i++;
                    }
                    else {
                        C[k] = B[j];
                        j++;
                    }
                }

                k++;
          }

          if (i < m) {
                for (let p = i; p < m; p++) {
                      C[k] = A[p];
                      k++;
                }
          } 
          else {
                for (let p = j; p < n; p++) {
                      C[k] = B[p];
                      k++;
                }
          }

          return C;
    }

    setResidMapping() { let ic = this.icn3d; ic.icn3dui;
        // set ic.ncbi2resid and ic.resid2ncbi
        for(let chainid in ic.chainsSeq) {
            for(let j = 0, jl = ic.chainsSeq[chainid].length; j < jl; ++j) {
                // NCBI residue number starts from 1 and increases continuously
                let residNCBI = chainid + '_' + (j+1).toString();
                let resid = chainid + '_' + ic.chainsSeq[chainid][j].resi;
                ic.ncbi2resid[residNCBI] = resid;
                ic.resid2ncbi[resid] = residNCBI;
            }
        }
    }

    setSsbond() { let ic = this.icn3d; ic.icn3dui;
        // get all Cys residues
        let structure2cys_resid = {};

        for(let chainid in ic.chainsSeq) {
            let seq = ic.chainsSeq[chainid];
            let structure = chainid.substr(0, chainid.indexOf('_'));

            for(let i = 0, il = seq.length; i < il; ++i) {
                // each seq[i] = {"resi": 1, "name":"C"}
                if(seq[i].name == 'C') {
                    if(structure2cys_resid[structure] == undefined) structure2cys_resid[structure] = [];
                    structure2cys_resid[structure].push(chainid + '_' + seq[i].resi);
                }
            }
        }

        // determine whether there are disulfide bonds
        // disulfide bond is about 2.05 angstrom
        let distMax = 4; //3; // https://icn3d.page.link/5KRXx6XYfig1fkye7
        let distSqrMax = distMax * distMax;
        for(let structure in structure2cys_resid) {
            let cysArray = structure2cys_resid[structure];

            for(let i = 0, il = cysArray.length; i < il; ++i) {
                for(let j = i + 1, jl = cysArray.length; j < jl; ++j) {
                    let resid1 = cysArray[i];
                    let resid2 = cysArray[j];

                    let coord1 = undefined, coord2 = undefined;
                    for(let serial in ic.residues[resid1]) {
                        if(ic.atoms[serial].elem == 'S') {
                            coord1 = ic.atoms[serial].coord;
                            break;
                        }
                    }
                    for(let serial in ic.residues[resid2]) {
                        if(ic.atoms[serial].elem == 'S') {
                            coord2 = ic.atoms[serial].coord;
                            break;
                        }
                    }

                    if(coord1 === undefined || coord2 === undefined) continue;

                    if(Math.abs(coord1.x - coord2.x) > distMax) continue;
                    if(Math.abs(coord1.y - coord2.y) > distMax) continue;
                    if(Math.abs(coord1.z - coord2.z) > distMax) continue;
                    let distSqr = (coord1.x - coord2.x)*(coord1.x - coord2.x) + (coord1.y - coord2.y)*(coord1.y - coord2.y) + (coord1.z - coord2.z)*(coord1.z - coord2.z);

                    if(distSqr < distSqrMax) { // disulfide bond
                        if(ic.ssbondpnts[structure] === undefined) ic.ssbondpnts[structure] = [];
                        ic.ssbondpnts[structure].push(resid1);
                        ic.ssbondpnts[structure].push(resid2);
                    }
                }
            }
        }
    }

    getChainCalpha(chains, atoms, bResi_ori, pdbid) { let ic = this.icn3d, me = ic.icn3dui;
        let chainCalphaHash = {};

        for(let chainid in chains) {
            if(pdbid !== undefined) {
                let textArray =  chainid.split('_');
                if(textArray[0] !== pdbid) continue; // skip different chain
            }

            let serialArray = Object.keys(chains[chainid]);

            let calphaArray = [];
            let cnt = 0;
            let lastResi = 0;
            for(let i = 0, il = serialArray.length; i < il; ++i) {
                let atom = atoms[serialArray[i]];
                if( (ic.proteins.hasOwnProperty(serialArray[i]) && atom.name == "CA")
                  || (ic.nucleotides.hasOwnProperty(serialArray[i]) && (atom.name == "O3'" || atom.name == "O3*")) ) {
                    if(atom.resi == lastResi) continue; // e.g., Alt A and B

                    let resn = (atom.resn.trim().length > 3) ? atom.resn.trim().substr(0, 3) : atom.resn.trim();
                    if(!me.parasCls.chargeColors.hasOwnProperty(resn)) {
                        continue; // regular residues
                    }

                    (bResi_ori) ? atom.resi_ori : atom.resi; // MMDB uses resi_ori for PDB residue number
                    //resi = resi - baseResi + 1;

                    //chainresiCalphaHash[atom.chain + '_' + resi] = atom.coord.clone();

                    calphaArray.push(atom.coord.clone());
                    ++cnt;

                    lastResi = atom.resi;
                }
            }

            if(cnt > 0) {
                //var chainid = atoms[serialArray[0]].structure + '_' + atoms[serialArray[0]].chain;
                let chain = atoms[serialArray[0]].chain;
                chainCalphaHash[chain] = calphaArray;
            }
        }

        return {'chainresiCalphaHash': chainCalphaHash, 'center': ic.center.clone()}
    }

    isSecondary(resid, residArray, bNMR) { let ic = this.icn3d; ic.icn3dui;
        if(!bNMR) {
            return $.inArray(resid, residArray) != -1;
        }
        else {
            let chain_resi = resid.substr(resid.indexOf('_') + 1);

            let bFound = false;
            for(let i = 0, il = residArray.length; i < il; ++i) {
                if(chain_resi == residArray[i].substr(residArray[i].indexOf('_') + 1)) {
                    bFound = true;
                    break;
                }
            }

            return bFound;
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Vastplus {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Load the VAST+ structure alignment for the pair of structures "align", e.g., "align" could be "1HHO,4N7N".
    // vastplusAtype: 0: VAST, global, 1: VAST, invarant core, 2: TM-align, global
    async vastplusAlign(structArray, vastplusAtype, bRealign) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;

        // 1. pairwise alignment
        let ajaxArray = [], chainidpairArray = [];
        if(structArray.length != 2) {
            console.log("VAST+ needs two input structures...");
            return;
        }

        let struct1 = structArray[0], struct2 = structArray[1];

        // get protein chains since TM-align doesn't work for nucleotides
        let chainidArray1 = [], chainidArray2 = [];
        for(let i = 0, il = ic.structures[struct1].length; i < il; ++i) {
            let chainid1 = ic.structures[struct1][i];
            if(!ic.proteins.hasOwnProperty(ic.firstAtomObjCls.getFirstAtomObj(ic.chains[chainid1]).serial)) continue;
            chainidArray1.push(chainid1);
        }
        for(let i = 0, il = ic.structures[struct2].length; i < il; ++i) {
            let chainid2 = ic.structures[struct2][i];
            if(!ic.proteins.hasOwnProperty(ic.firstAtomObjCls.getFirstAtomObj(ic.chains[chainid2]).serial)) continue;
            chainidArray2.push(chainid2);
        }

        let node2chainindex = {};
        let node = 0;

        // align A to A, B to B first
        for(let i = 0, il = chainidArray1.length; i < il; ++i) {
            let chainid1 = chainidArray1[i];
            for(let j = 0, jl = chainidArray2.length; j < jl; ++j) {
                let chainid2 = chainidArray2[j];
                if(i == j) {
                    let alignAjax = this.setAlignment(struct1, struct2, chainid1, chainid2, bRealign);

                    ajaxArray.push(alignAjax);
                    chainidpairArray.push(chainid1 + ',' + chainid2);
                    node2chainindex[node] = [i, j];

                    ++node;
                }
            }
        }

        for(let i = 0, il = chainidArray1.length; i < il; ++i) {
            let chainid1 = chainidArray1[i];
            for(let j = 0, jl = chainidArray2.length; j < jl; ++j) {
                let chainid2 = chainidArray2[j];
                if(i != j) {
                    let alignAjax = this.setAlignment(struct1, struct2, chainid1, chainid2, bRealign);

                    ajaxArray.push(alignAjax);
                    chainidpairArray.push(chainid1 + ',' + chainid2);
                    node2chainindex[node] = [i, j];

                    ++node;
                }
            }
        }

        let allPromise = Promise.allSettled(ajaxArray);
        try {
            let dataArray = await allPromise;

            // 2. cluster pairs
            thisClass.clusterAlignment(dataArray, chainidpairArray, node2chainindex, vastplusAtype);

            // 3. superpose the top selection

            ic.ParserUtilsCls.hideLoading();
            await ic.pdbParserCls.loadPdbDataRender(true);

            /// if(ic.deferredRealignByVastplus !== undefined) ic.deferredRealignByVastplus.resolve();
        }
        catch(err) {
            alert("There are some problems in aligning the chains...");
        }          
    }

    setAlignment(struct1, struct2, chainid1, chainid2, bRealign) { let ic = this.icn3d, me = ic.icn3dui;
        let urltmalign = me.htmlCls.baseUrl + "tmalign/tmalign.cgi";

        let sel_t = (bRealign) ? me.hashUtilsCls.intHash(ic.hAtoms, ic.chains[chainid1]) : ic.chains[chainid1];
        let sel_q = (bRealign) ? me.hashUtilsCls.intHash(ic.hAtoms, ic.chains[chainid2]) : ic.chains[chainid2];

        let pdb_target = ic.saveFileCls.getAtomPDB(sel_t, undefined, undefined, undefined, undefined, struct1);
        let pdb_query = ic.saveFileCls.getAtomPDB(sel_q, undefined, undefined, undefined, undefined, struct2);

        let dataObj = {'pdb_query': pdb_query, 'pdb_target': pdb_target};
        let alignAjax = me.getAjaxPostPromise(urltmalign, dataObj);

        return alignAjax;
    }

    async realignOnVastplus() { let ic = this.icn3d, me = ic.icn3dui;
        let structHash = [];
        for(let struct in ic.structures) {
            let chainidArray = ic.structures[struct];
            for(let i = 0, il = chainidArray.length; i < il; ++i) {
                let chainid = chainidArray[i];
                let atoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.chains[chainid]);               
                let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(atoms);
                structHash[firstAtom.structure] = 1;
            }
        }

        let bRealign = true, atype = 2; // VAST+ based on TM-align
        await ic.vastplusCls.vastplusAlign(Object.keys(structHash), atype, bRealign);
    }

    getResisFromSegs(segArray) { let ic = this.icn3d; ic.icn3dui;
        let resiArray_t = [], resiArray_q = [];
        for(let i = 0, il = segArray.length; i < il; ++i) {
            let seg = segArray[i];
            // for(let j = 0; j <= seg.t_end - seg.t_start; ++j) {
            //     resiArray_t.push(j);
            // }
            // for(let j = 0; j <= seg.q_end - seg.q_start; ++j) {
            //     resiArray_q.push(j);
            // }
            resiArray_t.push(seg.t_start + '-' + seg.t_end);
            resiArray_q.push(seg.q_start + '-' + seg.q_end);
        }

        return {resiArray_t: resiArray_t, resiArray_q: resiArray_q};
    }

    clusterAlignment(dataArray, chainidpairArray, node2chainindex, vastplusAtype) { let ic = this.icn3d, me = ic.icn3dui;

        let queryDataArray = [];
        for(let index = 0, indexl = chainidpairArray.length; index < indexl; ++index) {
            let queryData = dataArray[index].value; //[0];

            queryDataArray.push(queryData);
/*
            if(queryData !== undefined && JSON.stringify(queryData).indexOf('Oops there was a problem') === -1
                ) {
                queryDataArray.push(queryData);
            }
            else {
                console.log("The alignment data can NOT be retrieved for the pair " + chainidpairArray[index] + "...");
                //return;
                queryDataArray.push([]);
            }
*/            
        }

        //src/internal/structure/MMDBUpdateTools/Interactions/compbu/comparebuEngine.cpp
        //  Doing a new comparison; remove any existing results.
        let m_qpMatrixDist = [];

        let outlier = 1.0, maxDist = 0;

        let bAligned = false;
        for(let i = 0, il = chainidpairArray.length; i < il; ++i) {
            let vdist = [];
            if(queryDataArray[i].length > 0) bAligned = true;

            for(let j = 0, jl = chainidpairArray.length; j < jl; ++j) {
                let result = this.RotMatrixTransDist(queryDataArray[i][0], queryDataArray[j][0], outlier, vastplusAtype);

                // 1.0: not aligned
                let dist = (i == j) ? 0.0 : ( (queryDataArray[i].length == 0 || queryDataArray[j].length == 0) ? 1.0 : result);
                //if(dist < outlier && dist > maxDist) {
                if(dist > maxDist) {
                    maxDist = dist;
                }
                vdist.push(dist);                
            }

            m_qpMatrixDist.push(vdist);
        }

        if(!bAligned) {
            if(ic.bRender) alert("These structures can not be aligned...");
            return;
        }

        if(maxDist < 1e-6) maxDist = 1;

        // normalize the score matrix
        for(let i = 0, il = chainidpairArray.length; i < il; ++i) {
            for(let j = 0, jl = chainidpairArray.length; j < jl; ++j) {
                m_qpMatrixDist[i][j] = m_qpMatrixDist[i][j] / maxDist;
            }
        }
        
        // cluster
        let threshold = 1.0;

        let bLastTiedValue = false;
        let m_clusteringResult = this.clusterLinkage(threshold, m_qpMatrixDist, bLastTiedValue);

        let m_buChainMap = this.GetChainMappings(m_clusteringResult, chainidpairArray);

        //  By default, clusters populate m_buChainMap in order of increasing score.
        let allnodesHash = {};
        for (let i = 0, il = m_buChainMap.length; i < il; ++i) {
            let nodeArray = m_buChainMap[i].nodeArray;
            let allnodes = nodeArray.join(',');

            // use the sum of all pairs
            // let sum = 0;
            // for(let j = 0, jl = nodeArray.length; j < jl; ++j) {
            //     let chainindexArray = node2chainindex[parseInt(nodeArray[j])];
            //     sum += m_qpMatrixDist[chainindexArray[0]][chainindexArray[1]];
            // }

            // use the best match
            let chainindexArray = node2chainindex[parseInt(nodeArray[0])];
            let sum = m_qpMatrixDist[chainindexArray[0]][chainindexArray[1]];           

            if(!allnodesHash[allnodes]) {
                allnodesHash[allnodes] = sum;
            }
            else if(sum < allnodesHash[allnodes]) {
                allnodesHash[allnodes] = sum;
            }
        }

        // sort the hash by value, then sort by key
        let allnodesArray = Object.keys(allnodesHash).sort((key1, key2) => (allnodesHash[key1] < allnodesHash[key2]) ? -1 : ( (parseInt(10000*allnodesHash[key1]) == parseInt(10000*allnodesHash[key2])) ? ( (key1 < key2) ? -1 : 1 ) : 1 ));

        let badRmsd = parseInt($("#" + me.pre + "maxrmsd").val());
        if(!badRmsd) badRmsd = 30;
        
        bAligned = false;

        for(let i = 0, il = allnodesArray.length; i < il; ++i) {
            let nodeArray = allnodesArray[i].split(',');

            ic.opts['color'] = 'grey';
            ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

            // get the mapped coords
            let coor_t = [], coor_q = [];
            let chainid_t, chainid_q;
            let hAtomsAll = {};

            // reinitialize the alignment
            $("#" + ic.pre + "dl_sequence2").html('');
          
            for(let j = 0, jl = nodeArray.length; j < jl; ++j) {
                let node = parseInt(nodeArray[j]);
                let segs = queryDataArray[node][0].segs;
                let chainidArray = chainidpairArray[node].split(',');

                chainid_t = chainidArray[0];
                chainid_q = chainidArray[1];

                let resiArrays = this.getResisFromSegs(segs);
                let resiArray_t = resiArrays.resiArray_t;
                let resiArray_q = resiArrays.resiArray_q;

                //let base = parseInt(ic.chainsSeq[chainid_t][0].resi);
                let result_t = ic.realignParserCls.getSeqCoorResid(resiArray_t, chainid_t);
                coor_t = coor_t.concat(result_t.coor);

                //base = parseInt(ic.chainsSeq[chainid_q][0].resi);
                let result_q = ic.realignParserCls.getSeqCoorResid(resiArray_q, chainid_q);
                coor_q = coor_q.concat(result_q.coor);

                // align seq 
                ic.qt_start_end = [];
                ic.qt_start_end.push(segs);
                let bVastplus = true, bRealign = true;
                let hAtomsTmp = ic.chainalignParserCls.setMsa(chainidArray, bVastplus, bRealign);
                hAtomsAll = me.hashUtilsCls.unionHash(hAtomsAll, hAtomsTmp);
            }

            ic.hAtoms = me.hashUtilsCls.cloneHash(hAtomsAll);

            // ic.opts['color'] = 'identity';
            // ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

            // align residue by residue
            let n =(coor_q.length < coor_t.length) ? coor_q.length : coor_t.length;
   
            if(n < 4) continue;

            if(n >= 4) {
                ic.rmsd_suprTmp = me.rmsdSuprCls.getRmsdSuprCls(coor_q, coor_t, n);
     
                // superpose
                if(ic.rmsd_suprTmp.rot !== undefined) {
                    let rot = ic.rmsd_suprTmp.rot;
                    if(rot[0] === null) continue;
      
                    let centerFrom = ic.rmsd_suprTmp.trans1;
                    let centerTo = ic.rmsd_suprTmp.trans2;
                    let rmsd = ic.rmsd_suprTmp.rmsd;

                    if(rmsd < badRmsd) {
                        bAligned = true;

                        me.htmlCls.clickMenuCls.setLogCmd("realignment RMSD: " + rmsd.toPrecision(4), false);
                        $("#" + ic.pre + "dl_rmsd").html("<br><b>Realignment RMSD</b>: " + rmsd.toPrecision(4) + " &#8491;<br><br>");
                        if(!me.cfg.bSidebyside) me.htmlCls.dialogCls.openDlg('dl_rmsd', 'Realignment RMSD');

                        // apply matrix for each atom                       
                        ic.q_rotation = [];
                        ic.q_trans_sub = [];
                        ic.t_trans_add = [];

                        ic.q_rotation.push({x1: rot[0], y1: rot[1], z1: rot[2], x2: rot[3], y2: rot[4], z2: rot[5], x3: rot[6], y3: rot[7], z3: rot[8]});
                        ic.q_trans_sub.push(centerFrom);
                        ic.t_trans_add.push({x: -centerTo.x, y: -centerTo.y, z: -centerTo.z});

                        me.cfg.aligntool = 'vast'; //!= 'tmalign';
                        let index = 0, alignType = 'query';
                        let mmdbid_q = chainid_q.substr(0, chainid_q.indexOf('_'));
                        let bForce = true;
                        ic.chainalignParserCls.transformStructure(mmdbid_q, index, alignType, bForce);

                        let chainpairStr = '';
                        for(let j = 0, jl = nodeArray.length; j < jl; ++j) {
                            chainpairStr += chainidpairArray[parseInt(nodeArray[j])] + '; ';
                        }
                        console.log("Selected the alignment: " + chainpairStr);

                        break;
                    }
                    else {
                        let chainpairStr = '';
                        for(let j = 0, jl = nodeArray.length; j < jl; ++j) {
                            chainpairStr += chainidpairArray[parseInt(nodeArray[j])] + '; ';
                        }
                        console.log("skipped the alignment: " + chainpairStr);
                    }
                }
            }
        }

        if(!bAligned) {
            if(ic.bRender) alert("These structures can not be aligned...");
            return;
        }
    }

    // src/internal/structure/MMDBUpdateTools/Interactions/compbu/qaAlignment.cpp
    RotMatrixTransDist(qpa1, qpa2, outlier, vastplusAtype) { let ic = this.icn3d; ic.icn3dui;
        let cosval = 0.866, lenval = 8.0; 

        if(!qpa1 || !qpa2) return outlier;
        
        let rmat1 = this.GetRotMatrix(qpa1, 1.0, vastplusAtype);
        let rmat2 = this.GetRotMatrix(qpa2, 1.0, vastplusAtype);
        let tA1 = [], tA2 = [], tB1 = [], tB2 = [];
        tA1[0] = rmat1[9];  // qpa1.t1x;
        tA1[1] = rmat1[10]; // qpa1.t1y;
        tA1[2] = rmat1[11]; // qpa1.t1z;
        tA2[0] = rmat1[12]; // qpa1.t2x;
        tA2[1] = rmat1[13]; // qpa1.t2y;
        tA2[2] = rmat1[14]; // qpa1.t2z;
        tB1[0] = rmat2[9];  // qpa2.t1x;
        tB1[1] = rmat2[10]; // qpa2.t1y;
        tB1[2] = rmat2[11]; // qpa2.t1z;
        tB2[0] = rmat2[12]; // qpa2.t2x;
        tB2[1] = rmat2[13]; // qpa2.t2y;
        tB2[2] = rmat2[14]; // qpa2.t2z;
        let vecl = [], vecr = [];
        vecl[0] = tA2[0] - tB2[0];
        vecl[1] = tA2[1] - tB2[1];
        vecl[2] = tA2[2] - tB2[2];
        vecr[0] = tA1[0] - tB1[0];
        vecr[1] = tA1[1] - tB1[1];
        vecr[2] = tA1[2] - tB1[2];
    
        let sum = 0.0, l1, l2;
        sum += Math.pow(vecl[0], 2);
        sum += Math.pow(vecl[1], 2);
        sum += Math.pow(vecl[2], 2);
        l1 = Math.sqrt(sum);
        sum = 0.0;
        sum += Math.pow(vecr[0], 2);
        sum += Math.pow(vecr[1], 2);
        sum += Math.pow(vecr[2], 2);
        l2 = Math.sqrt(sum);

        // l1 == 0.0 or l2 == 0.0 may occur, if two of the molecules are the same
        if(vastplusAtype != 2) { // VAST
            if ((l1 < 1e-10) || (l2 < 1e-10)) {
                return outlier;
            }
        }
        else {
            if (l2 < 1e-10) {
                return outlier;
            }
        }
 
        if (Math.abs(l1 - l2) > lenval) {
            return outlier;
        }

        // additional check!
        let vecr0 = [];
        vecr0[0] = rmat1[0]*tA1[0] + rmat1[1]*tA1[1] + rmat1[2]*tA1[2];
        vecr0[1] = rmat1[3]*tA1[0] + rmat1[4]*tA1[1] + rmat1[5]*tA1[2];
        vecr0[2] = rmat1[6]*tA1[0] + rmat1[7]*tA1[1] + rmat1[8]*tA1[2];
        vecr0[0] -= rmat1[0]*tB1[0] + rmat1[1]*tB1[1] + rmat1[2]*tB1[2];
        vecr0[1] -= rmat1[3]*tB1[0] + rmat1[4]*tB1[1] + rmat1[5]*tB1[2];
        vecr0[2] -= rmat1[6]*tB1[0] + rmat1[7]*tB1[1] + rmat1[8]*tB1[2];
        let dot0 = 0.0;
        dot0 = vecl[0]*vecr0[0];
        dot0 += vecl[1]*vecr0[1];
        dot0 += vecl[2]*vecr0[2];
        dot0 /= (l1*l2);

        if (dot0 < cosval) {
            return outlier;
        }

        // additional check!
        vecr0[0] = rmat2[0]*tA1[0] + rmat2[1]*tA1[1] + rmat2[2]*tA1[2];
        vecr0[1] = rmat2[3]*tA1[0] + rmat2[4]*tA1[1] + rmat2[5]*tA1[2];
        vecr0[2] = rmat2[6]*tA1[0] + rmat2[7]*tA1[1] + rmat2[8]*tA1[2];
        vecr0[0] -= rmat2[0]*tB1[0] + rmat2[1]*tB1[1] + rmat2[2]*tB1[2];
        vecr0[1] -= rmat2[3]*tB1[0] + rmat2[4]*tB1[1] + rmat2[5]*tB1[2];
        vecr0[2] -= rmat2[6]*tB1[0] + rmat2[7]*tB1[1] + rmat2[8]*tB1[2];
        dot0 = vecl[0]*vecr0[0];
        dot0 += vecl[1]*vecr0[1];
        dot0 += vecl[2]*vecr0[2];
        dot0 /= (l1*l2);

        if (dot0 < cosval) {
            return outlier;
        }

        sum = 0.0;
        sum += Math.pow(qpa1.q_rotation.x1 - qpa2.q_rotation.x1, 2);
        sum += Math.pow(qpa1.q_rotation.y1 - qpa2.q_rotation.y1, 2);
        sum += Math.pow(qpa1.q_rotation.z1 - qpa2.q_rotation.z1, 2);
        sum += Math.pow(qpa1.q_rotation.x2 - qpa2.q_rotation.x2, 2);
        sum += Math.pow(qpa1.q_rotation.y2 - qpa2.q_rotation.y2, 2);
        sum += Math.pow(qpa1.q_rotation.z2 - qpa2.q_rotation.z2, 2);
        sum += Math.pow(qpa1.q_rotation.x3 - qpa2.q_rotation.x3, 2);
        sum += Math.pow(qpa1.q_rotation.y3 - qpa2.q_rotation.y3, 2);
        sum += Math.pow(qpa1.q_rotation.z3 - qpa2.q_rotation.z3, 2);
   
        return Math.sqrt(sum);
    }
    
    GetRotMatrix(qpa, scaleFactor, vastplusAtype) { let ic = this.icn3d; ic.icn3dui;
        let result = [];
        if (result) {
            result[0] = qpa.q_rotation.x1 / scaleFactor;
            result[1] = qpa.q_rotation.y1 / scaleFactor;
            result[2] = qpa.q_rotation.z1 / scaleFactor;
            result[3] = qpa.q_rotation.x2 / scaleFactor;
            result[4] = qpa.q_rotation.y2 / scaleFactor;
            result[5] = qpa.q_rotation.z2 / scaleFactor;
            result[6] = qpa.q_rotation.x3 / scaleFactor;
            result[7] = qpa.q_rotation.y3 / scaleFactor;
            result[8] = qpa.q_rotation.z3 / scaleFactor;
            
            if(vastplusAtype != 2) { // VAST
                result[9] = qpa.t_trans_add.x / scaleFactor;
                result[10] = qpa.t_trans_add.y / scaleFactor;
                result[11] = qpa.t_trans_add.z / scaleFactor;
                result[12] = -qpa.q_trans_sub.x / scaleFactor;
                result[13] = -qpa.q_trans_sub.y / scaleFactor;
                result[14] = -qpa.q_trans_sub.z / scaleFactor;
            }
            else {
                //TM-align
                result[9] = -qpa.q_trans_add.x / scaleFactor;
                result[10] = -qpa.q_trans_add.y / scaleFactor;
                result[11] = -qpa.q_trans_add.z / scaleFactor;
                result[12] = 0;
                result[13] = 0;
                result[14] = 0;
            }
        }
        
        return result;
    }

    cbu_dist( v1, v2, vvDist)  {
        return (v1 < v2) ?  vvDist[v1][v2] : vvDist[v2][v1];
    }  
    
    compareFloat(cumul, node1, node2 )  {
        // let v1 = cumul[node1].joinDist;
        // let v2 = cumul[node2].joinDist;
        let v1 = cumul[node1].dist;
        let v2 = cumul[node2].dist;

        if(parseInt(10000 * v1) == parseInt(10000 * v2)) {
            return 0;
        }
        else if(parseInt(10000 * v1) < parseInt(10000 * v2)) {
            return -1;
        }
        else {
            return 1;
        }
    } 

    //  This method has been adapted from the code at:
    //  src/internal/structure/PubChem/graphicsapi/graphicsapi.cpp
    // ref: Olson CF, 1995, Parallel algorithms for hierarchical clustering.
    // http://linkinghub.elsevier.com/retrieve/pii/016781919500017I
    
    // single linkage method
    clusterLinkage(threshold, distmat, bLastTiedValue) { let ic = this.icn3d, me = ic.icn3dui;
        let cumul = [];
    
        let CBU_ROOT = -1, CBU_TERMINAL = -2;

        let i, j, n = distmat.length;

        let oriNode, selI, selJ, count;

        let distTmp, distPair, maxDist = 2.0;

        for(i = 0; i < 2*n - 1; ++i) {
            cumul[i] = {};
            cumul[i].leaves = []; // array of array
        }
    
        // make a matrix to hold the dynamic distance
        let vvDist = [];
        for(i = 0; i < 2*n - 1; ++i) {
            vvDist[i] = [];
            for(j = 0; j < 2*n - 1; ++j) {
                vvDist[i][j] = maxDist;
            }
        }
    
        for(i = 0; i < n; ++i) {
            for(j = i; j < n; ++j) {
                vvDist[i][j] = distmat[i][j];
            }
        }

        // for each curernt nodes, assign its nearest neighbor and the distance
        let mNearestNB = {}, mNearestNBCopy = {}, mNearestNBDist = {};
    
        selI = n;
        selJ = n;
        for(i = 0; i < n; ++i) {
            distTmp = maxDist;
            for(j = 0; j < n; ++j) {
                let bComp = (bLastTiedValue) ? (parseInt(10000 * this.cbu_dist(i, j, vvDist)) <= parseInt(10000 * distTmp)) : (parseInt(10000 * this.cbu_dist(i, j, vvDist)) < parseInt(10000 * distTmp));
                if(j != i && bComp) {
                    distTmp = this.cbu_dist(i, j, vvDist);
                    selI = i;
                    selJ = j;
                }
            }
    
            mNearestNB[selI] = selJ;
            mNearestNBDist[selI] = distTmp;
        }

        let childDist = []; // the distance between its children
    
        for(count=0; count < n; ++count){
            cumul[count].child1     = CBU_TERMINAL;
            cumul[count].child2     = CBU_TERMINAL;
            cumul[count].parent     = count;
            cumul[count].dist       = 0.0;
            cumul[count].leaves.push([count]);
            childDist[count]     = 0.0;
        }

        let structArray = Object.keys(ic.structures);
        let nChain1 = ic.structures[structArray[0]].length;
        let nChain2 = ic.structures[structArray[1]].length;
        let nChain = (nChain1 < nChain2) ? nChain1 : nChain2;

        for(count = n; count < 2*n-1; ++count) {
            // find the min dist
            distTmp = maxDist;
            for(oriNode in mNearestNB) {
                distPair = mNearestNBDist[oriNode];
                if(distPair < distTmp) {
                    distTmp = distPair;
                    selI = oriNode;
                    selJ = mNearestNB[oriNode];
                }
            }

            let distance = distTmp;

            // update the nodes
            cumul[count].child1 = (selI < n) ? selI : -selI;
            cumul[count].child2 = (selJ < n) ? selJ : -selJ;
            cumul[count].parent = -1 * count;

            // distance of its two children
            cumul[selI].dist = distance - childDist[selI];
            cumul[selJ].dist = distance - childDist[selJ];
            childDist[count] = distance;

            // update the dist matrix for the current one "count"
            for(j = 0; j < 2*n - 1; ++j) {
                let v1 = this.cbu_dist(selI, j, vvDist);
                let v2 = this.cbu_dist(selJ, j, vvDist);
                if(count < j) vvDist[count][j] = (v1 < v2) ? v1 : v2;
                else vvDist[j][count] = (v1 < v2) ? v1 : v2;
            }

            // assign the connected nodes with maxDist
            for(j = 0; j < 2*n - 1; ++j) {
                if(selI < j) vvDist[selI][j] = maxDist;
                else vvDist[j][selI] = maxDist;

                if(selJ < j) vvDist[selJ][j] = maxDist;
                else vvDist[j][selJ] = maxDist;
            }

            let factor = 4; // 2-4 fold more chains/alignments
            if(cumul[selI].leaves.length < factor * nChain && cumul[selJ].leaves.length < factor * nChain) {
                cumul[count].leaves = [];
                
                for(let i = 0, il = cumul[selI].leaves.length; i < il; ++i) {
                    for(let j = 0, jl = cumul[selJ].leaves.length; j < jl; ++j) {
                        // let nodeI = cumul[selI].leaves[i][0];
                        // let nodeJ = cumul[selJ].leaves[j][0];

                        // skip non-similar alignments
                        // if(cumul[selI].dist > threshold) {
                        //     cumul[count].leaves.push(cumul[selJ].leaves[j]);
                        // } else if(cumul[selJ].dist > threshold) {
                        //     cumul[count].leaves = [];
                        // }
                        // else {
                            
                            // if(this.compareFloat(cumul, nodeI, nodeJ) == 0) {
                            //     cumul[count].leaves.push(cumul[selI].leaves[i].concat(cumul[selJ].leaves[j]));
                            //     cumul[count].leaves.push(cumul[selJ].leaves[j].concat(cumul[selI].leaves[i]));
                            // }
                            // else if(this.compareFloat(cumul, nodeI, nodeJ) == -1) {
                            //     cumul[count].leaves.push(cumul[selI].leaves[i].concat(cumul[selJ].leaves[j]));
                            // }
                            // else if(this.compareFloat(cumul, nodeI, nodeJ) == 1) {
                            //     cumul[count].leaves.push(cumul[selJ].leaves[j].concat(cumul[selI].leaves[i]));
                            // }

                            cumul[count].leaves.push(cumul[selI].leaves[i].concat(cumul[selJ].leaves[j]));
                            cumul[count].leaves.push(cumul[selJ].leaves[j].concat(cumul[selI].leaves[i]));

                        // }
                    }
                }

                cumul[selI].leaves = [];
                cumul[selJ].leaves = [];
            }
            
            // update mNearestNB and mNearestNBDist
            delete mNearestNB[selI];
            delete mNearestNB[selJ];

            delete mNearestNBDist[selI];
            delete mNearestNBDist[selJ];

            // replace previous node with the new merged one
            mNearestNBCopy = me.hashUtilsCls.cloneHash(mNearestNB);
            for(oriNode in mNearestNBCopy) {
                if(mNearestNBCopy[oriNode] == selI || mNearestNBCopy[oriNode] == selJ) {
                    delete mNearestNB[oriNode];
                    mNearestNB[oriNode] = count;
                }
            }

            // calculate the nearest neighbor of the current node
            let selNode = 2*n;
            distTmp = maxDist;
            for(j = 0; j < 2*n - 1; ++j) {
                if(j != count && this.cbu_dist(count, j, vvDist) < distTmp) {
                    distTmp = this.cbu_dist(count, j, vvDist);
                    selNode = j;
                }
            }

            mNearestNB[count] = selNode;
            mNearestNBDist[count] = distTmp;
        }

        if (count == 2*n - 1) {
            cumul[count-1].parent = CBU_ROOT;
            cumul[count-1].dist = 0.0;
        }

        return cumul;
    }

    GetChainMappings(m_clusteringResult, chainidpairArray) { let ic = this.icn3d; ic.icn3dui;
        let mappings = [];
        chainidpairArray.length;
        let chain1a, chain2a;
    
        let result = this.getClusters(m_clusteringResult, true);
        //let clusterScores = result.scores;
        let clusters = result.clusters;
        let nClusters = clusters.length;

        for(let i = 0; i < nClusters; ++i) {
            //isClusterOk = true;       

            let leavesArray = clusters[i];        
            for(let j = 0, jl = leavesArray.length; j < jl; ++j) {
                let bucm = {};
                //bucm.score = clusterScores[i];
                bucm.nodeArray = [];
  
                let chainSet1 = {}, chainSet2 = {};

                for(let k = 0, kl = leavesArray[j].length; k < kl; ++k) {
                    let node1 = leavesArray[j][k];

                    // if (node < nQpAligns) {
                        let chainArray1 = chainidpairArray[node1].split(',');
                        chain1a = chainArray1[0];
                        chain2a = chainArray1[1];
                        
                        // if (chainSet1.hasOwnProperty(chain1)) continue;
                        if (chainSet1.hasOwnProperty(chain1a) || chainSet2.hasOwnProperty(chain2a)) continue;
                        
                        bucm.nodeArray.push(node1.toString().padStart(5, '0'));
            
                        chainSet1[chain1a] = 1;
                        chainSet2[chain2a] = 1;
                    // } 
                    // else {
                    //     isClusterOk = false;
                    //     console.log("Skipping cluster");
                    //     break;
                    // }
                }
        
                //if (isClusterOk) {
                    mappings.push(bucm);
                //}
            }           
        }
        
        return mappings;
    }
    
    getClusters(tree, includeSingletons) { let ic = this.icn3d; ic.icn3dui;
        let clusters = [], scores = [];
        let i = 0, n = tree.length;
        let minClusterSize = (includeSingletons) ? 0 : 1;
    
        for (; i < n; ++i) {
            if (tree[i].leaves.length > minClusterSize) {
                clusters.push(tree[i].leaves);
                scores.push(tree[i].dist);
            }
        }

        return {"clusters": clusters, "scores": scores};
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ApplyCommand {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Execute a command. If the command is to load a structure, use the Method "applyCommandLoad".
    async applyCommand(commandStr) { let ic = this.icn3d, me = ic.icn3dui;
      ic.bAddCommands = false;

      let commandTransformation = commandStr.split('|||');
      let commandTransformation2 = commandTransformation[0].split('%7C%7C%7C'); // sometimes encoded transformation is also included

      let commandOri = commandTransformation2[0].replace(/\s+/g, ' ').trim();
      let command = commandOri.toLowerCase();

    // exact match =============

      //var file_pref =(ic.inputid) ? ic.inputid : "custom";
      if(command == 'share link') {
        await ic.shareLinkCls.shareLink();
      }
      else if(command == 'export state file') ;
      else if(command.indexOf('export canvas') == 0) {
        setTimeout(async function(){
               //ic.saveFileCls.saveFile(file_pref + '_icn3d_loadable.png', 'png');
               let scaleStr = command.substr(13).trim();
               ic.scaleFactor = (scaleStr === '') ? 1 : parseInt(scaleStr);
               let bPngOnly = (scaleStr === '') ? false : true;
               await ic.shareLinkCls.shareLink(true, bPngOnly);
            }, 500);
      }
      else if(command == 'export interactions') {
        ic.viewInterPairsCls.exportInteractions();
      }
      else if(command == 'export stl file') {
        setTimeout(function(){
               ic.export3DCls.exportStlFile('');
            }, 500);
      }
      else if(command == 'export vrml file') {
        setTimeout(function(){
               ic.export3DCls.exportVrmlFile('');
            }, 500);
      }
      else if(command == 'export stl stabilizer file') {
        setTimeout(function(){
               ic.threeDPrintCls.hideStabilizer();
               ic.threeDPrintCls.resetAfter3Dprint();
               ic.threeDPrintCls.addStabilizer();

               ic.export3DCls.exportStlFile('_stab');
            }, 500);
      }
      else if(command == 'export vrml stabilizer file') {
        setTimeout(function(){
               ic.threeDPrintCls.hideStabilizer();
               ic.threeDPrintCls.resetAfter3Dprint();
               ic.threeDPrintCls.addStabilizer();

               ic.export3DCls.exportVrmlFile('_stab');
            }, 500);
      }
      else if(command == 'export pdb') {
         me.htmlCls.setHtmlCls.exportPdb();
      }
      else if(command == 'export secondary structure') {
         me.htmlCls.setHtmlCls.exportSecondary();
      }
      else if(command == 'select all') {
         ic.selectionCls.selectAll();
         //ic.hlObjectsCls.addHlObjects();
      }
      else if(command == 'show all') {
         ic.selectionCls.showAll();
      }
      else if(command == 'select complement') {
         ic.resid2specCls.selectComplement();
      }
      else if(command == 'set pk atom') {
        ic.pk = 1;
        ic.opts['pk'] = 'atom';
      }
      else if(command == 'set pk off') {
        ic.pk = 0;
        ic.opts['pk'] = 'no';
        ic.drawCls.draw();
        ic.hlObjectsCls.removeHlObjects();
      }
      else if(command == 'set pk residue') {
        ic.pk = 2;
        ic.opts['pk'] = 'residue';
      }
      else if(command == 'set pk strand') {
        ic.pk = 3;
        ic.opts['pk'] = 'strand';
      }
      else if(command == 'set pk domain') {
        ic.pk = 4;
        ic.opts['pk'] = 'domain';
      }
      else if(command == 'set pk chain') {
        ic.pk = 5;
        ic.opts['pk'] = 'chain';
      }
      else if(command == 'set surface wireframe on') {
        ic.opts['wireframe'] = 'yes';
        ic.applyMapCls.applySurfaceOptions();
      }
      else if(command == 'set surface wireframe off') {
        ic.opts['wireframe'] = 'no';
        ic.applyMapCls.applySurfaceOptions();
      }
      else if(command == 'set map wireframe on') {
        ic.opts['mapwireframe'] = 'yes';
        ic.applyMapCls.applyMapOptions();
      }
      else if(command == 'set map wireframe off') {
        ic.opts['mapwireframe'] = 'no';
        ic.applyMapCls.applyMapOptions();
      }
      else if(command == 'set emmap wireframe on') {
        ic.opts['emmapwireframe'] = 'yes';
        ic.applyMapCls.applyEmmapOptions();
      }
      else if(command == 'set emmap wireframe off') {
        ic.opts['emmapwireframe'] = 'no';
        ic.applyMapCls.applyEmmapOptions();
      }
      else if(command == 'set surface neighbors on') {
        ic.bConsiderNeighbors = true;
        ic.applyMapCls.applySurfaceOptions();
      }
      else if(command == 'set surface neighbors off') {
        ic.bConsiderNeighbors = false;
        ic.applyMapCls.applySurfaceOptions();
      }
      else if(command == 'set axis on') {
        ic.opts['axis'] = 'yes';
      }
      else if(command == 'set pc1 axis') {
        ic.pc1 = true;
        ic.axesCls.setPc1Axes();
      }
      else if(command == 'set axis off') {
        ic.opts['axis'] = 'no';
        ic.pc1 = false;
      }
      else if(command == 'set fog on') {
        ic.opts['fog'] = 'yes';
        ic.fogCls.setFog(true);
      }
      else if(command == 'set fog off') {
        ic.opts['fog'] = 'no';
        ic.fogCls.setFog(true);
      }
      else if(command == 'set slab on') {
        ic.opts['slab'] = 'yes';
      }
      else if(command == 'set slab off') {
        ic.opts['slab'] = 'no';
      }
      else if(command == 'set assembly on') {
        ic.bAssembly = true;
      }
      else if(command == 'set assembly off') {
        ic.bAssembly = false;
      }
      else if(command == 'set chemicalbinding show') {
        ic.setOptionCls.setOption('chemicalbinding', 'show');
      }
      else if(command == 'set chemicalbinding hide') {
        ic.setOptionCls.setOption('chemicalbinding', 'hide');
      }
      else if(command == 'set hbonds off') {
        ic.hBondCls.hideHbonds();
        ic.drawCls.draw();
      }
      else if(command == 'set salt bridge off') {
        ic.saltbridgeCls.hideSaltbridge();
        ic.drawCls.draw();
      }
      else if(command == 'set contact off') {
        ic.contactCls.hideContact();
        ic.drawCls.draw();
      }
      else if(command == 'set halogen pi off') {
        ic.piHalogenCls.hideHalogenPi();
        ic.drawCls.draw();
      }

      else if(command == 'hydrogens') {
        ic.showInterCls.showHydrogens();
        ic.drawCls.draw();
      }
      else if(command == 'set hydrogens off') {
        ic.showInterCls.hideHydrogens();
        ic.drawCls.draw();
      }
      else if(command == 'close popup') {
          ic.resizeCanvasCls.closeDialogs();
      }
      else if(command == 'set stabilizer off') {
        ic.threeDPrintCls.hideStabilizer();
        ic.drawCls.draw();
      }
      else if(command == 'set disulfide bonds off') {
        ic.opts["ssbonds"] = "no";
        ic.drawCls.draw();
      }
      else if(command == 'set cross linkage off') {
        //ic.bShowCrossResidueBond = false;
        //ic.setOptionCls.setStyle('proteins', 'ribbon');

        ic.opts["clbonds"] = "no";
        ic.drawCls.draw();
      }
      else if(command == 'set lines off') {
        ic.labels['distance'] = [];
        ic.lines['distance'] = [];

        ic.drawCls.draw();
      }
      else if(command == 'set labels off') {
        //ic.labels['residue'] = [];
        //ic.labels['custom'] = [];

        for(let name in ic.labels) {
           //if(name === 'residue' || name === 'custom') {
               ic.labels[name] = [];
           //}
        }

        ic.drawCls.draw();
      }
      else if(command == 'set mode all') {
         ic.definedSetsCls.setModeAndDisplay('all');
      }
      else if(command == 'set mode selection') {
         ic.definedSetsCls.setModeAndDisplay('selection');
      }
      else if(command == 'set view detailed view') {
         ic.annotationCls.setAnnoViewAndDisplay('detailed view');
      }
      else if(command == 'set view overview') {
         ic.annotationCls.setAnnoViewAndDisplay('overview');
      }
      else if(command == 'set annotation custom') {
          ic.annotationCls.setAnnoTabCustom();
      }
      else if(command == 'set annotation interaction') {
          ic.annotationCls.setAnnoTabInteraction();
      }
      else if(command == 'set annotation ptm') {
        await ic.annotationCls.setAnnoTabPTM();
      }
      else if(command == 'set annotation cdd') {
          ic.annotationCls.setAnnoTabCdd();
      }
      else if(command == 'set annotation site') {
          ic.annotationCls.setAnnoTabSite();
      }
      else if(command == 'set annotation ssbond') {
          ic.annotationCls.setAnnoTabSsbond();
      }
      else if(command == 'set annotation crosslink') {
          ic.annotationCls.setAnnoTabCrosslink();
      }
      else if(command == 'set annotation transmembrane') {
          await ic.annotationCls.setAnnoTabTransmem();
      }
      else if(command == 'highlight level up') {
          ic.resid2specCls.switchHighlightLevelUp();
      }
      else if(command == 'highlight level down') {
          ic.resid2specCls.switchHighlightLevelDown();
      }
      else if(command.indexOf('hide annotation') == 0) {
          let pos = command.lastIndexOf(' ');
          let type = command.substr(pos + 1);

          if(type == 'all') {
              ic.annotationCls.hideAnnoTabAll();
          }
          else if(type == 'custom') {
              ic.annotationCls.hideAnnoTabCustom();
          }
          else if(type == 'clinvar') {
              ic.annotationCls.hideAnnoTabClinvar();
          }
          else if(type == 'snp') {
              ic.annotationCls.hideAnnoTabSnp();
          }
          else if(type == 'cdd') {
              ic.annotationCls.hideAnnoTabCdd();
          }
          else if(type == '3ddomain') {
              ic.annotationCls.hideAnnoTab3ddomain();
          }
          else if(type == 'site') {
              ic.annotationCls.hideAnnoTabSite();
          }
          else if(type == 'ptm') {
            ic.annotationCls.hideAnnoTabPTM();
        }
          else if(type == 'interaction') {
              ic.annotationCls.hideAnnoTabInteraction();
          }
          else if(type == 'ssbond') {
              ic.annotationCls.hideAnnoTabSsbond();
          }
          else if(type == 'crosslink') {
              ic.annotationCls.hideAnnoTabCrosslink();
          }
          else if(type == 'transmembrane') {
              ic.annotationCls.hideAnnoTabTransmem();
          }
      }
      else if(command == 'add residue labels') {
        ic.residueLabelsCls.addResidueLabels(ic.hAtoms);

        ic.drawCls.draw();
      }
      else if(command == 'add residue number labels') {
        ic.residueLabelsCls.addResidueLabels(ic.hAtoms, undefined, undefined, true);

        ic.drawCls.draw();
      }
      else if(command == 'add atom labels') {
        ic.residueLabelsCls.addAtomLabels(ic.hAtoms);

        ic.drawCls.draw();
      }
      else if(command == 'add element labels') {
        ic.residueLabelsCls.addAtomLabels(ic.hAtoms, true);

        ic.drawCls.draw();
      }
      else if(command == 'add chain labels') {
        ic.analysisCls.addChainLabels(ic.hAtoms);

        ic.drawCls.draw();
      }
      else if(command == 'add terminal labels') {
        ic.analysisCls.addTerminiLabels(ic.hAtoms);

        ic.drawCls.draw();
      }
      else if(command == 'rotate left') {
         ic.bStopRotate = false;
         ic.ROT_DIR = 'left';

         ic.resizeCanvasCls.rotStruc('left');
      }
      else if(command == 'rotate right') {
         ic.bStopRotate = false;
         ic.ROT_DIR = 'right';

         ic.resizeCanvasCls.rotStruc('right');
      }
      else if(command == 'rotate up') {
         ic.bStopRotate = false;
         ic.ROT_DIR = 'up';

         ic.resizeCanvasCls.rotStruc('up');
      }
      else if(command == 'rotate down') {
         ic.bStopRotate = false;
         ic.ROT_DIR = 'down';

         ic.resizeCanvasCls.rotStruc('down');
      }
      else if(command == 'rotate x') {
          let axis = new THREE.Vector3(1,0,0);
          let angle = 0.5 * Math.PI;

          ic.transformCls.setRotation(axis, angle);
      }
      else if(command == 'rotate y') {
          let axis = new THREE.Vector3(0,1,0);
          let angle = 0.5 * Math.PI;

          ic.transformCls.setRotation(axis, angle);
      }
      else if(command == 'rotate z') {
          let axis = new THREE.Vector3(0,0,1);
          let angle = 0.5 * Math.PI;

          ic.transformCls.setRotation(axis, angle);
      }
      else if(command === 'reset') {
          ic.selectionCls.resetAll();
      }
      else if(command === 'reset orientation') {
        ic.transformCls.resetOrientation();
        ic.drawCls.draw();
      }
      else if(command == 'reset thickness') {
        ic.threeDPrintCls.resetAfter3Dprint();
        ic.drawCls.draw();
      }
      else if(command == 'clear selection') {
        ic.hlObjectsCls.removeHlObjects();
        ic.hlUpdateCls.removeHl2D();
        ic.bShowHighlight = false;

        ic.bSelectResidue = false;
      }
      else if(command == 'zoom selection') {
        ic.transformCls.zoominSelection();
        ic.drawCls.draw();
      }
      else if(command == 'center selection') {
        ic.applyCenterCls.centerSelection();
        ic.drawCls.draw();
      }
      else if(command == 'show selection') {
        ic.selectionCls.showSelection();
      }
      else if(command == 'hide selection') {
        ic.selectionCls.hideSelection();
      }
      else if(command == 'output selection') {
          ic.threeDPrintCls.outputSelection();
      }
      else if(command == 'toggle selection') {
         ic.selectionCls.toggleSelection();
      }
      else if(command == 'toggle highlight') {
        ic.hlUpdateCls.toggleHighlight();
      }
      else if(command == 'stabilizer') {
        ic.threeDPrintCls.addStabilizer();

        ic.threeDPrintCls.prepareFor3Dprint();
        //ic.drawCls.draw();
      }
      else if(command == 'disulfide bonds') {
        ic.showInterCls.showSsbonds();
      }
      else if(command == 'cross linkage') {
        ic.showInterCls.showClbonds();
      }
      else if(command == 'back') {
        await ic.resizeCanvasCls.back();
      }
      else if(command == 'forward') {
        await ic.resizeCanvasCls.forward();
      }
      else if(command == 'clear all') {
         ic.selectionCls.selectAll();
      }
      else if(command == 'defined sets') {
         ic.definedSetsCls.showSets();
      }
      else if(command == 'delete selected sets') {
         ic.definedSetsCls.deleteSelectedSets();
      }
      else if(command == 'view interactions') {
         if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined) {
             ic.ParserUtilsCls.set2DDiagrams(ic.inputid);
         }
      }
      else if(command == 'show annotations all chains') {
         ic.annotationCls.showAnnoAllChains();
      }

      else if(command == 'save color') {
         ic.setOptionCls.saveColor();
      }
      else if(command == 'apply saved color') {
         ic.setOptionCls.applySavedColor();
      }
      else if(command == 'save style') {
         ic.setOptionCls.saveStyle();
      }
      else if(command == 'apply saved style') {
         ic.setOptionCls.applySavedStyle();
      }
      else if(command == 'select main chains') {
         ic.selectionCls.selectMainChains();
      }
      else if(command == 'select side chains') {
         ic.selectionCls.selectSideChains();
      }
      else if(command == 'select main side chains') {
         ic.selectionCls.selectMainSideChains();
      }
      else if(command == 'realign') {
         ic.realignParserCls.realign();
      }
      else if(command.indexOf('realign predefined ') != -1) {
        //e.g., realign predefined 1HHO_A,4M7N_A 1,5,10-50 | 1,5,10-50: 2,6,11-51 | 1,5,10-50
        let str = 'realign predefined ';
        let chainids_resdef = commandOri.substr(str.length);
        let pos = chainids_resdef.indexOf(' ');
        let chainidArray = chainids_resdef.substr(0, pos).split(',');
        me.cfg.resdef = chainids_resdef.substr(pos + 1).replace(/:/gi, ';'); // should be 1,5,10-50 | 1,5,10-50; 2,6,11-51 | 1,5,10-50

        await ic.realignParserCls.realignChainOnSeqAlign(undefined, chainidArray, true, true);
     }
      else if(command == 'area') {
         ic.analysisCls.calculateArea();
      }
      else if(command == 'table inter count only') {
         $(".icn3d-border").hide();
      }
      else if(command == 'table inter details') {
         $(".icn3d-border").show();
      }
      else if(command == 'setoption map nothing') {
         ic.setOptionCls.setOption('map', 'nothing');
      }
      else if(command == 'setoption emmap nothing') {
         ic.setOptionCls.setOption('emmap', 'nothing');
      }
      else if(command == 'setoption phimap nothing') {
         ic.setOptionCls.setOption('phimap', 'nothing');
      }
      else if(command == 'setoption phisurface nothing') {
         ic.setOptionCls.setOption('phisurface', 'nothing');
      }
      else if(command == 'clear symd symmetry') {
         ic.symdArray = [];
      }
      else if(command == 'show axis') {
         ic.bAxisOnly = true;
      }

    // start with =================
      else if(commandOri.indexOf('define helix sets') == 0) {
         let chainStr = commandOri.split(' | ')[1];
         let chainid = chainStr.split(' ')[1];

         ic.addTrackCls.defineSecondary(chainid, 'helix');
      }
      else if(commandOri.indexOf('define sheet sets') == 0) {
         let chainStr = commandOri.split(' | ')[1];
         let chainid = chainStr.split(' ')[1];

         ic.addTrackCls.defineSecondary(chainid, 'sheet');
      }
      else if(commandOri.indexOf('define coil sets') == 0) {
         let chainStr = commandOri.split(' | ')[1];
         let chainid = chainStr.split(' ')[1];

         ic.addTrackCls.defineSecondary(chainid, 'coil');
      }
      else if(commandOri.indexOf('select interaction') == 0) {
        let idArray = commandOri.substr(commandOri.lastIndexOf(' ') + 1).split(',');
        if(idArray !== null) {
            let mmdbid = idArray[0].split('_')[0];
            if(!ic.b2DShown) ic.ParserUtilsCls.download2Ddgm(mmdbid.toUpperCase());

            ic.diagram2dCls.selectInteraction(idArray[0], idArray[1]);
        }
      }

      else if(commandOri.indexOf('select saved atoms') == 0 || commandOri.indexOf('select sets') == 0) {
        // backward compatible: convert previous aligned_protein to protein_aligned
        commandOri = commandOri.replace(/aligned_protein/g, 'protein_aligned');

        let paraArray = commandOri.split(' | '); // atom names might be case-sensitive

        let select = paraArray[0].replace(/,/g, ' or ');

        let pos = 19; // 'select saved atoms '
        if(commandOri.indexOf('select sets') == 0) pos = 12; // 'select sets '

        let strSets = select.substr(pos);
        
        let commandname = strSets;

        if(paraArray.length == 2) commandname = paraArray[1].substr(5); // 'name ...'
        ic.definedSetsCls.selectCombinedSets(strSets, commandname);
      }
      else if(commandOri.indexOf('select chain') !== -1) {
        let idArray = commandOri.substr(commandOri.lastIndexOf(' ') + 1).split(',');

        //if(idArray !== null) ic.changeChainid(idArray);
        for(let i = 0, il = idArray.length; i < il; ++i) {
            ic.selectionCls.selectAChain(idArray[i], idArray[i], false);
        }
      }
      else if(commandOri.indexOf('select alignChain') !== -1) {
        let idArray = commandOri.substr(commandOri.lastIndexOf(' ') + 1).split(',');

        //if(idArray !== null) ic.changeChainid(idArray);
        for(let i = 0, il = idArray.length; i < il; ++i) {
            ic.selectionCls.selectAChain(idArray[i], 'align_' + idArray[i], true);
        }
      }
      else if(commandOri.indexOf('select zone cutoff') == 0) {
        let ret = this.getThresholdNameArrays(commandOri);

        ic.showInterCls.pickCustomSphere(ret.threshold, ret.nameArray2, ret.nameArray, ret.bHbondCalc);
        ic.bSphereCalc = true;

        //ic.hlUpdateCls.updateHlAll();
      }
      else if(command.indexOf('set surface opacity') == 0) {
        ic.transparentRenderOrder = false;

        let value = command.substr(command.lastIndexOf(' ') + 1);
        ic.opts['opacity'] = parseFloat(value);
        ic.applyMapCls.applySurfaceOptions();

        if(parseInt(100*value) < 100) ic.bTransparentSurface = true;
      }
      else if(command.indexOf('set surface2 opacity') == 0) {
        ic.transparentRenderOrder = true;
        let value = command.substr(command.lastIndexOf(' ') + 1);
        ic.opts['opacity'] = parseFloat(value);
        ic.applyMapCls.applySurfaceOptions();

        if(parseInt(100*value) < 100) ic.bTransparentSurface = true;
      }
      else if(command.indexOf('set label scale') == 0) {
        let value = command.substr(command.lastIndexOf(' ') + 1);
        ic.labelScale = parseFloat(value);
      }
      else if(command.indexOf('set surface') == 0) {
        let value = command.substr(12);

        ic.opts['surface'] = value;
        ic.applyMapCls.applySurfaceOptions();
      }
      else if(command.indexOf('set camera') == 0) {
        let value = command.substr(command.lastIndexOf(' ') + 1);
        ic.opts['camera'] = value;
      }
      else if(command.indexOf('set background') == 0) {
        let value = command.substr(command.lastIndexOf(' ') + 1);
        ic.opts['background'] = value;

        if(value == 'black') {
          $("#" + ic.pre + "title").css("color", me.htmlCls.GREYD);
          $("#" + ic.pre + "titlelink").css("color", me.htmlCls.GREYD);
        }
        else {
          $("#" + ic.pre + "title").css("color", "black");
          $("#" + ic.pre + "titlelink").css("color", "black");
        }
      }
      else if(command.indexOf('set label color') == 0) {
        ic.labelcolor = command.substr(command.lastIndexOf(' ') + 1);
      }
      else if(commandOri.indexOf('set thickness') == 0) {
        let paraArray = command.split(' | ');

        ic.bSetThickness = true;

        for(let i = 1, il = paraArray.length; i < il; ++i) {
            let p1Array = paraArray[i].split(' ');

            let para = p1Array[0];
            let value = parseFloat(p1Array[1]);

            if(para == 'linerad' && !isNaN(value)) ic.lineRadius = value;
            if(para == 'coilrad' && !isNaN(value)) ic.coilWidth = value;
            if(para == 'stickrad' && !isNaN(value)) ic.cylinderRadius = value;
            if(para == 'crosslinkrad' && !isNaN(value)) ic.crosslinkRadius = value;
            if(para == 'tracerad' && !isNaN(value)) ic.traceRadius = value;
            if(para == 'ballscale' && !isNaN(value)) ic.dotSphereScale = value;

            if(para == 'ribbonthick' && !isNaN(value)) ic.ribbonthickness = value;
            if(para == 'proteinwidth' && !isNaN(value)) ic.helixSheetWidth = value;
            if(para == 'nucleotidewidth' && !isNaN(value)) ic.nucleicAcidWidth = value;
        }

        ic.drawCls.draw();
      }
      else if(commandOri.indexOf('set light') == 0) {
        let paraArray = command.split(' | ');

        for(let i = 1, il = paraArray.length; i < il; ++i) {
            let p1Array = paraArray[i].split(' ');

            let para = p1Array[0];
            let value = parseFloat(p1Array[1]);

            if(para == 'light1') ic.light1 = value;
            if(para == 'light2') ic.light2 = value;
            if(para == 'light3') ic.light3 = value;
        }

        ic.drawCls.draw();
      }
      else if(commandOri.indexOf('set shininess') == 0) {
        let pos = command.lastIndexOf(' ');

        ic.shininess = parseFloat(command.substr(pos + 1));

        ic.drawCls.draw();
      }
      else if(commandOri.indexOf('set glycan') == 0) {
        let pos = command.lastIndexOf(' ');

        ic.bGlycansCartoon = parseInt(command.substr(pos + 1));

        ic.drawCls.draw();
      }
      else if(commandOri.indexOf('set membrane') == 0) {
        let pos = command.lastIndexOf(' ');

        ic.bMembrane = parseInt(command.substr(pos + 1));

        ic.drawCls.draw();
      }
      else if(commandOri.indexOf('set cmdwindow') == 0) {
        let pos = command.lastIndexOf(' ');

        let bCmdWindow = parseInt(command.substr(pos + 1));
        me.htmlCls.setMenuCls.setLogWindow(true, bCmdWindow);
      }
      else if(command.indexOf('set highlight color') == 0) {
           let color = command.substr(20);
           if(color === 'yellow') {
               ic.hColor = me.parasCls.thr(0xFFFF00);
               ic.matShader = ic.setColorCls.setOutlineColor('yellow');
           }
           else if(color === 'green') {
               ic.hColor = me.parasCls.thr(0x00FF00);
               ic.matShader = ic.setColorCls.setOutlineColor('green');
           }
           else if(color === 'red') {
               ic.hColor = me.parasCls.thr(0xFF0000);
               ic.matShader = ic.setColorCls.setOutlineColor('red');
           }
           ic.drawCls.draw(); // required to make it work properly
      }
      else if(command.indexOf('set highlight style') == 0) {
            let style = command.substr(20);

           if(style === 'outline') {
               ic.bHighlight = 1;
           }
           else if(style === '3d') {
               ic.bHighlight = 2;
           }

           ic.drawCls.draw();
      }
      else if(command.indexOf('add line') == 0) {
        let paraArray = command.split(' | ');
        let p1Array = paraArray[1].split(' ');
        let p2Array = paraArray[2].split(' ');
        let color = paraArray[3].substr(paraArray[3].lastIndexOf(' ') + 1);
        let dashed = paraArray[4].substr(paraArray[4].lastIndexOf(' ') + 1) === 'true' ? true : false;
        let type = paraArray[5].substr(paraArray[5].lastIndexOf(' ') + 1);
        let radius = (paraArray.length > 6) ? paraArray[6].substr(paraArray[6].lastIndexOf(' ') + 1) : 0;
        let opacity = (paraArray.length > 7) ? paraArray[7].substr(paraArray[7].lastIndexOf(' ') + 1) : 1.0;

        ic.analysisCls.addLine(parseFloat(p1Array[1]), parseFloat(p1Array[3]), parseFloat(p1Array[5]), parseFloat(p2Array[1]), parseFloat(p2Array[3]), parseFloat(p2Array[5]), color, dashed, type, parseFloat(radius), parseFloat(opacity));
        ic.drawCls.draw();
      }
      else if(command.indexOf('add sphere') == 0) {
        this.addShape(command, 'sphere');
        //ic.drawCls.draw();
      }
      else if(command.indexOf('add cube') == 0) {
        this.addShape(command, 'cube');
        //ic.drawCls.draw();
      }
      else if(command.indexOf('clear shape') == 0) {
        ic.shapeCmdHash = {};
        //ic.drawCls.draw();
      }
      else if(command.indexOf('clear line between sets') == 0) {
        ic.lines['cylinder'] = []; // reset
        //ic.drawCls.draw();
      }
      else if(commandOri.indexOf('add label') == 0) {
        let paraArray = commandOri.split(' | ');
        let text = paraArray[0].substr(('add label').length + 1);

        // add label Text | x 40.45 y 24.465000000000003 z 53.48 | size 40 | color #ffff00 | background #cccccc | type custom
        let x,y,z, size, color, background, type;
        let bPosition = false;
        for(let i = 1, il = paraArray.length; i < il; ++i) {
            let wordArray = paraArray[i].split(' ');
            if(wordArray[0] == 'x') {
                bPosition = true;
                x = parseFloat(wordArray[1]);
                y = parseFloat(wordArray[3]);
                z = parseFloat(wordArray[5]);
            }
            else if(wordArray[0] == 'size') {
                size = paraArray[i].substr(paraArray[i].lastIndexOf(' ') + 1);
            }
            else if(wordArray[0] == 'color') {
                color = paraArray[i].substr(paraArray[i].lastIndexOf(' ') + 1);
            }
            else if(wordArray[0] == 'background') {
                background = paraArray[i].substr(paraArray[i].lastIndexOf(' ') + 1);
            }
            else if(wordArray[0] == 'type') {
                type = paraArray[i].substr(paraArray[i].lastIndexOf(' ') + 1);
            }
        }

        if(!bPosition) {
          let position = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.hAtoms, ic.atoms));
          x = parseFloat(position.center.x);
          y = parseFloat(position.center.y);
          z = parseFloat(position.center.z);
        }

        ic.analysisCls.addLabel(text, x,y,z, size, color, background, type);
        ic.drawCls.draw();
      }
      else if(commandOri.indexOf('msa') == 0) {
          //"msa | " + JSON.stringify(ic.targetGapHash)
          let paraArray = commandOri.split(' | ');

          let pos_from_toArray = paraArray[1].split(' ');

          ic.targetGapHash = {};
          for(let i = 0, il = pos_from_toArray.length; i < il; ++i) {
              let pos_from_to = pos_from_toArray[i].split('_');
              ic.targetGapHash[parseInt(pos_from_to[0])] = {"from": parseInt(pos_from_to[1]), "to": parseInt(pos_from_to[2])};
          }

          await ic.annotationCls.resetAnnoAll();
      }
      else if(commandOri.indexOf('add track') == 0) {
          //"add track | chainid " + chainid + " | title " + title + " | text " + text
          // + " | type " + type + " | color " + color + " | msa " + color
          let paraArray = commandOri.split(' | ');

          let chainid = paraArray[1].substr(8);
          let title = paraArray[2].substr(6);
          let text = paraArray[3].substr(5);
          let type;
          if(paraArray.length >= 5) type = paraArray[4].substr(5);
          let color;
          if(paraArray.length >= 6) color = paraArray[5].substr(6);
          let msa;
          if(paraArray.length >= 7) msa = paraArray[6].substr(4);

          $("#" + ic.pre + "anno_custom")[0].checked = true;
          $("[id^=" + ic.pre + "custom]").show();

          if(color == '0') color = undefined;

          ic.addTrackCls.checkGiSeq(chainid, title, text, type, color, msa, 0);
      }
      else if(command.indexOf('remove one stabilizer') == 0) {
        let paraArray = command.split(' | ');
        let p1Array = paraArray[1].split(' ');

        let rmLineArray = [];
        rmLineArray.push(parseInt(p1Array[0]));
        rmLineArray.push(parseInt(p1Array[1]));

        ic.threeDPrintCls.removeOneStabilizer(rmLineArray);

        ic.drawCls.draw();
      }
      else if(command.indexOf('add one stabilizer') == 0) {
        let paraArray = command.split(' | ');
        let p1Array = paraArray[1].split(' ');

         if(ic.pairArray === undefined) ic.pairArray = [];
         ic.pairArray.push(parseInt(p1Array[0]));
         ic.pairArray.push(parseInt(p1Array[1]));

         ic.drawCls.draw();
      }
      else if(command.indexOf('select planes z-axis') == 0) {
        let paraArray = command.split(' ');
        if(paraArray.length == 5) {
            let large = parseFloat(paraArray[3]);
            let small = parseFloat(paraArray[4]);

            ic.selectionCls.selectBtwPlanes(large, small);
        }
      }
      else if(command.indexOf('adjust membrane z-axis') == 0) {
        let paraArray = command.split(' ');
        if(paraArray.length == 5) {
            let large = parseFloat(paraArray[3]);
            let small = parseFloat(paraArray[4]);

            ic.selectionCls.adjustMembrane(large, small);
        }
      }
      else if(command.indexOf('toggle membrane') == 0) {
        ic.selectionCls.toggleMembrane();
      }
      else if(commandOri.indexOf('calc buried surface') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 2) {
            let setNameArray = paraArray[1].split(' ');

            if(setNameArray.length == 2) {
                let nameArray2 = setNameArray[0].split(',');
                let nameArray = setNameArray[1].split(',');

                ic.analysisCls.calcBuriedSurface(nameArray2, nameArray);
            }
        }
      }
      else if(commandOri.indexOf('dist ') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 2) {
            let setNameArray = paraArray[1].split(' ');

            if(setNameArray.length == 2) {
                let nameArray = setNameArray[0].split(',');
                let nameArray2 = setNameArray[1].split(',');

                ic.analysisCls.measureDistTwoSets(nameArray, nameArray2);
            }
        }
      }
      else if(commandOri.indexOf('disttable') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 2) {
            let setNameArray = paraArray[1].split(' ');

            if(setNameArray.length == 2) {
                let nameArray = setNameArray[0].split(',');
                let nameArray2 = setNameArray[1].split(',');

                ic.analysisCls.measureDistManySets(nameArray, nameArray2);
                me.htmlCls.dialogCls.openDlg('dl_disttable', 'Distance among the sets');
            }
        }
      }
      else if(commandOri.indexOf('display interaction 3d') == 0
          || commandOri.indexOf('view interaction pairs') == 0
          || commandOri.indexOf('save1 interaction pairs') == 0
          || commandOri.indexOf('save2 interaction pairs') == 0
          || commandOri.indexOf('line graph interaction pairs') == 0
          || commandOri.indexOf('scatterplot interaction pairs') == 0
          ) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length >= 3) {
            let setNameArray = paraArray[1].split(' ');

            if(setNameArray.length == 2) {
                let nameArray2 = setNameArray[0].split(',');
                let nameArray = setNameArray[1].split(',');

                let bHbond = paraArray[2].indexOf('hbonds') !== -1;
                let bSaltbridge = paraArray[2].indexOf('salt bridge') !== -1;
                let bInteraction = paraArray[2].indexOf('interactions') !== -1;

                let bHalogen = paraArray[2].indexOf('halogen') !== -1;
                let bPication = paraArray[2].indexOf('pi-cation') !== -1;
                let bPistacking = paraArray[2].indexOf('pi-stacking') !== -1;

                let bHbondCalc;
                if(paraArray.length >= 4) {
                    bHbondCalc =(paraArray[3] == 'true') ? true : false;
                }

                if(paraArray.length >= 5) {
                   let thresholdArray = paraArray[4].split(' ');

                   if(thresholdArray.length >= 4) {
                       $("#" + ic.pre + "hbondthreshold").val(thresholdArray[1]);
                       $("#" + ic.pre + "saltbridgethreshold").val(thresholdArray[2]);
                       $("#" + ic.pre + "contactthreshold").val(thresholdArray[3]);

                       if(thresholdArray.length == 7) {
                           $("#" + ic.pre + "halogenthreshold").val(thresholdArray[4]);
                           $("#" + ic.pre + "picationthreshold").val(thresholdArray[5]);
                           $("#" + ic.pre + "pistackingthreshold").val(thresholdArray[6]);
                       }
                   }
                }

                let type;
                if(commandOri.indexOf('display interaction 3d') == 0) {
                    type = '3d';
                }
                else if(commandOri.indexOf('view interaction pairs') == 0) {
                    type = 'view';
                }
                else if(commandOri.indexOf('save1 interaction pairs') == 0) {
                    type = 'save1';
                }
                else if(commandOri.indexOf('save2 interaction pairs') == 0) {
                    type = 'save2';
                }
                else if(commandOri.indexOf('line graph interaction pairs') == 0) {
                    type = 'linegraph';
                }
                else if(commandOri.indexOf('scatterplot interaction pairs') == 0) {
                    type = 'scatterplot';
                }

                await ic.viewInterPairsCls.viewInteractionPairs(nameArray2, nameArray, bHbondCalc, type, bHbond, bSaltbridge, bInteraction, bHalogen, bPication, bPistacking);
            }
        }
      }
      else if(commandOri.indexOf('export pairs') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 3) {
            let setNameArray = paraArray[1].split(' ');

            if(setNameArray.length == 2) {
                let nameArray2 = setNameArray[0].split(',');
                let nameArray = setNameArray[1].split(',');

                let distArray = paraArray[2].split(' ');
                let radius = distArray[1];

                ic.showInterCls.pickCustomSphere(radius, nameArray2, nameArray, ic.bSphereCalc);
                ic.bSphereCalc = true;
                let text = ic.viewInterPairsCls.exportSpherePairs();
                let file_pref =(ic.inputid) ? ic.inputid : "custom";
                ic.saveFileCls.saveFile(file_pref + '_sphere_pairs.html', 'html', text);
            }
        }
      }
      else if(command.indexOf('graph label') == 0) {
        let pos = command.lastIndexOf(' ');
        let className = command.substr(pos + 1);

        $("#" + me.svgid + "_label").val(className);

        $("#" + me.svgid + " text").removeClass();
        $("#" + me.svgid + " text").addClass(className);
      }
      else if(command.indexOf('cartoon label') == 0) {
        let pos = command.lastIndexOf(' ');
        let className = command.substr(pos + 1);

        $("#" + me.svgid_ct + "_label").val(className);

        $("#" + me.svgid_ct + " text").removeClass();
        $("#" + me.svgid_ct + " text").addClass(className);
      }
      else if(command.indexOf('line graph scale') == 0) {
        let pos = command.lastIndexOf(' ');
        let scale = command.substr(pos + 1);

        $("#" + me.linegraphid + "_scale").val(scale);

        $("#" + me.linegraphid).attr("width",(ic.linegraphWidth * parseFloat(scale)).toString() + "px");
      }
      else if(command.indexOf('scatterplot scale') == 0) {
        let pos = command.lastIndexOf(' ');
        let scale = command.substr(pos + 1);

        $("#" + me.scatterplotid + "_scale").val(scale);

        $("#" + me.scatterplotid).attr("width",(ic.scatterplotWidth * parseFloat(scale)).toString() + "px");
      }
      else if(command.indexOf('contactmap scale') == 0) {
        let pos = command.lastIndexOf(' ');
        let scale = command.substr(pos + 1);

        $("#" + me.contactmapid + "_scale").val(scale);

        $("#" + me.contactmapid).attr("width",(ic.contactmapWidth * parseFloat(scale)).toString() + "px");
      }
      else if(command.indexOf('alignerrormap scale') == 0) {
        let pos = command.lastIndexOf(' ');
        let scale = command.substr(pos + 1);

        $("#" + me.alignerrormapid + "_scale").val(scale);

        $("#" + me.alignerrormapid).attr("width",(ic.alignerrormapWidth * parseFloat(scale)).toString() + "px");
      }
      else if(command.indexOf('graph force') == 0) {
        let pos = command.lastIndexOf(' ');
        me.htmlCls.force = parseInt(command.substr(pos + 1));

        $("#" + me.svgid + "_force").val(me.htmlCls.force);

        ic.getGraphCls.handleForce();
      }
      else if(command.indexOf('hide edges') == 0) {
        let pos = command.lastIndexOf(' ');
        me.htmlCls.hideedges = parseInt(command.substr(pos + 1));

        $("#" + me.svgid + "_hideedges").val(me.htmlCls.hideedges);

        if(me.htmlCls.hideedges) {
            me.htmlCls.contactInsideColor = 'FFF';
            me.htmlCls.hbondInsideColor = 'FFF';
            me.htmlCls.ionicInsideColor = 'FFF';
        }
        else {
            me.htmlCls.contactInsideColor = 'DDD';
            me.htmlCls.hbondInsideColor = 'AFA';
            me.htmlCls.ionicInsideColor = '8FF';
        }

        if(ic.graphStr !== undefined && ic.bRender && me.htmlCls.force) {
           ic.drawGraphCls.drawGraph(ic.graphStr, ic.pre + 'dl_graph');
        }
      }
      else if(command.indexOf('reset interaction pairs') == 0) {
        ic.viewInterPairsCls.resetInteractionPairs();
      }
      else if(command.indexOf('side by side') == 0) {
        let paraArray = command.split(' | ');
        let url = paraArray[1];

        let urlTarget = '_blank';
        window.open(url, urlTarget);
      }
      else if(commandOri.indexOf('your note') == 0) {
        let paraArray = commandOri.split(' | ');
        ic.yournote = paraArray[1];

        $("#" + ic.pre + "yournote").val(ic.yournote);
        if(me.cfg.shownote) document.title = ic.yournote;
      }
      else if(command.indexOf('cross structure interaction') == 0) {
        ic.crossstrucinter = parseInt(command.substr(command.lastIndexOf(' ') + 1));

        $("#" + ic.pre + "crossstrucinter").val(ic.crossstrucinter);
      }
      else if(command == 'replay on') {
        await ic.resizeCanvasCls.replayon();
      }
      else if(command == 'replay off') {
        await ic.resizeCanvasCls.replayoff();
      }

    // start with, single word =============
      else if(command.indexOf('contact map') == 0) {
        let strArray = command.split(" | ");

        if(strArray.length === 3) {
            let contactdist = parseFloat(strArray[1].split(' ')[1]);
            let contacttype = strArray[2].split(' ')[1];

            await ic.contactMapCls.contactMap(contactdist, contacttype);
        }
      }
      else if(command.indexOf('pickatom') == 0) {
        let atomid = parseInt(command.substr(command.lastIndexOf(' ') + 1));

        ic.pAtom = ic.atoms[atomid];

        ic.pickingCls.showPicking(ic.pAtom);
      }
      else if(commandOri.indexOf('set color spectrum') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 2) {
            let nameArray = paraArray[1].split(',');

            let bSpectrum = true;
            ic.setColorCls.setColorAcrossSets(nameArray, bSpectrum);
        }
      }
      else if(commandOri.indexOf('set residues color spectrum') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 2) {
            let nameArray = paraArray[1].split(',');

            let bSpectrum = true;
            ic.setColorCls.setColorBySets(nameArray, bSpectrum);
        }
      }
      else if(commandOri.indexOf('set color rainbow') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 2) {
            let nameArray = paraArray[1].split(',');

            let bSpectrum = false;
            ic.setColorCls.setColorAcrossSets(nameArray, bSpectrum);
        }
      }
      else if(commandOri.indexOf('set residues color rainbow') == 0) {
        let paraArray = commandOri.split(' | ');
        if(paraArray.length == 2) {
            let nameArray = paraArray[1].split(',');

            let bSpectrum = false;
            ic.setColorCls.setColorBySets(nameArray, bSpectrum);
        }
      }
      else if(commandOri.indexOf('color') == 0) {
        let strArray = commandOri.split(" | ");
        let color = strArray[0].substr(strArray[0].indexOf(' ') + 1);
        ic.opts['color'] = color;

        if(color == "residue custom" && strArray.length == 2) {
            ic.customResidueColors = JSON.parse(strArray[1]);
            for(let res in ic.customResidueColors) {
                ic.customResidueColors[res.toUpperCase()] = me.parasCls.thr("#" + ic.customResidueColors[res]);
            }
        }
        else if(color == "align custom" && strArray.length == 3) {
            let chainid = strArray[1];
            let resiScoreArray = strArray[2].split(', ');
            ic.queryresi2score = {};
            ic.queryresi2score[chainid] = {};
            for(let i = 0, il = resiScoreArray.length; i < il; ++i) {
                let resi_score = resiScoreArray[i].split(' ');

                ic.queryresi2score[chainid][resi_score[0]] = resi_score[1];
            }
        }
        else if(color == "align custom" && strArray.length >= 4) {
            // me.htmlCls.clickMenuCls.setLogCmd('color align custom | ' + chainid + ' | range ' + start + '_' + end + ' | ' + resiScoreStr, true);
            this.setQueryresi2score(strArray);
        }
        else if(color == "area" && strArray.length == 2) {
            ic.midpercent = strArray[1];
            $("#" + ic.pre + 'midpercent').val(ic.midpercent);
        }

        ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

        ic.hlUpdateCls.updateHlAll();

        // change graph color
        ic.getGraphCls.updateGraphColor();
      }
      else if(commandOri.indexOf('remove legend') == 0) {
        $("#" + me.pre + "legend").hide();
      }
      else if(commandOri.indexOf('custom tube') == 0) {
        let strArray = commandOri.split(" | ");

        this.setQueryresi2score(strArray);

        ic.setOptionCls.setStyle('proteins', 'custom tube');
      }
      else if(command.indexOf('style') == 0) {
        let secondPart = command.substr(command.indexOf(' ') + 1);

        let selectionType = secondPart.substr(0, secondPart.indexOf(' '));
        let style = secondPart.substr(secondPart.indexOf(' ') + 1);
        
        ic.setOptionCls.setStyle(selectionType, style);
      }
      else if(command.indexOf('window') == 0) {
        let secondPart = command.substr(command.indexOf(' ') + 1);

        if(secondPart == "aligned sequences") {
            me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences');
        }
        else if(secondPart == "interaction table") {
            me.htmlCls.dialogCls.openDlg('dl_allinteraction', 'Show interactions');
        }
        else if(secondPart == "interaction graph") {
            me.htmlCls.dialogCls.openDlg('dl_linegraph', 'Show interactions between two lines of residue nodes');
        }
        else if(secondPart == "interaction scatterplot") {
            me.htmlCls.dialogCls.openDlg('dl_scatterplot', 'Show interactions as scatterplot');
        }
        else if(secondPart == "force-directed graph") {
            me.htmlCls.dialogCls.openDlg('dl_graph', 'Force-directed graph');
        }
      }
      else if(command.indexOf('set theme') == 0) {
        let color = command.substr(command.lastIndexOf(' ') + 1);
        me.htmlCls.setMenuCls.setTheme(color);
      }
      else if(command.indexOf('set double color') == 0) {
        let value = command.substr(command.lastIndexOf(' ') + 1);
        if(value == 'on') {
            ic.bDoublecolor = true;
            ic.setOptionCls.setStyle('proteins', 'ribbon');
        }
        else if(value == 'off') {
            ic.bDoublecolor = false;
        }
      }
      else if(command.indexOf('adjust dialog') == 0) {
        let id = command.substr(command.lastIndexOf(' ') + 1);
        ic.scapCls.adjust2DWidth(id);
      }
      else if(command.indexOf('glycans cartoon') == 0) {
        let value = command.substr(command.lastIndexOf(' ') + 1);
        if(value == 'yes') {
            ic.bGlycansCartoon = true;
        }
        else {
            ic.bGlycansCartoon = false;
        }
      }
      else if(command.indexOf('save html') == 0) {
        let id = command.substr(command.lastIndexOf(' ') + 1);
        me.htmlCls.eventsCls.saveHtml(id);
      }
      else if(command.indexOf('resdef') == 0) {
        me.cfg.resdef = command.substr(command.indexOf(' ') + 1);
      }
      else if(command.indexOf('vast_search_chainid') == 0) {
        ic.chainidArray = commandOri.substr(commandOri.indexOf(' ') + 1).split(',');

        let bRealign = true, bPredefined = true;
        await ic.realignParserCls.realignChainOnSeqAlign(undefined, ic.chainidArray, bRealign, bPredefined);

        // reset annotations
        // $("#" + ic.pre + "dl_annotations").html("");
        // ic.bAnnoShown = false;
        // if($('#' + ic.pre + 'dl_selectannotations').dialog( 'isOpen' )) {
        //     $('#' + ic.pre + 'dl_selectannotations').dialog( 'close' );
        // }
      }
      else if(command.indexOf('ig refnum off') == 0) {
        await ic.refnumCls.hideIgRefNum();
      }
      else if(command.indexOf('custom refnum') == 0) {
        let paraArray = commandOri.split(' | ');
        let dataStr = paraArray[1].replace(/\\n/g, '\n');
        await ic.refnumCls.parseCustomRefFile(dataStr);
      }

    // special, select ==========

      else if(command.indexOf('select displayed set') !== -1) {
        //ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);
        ic.hAtoms = me.hashUtilsCls.cloneHash(ic.viewSelectionAtoms);
        ic.hlUpdateCls.updateHlAll();
      }
      else if(command.indexOf('select prop') !== -1) {
        let paraArray = commandOri.split(' | ');

        let property = paraArray[0].substr('select prop'.length + 1);

        let from, to;
        if(paraArray.length == 2) {
            let from_to = paraArray[1].split('_');
            from = from_to[0];
            to = from_to[1];
        }

        ic.resid2specCls.selectProperty(property, from, to);
      }
      else if(command.indexOf('select each residue') !== -1) {
        ic.selectionCls.saveEachResiInSel();
      }
      else if(command.indexOf('select') == 0 && command.indexOf('name') !== -1) {
        let paraArray = commandOri.split(' | '); // atom names might be case-sensitive

        let select = '', commandname = '', commanddesc = '';
        for(let i = 0, il = paraArray.length; i < il; ++i) {
            let para = paraArray[i];

            if(para.indexOf('select') !== -1) {
                select = para.substr(para.indexOf(' ') + 1);
            }
            else if(para.indexOf('name') !== -1) {
                commandname = para.substr(para.indexOf(' ') + 1);
            }
    //        else if(para.indexOf('description') !== -1) {
    //            commanddesc = para.substr(para.indexOf(' ') + 1);
    //        }
        }

    //    if(paraArray.length < 3) commanddesc = commandname;
        commanddesc = commandname;

        await ic.selByCommCls.selectByCommand(select, commandname, commanddesc);
      }
      else if(command.indexOf('select $') !== -1 || command.indexOf('select .') !== -1 || command.indexOf('select :') !== -1 
          || command.indexOf('select %') !== -1 || command.indexOf('select @') !== -1) {
        let paraArray = commandOri.split(' | '); // atom names might be case-sensitive

        let select = paraArray[0].substr(paraArray[0].indexOf(' ') + 1);
        let commandname = '', commanddesc = '';

        if(paraArray.length > 1) {
            commandname = paraArray[1].substr(paraArray[1].indexOf(' ') + 1);
        }

        if(paraArray.length > 2) {
            commanddesc = paraArray[2].substr(paraArray[2].indexOf(' ') + 1);
        }

        if(select.indexOf(' or ') !== -1) { // "select " command without " | name"
            await ic.selByCommCls.selectByCommand(select, commandname, commanddesc);
        }
        else { // only single query from selectByCommand()
            await ic.selByCommCls.selectBySpec(select, commandname, commanddesc);
        }
      }

      {
          me.htmlCls.clickMenuCls.setLogCmd(commandOri, false);
      }

      ic.bAddCommands = true;
    }

    setStrengthPara(paraArray) { let ic = this.icn3d; ic.icn3dui;
        if(paraArray.length >= 5) {
           let thresholdArray = paraArray[4].split(' ');

           if(thresholdArray.length >= 4) {
               $("#" + ic.pre + "hbondthreshold").val(thresholdArray[1]);
               $("#" + ic.pre + "saltbridgethreshold").val(thresholdArray[2]);
               $("#" + ic.pre + "contactthreshold").val(thresholdArray[3]);
               if(thresholdArray.length >= 7) {
                   $("#" + ic.pre + "halogenthreshold").val(thresholdArray[4]);
                   $("#" + ic.pre + "picationthreshold").val(thresholdArray[5]);
                   $("#" + ic.pre + "pistackingthreshold").val(thresholdArray[6]);
               }
           }
        }

        if(paraArray.length == 6) {
            let thicknessArray = paraArray[5].split(' ');

            if(thicknessArray.length >= 6) {
                $("#" + ic.pre + "dist_ss").val(thicknessArray[0]);
                $("#" + ic.pre + "dist_coil").val(thicknessArray[1]);
                $("#" + ic.pre + "dist_hbond").val(thicknessArray[2]);
                $("#" + ic.pre + "dist_inter").val(thicknessArray[3]);
                $("#" + ic.pre + "dist_ssbond").val(thicknessArray[4]);
                $("#" + ic.pre + "dist_ionic").val(thicknessArray[5]);

                if(thicknessArray.length == 9) {
                    $("#" + ic.pre + "dist_halogen").val(thicknessArray[6]);
                    $("#" + ic.pre + "dist_pication").val(thicknessArray[7]);
                    $("#" + ic.pre + "dist_pistacking").val(thicknessArray[8]);
                }
            }
        }
    }

    getThresholdNameArrays(commandOri) { let ic = this.icn3d, me = ic.icn3dui;
        me.htmlCls.clickMenuCls.SetChainsAdvancedMenu();

        let paraArray = commandOri.split(' | ');

        let threshold = parseFloat(paraArray[0].substr(paraArray[0].lastIndexOf(' ') + 1));
        let nameArray = [], nameArray2 = [];
        if(paraArray.length >= 2 && paraArray[1].length > 4) { //sets a,b,c e,f,g
            let setsArray = paraArray[1].split(" ");
            if(setsArray.length > 1) nameArray2 = setsArray[1].split(",");
            if(setsArray.length > 2) nameArray = setsArray[2].split(",");
        }
        else {
            nameArray2 = ['selected'];
            nameArray = ['non-selected'];
        }

        let bHbondCalc;
        if(paraArray.length == 3) {
            bHbondCalc =(paraArray[2] == 'true') ? true : false;
        }

        return {'threshold': threshold, 'nameArray2': nameArray2, 'nameArray': nameArray, 'bHbondCalc': bHbondCalc}
    }

    setQueryresi2score(strArray) { let ic = this.icn3d, me = ic.icn3dui;
        let chainid = strArray[1];
        let start_end = strArray[2].split(' ')[1].split('_');
        let resiScoreStr = strArray[3]; // score 0-9
        if(ic.queryresi2score === undefined) ic.queryresi2score = {};
        //if(ic.queryresi2score[chainid] === undefined) ic.queryresi2score[chainid] = {}
        ic.queryresi2score[chainid] = {};
        let factor = 100 / 9;
        for(let resi = parseInt(start_end[0]), i = 0; resi <= parseInt(start_end[1]); ++resi, ++i) {
            if(resiScoreStr[i] != '_') {
                ic.queryresi2score[chainid][resi] = parseInt(resiScoreStr[i]) * factor; // convert from 0-9 to 0-100
            }
        }

        // color range
        if(strArray.length > 4) {
            let colorArray = strArray[4].split(' ');
            ic.startColor = colorArray[1];
            ic.midColor = colorArray[2];
            ic.endColor = colorArray[3];

            let legendHtml = me.htmlCls.clickMenuCls.setLegendHtml();
            //$("#" + me.pre + "legend").html(legendHtml).show();
            $("#" + me.pre + "dl_legend").html(legendHtml);
            me.htmlCls.dialogCls.openDlg('dl_legend', 'Color Range');
        }
    }

    addShape(command, shape) { let ic = this.icn3d, me = ic.icn3dui;
      ic.shapeCmdHash[command] = 1;
      
      let paraArray = command.split(' | ');
      let p1Array = paraArray[1].split(' ');
      let colorStr = paraArray[2].substr(paraArray[2].lastIndexOf(' ') + 1);
      let opacity = paraArray[3].substr(paraArray[3].lastIndexOf(' ') + 1);
      let radius = paraArray[4].substr(paraArray[4].lastIndexOf(' ') + 1);

      colorStr = '#' + colorStr.replace(/\#/g, '');
      let color = me.parasCls.thr(colorStr);

      let pos1 = new THREE.Vector3(parseFloat(p1Array[1]), parseFloat(p1Array[3]), parseFloat(p1Array[5]));

      if(shape == 'sphere') {
        ic.sphereCls.createSphereBase(pos1, color, parseFloat(radius), undefined, undefined, undefined, parseFloat(opacity));
      }
      else { // 'cube'
        ic.boxCls.createBox_base(pos1, parseFloat(radius), color, undefined, undefined, undefined, parseFloat(opacity));
      }
    }

    getMenuFromCmd(cmd) { let ic = this.icn3d; ic.icn3dui;
        cmd = cmd.trim();

        let seqAnnoStr = 'Windows > View Sequences & Annotations';
        let hbondIntStr = 'Analysis > H-Bonds & Interactions';
        let forceStr = hbondIntStr + ' > 2D Graph(Force-Directed)';
        let rotStr1 = 'View > Rotate > Auto Rotation > Rotate ';
        let rotStr2 = 'View > Rotate > Rotate 90 deg > ';
        let sel3dStr = 'Select > Select on 3D > ';
        let labelStr = 'Analysis > Label > ';
        let printStr = 'File > 3D Printing > ';

        if(cmd.indexOf('load') == 0) return 'File > Retrieve by ID, Align';
        else if(cmd.indexOf('set map') == 0 && cmd.indexOf('set map wireframe') == -1) return 'Style > Electron Density';
        else if(cmd.indexOf('set emmap') == 0 && cmd.indexOf('set emmap wireframe') == -1) return 'Style > EM Density Map';
        else if(cmd.indexOf('set phi') == 0) return 'Analysis > Load Potential > URL(Same Host) Phi/Cube';
        else if(cmd.indexOf('set delphi') == 0) return 'Analysis > DelPhi Potential';
        else if(cmd.indexOf('setoption map') == 0) return 'Style > Remove Map';
        else if(cmd.indexOf('setoption emmap') == 0) return 'Style > Remove EM Map';
        //else if(cmd.indexOf('setoption phimap') == 0) return 'Analysis > Remove Potential';
        else if(cmd.indexOf('view annotations') == 0) return seqAnnoStr;
        else if(cmd.indexOf('set annotation all') == 0) return seqAnnoStr + ': "All" checkbox';
        else if(cmd.indexOf('set annotation clinvar') == 0) return seqAnnoStr + ': "ClinVar" checkbox';
        else if(cmd.indexOf('set annotation snp') == 0) return seqAnnoStr + ': "SNP" checkbox';
        else if(cmd.indexOf('set annotation 3ddomain') == 0) return seqAnnoStr + ': "3D Domains" checkbox';
        else if(cmd.indexOf('view interactions') == 0) return 'Windows > View 2D Diagram';
        else if(cmd.indexOf('symmetry') == 0) return 'Analysis > Symmetry';
        else if(cmd.indexOf('realign on seq align') == 0) return 'File > Realign Selection > on Sequence Alignment';
        else if(cmd.indexOf('realign') == 0) return 'File > Realign Selection > Residue by Residue';
        else if(cmd.indexOf('graph interaction pairs') == 0) return hbondIntStr + ' > 2D Graph(Force-Directed)';
        else if(cmd.indexOf('export canvas') == 0) return 'File > Save File > iCn3D PNG Image';
        else if(cmd == 'export stl file') return printStr + 'STL';
        else if(cmd == 'export vrml file') return printStr + 'VRML(Color)';
        else if(cmd == 'export stl stabilizer file') return printStr + 'STL W/ Stabilizers';
        else if(cmd == 'export vrml stabilizer file') return printStr + 'VRML(Color, W/ Stabilizers)';
        else if(cmd == 'select all') return 'Select > All; or Toggle to "All"(next to "Help")';
        else if(cmd == 'show all') return 'View > View Full Structure';
        else if(cmd == 'select complement') return 'Select > Inverse';
        else if(cmd == 'set pk atom') return sel3dStr + 'Atom';
        else if(cmd == 'set pk residue') return sel3dStr + 'Residue';
        else if(cmd == 'set pk strand') return sel3dStr + 'Strand/Helix';
        else if(cmd == 'set pk domain') return sel3dStr + '3D Domain';
        else if(cmd == 'set pk chain') return sel3dStr + 'Chain';
        else if(cmd == 'set surface wireframe on') return 'Style > Surface Wireframe > Yes';
        else if(cmd == 'set surface wireframe off') return 'Style > Surface Wireframe > No';
        else if(cmd == 'set map wireframe on') return 'Style > Map Wireframe > Yes';
        else if(cmd == 'set map wireframe off') return 'Style > Map Wireframe > No';
        else if(cmd == 'set emmap wireframe on') return 'Style > EM Map Wireframe > Yes';
        else if(cmd == 'set emmap wireframe off') return 'Style > EM Map Wireframe > No';
        else if(cmd == 'set surface neighbors on') return 'Style > Surface Type > ... with Context';
        //else if(cmd == 'set surface neighbors off') return 'Style > Surface Type > ... without Context';
        else if(cmd == 'set axis on') return 'View > XYZ-axes > Show';
        else if(cmd == 'set axis off') return 'View > XYZ-axes > Hide';
        else if(cmd == 'set fog on') return 'View > Fog for Selection > On';
        else if(cmd == 'set fog off') return 'View > Fog for Selection > Off';
        else if(cmd == 'set slab on') return 'View > Slab for Selection > On';
        else if(cmd == 'set slab off') return 'View > Slab for Selection > Off';
        else if(cmd == 'set assembly on') return 'Analysis > Assembly > Biological Assembly';
        else if(cmd == 'set assembly off') return 'Analysis > Assembly > Asymmetric Unit';
        else if(cmd == 'set chemicalbinding show') return 'Analysis > Chem. Binding > Show';
        else if(cmd == 'set chemicalbinding hide') return 'Analysis > Chem. Binding > Hide';
        else if(cmd == 'set hbonds off' || cmd == 'set salt bridge off' || cmd == 'set contact off'
          || cmd == 'set halogen pi off') return hbondIntStr + ' > Reset';
        else if(cmd == 'hydrogens') return 'Style > Hydrogens > Show';
        else if(cmd == 'set hydrogens off') return 'Style > Hydrogens > Hide';
        else if(cmd == 'set stabilizer off') return 'File > 3D Printing > Remove All Stabilizers';
        else if(cmd == 'set disulfide bonds off') return 'Analysis > Disulfide Bonds > Hide';
        else if(cmd == 'set cross linkage off') return 'Analysis > Cross-Linkages > Hide';
        else if(cmd == 'set lines off') return 'Analysis > Distance > Hide';
        else if(cmd == 'set labels off') return 'Analysis > Label > Remove';
        else if(cmd == 'set mode all') return 'Toggle to "All"(next to "Help")';
        else if(cmd == 'set mode selection') return 'Toggle to "Selection"(next to "Help")';
        else if(cmd == 'set view detailed view') return seqAnnoStr + ': "Details" tab';
        else if(cmd== 'set view overview') return seqAnnoStr + ': "Summary" tab';
        else if(cmd == 'set annotation custom') return seqAnnoStr + ': "Custom" checkbox';
        else if(cmd == 'set annotation interaction') return seqAnnoStr + ': "Interactions" checkbox';
        else if(cmd == 'set annotation ptm') return seqAnnoStr + ': "PTM" checkbox';
        else if(cmd == 'set annotation cdd') return seqAnnoStr + ': "Conserved Domains" checkbox';
        else if(cmd == 'set annotation site') return seqAnnoStr + ': "Functional Sites" checkbox';
        else if(cmd == 'set annotation ssbond') return seqAnnoStr + ': "Disulfide Bonds" checkbox';
        else if(cmd == 'set annotation crosslink') return seqAnnoStr + ': "Cross-Linkages" checkbox';
        else if(cmd == 'set annotation transmembrane') return seqAnnoStr + ': "Transmembrane" checkbox';
        else if(cmd == 'highlight level up') return 'Keyboard Arrow Up';
        else if(cmd == 'highlight level down') return 'Keyboard Arrow Down';
        else if(cmd.indexOf('hide annotation') == 0) return seqAnnoStr + ': checkboxes off';
        else if(cmd == 'add residue labels') return labelStr + 'per Residue';
        else if(cmd == 'add residue number labels') return labelStr + 'per Residue & Number';
        else if(cmd == 'add atom labels') return labelStr + 'per Atom';
        else if(cmd == 'add chain labels') return labelStr + 'per Chain';
        else if(cmd == 'add terminal labels') return labelStr + 'N- & C- Termini';
        else if(cmd == 'rotate left') return rotStr1 + 'Left; or Key l';
        else if(cmd == 'rotate right') return rotStr1 + 'Right; or Key j';
        else if(cmd == 'rotate up') return rotStr1 + 'Up; or Key i';
        else if(cmd == 'rotate down') return rotStr1 + 'Down; or Key m';
        else if(cmd == 'rotate x') return rotStr2 + 'X-axis';
        else if(cmd == 'rotate y') return rotStr2 + 'Y-axis';
        else if(cmd == 'rotate z') return rotStr2 + 'Z-axis';
        else if(cmd == 'reset') return 'View > Reset > All';
        else if(cmd == 'reset orientation') return 'View > Reset > Orientation';
        //else if(cmd == 'reset thickness') return 'File > 3D Printing > Reset Thickness';
        else if(cmd == 'clear selection') return 'Select > Clear Selection';
        else if(cmd == 'zoom selection') return 'Select > Zoom in Selection';
        else if(cmd == 'center selection') return 'Select > Center Selection';
        else if(cmd == 'show selection') return 'Select > View Only Selection';
        else if(cmd == 'hide selection') return 'Select > Hide Selection';
        else if(cmd == 'output selection') return 'Select > Clear Selection';
        else if(cmd == 'toggle highlight') return 'Select > Toggle Highlight';
        else if(cmd == 'stabilizer') return 'File > 3D Printing > Add all Stabilizers';
        else if(cmd == 'disulfide bonds') return 'Analysis > Disulfide Bonds > Show';
        else if(cmd == 'cross linkage') return 'Analysis > Cross-Linkages > Show';
        else if(cmd == 'back') return 'View > Undo';
        else if(cmd == 'forward') return 'View > Redo';
        else if(cmd == 'clear all') return 'Select > Clear Selection';
        else if(cmd == 'defined sets') return 'Windows > Defined Sets';
        else if(cmd == 'delete selected sets') return 'Windows > Defined Sets: "Delete Selected Sets" button';
        else if(cmd == 'view interactions') return 'Windows > View Interactions';
        else if(cmd == 'show annotations all chains') return seqAnnoStr + ': "Show All Chains" button';
        else if(cmd == 'save color') return 'Color > Save Color';
        else if(cmd == 'apply saved color') return 'Color > Apply Saved Color';
        else if(cmd == 'save style') return 'Style > Save Style';
        else if(cmd == 'apply saved style') return 'Style > Apply Saved Style';
        else if(cmd == 'select main chains') return 'Select > Main Chains';
        else if(cmd == 'select side chains') return 'Select > Side Chains';
        else if(cmd == 'select main side chains') return 'Select > Main & Side Chains';
        else if(cmd == 'area') return 'View > Surface Area';
        else if(cmd == 'table inter count only') return hbondIntStr + ': "Set 1" button: "Show Count Only" button';
        else if(cmd == 'table inter details') return hbondIntStr + ': "Set 1" button: "Show Details" button';
        else if(cmd.indexOf('define helix sets') == 0) return seqAnnoStr + ': "Helix Sets" button';
        else if(cmd.indexOf('define sheet sets') == 0) return seqAnnoStr + ': "Sheet Sets" button';
        else if(cmd.indexOf('define coil sets') == 0) return seqAnnoStr + ': "Coil Sets" button';
        else if(cmd.indexOf('select interaction') == 0) return 'Windows > View 2D Diagram: click on edges';
        else if(cmd.indexOf('select saved atoms') == 0 || cmd.indexOf('select sets') == 0) return 'Windows > Defined Sets: select in menu';
        else if(cmd.indexOf('select chain') !== -1) return seqAnnoStr + ': click on chain names';
        else if(cmd.indexOf('select alignChain') !== -1) return 'Windows > View Aligned Sequences: click on chain names';
        else if(cmd.indexOf('select zone cutoff') == 0) return 'Select > by Distance';
        else if(cmd.indexOf('set surface opacity') == 0) return 'Style > Surface Opacity';
        else if(cmd.indexOf('set label scale') == 0) return 'View > Label Scale';
        else if(cmd.indexOf('set surface') == 0) return 'Style > Surface Type';
        else if(cmd.indexOf('set camera') == 0) return 'View > Camera';
        else if(cmd.indexOf('set background') == 0) return 'Style > Background';
        else if(cmd.indexOf('set thickness') == 0) return 'File > 3D Printing > Set Thickness';
        else if(cmd.indexOf('set highlight color') == 0) return 'Select > Highlight Color';
        else if(cmd.indexOf('set highlight style') == 0) return 'Select > Highlight Style';
        else if(cmd.indexOf('add line') == 0) return 'Analysis > Distance > between Two Atoms';
        else if(cmd.indexOf('add label') == 0) return 'Analysis > Distance > between Two Atoms';
        else if(cmd.indexOf('dist') == 0) return 'Analysis > Distance > between Two Sets';
        else if(cmd.indexOf('msa') == 0) return seqAnnoStr + ': "Add Track" button: "FASTA Alignment" button';
        else if(cmd.indexOf('add track') == 0) return seqAnnoStr + ': "Add Track" button';
        else if(cmd.indexOf('remove one stabilizer') == 0) return 'File > 3D Printing > Remove One Stablizer';
        else if(cmd.indexOf('add one stabilizer') == 0) return 'File > 3D Printing > Add One Stablizer';
        else if(cmd.indexOf('select planes z-axis') == 0) return 'View > Select between Two X-Y Planes';
        else if(cmd.indexOf('adjust membrane z-axis') == 0) return 'View > Adjust Membrane';
        else if(cmd.indexOf('toggle membrane') == 0) return 'View > Toggle Membrane';
        else if(cmd.indexOf('calc buried surface') == 0) return hbondIntStr + ': "Buried Surface Area" button';
        else if(cmd.indexOf('display interaction 3d') == 0) return hbondIntStr + ': "3D Display Interactions" button';
        else if(cmd.indexOf('view interaction pairs') == 0) return hbondIntStr + ': "Highlight Interactions in Table" button';
        else if(cmd.indexOf('save1 interaction pairs') == 0) return hbondIntStr + ': "Set 1" button';
        else if(cmd.indexOf('save2 interaction pairs') == 0) return hbondIntStr + ': "Set 2" button';
        else if(cmd.indexOf('line graph interaction pairs') == 0) return hbondIntStr + ': "2D Interaction Network" button';
        else if(cmd.indexOf('scatterplot interaction pairs') == 0) return hbondIntStr + ': "2D Interaction Map" button';
        else if(cmd.indexOf('graph label') == 0) return forceStr + ': "Label Size" menu';
        else if(cmd.indexOf('graph force') == 0) return forceStr + ': "Force on Nodes" menu';
        else if(cmd.indexOf('hide edges') == 0) return forceStr + ': "Internal Edges" menu';
        else if(cmd.indexOf('reset interaction pairs') == 0) return hbondIntStr + ' > Reset';
        else if(cmd.indexOf('side by side') == 0) return 'View > Side by Side';
        else if(cmd.indexOf('your note') == 0) return 'Windows > Your Notes / Window Title';
        else if(cmd.indexOf('pickatom') == 0) return 'Hold Alt key and click on 3D structure';
        else if(cmd.indexOf('color') == 0) return 'Color menu';
        else if(cmd.indexOf('custom tube') == 0) return seqAnnoStr + ': "Custom Color/Tube" button: "Custom Tube" button';
        else if(cmd.indexOf('style') == 0) return 'Style menu';
        else if(cmd.indexOf('select displayed set') !== -1) return 'Select > Displayed Set';
        else if(cmd.indexOf('select prop') !== -1) return 'Select > by Property';
        else if(cmd.indexOf('select') == 0 && cmd.indexOf('name') !== -1) return seqAnnoStr + ': drag on residues to select';
        else if(cmd.indexOf('select $') !== -1 || cmd.indexOf('select .') !== -1 || cmd.indexOf('select :') !== -1 || cmd.indexOf('select @') !== -1) return 'Select > Advanced; or other selection';
        else if(cmd.indexOf('replay on') !== -1) return 'File > Replay Each Step > On';
        else if(cmd.indexOf('replay off') !== -1) return 'File > Replay Each Step > Off';
        else if(cmd.indexOf('set theme') !== -1) return 'Style > Theme Color';
        else if(cmd.indexOf('set double color') !== -1) return 'Style > Two-color Helix';
        else return '';
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class DefinedSets {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    setProtNuclLigInMenu() { let ic = this.icn3d; ic.icn3dui;
        // Initially, add proteins, nucleotides, chemicals, ions, water into the menu "custom selections"
        if(Object.keys(ic.proteins).length > 0) {
          //ic.defNames2Atoms['proteins'] = Object.keys(ic.proteins);
          ic.defNames2Residues['proteins'] = Object.keys(ic.firstAtomObjCls.getResiduesFromAtoms(ic.proteins));
          ic.defNames2Descr['proteins'] = 'proteins';
          ic.defNames2Command['proteins'] = 'select :proteins';
        }

        if(Object.keys(ic.nucleotides).length > 0) {
          //ic.defNames2Atoms['nucleotides'] = Object.keys(ic.nucleotides);
          ic.defNames2Residues['nucleotides'] = Object.keys(ic.firstAtomObjCls.getResiduesFromAtoms(ic.nucleotides));
          ic.defNames2Descr['nucleotides'] = 'nucleotides';
          ic.defNames2Command['nucleotides'] = 'select :nucleotides';
        }

        if(Object.keys(ic.chemicals).length > 0) {
          //ic.defNames2Atoms['chemicals'] = Object.keys(ic.chemicals);
          if(ic.bOpm) {
              let chemicalResHash = {}, memResHash = {};
              for(let serial in ic.chemicals) {
                  let atom = ic.atoms[serial];
                  let residueid = atom.structure + '_' + atom.chain + '_' + atom.resi;
                  if(atom.resn === 'DUM') {
                      memResHash[residueid] = 1;
                  }
                  else {
                      chemicalResHash[residueid] = 1;
                  }
              }

              if(Object.keys(chemicalResHash).length > 0) {
                  ic.defNames2Residues['chemicals'] = Object.keys(chemicalResHash);
                  ic.defNames2Descr['chemicals'] = 'chemicals';
                  ic.defNames2Command['chemicals'] = 'select :chemicals';
              }

              if(Object.keys(memResHash).length > 0) {
                  ic.defNames2Residues['membrane'] = Object.keys(memResHash);
                  ic.defNames2Descr['membrane'] = 'membrane';
                  ic.defNames2Command['membrane'] = 'select :membrane';
              }
          }
          else {
              ic.defNames2Residues['chemicals'] = Object.keys(ic.firstAtomObjCls.getResiduesFromAtoms(ic.chemicals));
              ic.defNames2Descr['chemicals'] = 'chemicals';
              ic.defNames2Command['chemicals'] = 'select :chemicals';
          }
        }

        if(Object.keys(ic.ions).length > 0) {
          //ic.defNames2Atoms['ions'] = Object.keys(ic.ions);
          ic.defNames2Residues['ions'] = Object.keys(ic.firstAtomObjCls.getResiduesFromAtoms(ic.ions));
          ic.defNames2Descr['ions'] = 'ions';
          ic.defNames2Command['ions'] = 'select :ions';
        }

        if(Object.keys(ic.water).length > 0) {
          //ic.defNames2Atoms['water'] = Object.keys(ic.water);
          ic.defNames2Residues['water'] = Object.keys(ic.firstAtomObjCls.getResiduesFromAtoms(ic.water));
          ic.defNames2Descr['water'] = 'water';
          ic.defNames2Command['water'] = 'select :water';
        }

        this.setTransmemInMenu(ic.halfBilayerSize, -ic.halfBilayerSize);
    }

    setPredefinedInMenu() { let ic = this.icn3d, me = ic.icn3dui;
          // predefined sets: all chains
          this.setChainsInMenu();

          // predefined sets: proteins,nucleotides, chemicals
          this.setProtNuclLigInMenu();

          // show 3d domains for mmdbid
          if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined  || me.cfg.chainalign !== undefined || me.cfg.mmdbafid !== undefined) {
              for(let tddomainName in ic.tddomains) {
                  ic.selectionCls.selectResidueList(ic.tddomains[tddomainName], tddomainName, tddomainName, false, false);
              }
          }

          //if((me.cfg.align !== undefined || me.cfg.chainalign !== undefined) && ic.bFullUi) {
          // deal with multiple chain align separately
          if((me.cfg.align !== undefined ||(me.cfg.chainalign !== undefined && ic.chainidArray.length == 2) ) && ic.bFullUi) {
            ic.selectionCls.selectResidueList(ic.consHash1, ic.conservedName1, ic.conservedName1, false, false);
            ic.selectionCls.selectResidueList(ic.consHash2, ic.conservedName2, ic.conservedName2, false, false);

            ic.selectionCls.selectResidueList(ic.nconsHash1, ic.nonConservedName1, ic.nonConservedName1, false, false);
            ic.selectionCls.selectResidueList(ic.nconsHash2, ic.nonConservedName2, ic.nonConservedName2, false, false);

            ic.selectionCls.selectResidueList(ic.nalignHash1, ic.notAlignedName1, ic.notAlignedName1, false, false);
            ic.selectionCls.selectResidueList(ic.nalignHash2, ic.notAlignedName2, ic.notAlignedName2, false, false);

            // for alignment, show aligned residues, chemicals, and ions
            let dAtoms = {};
            for(let alignChain in ic.alnChains) {
                dAtoms = me.hashUtilsCls.unionHash(dAtoms, ic.alnChains[alignChain]);
            }

            let residuesHash = ic.firstAtomObjCls.getResiduesFromAtoms(dAtoms);

            let commandname = 'protein_aligned';
            let commanddescr = 'aligned protein and nucleotides';
            let select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residuesHash));

            //ic.selectionCls.addCustomSelection(Object.keys(residuesHash), Object.keys(dAtoms), commandname, commanddescr, select, true);
            ic.selectionCls.addCustomSelection(Object.keys(residuesHash), commandname, commanddescr, select, true);
          }
    }

    //Set the menu of defined sets with an array of defined names "commandnameArray".
    setAtomMenu(commandnameArray) { let ic = this.icn3d; ic.icn3dui;
      let html = "";

      let nameArray1 =(ic.defNames2Residues !== undefined) ? Object.keys(ic.defNames2Residues) : [];
      let nameArray2 =(ic.defNames2Atoms !== undefined) ? Object.keys(ic.defNames2Atoms) : [];

      let nameArrayTmp = nameArray1.concat(nameArray2).sort();

      let nameArray = [];
    //  $.each(nameArrayTmp, function(i, el){
    //       if($.inArray(el, nameArray) === -1) nameArray.push(el);
    //  });
      nameArrayTmp.forEach(elem => {
           if($.inArray(elem, nameArray) === -1) nameArray.push(elem);
      });

      //for(let i in ic.defNames2Atoms) {
      for(let i = 0, il = nameArray.length; i < il; ++i) {
          let name = nameArray[i];

          let atom, atomHash;
          if(ic.defNames2Atoms !== undefined && ic.defNames2Atoms.hasOwnProperty(name)) {
              let atomArray = ic.defNames2Atoms[name];

              if(atomArray.length > 0) atom = ic.atoms[atomArray[0]];
          }
          else if(ic.defNames2Residues !== undefined && ic.defNames2Residues.hasOwnProperty(name)) {
              let residueArray = ic.defNames2Residues[name];
              if(residueArray.length > 0) {
                  atomHash = ic.residues[residueArray[0]];
                  if(atomHash) {
                      atom = ic.atoms[Object.keys(atomHash)[0]];
                  }
              }
          }

          let colorStr =(atom === undefined || atom.color === undefined || atom.color.getHexString().toUpperCase() === 'FFFFFF') ? 'DDDDDD' : atom.color.getHexString();
          let color =(atom !== undefined && atom.color !== undefined) ? colorStr : '000000';

          if(commandnameArray.indexOf(name) != -1) {
            html += "<option value='" + name + "' style='color:#" + color + "' selected='selected'>" + name + "</option>";
          }
          else {
            html += "<option value='" + name + "' style='color:#" + color + "'>" + name + "</option>";
          }
      }

      return html;
    }

    setChainsInMenu() { let ic = this.icn3d; ic.icn3dui;
        for(let chainid in ic.chains) {
            // skip chains with one residue/chemical
            if(ic.chainsSeq[chainid] && ic.chainsSeq[chainid].length > 1) {
              //ic.defNames2Atoms[chainid] = Object.keys(ic.chains[chainid]);
              ic.defNames2Residues[chainid] = Object.keys(ic.firstAtomObjCls.getResiduesFromAtoms(ic.chains[chainid]));
              ic.defNames2Descr[chainid] = chainid;

              let pos = chainid.indexOf('_');
              let structure = chainid.substr(0, pos);
              let chain = chainid.substr(pos + 1);

              ic.defNames2Command[chainid] = 'select $' + structure + '.' + chain;
            }
        }

        // select whole structure
        if(Object.keys(ic.structures) == 1) {
          let structure = Object.keys(ic.structures)[0];

          ic.defNames2Residues[structure] = Object.keys(ic.residues);
          ic.defNames2Descr[structure] = structure;

          ic.defNames2Command[structure] = 'select $' + structure;
        }
        else {
            let resArray = Object.keys(ic.residues);
            let structResHash = {};
            for(let i = 0, il = resArray.length; i < il; ++i) {
                let resid = resArray[i];
                let pos = resid.indexOf('_');
                let structure = resid.substr(0, pos);
                if(structResHash[structure] === undefined) {
                    structResHash[structure] = [];
                }
                structResHash[structure].push(resid);
            }

            for(let structure in structResHash) {
              ic.defNames2Residues[structure] = structResHash[structure];
              ic.defNames2Descr[structure] = structure;

              ic.defNames2Command[structure] = 'select $' + structure;
            }
        }
    }

    setTransmemInMenu(posZ, negZ, bReset) { let ic = this.icn3d; ic.icn3dui;
        // set transmembrane, extracellular, intracellular
        if(ic.bOpm) {
          let transmembraneHash = {}, extracellularHash = {}, intracellularHash = {};
          for(let serial in ic.atoms) {
              let atom = ic.atoms[serial];

              if(atom.resn === 'DUM') continue;

              let residueid = atom.structure + '_' + atom.chain + '_' + atom.resi;
              if(atom.coord.z > posZ) {
                  extracellularHash[residueid] = 1;
              }
              else if(atom.coord.z < negZ) {
                  intracellularHash[residueid] = 1;
              }
              else {
                  transmembraneHash[residueid] = 1;
              }
          }

          let extraStr =(bReset) ? '2' : '';

          if(Object.keys(transmembraneHash).length > 0) {
              ic.defNames2Residues['transmembrane' + extraStr] = Object.keys(transmembraneHash);
              ic.defNames2Descr['transmembrane' + extraStr] = 'transmembrane' + extraStr;
              ic.defNames2Command['transmembrane' + extraStr] = 'select :transmembrane' + extraStr;
          }

          if(Object.keys(extracellularHash).length > 0) {
              ic.defNames2Residues['extracellular' + extraStr] = Object.keys(extracellularHash);
              ic.defNames2Descr['extracellular' + extraStr] = 'extracellular' + extraStr;
              ic.defNames2Command['extracellular' + extraStr] = 'select :extracellular' + extraStr;
          }

          if(Object.keys(intracellularHash).length > 0) {
              ic.defNames2Residues['intracellular' + extraStr] = Object.keys(intracellularHash);
              ic.defNames2Descr['intracellular' + extraStr] = 'intracellular' + extraStr;
              ic.defNames2Command['intracellular' + extraStr] = 'select :intracellular' + extraStr;
          }
        }
    }

    //Display the menu of defined sets. All chains and defined custom sets are listed in the menu.
    //All new custom sets will be displayed in the menu.
    showSets() { let ic = this.icn3d, me = ic.icn3dui;
        if(!me.bNode) {
            me.htmlCls.dialogCls.openDlg('dl_definedsets', 'Select sets');
            $("#" + ic.pre + "dl_setsmenu").show();
            $("#" + ic.pre + "dl_setoperations").show();

            $("#" + ic.pre + "dl_command").hide();

            $("#" + ic.pre + "atomsCustom").resizable();
        }

        let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
        let prevDAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);

        if(ic.bSetChainsAdvancedMenu === undefined || !ic.bSetChainsAdvancedMenu || ic.bResetSets) {
           this.setPredefinedInMenu();

           ic.bSetChainsAdvancedMenu = true;
        }

        ic.hAtoms = me.hashUtilsCls.cloneHash(prevHAtoms);
        ic.dAtoms = me.hashUtilsCls.cloneHash(prevDAtoms);

        ic.hlUpdateCls.updateHlMenus();
    }

    clickCustomAtoms() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        //me.myEventCls.onIds("#" + ic.pre + "atomsCustom", "change", function(e) { let ic = thisClass.icn3d;
        $("#" + ic.pre + "atomsCustom").change(function(e) { let ic = thisClass.icn3d;
           let nameArray = $(this).val();
           ic.nameArray = nameArray;

           if(nameArray !== null) {
             // log the selection
             //me.htmlCls.clickMenuCls.setLogCmd('select saved atoms ' + nameArray.toString(), true);

             let bUpdateHlMenus = false;
             thisClass.changeCustomAtoms(nameArray, bUpdateHlMenus);
             //me.htmlCls.clickMenuCls.setLogCmd('select saved atoms ' + nameArray.join(' ' + ic.setOperation + ' '), true);
             me.htmlCls.clickMenuCls.setLogCmd('select sets ' + nameArray.join(' ' + ic.setOperation + ' '), true);

             ic.bSelectResidue = false;
           }
        });

        me.myEventCls.onIds("#" + ic.pre + "atomsCustom", "focus", function(e) { let ic = thisClass.icn3d;
           if(me.utilsCls.isMobile()) $("#" + ic.pre + "atomsCustom").val("");
        });
    }

    //Delete selected sets in the menu of "Defined Sets".
    deleteSelectedSets() { let ic = this.icn3d; ic.icn3dui;
       let nameArray = $("#" + ic.pre + "atomsCustom").val();

       for(let i = 0; i < nameArray.length; ++i) {
         let selectedSet = nameArray[i];

         if((ic.defNames2Atoms === undefined || !ic.defNames2Atoms.hasOwnProperty(selectedSet)) &&(ic.defNames2Residues === undefined || !ic.defNames2Residues.hasOwnProperty(selectedSet)) ) continue;

         if(ic.defNames2Atoms !== undefined && ic.defNames2Atoms.hasOwnProperty(selectedSet)) {
             delete ic.defNames2Atoms[selectedSet];
         }

         if(ic.defNames2Residues !== undefined && ic.defNames2Residues.hasOwnProperty(selectedSet)) {
             delete ic.defNames2Residues[selectedSet];
         }
       } // outer for

       ic.hlUpdateCls.updateHlMenus();
    }

    //HighlightAtoms are set up based on the selected custom names "nameArray" in the atom menu.
    //The corresponding atoms are neither highlighted in the sequence dialog nor in the 3D structure
    //since not all residue atom are selected.
    changeCustomAtoms(nameArray, bUpdateHlMenus) { let ic = this.icn3d, me = ic.icn3dui;
       ic.hAtoms = {};

       for(let i = 0; i < nameArray.length; ++i) {
         let selectedSet = nameArray[i];

         if((ic.defNames2Atoms === undefined || !ic.defNames2Atoms.hasOwnProperty(selectedSet)) &&(ic.defNames2Residues === undefined || !ic.defNames2Residues.hasOwnProperty(selectedSet)) ) continue;

         if(ic.defNames2Atoms !== undefined && ic.defNames2Atoms.hasOwnProperty(selectedSet)) {
             let atomArray = ic.defNames2Atoms[selectedSet];

             for(let j = 0, jl = atomArray.length; j < jl; ++j) {
                 ic.hAtoms[atomArray[j]] = 1;
             }
         }

         if(ic.defNames2Residues !== undefined && ic.defNames2Residues.hasOwnProperty(selectedSet)) {
             let residueArrayTmp = ic.defNames2Residues[selectedSet];

             let atomHash = {};
             for(let j = 0, jl = residueArrayTmp.length; j < jl; ++j) {
                 atomHash = me.hashUtilsCls.unionHash(atomHash, ic.residues[residueArrayTmp[j]]);
             }

             ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, atomHash);
         }
       } // outer for

       ic.hlUpdateCls.updateHlAll(nameArray, bUpdateHlMenus);

       // show selected chains in annotation window
       ic.annotationCls.showAnnoSelectedChains();

       // clear commmand
       $("#" + ic.pre + "command").val("");
       $("#" + ic.pre + "command_name").val("");
       //$("#" + ic.pre + "command_desc").val("");

       // update the commands in the dialog
       for(let i = 0, il = nameArray.length; i < il; ++i) {
           ic.defNames2Atoms[nameArray[i]];
           ic.defNames2Residues[nameArray[i]];
           ic.defNames2Descr[nameArray[i]];

           if(i === 0) {
             //$("#" + ic.pre + "command").val(atomCommand);
             $("#" + ic.pre + "command").val('saved atoms ' + nameArray[i]);
             $("#" + ic.pre + "command_name").val(nameArray[i]);
           }
           else {
             let prevValue = $("#" + ic.pre + "command").val();
             $("#" + ic.pre + "command").val(prevValue + ' ' + ic.setOperation + ' ' + nameArray[i]);

             prevValue = $("#" + ic.pre + "command_name").val();
             $("#" + ic.pre + "command_name").val(prevValue + ' ' + ic.setOperation + ' ' + nameArray[i]);
           }
       } // outer for
    }

    setHAtomsFromSets(nameArray, type) { let ic = this.icn3d, me = ic.icn3dui;
       for(let i = 0; i < nameArray.length; ++i) {
         let selectedSet = nameArray[i];

         if((ic.defNames2Atoms === undefined || !ic.defNames2Atoms.hasOwnProperty(selectedSet)) &&(ic.defNames2Residues === undefined || !ic.defNames2Residues.hasOwnProperty(selectedSet)) ) continue;

         if(ic.defNames2Atoms !== undefined && ic.defNames2Atoms.hasOwnProperty(selectedSet)) {

             let atomArray = ic.defNames2Atoms[selectedSet];

             if(type === 'or') {
                 for(let j = 0, jl = atomArray.length; j < jl; ++j) {
                     ic.hAtoms[atomArray[j]] = 1;
                 }
             }
             else if(type === 'and') {
                 let atomHash = {};
                 for(let j = 0, jl = atomArray.length; j < jl; ++j) {
                     atomHash[atomArray[j]] = 1;
                 }

                 ic.hAtoms = me.hashUtilsCls.intHash(ic.hAtoms, atomHash);
             }
             else if(type === 'not') {
                 //for(let j = 0, jl = atomArray.length; j < jl; ++j) {
                 //    ic.hAtoms[atomArray[j]] = undefined;
                 //}

                 let atomHash = {};
                 for(let j = 0, jl = atomArray.length; j < jl; ++j) {
                     atomHash[atomArray[j]] = 1;
                 }

                 ic.hAtoms = me.hashUtilsCls.exclHash(ic.hAtoms, atomHash);
             }
         }

         if(ic.defNames2Residues !== undefined && ic.defNames2Residues.hasOwnProperty(selectedSet)) {
             let residueArrayTmp = ic.defNames2Residues[selectedSet];

             let atomHash = {};
             for(let j = 0, jl = residueArrayTmp.length; j < jl; ++j) {
                 atomHash = me.hashUtilsCls.unionHash(atomHash, ic.residues[residueArrayTmp[j]]);
             }

             if(type === 'or') {
                 ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, atomHash);
             }
             else if(type === 'and') {
                 ic.hAtoms = me.hashUtilsCls.intHash(ic.hAtoms, atomHash);
             }
             else if(type === 'not') {
                 ic.hAtoms = me.hashUtilsCls.exclHash(ic.hAtoms, atomHash);
             }
         }
       } // outer for
    }

    updateAdvancedCommands(nameArray, type) { let ic = this.icn3d; ic.icn3dui;
       // update the commands in the dialog
       let separator = ' ' + type + ' ';
       for(let i = 0, il = nameArray.length; i < il; ++i) {
           if(i === 0 && type == 'or') {
             $("#" + ic.pre + "command").val('saved atoms ' + nameArray[i]);
             $("#" + ic.pre + "command_name").val(nameArray[i]);
           }
           else {
             let prevValue = $("#" + ic.pre + "command").val();
             $("#" + ic.pre + "command").val(prevValue + separator + nameArray[i]);

             prevValue = $("#" + ic.pre + "command_name").val();
             $("#" + ic.pre + "command_name").val(prevValue + separator + nameArray[i]);
           }
       } // outer for
    }

    combineSets(orArray, andArray, notArray, commandname) { let ic = this.icn3d, me = ic.icn3dui;
       ic.hAtoms = {};
       this.setHAtomsFromSets(orArray, 'or');

       if(Object.keys(ic.hAtoms).length == 0) {
           //ic.hAtoms = me.hashUtilsCls.cloneHash(ic.atoms);
           ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);
       }
       this.setHAtomsFromSets(andArray, 'and');

       this.setHAtomsFromSets(notArray, 'not');

       // expensive to update, avoid it when loading script
       //ic.hlUpdateCls.updateHlAll();
       if(!ic.bInitial) ic.hlUpdateCls.updateHlAll();

       // show selected chains in annotation window
       ic.annotationCls.showAnnoSelectedChains();

       // clear commmand
       $("#" + ic.pre + "command").val("");
       $("#" + ic.pre + "command_name").val("");

       this.updateAdvancedCommands(orArray, 'or');
       this.updateAdvancedCommands(andArray, 'and');
       this.updateAdvancedCommands(notArray, 'not');

       if(commandname !== undefined) {
           let select = "select " + $("#" + ic.pre + "command").val();

           $("#" + ic.pre + "command_name").val(commandname);
           ic.selectionCls.addCustomSelection(Object.keys(ic.hAtoms), commandname, commandname, select, false);
       }
    }

    async commandSelect(postfix) { let ic = this.icn3d, me = ic.icn3dui;
           let select = $("#" + ic.pre + "command" + postfix).val();

           let commandname = $("#" + ic.pre + "command_name" + postfix).val().replace(/;/g, '_').replace(/\s+/g, '_');

           if(select) {
               await ic.selByCommCls.selectByCommand(select, commandname, commandname);
               me.htmlCls.clickMenuCls.setLogCmd('select ' + select + ' | name ' + commandname, true);
           }
    }

    clickCommand_apply() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        me.myEventCls.onIds("#" + ic.pre + "command_apply", "click", async function(e) { thisClass.icn3d;
           e.preventDefault();

           await thisClass.commandSelect('');
        });

        me.myEventCls.onIds("#" + ic.pre + "command_apply2", "click", async function(e) { thisClass.icn3d;
           e.preventDefault();
           await thisClass.commandSelect('2');
        });

    }

    selectCombinedSets(strSets, commandname) { let ic = this.icn3d; ic.icn3dui;
        let idArray = strSets.split(' ');

        let orArray = [], andArray = [], notArray = [];
        let prevLabel = 'or';

        for(let i = 0, il = idArray.length; i < il; ++i) {
            if(idArray[i] === 'or' || idArray[i] === 'and' || idArray[i] === 'not') {
                prevLabel = idArray[i];
                continue;
            }
            else {
                // make it backward compatible for names of defined sets containing atom serial by replacing the serial with 'auto' 
                // start from iCn3D 3.21.0 on Jan 2023============
                let nameArray = ['hbonds_', 'saltbridge_', 'halogen_', 'pi-cation_', 'pi-stacking_'];
                for(let j = 0, jl = nameArray.length; j < jl; ++j) {
                    const re = new RegExp('^' + nameArray[j] + '\\d+$'); // use '\\'

                    if(idArray[i].match(re)) {
                        idArray[i] = nameArray[j] + 'auto';
                    }
                }
                // end============

                if(prevLabel === 'or') {
                    orArray.push(idArray[i]);
                }
                else if(prevLabel === 'and') {
                    andArray.push(idArray[i]);
                }
                else if(prevLabel === 'not') {
                    notArray.push(idArray[i]);
                }
            }
        }

        if(idArray !== null) this.combineSets(orArray, andArray, notArray, commandname);
    }

    clickModeswitch() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        me.myEventCls.onIds("#" + ic.pre + "modeswitch", "click", function(e) {
            if($("#" + ic.pre + "modeswitch")[0] !== undefined && $("#" + ic.pre + "modeswitch")[0].checked) { // mode: selection
                thisClass.setModeAndDisplay('selection');
            }
            else { // mode: all
                thisClass.setModeAndDisplay('all');
            }
        });
    }

    setModeAndDisplay(mode) { let ic = this.icn3d, me = ic.icn3dui;
        if(mode === 'all') { // mode all
            this.setMode('all');

            // remember previous selection
            ic.prevHighlightAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

           // select all
           me.htmlCls.clickMenuCls.setLogCmd("set mode all", true);

           ic.selectionCls.selectAll();

           ic.drawCls.draw();
        }
        else { // mode selection
            this.setMode('selection');

            // get the previous hAtoms
            if(ic.prevHighlightAtoms !== undefined) {
                ic.hAtoms = me.hashUtilsCls.cloneHash(ic.prevHighlightAtoms);
            }
            else {
                ic.selectionCls.selectAll();
            }

            me.htmlCls.clickMenuCls.setLogCmd("set mode selection", true);

            ic.hlUpdateCls.updateHlAll();
        }
    }

    setMode(mode) { let ic = this.icn3d; ic.icn3dui;
        if(mode === 'all') { // mode all
            // set text
            $("#" + ic.pre + "modeall").show();
            $("#" + ic.pre + "modeselection").hide();

            if($("#" + ic.pre + "modeswitch")[0] !== undefined) $("#" + ic.pre + "modeswitch")[0].checked = false;

            if($("#" + ic.pre + "style").hasClass('icn3d-modeselection')) $("#" + ic.pre + "style").removeClass('icn3d-modeselection');
            if($("#" + ic.pre + "color").hasClass('icn3d-modeselection')) $("#" + ic.pre + "color").removeClass('icn3d-modeselection');
            //if($("#" + ic.pre + "surface").hasClass('icn3d-modeselection')) $("#" + ic.pre + "surface").removeClass('icn3d-modeselection');
        }
        else { // mode selection
            //if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) {
                // set text
                $("#" + ic.pre + "modeall").hide();
                $("#" + ic.pre + "modeselection").show();

                if($("#" + ic.pre + "modeswitch")[0] !== undefined) $("#" + ic.pre + "modeswitch")[0].checked = true;

                if(!$("#" + ic.pre + "style").hasClass('icn3d-modeselection')) $("#" + ic.pre + "style").addClass('icn3d-modeselection');
                if(!$("#" + ic.pre + "color").hasClass('icn3d-modeselection')) $("#" + ic.pre + "color").addClass('icn3d-modeselection');
                //if(!$("#" + ic.pre + "surface").hasClass('icn3d-modeselection')) $("#" + ic.pre + "surface").addClass('icn3d-modeselection');

                // show selected chains in annotation window
                //ic.annotationCls.showAnnoSelectedChains();
            //}
        }
    }
    getAtomsFromOneSet(commandname) {  let ic = this.icn3d, me = ic.icn3dui;  // ic.pAtom is set already
       let residuesHash = {};
       // defined sets is not set up
       if(ic.defNames2Residues['proteins'] === undefined) {
           this.showSets();
       }
       //for(let i = 0, il = nameArray.length; i < il; ++i) {
           //var commandname = nameArray[i];
           if(Object.keys(ic.chains).indexOf(commandname) !== -1) {
               residuesHash = me.hashUtilsCls.unionHash(residuesHash, ic.chains[commandname]);
           }
           else {
               if(ic.defNames2Residues[commandname] !== undefined && ic.defNames2Residues[commandname].length > 0) {
                   for(let j = 0, jl = ic.defNames2Residues[commandname].length; j < jl; ++j) {
                       let resid = ic.defNames2Residues[commandname][j]; // return an array of resid
                       residuesHash = me.hashUtilsCls.unionHash(residuesHash, ic.residues[resid]);
                   }
               }
               if(ic.defNames2Atoms[commandname] !== undefined && ic.defNames2Atoms[commandname].length > 0) {
                   for(let j = 0, jl = ic.defNames2Atoms[commandname].length; j < jl; ++j) {
                       //var resid = ic.defNames2Atoms[commandname][j]; // return an array of serial
                       //residuesHash = me.hashUtilsCls.unionHash(residuesHash, ic.residues[resid]);
                       let serial = ic.defNames2Atoms[commandname][j]; // return an array of serial
                       residuesHash[serial] = 1;
                   }
               }
           }
       //}
       return residuesHash;
    }
    
    getAtomsFromNameArray(nameArray) {  let ic = this.icn3d, me = ic.icn3dui;
        let selAtoms = {};
        for(let i = 0, il = nameArray.length; i < il; ++i) {
            if(nameArray[i] === 'non-selected') { // select all hAtoms
               let currAtoms = {};
               for(let i in ic.atoms) {
                   if(!ic.hAtoms.hasOwnProperty(i) && ic.dAtoms.hasOwnProperty(i)) {
                       currAtoms[i] = ic.atoms[i];
                   }
               }
               selAtoms = me.hashUtilsCls.unionHash(selAtoms, currAtoms);
            }
            else if(nameArray[i] === 'selected') {
                selAtoms = me.hashUtilsCls.unionHash(selAtoms, me.hashUtilsCls.hash2Atoms(ic.hAtoms, ic.atoms) );
            }
            else {
                selAtoms = me.hashUtilsCls.unionHash(selAtoms, me.hashUtilsCls.hash2Atoms(this.getAtomsFromOneSet(nameArray[i]), ic.atoms) );
            }
        }
        if(nameArray.length == 0) selAtoms = ic.atoms;
        return selAtoms;
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class LoadScript {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Run commands one after another. The commands can be semicolon ';' or new line '\n' separated.
    async loadScript(dataStr, bStatefile, bStrict) { let ic = this.icn3d; ic.icn3dui;
      if(!dataStr) return;
      
      // allow the "loading structure..." message to be shown while loading script
      ic.bCommandLoad = true;

      ic.bRender = false;
      ic.bStopRotate = true;
      
      // firebase dynamic links replace " " with "+". So convert it back
      dataStr =(bStatefile) ? dataStr.replace(/\+/g, ' ') : dataStr.replace(/\+/g, ' ').replace(/;/g, '\n');

      let preCommands = [];
      if(!bStrict && ic.commands.length > 0) preCommands[0] = ic.commands[0];

      let commandArray = dataStr.trim().split('\n');
      ic.commands = commandArray;

      let pos = commandArray[0].indexOf('command=');
      if(bStatefile && pos != -1) {
          let commandFirst = commandArray[0].substr(0, pos - 1);
          ic.commands.splice(0, 1, commandFirst);
      }
      
      //ic.commands = dataStr.trim().split('\n');
      ic.STATENUMBER = ic.commands.length;

      ic.commands = preCommands.concat(ic.commands);
      
      ic.STATENUMBER = ic.commands.length;

    /*
      if(bStatefile || ic.bReplay) {
          ic.CURRENTNUMBER = 0;
      }
      else {
          // skip the first loading step
          ic.CURRENTNUMBER = 1;
      }
    */

      ic.CURRENTNUMBER = 0;

      if(ic.bReplay) {
          await this.replayFirstStep(ic.CURRENTNUMBER);
      }
      else {
          await this.execCommands(ic.CURRENTNUMBER, ic.STATENUMBER-1, ic.STATENUMBER, bStrict);
      }
    }

    //Execute a list of commands. "steps" is the total number of commands.
    async execCommands(start, end, steps, bStrict) { let ic = this.icn3d; ic.icn3dui;
        ic.bRender = false;

        // fresh start
        if(!bStrict) ic.reinitAfterLoad();

        //ic.opts = me.hashUtilsCls.cloneHash(ic.opts);

        await this.execCommandsBase(start, end, steps);
    }

    async execCommandsBase(start, end, steps, bFinalStep) { let ic = this.icn3d, me = ic.icn3dui;
      let thisClass = this;
      let i;

      for(i=start; i <= end; ++i) {      
          let bFinalStep =(i === steps - 1) ? true : false;

          if(!ic.commands[i].trim()) continue;
          if(!ic.atoms && ic.commands[i].indexOf('load') == -1) continue;

          let strArray = ic.commands[i].split("|||");
          let command = strArray[0].trim();
          
          if(command.indexOf('load') !== -1) {
              if(end === 0 && start === end) {
                    if(ic.bNotLoadStructure) {
                        ic.hAtoms = me.hashUtilsCls.cloneHash(ic.atoms);

                        // end of all commands
                        if(1 === ic.commands.length) ic.bAddCommands = true;
                        if(bFinalStep) this.renderFinalStep(steps);                  
                    }
                    else {
                        await thisClass.applyCommandLoad(ic.commands[i]);

                        // end of all commands
                        if(1 === ic.commands.length) ic.bAddCommands = true;
                        if(bFinalStep) thisClass.renderFinalStep(steps);
                  }
                  return;
              }
              else {
                    if(ic.bNotLoadStructure) {
                        ic.hAtoms = me.hashUtilsCls.cloneHash(ic.atoms);

                        // undo/redo requires render the first step
                        if(ic.backForward) this.renderFinalStep(1);
                    }
                    else {
                        await thisClass.applyCommandLoad(ic.commands[i]);

                        // undo/redo requires render the first step
                        if(ic.backForward) thisClass.renderFinalStep(1);
                    }
              }
          }
          else if(command.indexOf('set map') == 0 && command.indexOf('set map wireframe') == -1) {
              //set map 2fofc sigma 1.5
              let urlArray = strArray[0].trim().split(' | ');

              let str = urlArray[0].substr(8);
              let paraArray = str.split(" ");

              if(paraArray.length == 3 && paraArray[1] == 'sigma') {
                paraArray[2];
                paraArray[0];

                await thisClass.applyCommandMap(strArray[0].trim());
              }
          }
          else if(command.indexOf('set emmap') == 0 && command.indexOf('set emmap wireframe') == -1) {
              //set emmap percentage 70
              let str = strArray[0].trim().substr(10);
              let paraArray = str.split(" ");

              if(paraArray.length == 2 && paraArray[0] == 'percentage') {
                paraArray[1];

                await thisClass.applyCommandEmmap(strArray[0].trim());
              }
          }
          else if(command.indexOf('set phi') == 0) {
              await ic.delphiCls.applyCommandPhi(strArray[0].trim());
          }
          else if(command.indexOf('set delphi') == 0) {
              await ic.delphiCls.applyCommandDelphi(strArray[0].trim());
          }
          else if(command.indexOf('view annotations') == 0) { // the command may have "|||{"factor"...
              if(Object.keys(ic.proteins).length > 0) {
                await thisClass.applyCommandAnnotationsAndCddSite(strArray[0].trim());
              }
          }
          else if(command.indexOf('set annotation clinvar') == 0 ) { // the command may have "|||{"factor"...
              if(Object.keys(ic.proteins).length > 0) {
                await thisClass.applyCommandClinvar(strArray[0].trim());
              }
          }
          else if(command.indexOf('set annotation snp') == 0) { // the command may have "|||{"factor"...
              if(Object.keys(ic.proteins).length > 0 ) {
                await thisClass.applyCommandSnp(strArray[0].trim());
              }
          }
          else if(command.indexOf('set annotation ptm') == 0 ) { // the command may have "|||{"factor"...
            if(Object.keys(ic.proteins).length > 0) {
                await thisClass.applyCommandPTM(strArray[0].trim());
            }
          }
          else if(command.indexOf('ig refnum on') == 0 ) { 
            await ic.refnumCls.showIgRefNum();
          }
          else if(command.indexOf('set annotation 3ddomain') == 0) { // the command may have "|||{"factor"...
              if(Object.keys(ic.proteins).length > 0) {
                  thisClass.applyCommand3ddomain(strArray[0].trim());   
              }
          }
          else if(command.indexOf('set annotation all') == 0) { // the command may have "|||{"factor"...
            if(Object.keys(ic.proteins).length > 0) {
                await thisClass.applyCommandClinvar(strArray[0].trim());
                await thisClass.applyCommandSnp(strArray[0].trim());
                thisClass.applyCommand3ddomain(strArray[0].trim());
            }

            await ic.annotationCls.setAnnoTabAll();
          }
          else if(command.indexOf('view interactions') == 0 && me.cfg.align !== undefined) { // the command may have "|||{"factor"...
              await thisClass.applyCommandViewinteraction(strArray[0].trim());

          }
          else if(command.indexOf('symmetry') == 0) {
            ic.bAxisOnly = false;

            let title = command.substr(command.indexOf(' ') + 1);
            ic.symmetrytitle =(title === 'none') ? undefined : title;

            if(title !== 'none') {
                await ic.symdCls.retrieveSymmetry(Object.keys(ic.structures)[0]);
            }

            ic.drawCls.draw();
          }
          else if(command.indexOf('symd symmetry') == 0) {
            ic.bAxisOnly = false;

            await ic.symdCls.applyCommandSymd(command);

            ic.drawCls.draw();
          }
          else if(command.indexOf('scap') == 0) {
            await ic.scapCls.applyCommandScap(command);
          }
          else if(command.indexOf('realign on seq align') == 0) {
            let paraArray = command.split(' | ');
            if(paraArray.length == 2) {
                let nameArray = paraArray[1].split(',');
                ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
            }

            await thisClass.applyCommandRealign(command);
          }
          else if(command.indexOf('realign on structure align') == 0) {
            let paraArray = command.split(' | ');
            if(paraArray.length == 2) {
                let nameArray = paraArray[1].split(',');
                ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
            }

            me.cfg.aligntool = 'vast';

            await thisClass.applyCommandRealignByStruct(command);
          }
          else if(command.indexOf('realign on tmalign') == 0) {
            thisClass.getHAtoms(ic.commands[i]);

            me.cfg.aligntool = 'tmalign';

            await thisClass.applyCommandRealignByStruct(ic.commands[i]);
          }
          else if(command.indexOf('realign on vastplus') == 0) {
            thisClass.getHAtoms(ic.commands[i]);

            await ic.vastplusCls.realignOnVastplus();
          }
          else if(command.indexOf('graph interaction pairs') == 0) {
            await thisClass.applyCommandGraphinteraction(command);
          }
          else if(command.indexOf('cartoon 2d domain') == 0) {
            await thisClass.applyCommandCartoon2d(command);
          }
          else if(command.indexOf('set half pae map') == 0) {
            await thisClass.applyCommandAfmap(command);
          }
          else if(command.indexOf('set full pae map') == 0) {
            await thisClass.applyCommandAfmap(command, true);
          }
          else if(command.indexOf('export pqr') == 0) {
            await me.htmlCls.setHtmlCls.exportPqr();
          }
          else if(command.indexOf('cartoon 2d chain') == 0 || command.indexOf('cartoon 2d secondary') == 0) {
            let pos = command.lastIndexOf(' ');
            let type = command.substr(pos + 1);
    
            await ic.cartoon2dCls.draw2Dcartoon(type);
          }
          else {
            await ic.applyCommandCls.applyCommand(ic.commands[i]);
          }
      }
      
      //if(i === steps - 1) {
      if(i === steps || bFinalStep) {
          this.renderFinalStep(i);
      }
    }

    pressCommandtext() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        $("#" + ic.pre + "logtext").keypress(async function(e) { let ic = thisClass.icn3d;
           ic.bAddLogs = false; // turn off log
           let code =(e.keyCode ? e.keyCode : e.which);
           if(code == 13) { //Enter keycode
              e.preventDefault();
              let dataStr = $(this).val();
              ic.bRender = true;
              let commandArray = dataStr.split('\n');

              let prevLogLen = ic.logs.length;
              for(let i = prevLogLen, il = commandArray.length; i < il; ++i) {
                  let lastCommand = (i == prevLogLen) ? commandArray[i].substr(2).trim() : commandArray[i].trim(); // skip "> "
                  if(lastCommand === '') continue;

                  ic.logs.push(lastCommand);
                  //$("#" + ic.pre + "logtext").val("> " + ic.logs.join("\n> ") + "\n> ").scrollTop($("#" + ic.pre + "logtext")[0].scrollHeight);
                  //if(lastCommand !== '') {
                    let transformation = {};
                    transformation.factor = ic._zoomFactor;
                    transformation.mouseChange = ic.mouseChange;
                    transformation.quaternion = ic.quaternion;
                    ic.commands.push(lastCommand + '|||' + ic.transformCls.getTransformationStr(transformation));
                    ic.optsHistory.push(me.hashUtilsCls.cloneHash(ic.opts));
                    ic.optsHistory[ic.optsHistory.length - 1].hlatomcount = Object.keys(ic.hAtoms).length;
                    if(me.utilsCls.isSessionStorageSupported()) ic.setStyleCls.saveCommandsToSession();
                    ic.STATENUMBER = ic.commands.length;
                    if(lastCommand.indexOf('load') !== -1) {
                        await thisClass.applyCommandLoad(lastCommand);
                    }
                    else if(lastCommand.indexOf('set map') !== -1 && lastCommand.indexOf('set map wireframe') === -1) {
                        thisClass.applyCommandMap(lastCommand);
                    }
                    else if(lastCommand.indexOf('set emmap') !== -1 && lastCommand.indexOf('set emmap wireframe') === -1) {
                        thisClass.applyCommandEmmap(lastCommand);
                    }
                    else if(lastCommand.indexOf('set phi') !== -1) {
                        await ic.delphiCls.applyCommandPhi(lastCommand);
                    }
                    else if(lastCommand.indexOf('set delphi') !== -1) {
                        await ic.delphiCls.applyCommandDelphi(lastCommand);
                    }
                    else if(lastCommand.indexOf('view annotations') == 0
                      //|| lastCommand.indexOf('set annotation cdd') == 0
                      //|| lastCommand.indexOf('set annotation site') == 0
                      ) {
                        await thisClass.applyCommandAnnotationsAndCddSite(lastCommand);
                    }
                    else if(lastCommand.indexOf('set annotation clinvar') == 0 ) {
                        await thisClass.applyCommandClinvar(lastCommand);
                    }
                    else if(lastCommand.indexOf('set annotation snp') == 0) {
                        await thisClass.applyCommandSnp(lastCommand);
                    }
                    else if(lastCommand.indexOf('set annotation ptm') == 0) {
                        await thisClass.applyCommandPTM(lastCommand);
                    }
                    else if(lastCommand.indexOf('ig refnum on') == 0) {
                        await ic.refnumCls.showIgRefNum();
                    }
                    else if(lastCommand.indexOf('set annotation 3ddomain') == 0) {
                        thisClass.applyCommand3ddomain(lastCommand);
                    }
                    else if(lastCommand.indexOf('set annotation all') == 0) {
                        await thisClass.applyCommandClinvar(lastCommand);
                        await thisClass.applyCommandSnp(lastCommand);
                        thisClass.applyCommand3ddomain(lastCommand);
                        await ic.annotationCls.setAnnoTabAll();
                    }
                    else if(lastCommand.indexOf('view interactions') == 0 && me.cfg.align !== undefined) {
                        await thisClass.applyCommandViewinteraction(lastCommand);
                    }
                    else if(lastCommand.indexOf('symmetry') == 0) {
                        let title = lastCommand.substr(lastCommand.indexOf(' ') + 1);
                        ic.symmetrytitle =(title === 'none') ? undefined : title;
                        if(title !== 'none') {
                            if(ic.symmetryHash === undefined) {
                                await ic.symdCls.retrieveSymmetry(Object.keys(ic.structures)[0]);
                            }
                        }
                    }
                    else if(lastCommand.indexOf('symd symmetry') == 0) {
                        await ic.symdCls.applyCommandSymd(lastCommand);
                    }
                    else if(lastCommand.indexOf('scap ') == 0) {
                        await ic.scapCls.applyCommandScap(lastCommand);
                    }
                    else if(lastCommand.indexOf('realign on seq align') == 0) {
                        let paraArray = lastCommand.split(' | ');
                        if(paraArray.length == 2) {
                            let nameArray = paraArray[1].split(',');
                            ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
                        }
                        await thisClass.applyCommandRealign(lastCommand);
                    }
                    else if(lastCommand.indexOf('realign on structure align') == 0) {
                        let paraArray = lastCommand.split(' | ');
                        if(paraArray.length == 2) {
                            let nameArray = paraArray[1].split(',');
                            ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
                        }

                        me.cfg.aligntool = 'vast';

                        await thisClass.applyCommandRealignByStruct(lastCommand);
                    }
                    else if(lastCommand.indexOf('realign on tmalign') == 0) {
                        let paraArray = lastCommand.split(' | ');
                        if(paraArray.length == 2) {
                            let nameArray = paraArray[1].split(',');
                            ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
                        }
                        
                        me.cfg.aligntool = 'tmalign';

                        await thisClass.applyCommandRealignByStruct(lastCommand);
                    }
                    else if(lastCommand.indexOf('realign on vastplus') == 0) {
                        let paraArray = lastCommand.split(' | ');
                        if(paraArray.length == 2) {
                            let nameArray = paraArray[1].split(',');
                            ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
                        }
                        
                        await ic.vastplusCls.realignOnVastplus();
                    }
                    else if(lastCommand.indexOf('graph interaction pairs') == 0) {
                        await thisClass.applyCommandGraphinteraction(lastCommand);
                    }
                    else {
                        await ic.applyCommandCls.applyCommand(lastCommand + '|||' + ic.transformCls.getTransformationStr(transformation));
                    }
                    //ic.selectionCls.saveSelectionIfSelected();
                    //ic.drawCls.draw();
                  //} // if
              } // for

              ic.selectionCls.saveSelectionIfSelected();
              ic.drawCls.draw();

              $("#" + ic.pre + "logtext").val("> " + ic.logs.join("\n> ") + "\n> ").scrollTop($("#" + ic.pre + "logtext")[0].scrollHeight);
           }
           ic.bAddLogs = true;
        });
    }

    //Execute the command to load a structure. This step is different from the rest steps since
    //it has to finish before the rest steps start.
    async applyCommandLoad(commandStr) { let ic = this.icn3d, me = ic.icn3dui;

      //ic.bCommandLoad = true;

      if(ic.atoms !== undefined && Object.keys(ic.atoms).length > 0) return;

      // chain functions together
///      ic.deferred2 = $.Deferred(function() {
      ic.bAddCommands = false;
      let commandTransformation = commandStr.split('|||');

      let commandOri = commandTransformation[0].replace(/\s\s/g, ' ').trim();
      let command = commandOri; //.toLowerCase();

      if(command.indexOf('load') !== -1) { // 'load pdb [pdbid]'
        let load_parameters = command.split(' | ');

        let loadStr = load_parameters[0];
        if(load_parameters.length > 1) {
            let firstSpacePos = load_parameters[load_parameters.length - 1].indexOf(' ');
            me.cfg.inpara = load_parameters[load_parameters.length - 1].substr(firstSpacePos + 1);
            if(me.cfg.inpara === 'undefined') {
                me.cfg.inpara = '';
            }
        }

        // load pdb, mmcif, mmdb, cid
        let id = loadStr.substr(loadStr.lastIndexOf(' ') + 1);
        ic.inputid = id;
        if(command.indexOf('load mmtf') !== -1) {
          me.cfg.mmtfid = id;
          await ic.mmtfParserCls.downloadMmtf(id);
        }
        else if(command.indexOf('load pdb') !== -1) {
          me.cfg.pdbid = id;

          await ic.pdbParserCls.downloadPdb(id);
        }
        else if(command.indexOf('load af') !== -1) {
          me.cfg.afid = id;  
          await ic.pdbParserCls.downloadPdb(id, true);
        }
        else if(command.indexOf('load opm') !== -1) {
          me.cfg.opmid = id;
          await ic.opmParserCls.downloadOpm(id);
        }
        else if(command.indexOf('load mmcif') !== -1) {
          me.cfg.mmcifid = id;
          await ic.mmcifParserCls.downloadMmcif(id);
        }
        else if(command.indexOf('load mmdb ') !== -1 || command.indexOf('load mmdb1 ') !== -1) {
          me.cfg.mmdbid = id;
          me.cfg.bu = 1;

          await ic.mmdbParserCls.downloadMmdb(id);
        }
        else if(command.indexOf('load mmdb0') !== -1) {
            me.cfg.mmdbid = id;
            me.cfg.bu = 0;
  
            await ic.mmdbParserCls.downloadMmdb(id);
        }
        else if(command.indexOf('load mmdbaf1') !== -1) {
            me.cfg.mmdbafid = id;
            me.cfg.bu = 1;
  
            await ic.chainalignParserCls.downloadMmdbAf(id);
        }
        else if(command.indexOf('load mmdbaf0') !== -1) {
            me.cfg.mmdbafid = id;
            me.cfg.bu = 0;

            await ic.chainalignParserCls.downloadMmdbAf(id);
        }
        else if(command.indexOf('load gi') !== -1) {
            me.cfg.gi = id;
            await ic.mmdbParserCls.downloadGi(id);
        }
        else if(command.indexOf('load refseq') !== -1) {
            me.cfg.refseqid = id;
            await ic.mmdbParserCls.downloadRefseq(id);
        }
        else if(command.indexOf('load seq_struct_ids ') !== -1) {
          ic.bSmithwm = false;
          ic.bLocalSmithwm = false;
          await ic.mmdbParserCls.downloadBlast_rep_id(id);
        }
        else if(command.indexOf('load seq_struct_ids_smithwm ') !== -1) {
            ic.bSmithwm = true;
            await ic.mmdbParserCls.downloadBlast_rep_id(id);
        }
        else if(command.indexOf('load seq_struct_ids_local_smithwm ') !== -1) {
            ic.bLocalSmithwm = true;
            await ic.mmdbParserCls.downloadBlast_rep_id(id);
        }
        else if(command.indexOf('load cid') !== -1) {
          me.cfg.cid = id;
          await ic.sdfParserCls.downloadCid(id);
        }
        else if(command.indexOf('load alignment') !== -1) {
          me.cfg.align = id;

          if(me.cfg.inpara.indexOf('atype=2') == -1) {
            await ic.alignParserCls.downloadAlignment(me.cfg.align);
          }
          else {
            let vastplusAtype = 2; // Tm-align
            await ic.chainalignParserCls.downloadMmdbAf(me.cfg.align, undefined, vastplusAtype);
          }
        }
        else if(command.indexOf('load chainalignment') !== -1) {
          //load chainalignment [id] | resnum [resnum] | resdef [resnum] | aligntool [aligntool] | parameters [inpara]
          let urlArray = command.split(" | ");
          if(urlArray.length > 1 && urlArray[1].indexOf('resnum') != -1) {
                me.cfg.resnum = urlArray[1].substr(urlArray[1].indexOf('resnum') + 7);
          }
          if(urlArray.length > 2 && urlArray[2].indexOf('resdef') != -1) {
                me.cfg.resdef = urlArray[2].substr(urlArray[1].indexOf('resdef') + 7);
          }
          if(urlArray.length > 3 && urlArray[3].indexOf('aligntool') != -1) {
                me.cfg.aligntool = urlArray[3].substr(urlArray[1].indexOf('aligntool') + 10);
          }

          me.cfg.chainalign = id;
          await ic.chainalignParserCls.downloadChainalignment(id, me.cfg.resnum, me.cfg.resdef);
        }
        else if(command.indexOf('load url') !== -1) {
            let typeStr = load_parameters[1]; // type pdb
            let pos =(typeStr !== undefined) ? typeStr.indexOf('type ') : -1;
            let type = 'pdb';

            if(pos !== -1) {
                type = typeStr.substr(pos + 5);
            }

            me.cfg.url = id;
            await ic.pdbParserCls.downloadUrl(id, type);
        }
      }

      ic.bAddCommands = true;
///      }); // end of me.deferred = $.Deferred(function() {

///      return ic.deferred2.promise();
    }

    //Apply the command to show electron density map.
    applyCommandMap(command) { let ic = this.icn3d; ic.icn3dui;

      // chain functions together
    //   ic.deferredMap = $.Deferred(function() { let ic = thisClass.icn3d;
          //"set map 2fofc sigma 1.5"
          // or "set map 2fofc sigma 1.5 | [url]"
          let urlArray = command.split(" | ");

          let str = urlArray[0].substr(8);
          let paraArray = str.split(" ");

          if(paraArray.length == 3 && paraArray[1] == 'sigma') {
              let sigma = paraArray[2];
              let type = paraArray[0];

              if(urlArray.length == 2) {
                  ic.dsn6ParserCls.dsn6ParserBase(urlArray[1], type, sigma);
              }
              else {
                  ic.dsn6ParserCls.dsn6Parser(ic.inputid, type, sigma);
              }
          }
    //   }); // end of me.deferred = $.Deferred(function() {

    //   return ic.deferredMap.promise();
    }

    //Apply the command to show EM density map.
    applyCommandEmmap(command) { let ic = this.icn3d; ic.icn3dui;

      // chain functions together
    //   ic.deferredEmmap = $.Deferred(function() { let ic = thisClass.icn3d;
          let str = command.substr(10);
          let paraArray = str.split(" ");

          if(paraArray.length == 2 && paraArray[0] == 'percentage') {
              let percentage = paraArray[1];
              let type = 'em';

              ic.densityCifParserCls.densityCifParser(ic.inputid, type, percentage, ic.emd);
          }
    //   }); // end of me.deferred = $.Deferred(function() {

    //   return ic.deferredEmmap.promise();
    }

    async applyCommandRealign(command) { let ic = this.icn3d; ic.icn3dui;
        await ic.realignParserCls.realignOnSeqAlign();
    }

    async applyCommandRealignByStruct(command) { let ic = this.icn3d; ic.icn3dui;
      ic.drawCls.draw();
      await ic.realignParserCls.realignOnStructAlign();
    }

    async applyCommandAfmap(command, bFull) { let ic = this.icn3d; ic.icn3dui;
      let afid = command.substr(command.lastIndexOf(' ') + 1);
     
      await ic.contactMapCls.afErrorMap(afid, bFull);
    }

    async applyCommandGraphinteraction(command) { let ic = this.icn3d; ic.icn3dui;
      let paraArray = command.split(' | ');
      if(paraArray.length >= 3) {
          let setNameArray = paraArray[1].split(' ');
          let nameArray2 = setNameArray[0].split(',');
          let nameArray = setNameArray[1].split(',');

          let bHbond = paraArray[2].indexOf('hbonds') !== -1;
          let bSaltbridge = paraArray[2].indexOf('salt bridge') !== -1;
          let bInteraction = paraArray[2].indexOf('interactions') !== -1;

          let bHalogen = paraArray[2].indexOf('halogen') !== -1;
          let bPication = paraArray[2].indexOf('pi-cation') !== -1;
          let bPistacking = paraArray[2].indexOf('pi-stacking') !== -1;

          let bHbondCalc;
          if(paraArray.length >= 4) {
              bHbondCalc =(paraArray[3] == 'true') ? true : false;
          }

          ic.applyCommandCls.setStrengthPara(paraArray);

          await ic.viewInterPairsCls.viewInteractionPairs(nameArray2, nameArray, bHbondCalc, 'graph',
              bHbond, bSaltbridge, bInteraction, bHalogen, bPication, bPistacking);
      }
    }

    async applyCommandCartoon2d(command) { let ic = this.icn3d; ic.icn3dui;
        let type = command.substr(command.lastIndexOf(' ') + 1);
        await ic.cartoon2dCls.draw2Dcartoon(type);
    }

    //The annotation window calls many Ajax calls. Thus the command "view interactions"
    //(in Share Link or loading state file) is handled specially to wait for the Ajax calls
    //to finish before executing the next command.
    async applyCommandAnnotationsAndCddSite(command) { let ic = this.icn3d; ic.icn3dui;
        if(command == "view annotations") {
            //if(me.cfg.showanno === undefined || !me.cfg.showanno) {
                await ic.showAnnoCls.showAnnotations();
            //}
        }
    }

    async applyCommandClinvar(command) { let ic = this.icn3d; ic.icn3dui;
        // chain functions together
        let pos = command.lastIndexOf(' '); // set annotation clinvar
        command.substr(pos + 1);
        
        await ic.annotationCls.setAnnoTabClinvar();
    }

    async applyCommandSnp(command) { let ic = this.icn3d; ic.icn3dui;
        // chain functions together
        let pos = command.lastIndexOf(' '); // set annotation clinvar
        command.substr(pos + 1);
        
        await ic.annotationCls.setAnnoTabSnp();
    }

    async applyCommandPTM(command) { let ic = this.icn3d; ic.icn3dui;
        // chain functions together
        let pos = command.lastIndexOf(' '); // set annotation clinvar
        command.substr(pos + 1);
  
        await ic.annotationCls.setAnnoTabPTM();
    }

    applyCommand3ddomain(command) { let ic = this.icn3d; ic.icn3dui;
        // chain functions together
        let pos = command.lastIndexOf(' ');
        let type = command.substr(pos + 1);
    
        if(type == '3ddomain' || type == 'all') {
            ic.annotationCls.setAnnoTab3ddomain();
        }
    }

    async applyCommandViewinteraction(command) { let ic = this.icn3d, me = ic.icn3dui;
        // chain functions together
        if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined) {
            let structureArray = Object.keys(ic.structures);
            await ic.ParserUtilsCls.set2DDiagramsForAlign(structureArray[0].toUpperCase(), structureArray[1].toUpperCase());
        }
    }

    //When reading a list of commands, apply transformation at the last step.
    renderFinalStep(steps) { let ic = this.icn3d, me = ic.icn3dui;
        // enable ic.ParserUtilsCls.hideLoading
        ic.bCommandLoad = false;

        // hide "loading ..."
        ic.ParserUtilsCls.hideLoading();

        //ic.bRender = true;

        // end of all commands
        if(steps + 1 === ic.commands.length) ic.bAddCommands = true;


        ic.bRender = true;

        let commandTransformation = (ic.commands[steps-1]) ? ic.commands[steps-1].split('|||') : [];

        if(commandTransformation.length == 2) {
            let transformation = JSON.parse(commandTransformation[1]);

            ic._zoomFactor = transformation.factor;

            ic.mouseChange.x = transformation.mouseChange.x;
            ic.mouseChange.y = transformation.mouseChange.y;

            ic.quaternion._x = transformation.quaternion._x;
            ic.quaternion._y = transformation.quaternion._y;
            ic.quaternion._z = transformation.quaternion._z;
            ic.quaternion._w = transformation.quaternion._w;
        }

        ic.selectionCls.oneStructurePerWindow();

        // simple if all atoms are modified
        //if( me.cfg.command === undefined &&(steps === 1 ||(Object.keys(ic.hAtoms).length === Object.keys(ic.atoms).length) ||(ic.optsHistory[steps - 1] !== undefined && ic.optsHistory[steps - 1].hasOwnProperty('hlatomcount') && ic.optsHistory[steps - 1].hlatomcount === Object.keys(ic.atoms).length) ) ) {
        if(steps === 1
          || (ic.hAtoms && ic.atoms && Object.keys(ic.hAtoms).length === Object.keys(ic.atoms).length)
          || (ic.optsHistory[steps - 1] !== undefined && ic.optsHistory[steps - 1].hasOwnProperty('hlatomcount') && ic.optsHistory[steps - 1].hlatomcount === Object.keys(ic.atoms).length) ) {
    // the following code caused problem for many links,e.g., https://structure.ncbi.nlm.nih.gov/icn3d/share.html?17g3r1JDvZ7ZL39e6
    //        if(steps === 1) {
                // assign styles and color using the options at that stage
    //            ic.setStyleCls.setAtomStyleByOptions(ic.optsHistory[steps - 1]);
    //            ic.setColorCls.setColorByOptions(ic.optsHistory[steps - 1], ic.hAtoms);
    //        }

            if(ic.optsHistory.length >= steps) {
                let pkOption = ic.optsHistory[steps - 1].pk;
                if(pkOption === 'no') {
                    ic.pk = 0;
                }
                else if(pkOption === 'atom') {
                    ic.pk = 1;
                }
                else if(pkOption === 'residue') {
                    ic.pk = 2;
                }
                else if(pkOption === 'strand') {
                    ic.pk = 3;
                }

    // the following code caused problem for many links,e.g., https://structure.ncbi.nlm.nih.gov/icn3d/share.html?17g3r1JDvZ7ZL39e6
    //            if(steps === 1) {
    //                ic.setColorCls.applyOriginalColor();
    //            }

                ic.hlUpdateCls.updateHlAll();

                // caused some problem witht the following line
    //            $.extend(ic.opts, ic.optsHistory[steps - 1]);
                ic.drawCls.draw();
            }
            else {
                ic.hlUpdateCls.updateHlAll();

                ic.drawCls.draw();
            }
        }
        else { // more complicated if partial atoms are modified
            ic.hlUpdateCls.updateHlAll();

            ic.drawCls.draw();
        }

        if(me.cfg.closepopup) {
            setTimeout(function(){
                ic.resizeCanvasCls.closeDialogs();
            }, 100);

            ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH, me.htmlCls.HEIGHT, true);
        }

        // an extra render to remove artifacts in transparent surface
        if(ic.bTransparentSurface && ic.bRender) ic.drawCls.render();

        /// if(ic.deferred !== undefined) ic.deferred.resolve(); /// if(ic.deferred2 !== undefined) ic.deferred2.resolve();
        /// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
    }

    async replayFirstStep(currentNumber) { let ic = this.icn3d, me = ic.icn3dui;
          // fresh start
          ic.reinitAfterLoad();
          //ic.selectionCls.resetAll();

          //ic.opts = me.hashUtilsCls.cloneHash(ic.opts);

          await this.execCommandsBase(currentNumber, currentNumber, ic.STATENUMBER);

          let cmdStrOri = ic.commands[currentNumber];
          //var pos = ic.commands[currentNumber].indexOf(' | ');
          let pos = ic.commands[currentNumber].indexOf('|');
          if(pos != -1) cmdStrOri = ic.commands[currentNumber].substr(0, pos);

          let maxLen = 20;
          let cmdStr =(cmdStrOri.length > maxLen) ? cmdStrOri.substr(0, maxLen) + '...' : cmdStrOri;

          let menuStr = ic.applyCommandCls.getMenuFromCmd(cmdStrOri); // 'File > Retrieve by ID, Align';

          $("#" + ic.pre + "replay_cmd").html('Cmd: ' + cmdStr);
          $("#" + ic.pre + "replay_menu").html('Menu: ' + menuStr);

          me.htmlCls.clickMenuCls.setLogCmd(cmdStrOri, true);

          ic.bCommandLoad = false;

          // hide "loading ..."
          ic.ParserUtilsCls.hideLoading();

          ic.bRender = true;
          ic.drawCls.draw();
    }

    getHAtoms(fullcommand) { let ic = this.icn3d; ic.icn3dui;
        let strArray = fullcommand.split("|||");
        let command = strArray[0].trim();

        let paraArray = command.split(' | ');
        if(paraArray.length == 2) {
            let nameArray = paraArray[1].split(',');
            ic.hAtoms = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
        }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SelectByCommand {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Set a custom selection with the "command", its name "commandname" and its description "commanddesc".
    async selectByCommand(select, commandname, commanddesc) { let ic = this.icn3d, me = ic.icn3dui;
           if(select.indexOf('saved atoms') === 0) {
                let pos = 12; // 'saved atoms '
                let strSets = select.substr(pos);

                ic.definedSetsCls.selectCombinedSets(strSets, commandname);
           }
           else {
               let selectTmp = select.replace(/ AND /g, ' and ').replace(/ OR /g, ' or ').replace(/ or and /g, ' and ').replace(/ and /g, ' or and ').replace(/ or not /g, ' not ').replace(/ not /g, ' or not ');

               let commandStr =(selectTmp.trim().substr(0, 6) === 'select') ? selectTmp.trim().substr(7) : selectTmp.trim();

               // each select command may have several commands separated by ' or '
               let commandArray = commandStr.split(' or ');
               let allHighlightAtoms = {};

               for(let i = 0, il = commandArray.length; i < il; ++i) {
                   let command = commandArray[i].trim().replace(/\s+/g, ' ');
                   let pos = command.indexOf(' ');

                   ic.hAtoms = {};

                   if(command.substr(0, pos).toLowerCase() === 'and') { // intersection
                      await ic.applyCommandCls.applyCommand('select ' + command.substr(pos + 1));

                      allHighlightAtoms = me.hashUtilsCls.intHash(allHighlightAtoms, ic.hAtoms);
                   }
                   else if(command.substr(0, pos).toLowerCase() === 'not') { // negation
                      await ic.applyCommandCls.applyCommand('select ' + command.substr(pos + 1));

                      allHighlightAtoms = me.hashUtilsCls.exclHash(allHighlightAtoms, ic.hAtoms);
                   }
                   else { // union
                      await ic.applyCommandCls.applyCommand('select ' + command);
                      allHighlightAtoms = me.hashUtilsCls.unionHash(allHighlightAtoms, ic.hAtoms);
                   }
               }

               ic.hAtoms = me.hashUtilsCls.cloneHash(allHighlightAtoms);

               let atomArray = Object.keys(ic.hAtoms);

               if(commandname !== "") {
                   ic.selectionCls.addCustomSelection(atomArray, commandname, commanddesc, select, false);

                   let nameArray = [commandname];
                   //ic.changeCustomResidues(nameArray);

                   ic.definedSetsCls.changeCustomAtoms(nameArray);
               }
           }
    }

    selectBySpec(select, commandname, commanddesc, bDisplay, bNoUpdateAll) { let ic = this.icn3d, me = ic.icn3dui;
       select =(select.trim().substr(0, 6) === 'select') ? select.trim().substr(7) : select.trim();
       ic.hAtoms = {};

       // selection definition is similar to Chimera: https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/frameatom_spec.html
       // There will be no ' or ' in the spec. It's already separated in selectByCommand()
       // There could be ' and ' in the spec.
       let commandArray = select.replace(/\s+/g, ' ').replace(/ AND /g, ' and ').split(' and ');
       let residueHash = {};
       let atomHash = {};

       let bSelectResidues = true;
       for(let i = 0, il=commandArray.length; i < il; ++i) {
           //$1,2,3.A,B,C:5-10,LYS,chemicals@CA,C
           // $1,2,3: Structure
           // .A,B,C: chain
           // :5-10,K,chemicals: residues, could be 'proteins', 'nucleotides', 'chemicals', 'ions', and 'water'
           // @CA,C: atoms
           // wild card * can be used to select all
           //var currHighlightAtoms = {}

           let dollarPos = commandArray[i].indexOf('$');
           let periodPos = commandArray[i].indexOf('.');
           let colonPos = commandArray[i].indexOf(':');
           let colonPos2 = commandArray[i].indexOf('%'); // for reference numbers
           let atPos = commandArray[i].indexOf('@');

           let moleculeStr, chainStr, residueStr, refResStr, atomStrArray;
           let testStr = commandArray[i];

           if(atPos === -1) {
             atomStrArray = ["*"];
           }
           else {
             atomStrArray = testStr.substr(atPos + 1).split(',');
             testStr = testStr.substr(0, atPos);
           }

           if(colonPos === -1 && colonPos2 === -1 ) {
             residueStr = "*";
           }
           else if(colonPos != -1) {
             residueStr = testStr.substr(colonPos + 1);
             testStr = testStr.substr(0, colonPos);
           }
           else if(colonPos2 != -1) {
            refResStr = testStr.substr(colonPos2 + 1);
            testStr = testStr.substr(0, colonPos2);
          }

           if(periodPos === -1) {
             chainStr = "*";
           }
           else {
             chainStr = testStr.substr(periodPos + 1);
             testStr = testStr.substr(0, periodPos);
           }

           if(dollarPos === -1) {
             moleculeStr = "*";
           }
           else {
             //moleculeStr = testStr.substr(dollarPos + 1).toUpperCase();
             moleculeStr = testStr.substr(dollarPos + 1);
             testStr = testStr.substr(0, dollarPos);
           }

           if(atomStrArray.length == 1 && atomStrArray[0] !== '*') {
             bSelectResidues = false; // selected atoms
           }

           let molecule, molecule_chain, moleculeArray=[], Molecule_ChainArray=[], start, end;

           if(moleculeStr === '*') {
             moleculeArray = Object.keys(ic.structures);
           }
           else {
             moleculeArray = moleculeStr.split(",");
           }

           if(chainStr === '*') {
             let tmpArray = Object.keys(ic.chains);  // 1_A(molecule_chain)

             for(let j = 0, jl = tmpArray.length; j < jl; ++j) {
               molecule_chain = tmpArray[j];

               molecule = molecule_chain.substr(0, molecule_chain.indexOf('_'));
               //if(moleculeArray.toString().toLowerCase().indexOf(molecule.toLowerCase()) !== -1) {
               let moleculeArrayLower = moleculeArray.map(function(x){ return x.toLowerCase(); });
               if(moleculeArrayLower.indexOf(molecule.toLowerCase()) !== -1 ) {
                 Molecule_ChainArray.push(molecule_chain);
               }
             }
           }
           else {
             for(let j = 0, jl = moleculeArray.length; j < jl; ++j) {
               molecule = moleculeArray[j];

               let chainArray = chainStr.split(",");
               for(let k in chainArray) {
                 Molecule_ChainArray.push(molecule + '_' + chainArray[k]);
               }
             }
           }

           let bRefnum = (refResStr) ? true : false;
           let residueStrArray = (bRefnum) ? refResStr.split(',') : residueStr.split(',');

           for(let j = 0, jl = residueStrArray.length; j < jl; ++j) {
               let bResidueId = false;

               //var hyphenPos = residueStrArray[j].indexOf('-');
               let hyphenPos = residueStrArray[j].lastIndexOf('-');

               let oneLetterResidueStr = undefined, threeLetterResidueStr = undefined;
               let bAllResidues = false;
               let bResidueArray = false;
               let bResidueArrayThree = false; // three letter residues

               if(hyphenPos !== -1) {
                 start = residueStrArray[j].substr(0, hyphenPos);
                 end = residueStrArray[j].substr(hyphenPos+1);
                 bResidueId = true;
               }
               else {
                 //if(residueStrArray[j].length > 1 && residueStrArray[j][0] === '3' && (residueStrArray[j].length - 1) % 3 === 0) { // three letter residue string, such as :3LysArg
                 if(!bRefnum && residueStrArray[j].length > 1 && residueStrArray[j][0] === '3' 
                     && isNaN(residueStrArray[j][1]) && residueStrArray[j][0] !== '-') { // three letter residue string, such as :3LysArg or :3ZN, but not :30 neither :3-10
                   let tmpStr = residueStrArray[j].toUpperCase();
                   threeLetterResidueStr = tmpStr.substr(1);
                   bResidueArrayThree = true;
                 }
                 // some residue ID could be "35A"
                 //else if(residueStrArray[j] !== '' && !isNaN(residueStrArray[j])) { // residue id
                 else if(residueStrArray[j] !== '' && !isNaN(parseInt(residueStrArray[j]))) { // residue id
                   start = residueStrArray[j];
                   end = start;
                   bResidueId = true;
                 }
                 else if(residueStrArray[j] === '*') { // all resiues
                   bAllResidues = true;
                 }
                 else if(residueStrArray[j] !== 'proteins' && residueStrArray[j] !== 'nucleotides' && residueStrArray[j] !== 'chemicals' && residueStrArray[j] !== 'ions' && residueStrArray[j] !== 'water') { // residue name
                   let tmpStr = residueStrArray[j].toUpperCase();
                   //oneLetterResidue =(residueStrArray[j].length === 1) ? tmpStr : me.utilsCls.residueName2Abbr(tmpStr);
                   oneLetterResidueStr = tmpStr;
                   bResidueArray = true;
                 }
               }

               for(let mc = 0, mcl = Molecule_ChainArray.length; mc < mcl; ++mc) {
                 molecule_chain = Molecule_ChainArray[mc];

                 if(bResidueId) {
                   // start and end could be a string such as 35A
                   //for(let k = parseInt(start); k <= parseInt(end); ++k) {
                   start = !isNaN(start) ? parseInt(start) : start;
                   end = !isNaN(end) ? parseInt(end) : end;
                   for(let k = start; k <= end; ++k) {
                     let residArray = [];

                     if(bRefnum) {
                      let residArrayTmp = ic.refnum2residArray[k];
                      for(let m = 0, ml = residArrayTmp.length; m < ml; ++m) {
                        let residueId = residArrayTmp[m];
                        if(residueId.substr(0, residueId.lastIndexOf('_')) == molecule_chain) {
                          residArray.push(residueId);
                        }
                      }
                     }
                     else {
                      let residueId = molecule_chain + '_' + k;
                      residArray = [residueId];
                     }

                     for(let l = 0, ll = residArray.length; l < ll; ++l) {
                        let residueId = residArray[l];

                        if(i === 0) {
                              residueHash[residueId] = 1;
                        }
                        else {
                            // if not exit previously, "and" operation will remove this one
                            //if(!residueHash.hasOwnProperty(residueId)) residueHash[residueId] = undefined;
                            if(!residueHash.hasOwnProperty(residueId)) delete residueHash[residueId];
                        }

                        for(let m in ic.residues[residueId]) {
                          for(let n = 0, nl = atomStrArray.length; n < nl; ++n) {
                              let atomStr = atomStrArray[n];
                              if(atomStr === '*' || atomStr === ic.atoms[m].name) {
                                if(i === 0) {
                                    //currHighlightAtoms[m] = 1;
                                    atomHash[m] = 1;
                                }
                                else {
                                    //if(!currHighlightAtoms.hasOwnProperty(m)) currHighlightAtoms[m] = undefined;
                                    //if(!atomHash.hasOwnProperty(m)) atomHash[m] = undefined;
                                    if(!atomHash.hasOwnProperty(m)) delete atomHash[m];
                                }
                              }
                          }
                        }
                      } // end for(let l = 0, 
                   } // end for
                 }
                 else {
                   if(molecule_chain in ic.chains) {
                     let chainAtomHash = ic.chains[molecule_chain];
                     for(let m in chainAtomHash) {
                       // residue could also be 'proteins', 'nucleotides', 'chemicals', 'ions', and 'water'
                       ic.atoms[m].resn.substr(0,3).toUpperCase();
                       if(bAllResidues
                           //|| me.utilsCls.residueName2Abbr(tmpStr) === oneLetterResidue
                           ||(residueStrArray[j] === 'proteins' && m in ic.proteins)
                           ||(residueStrArray[j] === 'nucleotides' && m in ic.nucleotides)
                           ||(residueStrArray[j] === 'chemicals' && m in ic.chemicals)
                           ||(residueStrArray[j] === 'ions' && m in ic.ions)
                           ||(residueStrArray[j] === 'water' && m in ic.water)
                           ) {
                         // many duplicates
                         if(i === 0) {
                             residueHash[molecule_chain + '_' + ic.atoms[m].resi] = 1;
                         }
                         else {
                             let residTmp = molecule_chain + '_' + ic.atoms[m].resi;
                             //if(!residueHash.hasOwnProperty(residTmp)) residueHash[residTmp] = undefined;
                             if(!residueHash.hasOwnProperty(residTmp)) delete residueHash[residTmp];
                         }

                         for(let n = 0, nl = atomStrArray.length; n < nl; ++n) {
                             let atomStr = atomStrArray[n];

                             if(atomStr === '*' || atomStr === ic.atoms[m].name) {
                                 if(i === 0) {
                                     //currHighlightAtoms[m] = 1;
                                     atomHash[m] = 1;
                                 }
                                 else {
                                     //if(!currHighlightAtoms.hasOwnProperty(m)) currHighlightAtoms[m] = undefined;
                                     //if(!atomHash.hasOwnProperty(m)) atomHash[m] = undefined;
                                     if(!atomHash.hasOwnProperty(m)) delete atomHash[m];
                                 }
                             }
                         }

                       }
                     } // end for(let m in atomHash) {

                     if(bResidueArray || bResidueArrayThree) {
                       let n =(bResidueArray) ? 1 : 3;
                       let residueStrTmp =(bResidueArray) ? oneLetterResidueStr : threeLetterResidueStr;

                       let chainSeq = '', resiArray = [];
                       for(let s = 0, sl = ic.chainsSeq[molecule_chain].length; s < sl;  ++s) {
                           if(bResidueArray) {
                               chainSeq +=(ic.chainsSeq[molecule_chain][s].name.length == 1) ? ic.chainsSeq[molecule_chain][s].name : ' ';
                           }
                           else if(bResidueArrayThree) {
                               let threeLetter = me.utilsCls.residueAbbr2Name(ic.chainsSeq[molecule_chain][s].name);

                               chainSeq +=(threeLetter.length == 3) ? threeLetter : threeLetter.padEnd(3, '_');
                           }
                           resiArray.push(ic.chainsSeq[molecule_chain][s].resi);
                       }

                       chainSeq = chainSeq.toUpperCase();

                       let seqReg = residueStrTmp.replace(/x/gi, ".");
                       let posArray = [];

                       let searchReg = new RegExp(seqReg, 'i');

                       let targetStr = chainSeq;
                       let pos = targetStr.search(searchReg);
                       let sumPos = pos / n;
                       while(pos !== -1) {
                           posArray.push(sumPos);
                           targetStr = targetStr.substr(pos + n);
                           pos = targetStr.search(searchReg);
                           sumPos += pos / n + 1;
                       }

                       for(let s = 0, sl = posArray.length; s < sl; ++s) {
                           let pos = posArray[s];

                           for(let t = 0, tl = residueStrTmp.length / n; t < tl;  t += n) {
                             let residueId = molecule_chain + '_' + resiArray[t/n + pos];
                             if(i === 0) {
                                 residueHash[residueId] = 1;
                             }
                             else {
                                 //if(!residueHash.hasOwnProperty(residueId)) residueHash[residueId] = undefined;
                                 if(!residueHash.hasOwnProperty(residueId)) delete residueHash[residueId];
                             }

                             for(let m in ic.residues[residueId]) {
                               for(let n = 0, nl = atomStrArray.length; n < nl; ++n) {
                                   let atomStr = atomStrArray[n];
                                   if(atomStr === '*' || atomStr === ic.atoms[m].name) {
                                     if(i === 0) {
                                         //currHighlightAtoms[m] = 1;
                                         atomHash[m] = 1;
                                     }
                                     else {
                                         //if(!currHighlightAtoms.hasOwnProperty(m)) currHighlightAtoms[m] = undefined;
                                         //if(!atomHash.hasOwnProperty(m)) atomHash[m] = undefined;
                                         if(!atomHash.hasOwnProperty(m)) delete atomHash[m];
                                     }
                                   }
                               }
                             }
                           } // for
                       } // end for(s = 0
                     } // end if

                   } // end if(molecule_chain
                 } // end else
               } // end for(let mc = 0
           } // for(j
       }  // for(i

       ic.hAtoms = me.hashUtilsCls.cloneHash(atomHash);

       if(Object.keys(ic.hAtoms).length == 0) {
           console.log("No residues were selected. Please try another search.");
       }

       if(bDisplay === undefined || bDisplay) ic.hlUpdateCls.updateHlAll();

       let residueAtomArray;
       if(bSelectResidues) {
           residueAtomArray = Object.keys(residueHash);
       }
       else {
           residueAtomArray = Object.keys(atomHash);
       }

       if(commandname != "") {
           ic.selectionCls.addCustomSelection(residueAtomArray, commandname, commanddesc, select, bSelectResidues);

           let nameArray = [commandname];          
           if(!bNoUpdateAll) ic.definedSetsCls.changeCustomAtoms(nameArray);
       }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Selection {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Select all atom in the structures.
    selectAll() { let ic = this.icn3d; ic.icn3dui;
        this.selectAll_base();

        ic.hlObjectsCls.removeHlObjects();
        ic.hlUpdateCls.removeHl2D();
        ic.hlUpdateCls.removeHlMenus();

        ic.bSelectResidue = false;
        ic.bSelectAlignResidue = false;

        ic.hlUpdateCls.removeSeqResidueBkgd();
        ic.hlUpdateCls.update2DdgmContent();

        // show annotations for all protein chains
        $("#" + ic.pre + "dl_annotations > .icn3d-annotation").show();

        ic.definedSetsCls.setMode('all');

        //let title =(ic.molTitle.length > 40) ? ic.molTitle.substr(0, 40) + "..." : ic.molTitle;
        //$("#" + ic.pre + "title").html(title);
        ic.saveFileCls.showTitle();
    }

    selectAll_base() { let ic = this.icn3d, me = ic.icn3dui;
        ic.hAtoms = {};
        ic.dAtoms = {};

        for(let i in ic.chains) {
           ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.chains[i]);
           //ic.dAtoms = me.hashUtilsCls.unionHash(ic.dAtoms, ic.chains[i]);
        }

        ic.dAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
        ic.viewSelectionAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
        ic.ALTERNATE_STRUCTURE = -1;
    }

    //Select a chain with the chain id "chainid" in the sequence dialog and save it as a custom selection with the name "commandname".
    selectAChain(chainid, commandname, bAlign, bUnion) { let ic = this.icn3d, me = ic.icn3dui;
        commandname = commandname.replace(/\s/g, '');
        let command =(bAlign !== undefined || bAlign) ? 'select alignChain ' + chainid : 'select chain ' + chainid;

        //var residueHash = {}, chainHash = {}

        if(bUnion === undefined || !bUnion) {
            ic.hAtoms = {};
            ic.nameArray = [];
        }
        else {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.chains[chainid]);

            if(ic.nameArray === undefined) ic.nameArray = [];
        }

        ic.nameArray.push(chainid);

        //chainHash[chainid] = 1;

        let chnsSeq =(bAlign) ? ic.alnChainsSeq[chainid] : ic.chainsSeq[chainid];
        let chnsSeqLen;
        if(chnsSeq === undefined) chnsSeqLen = 0;
        else chnsSeqLen = chnsSeq.length;

        let oriResidueHash = {};
        for(let i = 0, il = chnsSeqLen; i < il; ++i) { // get residue number
            let resObj = chnsSeq[i];
            let residueid = chainid + "_" + resObj.resi;

            let value = resObj.name;

            if(value !== '' && value !== '-') {
              oriResidueHash[residueid] = 1;
              for(let j in ic.residues[residueid]) {
                ic.hAtoms[j] = 1;
              }
            }
        }

        if((ic.defNames2Atoms === undefined || !ic.defNames2Atoms.hasOwnProperty(commandname)) &&(ic.defNames2Residues === undefined || !ic.defNames2Residues.hasOwnProperty(commandname)) ) {
            this.addCustomSelection(Object.keys(oriResidueHash), commandname, commandname, command, true);
        }

        let bForceHighlight = true;

        if(bAlign) {
            ic.hlUpdateCls.updateHlAll(undefined, undefined, bUnion, bForceHighlight);
        }
        else {
            ic.hlUpdateCls.updateHlAll(ic.nameArray, undefined, bUnion, bForceHighlight);
        }
    }

    selectResidueList(residueHash, commandname, commanddescr, bUnion, bUpdateHighlight, bAtom) { let ic = this.icn3d; ic.icn3dui;
      if(residueHash !== undefined && Object.keys(residueHash).length > 0) {
        if(bUnion === undefined || !bUnion) {
            ic.hAtoms = {};
            ic.nameArray = [];
        }
        else {
            if(ic.nameArray === undefined) ic.nameArray = [];
        }

        if(bAtom) {
            for(let i in residueHash) {
                ic.hAtoms[i] = 1;
            }
        }
        else {
            for(let i in residueHash) {
                for(let j in ic.residues[i]) {
                  ic.hAtoms[j] = 1;
                }
            }
        }

        commandname = commandname.replace(/\s/g, '');

        ic.nameArray.push(commandname);

        let select, bSelectResidues;

        if(bAtom) {
            select = "select " + ic.resid2specCls.atoms2spec(ic.hAtoms);
            bSelectResidues = false;
        }
        else {
            select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residueHash));
            bSelectResidues = true;
        }

        let residueAtomArray = Object.keys(residueHash);

        //if((ic.defNames2Atoms === undefined || !ic.defNames2Atoms.hasOwnProperty(commandname)) &&(ic.defNames2Residues === undefined || !ic.defNames2Residues.hasOwnProperty(commandname)) ) {
            this.addCustomSelection(residueAtomArray, commandname, commanddescr, select, bSelectResidues);
        //}

        if(bUpdateHighlight === undefined || bUpdateHighlight) ic.hlUpdateCls.updateHlAll(ic.nameArray, undefined, bUnion);
      }
    }

    selectMainChains() { let ic = this.icn3d, me = ic.icn3dui;
        let currHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

        ic.hAtoms = ic.applyDisplayCls.selectMainChainSubset(currHAtoms);

        ic.hlUpdateCls.showHighlight();
    }

    //Select only the side chain atoms of the current selection.
    selectSideChains() { let ic = this.icn3d, me = ic.icn3dui;
        let currHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

        //let nuclMainArray = ["C1'", "C1*", "C2'", "C2*", "C3'", "C3*", "C4'", "C4*", "C5'", "C5*", "O3'", "O3*", "O4'", "O4*", "O5'", "O5*", "P", "OP1", "O1P", "OP2", "O2P"];

        ic.hAtoms = {};
        for(let i in currHAtoms) {
            if((ic.proteins.hasOwnProperty(i) && ic.atoms[i].name !== "N" && ic.atoms[i].name !== "H" 
              && ic.atoms[i].name !== "C" && ic.atoms[i].name !== "O"
              && !(ic.atoms[i].name === "CA" && ic.atoms[i].elem === "C") && ic.atoms[i].name !== "HA")
              ||(ic.nucleotides.hasOwnProperty(i) && me.parasCls.nuclMainArray.indexOf(ic.atoms[i].name) === -1) ) {
                ic.hAtoms[i] = 1;
            }
        }

        ic.hlUpdateCls.showHighlight();
    }

    selectMainSideChains() { let ic = this.icn3d, me = ic.icn3dui;
        let residHash = ic.firstAtomObjCls.getResiduesFromAtoms(ic.hAtoms);

        ic.hAtoms = {};
        for(let resid in residHash) {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid]);
            ic.dAtoms = me.hashUtilsCls.unionHash(ic.dAtoms, ic.residues[resid]);
        }

        ic.drawCls.draw();

        ic.hlUpdateCls.showHighlight();
    }

    clickShow_selected() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        me.myEventCls.onIds(["#" + ic.pre + "show_selected", "#" + ic.pre + "mn2_show_selected"], "click", function(e) { thisClass.icn3d;
           //me.htmlCls.clickMenuCls.setLogCmd("show selection", true);

           thisClass.showSelection();
           me.htmlCls.clickMenuCls.setLogCmd("show selection", true);
        });
    }

    clickHide_selected() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;
        me.myEventCls.onIds("#" + ic.pre + "mn2_hide_selected", "click", function(e) { thisClass.icn3d;
           thisClass.hideSelection();
           me.htmlCls.clickMenuCls.setLogCmd("hide selection", true);
        });
    }

    getGraphDataForDisplayed() { let ic = this.icn3d; ic.icn3dui;
          let graphJson = JSON.parse(ic.graphStr);

          let residHash = ic.firstAtomObjCls.getResiduesFromAtoms(ic.dAtoms);

          let nodeArray = [], linkArray = [];

          let nodeHash = {};
          for(let i = 0, il = graphJson.nodes.length; i < il; ++i) {
              let node = graphJson.nodes[i];
              let resid = node.r.substr(4); // 1_1_1KQ2_A_1

              if(residHash.hasOwnProperty(resid)) {
                  nodeArray.push(node);
                  nodeHash[node.id] = 1;
              }
          }

          for(let i = 0, il = graphJson.links.length; i < il; ++i) {
              let link = graphJson.links[i];

              if(nodeHash.hasOwnProperty(link.source) && nodeHash.hasOwnProperty(link.target)) {
                  linkArray.push(link);
              }
          }

          graphJson.nodes = nodeArray;
          graphJson.links = linkArray;

          ic.graphStr = JSON.stringify(graphJson);

          return ic.graphStr;
    }

    updateSelectionNameDesc() { let ic = this.icn3d; ic.icn3dui;
        let numDef = Object.keys(ic.defNames2Residues).length + Object.keys(ic.defNames2Atoms).length;

        $("#" + ic.pre + "seq_command_name").val("seq_" + numDef);
        //$("#" + ic.pre + "seq_command_desc").val("seq_desc_" + numDef);

        $("#" + ic.pre + "seq_command_name2").val("seq_" + numDef);
        //$("#" + ic.pre + "seq_command_desc2").val("seq_desc_" + numDef);

        $("#" + ic.pre + "alignseq_command_name").val("alseq_" + numDef);
        //$("#" + ic.pre + "alignseq_command_desc").val("alseq_desc_" + numDef);
    }

    //Define a custom selection based on the array of residues or atoms. The custom selection is defined
    //by the "command" with the name "commandname" and the description "commanddesc". If "bResidue" is true,
    //the custom selection is based on residues. Otherwise, the custom selection is based on atoms.
    addCustomSelection(residueAtomArray, commandname, commanddesc, select, bSelectResidues) { let ic = this.icn3d; ic.icn3dui;
        if(bSelectResidues) {
            ic.defNames2Residues[commandname] = residueAtomArray;
        }
        else {
            ic.defNames2Atoms[commandname] = residueAtomArray;
        }

        ic.defNames2Command[commandname] = select;
        ic.defNames2Descr[commandname] = commanddesc;

        ic.hlUpdateCls.updateHlMenus([commandname]);
    }

    //Show the selection.
    showSelection() { let ic = this.icn3d, me = ic.icn3dui;
        //ic.dAtoms = {};

        if(Object.keys(ic.hAtoms).length == 0) {
            //this.selectAll_base();
            ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);
        }

        ic.dAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
        ic.viewSelectionAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
        ic.ALTERNATE_STRUCTURE = -1;

        let centerAtomsResults = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.dAtoms, ic.atoms));
        ic.maxD = centerAtomsResults.maxD;
        if(ic.maxD < 5) ic.maxD = 5;

        //show selected rotationcenter
        ic.opts['rotationcenter'] = 'display center';

        this.saveSelectionIfSelected();

        ic.drawCls.draw();

        ic.hlUpdateCls.update2DdgmContent();
        ic.hlUpdateCls.updateHl2D();

        // show selected chains in annotation window
        ic.annotationCls.showAnnoSelectedChains();

        // update 2d graph
        if(ic.graphStr !== undefined) {
          ic.graphStr = this.getGraphDataForDisplayed();
        }

        // don not redraw graphs after the selection changes
        /*
        if(ic.bGraph) ic.drawGraphCls.drawGraph(ic.graphStr, ic.pre + 'dl_graph');
        if(ic.bLinegraph) ic.lineGraphCls.drawLineGraph(ic.graphStr);
        if(ic.bScatterplot) ic.lineGraphCls.drawLineGraph(ic.graphStr, true);
        */
    }

    hideSelection() { let ic = this.icn3d, me = ic.icn3dui;
        ic.dAtoms = me.hashUtilsCls.exclHash(ic.dAtoms, ic.hAtoms);

        ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);

        let centerAtomsResults = ic.applyCenterCls.centerAtoms(me.hashUtilsCls.hash2Atoms(ic.dAtoms, ic.atoms));
        ic.maxD = centerAtomsResults.maxD;
        if(ic.maxD < 5) ic.maxD = 5;

        //show selected rotationcenter
        ic.opts['rotationcenter'] = 'display center';

        this.saveSelectionIfSelected();

        ic.drawCls.draw();

        ic.hlUpdateCls.update2DdgmContent();
        ic.hlUpdateCls.updateHl2D();

        // show selected chains in annotation window
        ic.annotationCls.showAnnoSelectedChains();
    }

    saveSelection(name, description) { let ic = this.icn3d, me = ic.icn3dui;
        ic.selectedResidues = {};

        ic.selectedResidues = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(ic.hAtoms);

        if(Object.keys(ic.selectedResidues).length > 0) {
            if(ic.pk == 1) {
                let bAtom = true;
                this.selectResidueList(ic.hAtoms, name, description,undefined, undefined, bAtom);
                //ic.hlUpdateCls.updateHlAll();

                this.updateSelectionNameDesc();

                me.htmlCls.clickMenuCls.setLogCmd('select ' + ic.resid2specCls.atoms2spec(ic.hAtoms) + ' | name ' + name, true);
            }
            else {
                this.selectResidueList(ic.selectedResidues, name, description);
                //ic.hlUpdateCls.updateHlAll();

                this.updateSelectionNameDesc();

                me.htmlCls.clickMenuCls.setLogCmd('select ' + ic.resid2specCls.residueids2spec(Object.keys(ic.selectedResidues)) + ' | name ' + name, true);
            }
        }
    }

    saveSelInCommand() { let ic = this.icn3d, me = ic.icn3dui;
        ic.selectedResidues = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(ic.hAtoms);

        me.htmlCls.clickMenuCls.setLogCmd('select ' + ic.resid2specCls.residueids2spec(Object.keys(ic.selectedResidues)), true);
    }

    saveEachResiInSel() { let ic = this.icn3d; ic.icn3dui;
        ic.selectionCls.saveSelectionPrep();
        
        ic.selectedResidues = {};

        ic.selectedResidues = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(ic.hAtoms);

        for(let resid in ic.selectedResidues) {
            let eachResidueHash = {};
            eachResidueHash[resid] = 1;
            let name = resid + '_' + ic.selectedResidues[resid];

            this.selectResidueList(eachResidueHash, name, name);
        }
    }

    removeSelection() { let ic = this.icn3d; ic.icn3dui;
        if(!ic.bAnnotations) {
            ic.hlUpdateCls.removeSeqChainBkgd();
        }

        if(!ic.bCtrl && !ic.bShift) {
            ic.hlUpdateCls.removeSeqResidueBkgd();

            ic.hlUpdateCls.removeSeqChainBkgd();
        }

          ic.selectedResidues = {};
          ic.bSelectResidue = false;

          ic.hAtoms = {};

          ic.hlObjectsCls.removeHlObjects();

          ic.hlUpdateCls.removeHl2D();
    }

    resetAll() { let ic = this.icn3d, me = ic.icn3dui;
        ic.maxD = ic.oriMaxD;
        ic.center = ic.oriCenter.clone();

        ic.reinitAfterLoad();

        ic.loadScriptCls.renderFinalStep(1);
        ic.definedSetsCls.setMode('all');

        ic.selectionCls.selectAll();

        me.htmlCls.clickMenuCls.setLogCmd("reset", true);

        ic.hlUpdateCls.removeSeqChainBkgd();
        ic.hlUpdateCls.removeSeqResidueBkgd();
        ic.hlUpdateCls.removeHl2D();
        ic.hlUpdateCls.removeHlMenus();
    }

    async loadSelection(dataStr) { let ic = this.icn3d, me = ic.icn3dui;
      let nameCommandArray = dataStr.trim().split('\n');

      for(let i = 0, il = nameCommandArray.length; i < il; ++i) {
          //let nameCommand = nameCommandArray[i].split('\t');
          //let name = nameCommand[0];
          //let command = nameCommand[1];

          let nameCommand = nameCommandArray[i].replace(/\t/g, ' ');
          let pos1 = nameCommand.indexOf(' ');
          
          let name = nameCommand.substr(0, pos1);
          let command = nameCommand.substr(pos1 + 1);

          let pos = command.indexOf(' '); // select ...

          await ic.selByCommCls.selectByCommand(command.substr(pos + 1), name, name);

          me.htmlCls.clickMenuCls.setLogCmd('select ' + command.substr(pos + 1) + ' | name ' + name, true);
      }
    }

    oneStructurePerWindow() { let ic = this.icn3d, me = ic.icn3dui;
        // only display one of the two aligned structures

        let structureArray = (ic.structures) ? Object.keys(ic.structures) : [];
        if(me.cfg.bSidebyside && structureArray.length == 2) {
            let dividArray = Object.keys(window.icn3duiHash);
            let pos = dividArray.indexOf(ic.divid);

            let structure = structureArray[pos];
            let chainArray = ic.structures[structure];
            let structAtoms = {};
            for(let i = 0, il = chainArray.length; i < il; ++i) {
                structAtoms = me.hashUtilsCls.unionHash(structAtoms, ic.chains[chainArray[i]]);
            }

            ic.dAtoms = me.hashUtilsCls.intHash(structAtoms, ic.dAtoms);
            ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);
        }
    }

    showAll() {var ic = this.icn3d, me = ic.icn3dui;
           ic.dAtoms = me.hashUtilsCls.cloneHash(ic.atoms);
           ic.maxD = ic.oriMaxD;
           ic.drawCls.draw();
    }

    saveSelectionIfSelected(id, value) {var ic = this.icn3d; ic.icn3dui;
      if(ic.bSelectResidue || ic.bSelectAlignResidue) {
          let name = $("#" + ic.pre + "seq_command_name2").val().replace(/\s+/g, '_');
          //var description = $("#" + ic.pre + "seq_command_desc2").val();
          if(name === "") {
            name = $("#" + ic.pre + "alignseq_command_name").val().replace(/\s+/g, '_');
            //description = $("#" + ic.pre + "alignseq_command_desc").val();
          }
          if(name !== "") this.saveSelection(name, name);
          ic.bSelectResidue = false;
          ic.bSelectAlignResidue = false;
      }
    }

    saveSelectionPrep() {var ic = this.icn3d, me = ic.icn3dui;
           if(!me.cfg.notebook) {
               if(!$('#' + ic.pre + 'dl_definedsets').hasClass('ui-dialog-content') || !$('#' + ic.pre + 'dl_definedsets').dialog( 'isOpen' )) {
                 me.htmlCls.dialogCls.openDlg('dl_definedsets', 'Select sets');
                 $("#" + ic.pre + "atomsCustom").resizable();
               }
           }
           else {
               $('#' + ic.pre + 'dl_definedsets').show();
               $("#" + ic.pre + "atomsCustom").resizable();
           }
           ic.bSelectResidue = false;
           ic.bSelectAlignResidue = false;
    }
    selectOneResid(idStr, bUnchecked) {var ic = this.icn3d; ic.icn3dui;
      //var idStr = idArray[i]; // TYR $1KQ2.B:56@OH, $1KQ2.B:40 ASP
      //change to: let idStr = idArray[i]; // TYR $1KQ2.B:56@OH, or ASP $1KQ2.B:40
      let posStructure = idStr.indexOf('$');
      let posChain = idStr.indexOf('.');
      let posRes = idStr.indexOf(':');
      let posAtom = idStr.indexOf('@');
      if(posAtom == -1) posAtom = idStr.length; //idStr.indexOf(' ');
      let structure = idStr.substr(posStructure + 1, posChain - posStructure - 1);
      let chain = idStr.substr(posChain + 1, posRes - posChain - 1);
      let resi = idStr.substr(posRes + 1, posAtom - posRes - 1);
      let resid = structure + '_' + chain + '_' + resi;
      for(let j in ic.residues[resid]) {
          if(bUnchecked) {
              delete ic.hAtoms[j];
          }
          else {
              ic.hAtoms[j] = 1;
          }
      }
      if(bUnchecked) {
          delete ic.selectedResidues[resid];
      }
      else {
          ic.selectedResidues[resid] = 1;
      }
      let cmd = '$' + structure + '.' + chain + ':' + resi;
      return cmd;
    }

    //Toggle on and off the current selection.
    toggleSelection() {var ic = this.icn3d, me = ic.icn3dui;
        if(ic.bHideSelection) {
            for(let i in ic.dAtoms) {
                if(ic.hAtoms.hasOwnProperty(i)) delete ic.dAtoms[i];
            }
              ic.bHideSelection = false;
        }
        else {
            ic.dAtoms = me.hashUtilsCls.unionHash(ic.dAtoms, ic.hAtoms);
              ic.bHideSelection = true;
        }
        ic.drawCls.draw();
    }

    toggleMembrane(bShowMembrane) {var ic = this.icn3d, me = ic.icn3dui;
        let structureArray = (ic.structures) ? Object.keys(ic.structures) : [];

        for(let i = 0, il = structureArray.length; i < il; ++i) {
            let structure = structureArray[i];
            let atomsHash = ic.residues[structure + '_MEM_1'];
            let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(atomsHash);
            if(firstAtom === undefined) continue;

            let oriStyle = firstAtom.style;
            if(!ic.dAtoms.hasOwnProperty(firstAtom.serial)) {
                // add membrane to displayed atoms if the membrane is not part of the display
                ic.dAtoms = me.hashUtilsCls.unionHash(ic.dAtoms, atomsHash);
                oriStyle = 'nothing';
            }

            for(let j in atomsHash) {
                let atom = ic.atoms[j];
                if(oriStyle !== 'nothing') {
                    atom.style = 'nothing';
                }
                else {
                    atom.style = 'stick';
                }

                if(bShowMembrane !== undefined) {
                    atom.style = (bShowMembrane) ? 'stick' : 'nothing';
                }
            }
        }

        if(bShowMembrane === undefined) ic.drawCls.draw();
    }

    adjustMembrane(extra_mem_z, intra_mem_z) {var ic = this.icn3d; ic.icn3dui;
        for(let i in ic.chains[ic.inputid.toUpperCase() + '_MEM']) {
            let atom = ic.atoms[i];
            if(atom.name == 'O') {
                atom.coord.z = extra_mem_z;
            }
            else if(atom.name == 'N') {
                atom.coord.z = intra_mem_z;
            }
        }
        // reset transmembrane set
        let bReset = true;
        ic.definedSetsCls.setTransmemInMenu(extra_mem_z, intra_mem_z, bReset);
        ic.hlUpdateCls.updateHlMenus();
        ic.drawCls.draw();
    }
    selectBtwPlanes(large, small) {var ic = this.icn3d; ic.icn3dui;
        if(large < small) {
            let tmp = small;
            small = large;
            large = tmp;
        }
        let residueHash = {};
        for(let i in ic.atoms) {
            let atom = ic.atoms[i];
            if(atom.resn == 'DUM') continue;
            if(atom.coord.z >= small && atom.coord.z <= large) {
                let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
                residueHash[resid] = 1;
            }
        }
        let commandname = "z_planes_" + large + "_" + small;
        let commanddescr = commandname;
        this.selectResidueList(residueHash, commandname, commanddescr, false);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Resid2spec {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    residueids2spec(residueArray) {var ic = this.icn3d; ic.icn3dui;
         let spec = "";

         if(residueArray !== undefined){
             let residueArraySorted = residueArray.sort(function(a, b) {
                if(a !== '' && !isNaN(a)) {
                    return parseInt(a) - parseInt(b);
                }
                else {
                    let lastPosA = a.lastIndexOf('_');
                    let lastPosB = b.lastIndexOf('_');
                    if(a.substr(0, lastPosA) < b.substr(0, lastPosB)) return -1;
                    else if(a.substr(0, lastPosA) > b.substr(0, lastPosB)) return 1;
                    else if(a.substr(0, lastPosA) == b.substr(0, lastPosB)) {
                        if(parseInt(a.substr(lastPosA + 1)) < parseInt(b.substr(lastPosB + 1)) ) return -1;
                        else if(parseInt(a.substr(lastPosA + 1)) > parseInt(b.substr(lastPosB + 1)) ) return 1;
                        else if(parseInt(a.substr(lastPosA + 1)) == parseInt(b.substr(lastPosB + 1)) ) return 0;
                    }
                }
             });
             let prevChain = '', chain, prevResi = 0, resi, lastDashPos, firstDashPos, struturePart, chainPart;
             let startResi;
             let bMultipleStructures =(Object.keys(ic.structures).length == 1) ? false : true;
             for(let j = 0, jl = residueArraySorted.length; j < jl; ++j) {
                 let residueid = residueArraySorted[j];
                 lastDashPos = residueid.lastIndexOf('_');
                 chain = residueid.substr(0, lastDashPos);
                 // allow resi such as 35A
                 //resi = parseInt(residueid.substr(lastDashPos+1));
                 resi = residueid.substr(lastDashPos+1);
                 firstDashPos = prevChain.indexOf('_');
                 struturePart = prevChain.substr(0, firstDashPos);
                 chainPart = prevChain.substr(firstDashPos + 1);
                 if(prevChain !== chain) {
                     if(j > 0) {
                         if(prevResi === startResi) {
                             if(bMultipleStructures) {
                                 spec += '$' + struturePart + '.' + chainPart + ':' + startResi + ' or ';
                             }
                             else {
                                 spec += '.' + chainPart + ':' + startResi + ' or ';
                             }
                         }
                         else {
                             if(bMultipleStructures) {
                                 spec += '$' + struturePart + '.' + chainPart + ':' + startResi + '-' + prevResi + ' or ';
                             }
                             else {
                                 spec += '.' + chainPart + ':' + startResi + '-' + prevResi + ' or ';
                             }
                         }
                     }
                     startResi = resi;
                 }
                 else if(prevChain === chain) {
                     // some residue number could be "35A"
                     //let tmpPrevResi = !isNaN(prevResi) ? parseInt(prevResi) : prevResi;
                     let tmpPrevResi = ic.ParserUtilsCls.getResiNCBI(prevChain, prevResi);
                     //if(resi != parseInt(prevResi) + 1) {
                     //if(resi != tmpPrevResi + 1) {
                     if(ic.ParserUtilsCls.getResiNCBI(chain, resi) != tmpPrevResi + 1) {
                         if(prevResi === startResi) {
                             if(bMultipleStructures) {
                                 spec += '$' + struturePart + '.' + chainPart + ':' + startResi + ' or ';
                             }
                             else {
                                 spec += '.' + chainPart + ':' + startResi + ' or ';
                             }
                         }
                         else {
                             if(bMultipleStructures) {
                                 spec += '$' + struturePart + '.' + chainPart + ':' + startResi + '-' + prevResi + ' or ';
                             }
                             else {
                                 spec += '.' + chainPart + ':' + startResi + '-' + prevResi + ' or ';
                             }
                         }
                         startResi = resi;
                     }
                 }
                 prevChain = chain;
                 prevResi = resi;
             }
             // last residue
             firstDashPos = prevChain.indexOf('_');
             struturePart = prevChain.substr(0, firstDashPos);
             chainPart = prevChain.substr(firstDashPos + 1);
             if(prevResi === startResi) {
                 if(bMultipleStructures) {
                     spec += '$' + struturePart + '.' + chainPart + ':' + startResi;
                 }
                 else {
                     spec += '.' + chainPart + ':' + startResi;
                 }
             }
             else {
                 if(bMultipleStructures) {
                     spec += '$' + struturePart + '.' + chainPart + ':' + startResi + '-' + prevResi;
                 }
                 else {
                     spec += '.' + chainPart + ':' + startResi + '-' + prevResi;
                 }
             }
         }

         return spec;
    }

    atoms2spec(atomHash) {var ic = this.icn3d; ic.icn3dui;
        let spec = "";
        let i = 0;
        let structureHash = {}, chainHash = {}, resiHash = {};

        let atom;
        for(let serial in atomHash) {
            atom = ic.atoms[serial];
            if(i > 0) {
                spec += ' or ';
            }
            spec += '$' + atom.structure + '.' + atom.chain + ':' + atom.resi + '@' + atom.name;

            structureHash[atom.structure] = 1;
            chainHash[atom.structure + '_' + atom.chain] = 1;
            resiHash[atom.structure + '_' + atom.chain + '_' + atom.resi] = 1;

            ++i;
        }

        if(Object.keys(resiHash).length == 1) {
            let tmpStr = '\\$' + atom.structure + '\\.' + atom.chain + ':' + atom.resi;
            spec = spec.replace(new RegExp(tmpStr,'g'), '');
        }
        else if(Object.keys(chainHash).length == 1) {
            let tmpStr = '\\$' + atom.structure + '\\.' + atom.chain;
            spec = spec.replace(new RegExp(tmpStr,'g'), '');
        }
        else if(Object.keys(structureHash).length == 1) {
            let tmpStr = '\\$' + atom.structure;
            spec = spec.replace(new RegExp(tmpStr,'g'), '');
        }

        return spec;
    }

    atoms2residues(atomArray) {var ic = this.icn3d; ic.icn3dui;
         let atoms = {};
         for(let j = 0, jl = atomArray.length; j < jl; ++j) {
             atoms[atomArray[j]] = 1;
         }
         //var residueHash = ic.firstAtomObjCls.getResiduesFromCalphaAtoms(atoms);
         let residueHash = ic.firstAtomObjCls.getResiduesFromAtoms(atoms);
         return Object.keys(residueHash);
    }

    atoms2structureArray(atoms) {var ic = this.icn3d; ic.icn3dui;
         let structures = {};
         for(let i in atoms) {
             let atom = ic.atoms[i];
             structures[atom.structure] = 1;
         }
         return Object.keys(structures);
    }

    selectProperty(property, from, to) {var ic = this.icn3d, me = ic.icn3dui;
        let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
        if(property == 'positive') {
            let select = ':r,k,h';
            ic.hAtoms = {};
            ic.selByCommCls.selectBySpec(select, select, select);
        }
        else if(property == 'negative') {
            let select = ':d,e';
            ic.hAtoms = {};
            ic.selByCommCls.selectBySpec(select, select, select);
            // add nucleotides
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.nucleotides);
        }
        else if(property == 'hydrophobic') {
            let select = ':w,f,y,l,i,c,m';
            ic.hAtoms = {};
            ic.selByCommCls.selectBySpec(select, select, select);
            // only proteins
            ic.hAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.proteins);
        }
        else if(property == 'polar') {
            let select = ':g,v,s,t,a,n,p,q';
            ic.hAtoms = {};
            ic.selByCommCls.selectBySpec(select, select, select);
            // only proteins
            ic.hAtoms = me.hashUtilsCls.intHash(ic.hAtoms, ic.proteins);
        }
        else if(property == 'b factor') {
            let atoms = me.hashUtilsCls.cloneHash(ic.calphas);
            atoms = me.hashUtilsCls.unionHash(atoms, ic.nucleotidesO3);
            atoms = me.hashUtilsCls.unionHash(atoms, ic.chemicals);
            atoms = me.hashUtilsCls.unionHash(atoms, ic.ions);
            atoms = me.hashUtilsCls.unionHash(atoms, ic.water);
            ic.hAtoms = {};
            for(let i in atoms) {
                let atom = ic.atoms[i];
                if(atom.b >= parseInt(from) && atom.b <= parseInt(to)) {
                    ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[atom.structure + '_' + atom.chain + '_' + atom.resi]);
                }
            }
        }
        else if(property == 'percent out') {
           ic.bCalcArea = true;
           ic.opts.surface = 'solvent accessible surface';
           ic.applyMapCls.applySurfaceOptions();
           ic.bCalcArea = false;
           ic.hAtoms = {};

           for(let resid in ic.resid2area) { // resid: structure_chain_resi_resn
                let pos = resid.lastIndexOf('_');
                let resn = resid.substr(pos + 1);

                if(me.parasCls.residueArea.hasOwnProperty(resn)) {
                    let percent = parseInt(ic.resid2area[resid] / me.parasCls.residueArea[resn] * 100);
                    if(percent >= from && percent <= to) {
                        let residReal = resid.substr(0, pos);
                        ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[residReal]);
                    }
                }
           }
        }
        ic.hAtoms = me.hashUtilsCls.intHash(ic.hAtoms, prevHAtoms);
        ic.drawCls.draw();
        ic.hlUpdateCls.updateHlAll();
    }

    //Select the complement of the current selection.
    selectComplement() { let ic = this.icn3d, me = ic.icn3dui;
       let complement = {};
       for(let i in ic.atoms) {
           if(!ic.hAtoms.hasOwnProperty(i)) {
               complement[i] = 1;
           }
       }
       ic.hAtoms = me.hashUtilsCls.cloneHash(complement);
       //ic.highlightResidues(Object.keys(residueHash), Object.keys(chainHash));
       ic.hlUpdateCls.updateHlAll();
    }

    switchHighlightLevel() {var ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      let thisClass = this;

      //$(document).bind('keydown', function(e) { let ic = thisClass.icn3d;
      document.addEventListener('keydown', function(e) { let ic = thisClass.icn3d;
        if(e.keyCode === 38) { // arrow up, select upper level of atoms
          e.preventDefault();
          if(Object.keys(ic.pickedAtomList).length == 0 || !ic.hAtoms.hasOwnProperty(ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList).serial)) {
              ic.pickedAtomList = me.hashUtilsCls.cloneHash(ic.hAtoms);
              //ic.pk = 2;
          }
          thisClass.switchHighlightLevelUp();
          me.htmlCls.clickMenuCls.setLogCmd("highlight level up", true);
        }
        else if(e.keyCode === 40) { // arrow down, select down level of atoms
          e.preventDefault();
          if(Object.keys(ic.pickedAtomList).length == 0 || !ic.hAtoms.hasOwnProperty(ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList).serial)) {
              ic.pickedAtomList = me.hashUtilsCls.cloneHash(ic.hAtoms);
              //ic.pk = 2;
          }
          thisClass.switchHighlightLevelDown();
          me.htmlCls.clickMenuCls.setLogCmd("highlight level down", true);
        }
      });
    }

    //When users pick an atom, a residue, a strand/helix, a chain, or a structure, they can use upper arrow
    //to increase the highlight level by one, or use down arrow to decrease the highlight level by one. This
    //function switchHighlightLevelUp() increases the highlight level by one.
    switchHighlightLevelUp() {var ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      if(!ic.bShift && !ic.bCtrl) ic.hlObjectsCls.removeHlObjects();
      if(ic.pickedAtomList === undefined || Object.keys(ic.pickedAtomList).length === 0) {
          ic.pickedAtomList = me.hashUtilsCls.cloneHash(ic.hAtoms);
      }
      if(Object.keys(ic.pickedAtomList).length === 0) {
          ic.pickedAtomList = ic.dAtoms;
      }
      if(ic.highlightlevel === 1) { // atom -> residue
          ic.highlightlevel = 2;
          let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
          if(!ic.bShift && !ic.bCtrl) {
              ic.hAtoms = me.hashUtilsCls.cloneHash(ic.residues[firstAtom.structure + '_' + firstAtom.chain + '_' + firstAtom.resi]);
        }
        else {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[firstAtom.structure + '_' + firstAtom.chain + '_' + firstAtom.resi]);
        }
      }
      else if(ic.highlightlevel === 2) { // residue -> strand
          ic.highlightlevel = 3;
          let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
          if(!ic.bShift && !ic.bCtrl) {
              ic.hAtoms = me.hashUtilsCls.cloneHash(ic.pickingCls.selectStrandHelixFromAtom(firstAtom));
        }
        else {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.pickingCls.selectStrandHelixFromAtom(firstAtom));
        }
      }
      else if(ic.highlightlevel === 3) {
          let atomLevel4;
          if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined) { // strand -> domain
              ic.highlightlevel = 4;
              let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
              atomLevel4 = ic.pickingCls.select3ddomainFromAtom(firstAtom);
              if(!ic.bShift && !ic.bCtrl) {
                  ic.hAtoms = me.hashUtilsCls.cloneHash(atomLevel4);
              }
              else {
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, atomLevel4);
              }
          }
          if((me.cfg.mmdbid === undefined && me.cfg.gi === undefined) || Object.keys(atomLevel4).length == 0) { // strand -> chain
              ic.highlightlevel = 5;
              let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
              if(!ic.bShift && !ic.bCtrl) {
                  ic.hAtoms = me.hashUtilsCls.cloneHash(ic.chains[firstAtom.structure + '_' + firstAtom.chain]);
              }
              else {
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.chains[firstAtom.structure + '_' + firstAtom.chain]);
              }
          }
      }
      else if(ic.highlightlevel === 4) { // domain -> chain
          ic.highlightlevel = 5;
          let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
          if(!ic.bShift && !ic.bCtrl) {
              ic.hAtoms = me.hashUtilsCls.cloneHash(ic.chains[firstAtom.structure + '_' + firstAtom.chain]);
          }
          else {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.chains[firstAtom.structure + '_' + firstAtom.chain]);
          }
      }
      else if(ic.highlightlevel === 5 || ic.highlightlevel === 6) { // chain -> structure
          ic.highlightlevel = 6;
          let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
          if(!ic.bShift && !ic.bCtrl) ic.hAtoms = {};
          let chainArray = ic.structures[firstAtom.structure];
          for(let i = 0, il = chainArray.length; i < il; ++i) {
              ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.chains[chainArray[i]]);
        }
      }
      ic.hlObjectsCls.addHlObjects();
      ic.hlUpdateCls.updateHlAll();
    }

    //When users pick an atom, a residue, a strand/helix, a chain, or a structure, they can use upper
    //arrow to increase the highlight level by one, or use down arrow to decrease the highlight level
    //by one. This function switchHighlightLevelDown() decreases the highlight level by one.
    switchHighlightLevelDown() {var ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      ic.hlObjectsCls.removeHlObjects();
      if(ic.pickedAtomList === undefined || Object.keys(ic.pickedAtomList).length === 0) {
          ic.pickedAtomList = me.hashUtilsCls.cloneHash(ic.hAtoms);
      }
      if((ic.highlightlevel === 2 || ic.highlightlevel === 1) && Object.keys(ic.pickedAtomList).length === 1) { // residue -> atom
          ic.highlightlevel = 1;
          ic.hAtoms = me.hashUtilsCls.cloneHash(ic.pickedAtomList);
          if(!ic.bShift && !ic.bCtrl) {
              ic.hAtoms = me.hashUtilsCls.cloneHash(ic.pickedAtomList);
        }
        else {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.pickedAtomList);
        }
      }
      else if(ic.highlightlevel === 3) { // strand -> residue
        let residueHash = {};
        for(let i in ic.pickedAtomList) {
            residueid = ic.atoms[i].structure + '_' + ic.atoms[i].chain + '_' + ic.atoms[i].resi;
            residueHash[residueid] = 1;
        }
        if(Object.keys(residueHash).length === 1) {
            ic.highlightlevel = 2;
            let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
            if(!ic.bShift && !ic.bCtrl) {
                ic.hAtoms = me.hashUtilsCls.cloneHash(ic.residues[firstAtom.structure + '_' + firstAtom.chain + '_' + firstAtom.resi]);
            }
            else {
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[firstAtom.structure + '_' + firstAtom.chain + '_' + firstAtom.resi]);
            }
        }
      }
      else if(ic.highlightlevel === 4) { // domain -> strand
          ic.highlightlevel = 3;
          let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
          if(!ic.bShift && !ic.bCtrl) {
              ic.hAtoms = me.hashUtilsCls.cloneHash(ic.pickingCls.selectStrandHelixFromAtom(firstAtom));
          }
          else {
              ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.pickingCls.selectStrandHelixFromAtom(firstAtom));
          }
      }
      else if(ic.highlightlevel === 5) {
          let atomLevel4;
          if(me.cfg.mmdbid !== undefined || me.cfg.gi !== undefined) { // chain -> domain
              ic.highlightlevel = 4;
              let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
              atomLevel4 = ic.pickingCls.select3ddomainFromAtom(firstAtom);
              if(!ic.bShift && !ic.bCtrl) {
                  ic.hAtoms = me.hashUtilsCls.cloneHash(atomLevel4);
              }
              else {
                  ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, atomLevel4);
              }
          }
          if((me.cfg.mmdbid === undefined && me.cfg.gi === undefined) || Object.keys(atomLevel4).length == 0) { // chain -> strand
              ic.highlightlevel = 3;
              let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
              if(!ic.bShift && !ic.bCtrl) {
                  ic.hAtoms = me.hashUtilsCls.cloneHash(ic.pickingCls.selectStrandHelixFromAtom(firstAtom));
              }
              else {
                  ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.pickingCls.selectStrandHelixFromAtom(firstAtom));
              }
          }
      }
      else if(ic.highlightlevel === 6) { // structure -> chain
          ic.highlightlevel = 5;
          let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);
          if(!ic.bShift && !ic.bCtrl) {
              ic.hAtoms = me.hashUtilsCls.cloneHash(ic.chains[firstAtom.structure + '_' + firstAtom.chain]);
        }
        else {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.chains[firstAtom.structure + '_' + firstAtom.chain]);
        }
      }
      ic.hlObjectsCls.addHlObjects();
      ic.hlUpdateCls.updateHlAll();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class FirstAtomObj {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Return the first atom in the atom hash, which has the atom serial number as the key.
    getFirstAtomObj(atomsHash) { let ic = this.icn3d; ic.icn3dui;
        if(atomsHash === undefined || Object.keys(atomsHash).length === 0) {
            return undefined;
        }

        let atomKeys = Object.keys(atomsHash);
        let firstIndex = atomKeys[0];

        return ic.atoms[firstIndex];
    }

    getFirstCalphaAtomObj(atomsHash) { let ic = this.icn3d; ic.icn3dui;
        if(atomsHash === undefined || Object.keys(atomsHash).length === 0) {
            return undefined;
        }

        let firstIndex;

        for(let i in atomsHash) {
            if(ic.atoms[i].name == 'CA') {
                firstIndex = i;
                break;
            }
        }

        return (firstIndex !== undefined) ? ic.atoms[firstIndex] : this.getFirstAtomObj(atomsHash);
    }

    getFirstAtomObjByName(atomsHash, atomName) { let ic = this.icn3d; ic.icn3dui;
        if(atomsHash === undefined || Object.keys(atomsHash).length === 0) {
            return ic.atoms[0];
        }

        let firstIndex;

        for(let i in atomsHash) {
            if(ic.atoms[i].name == atomName) {
                firstIndex = i;
                break;
            }
        }

        return (firstIndex !== undefined) ? ic.atoms[firstIndex] : undefined;
    }

    //Return the last atom in the atom hash, which has the atom serial number as the key.
    getLastAtomObj(atomsHash) { let ic = this.icn3d; ic.icn3dui;
        if(atomsHash === undefined || Object.keys(atomsHash).length === 0) {
            return ic.atoms[0];
        }

        let atomKeys = Object.keys(atomsHash);
        let lastIndex = atomKeys[atomKeys.length - 1];

        return ic.atoms[lastIndex];
    }

    //Return the residue hash from the atom hash. The residue hash has the resid as the key and 1 as the value.
    getResiduesFromAtoms(atomsHash) { let ic = this.icn3d; ic.icn3dui;
        let residuesHash = {};
        for(let i in atomsHash) {
            let residueid = ic.atoms[i].structure + '_' + ic.atoms[i].chain + '_' + ic.atoms[i].resi;
            residuesHash[residueid] = 1;
        }

        return residuesHash;
    }

    getResiduesFromCalphaAtoms(atomsHash) { let ic = this.icn3d; ic.icn3dui;
        let residuesHash = {};
        for(let i in atomsHash) {
            if((ic.atoms[i].name == 'CA' && ic.proteins.hasOwnProperty(i)) || !ic.proteins.hasOwnProperty(i)) {
                let residueid = ic.atoms[i].structure + '_' + ic.atoms[i].chain + '_' + ic.atoms[i].resi;
                //residuesHash[residueid] = 1;
                residuesHash[residueid] = ic.atoms[i].resn;
            }
        }

        return residuesHash;
    }

    //Return the chain hash from the atom hash. The chain hash has the chainid as the key and 1 as the value.
    getChainsFromAtoms(atomsHash) { let ic = this.icn3d; ic.icn3dui;
        let chainsHash = {};
        for(let i in atomsHash) {
           let atom = ic.atoms[i];
           let chainid = atom.structure + "_" + atom.chain;

           chainsHash[chainid] = 1;
        }

        return chainsHash;
    }

    getAtomFromResi(resid, atomName) { let ic = this.icn3d; ic.icn3dui;
        if(ic.residues.hasOwnProperty(resid)) {
            for(let i in ic.residues[resid]) {
                if(ic.atoms[i].name === atomName && !ic.atoms[i].het) {
                    return ic.atoms[i];
                }
            }
        }

        return undefined;
    }

    getAtomCoordFromResi(resid, atomName) { let ic = this.icn3d; ic.icn3dui;
        let atom = this.getAtomFromResi(resid, atomName);
        if(atom !== undefined) {
            let coord = (atom.coord2 !== undefined) ? atom.coord2 : atom.coord;

            return coord;
        }

        return undefined;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Delphi {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    CalcPhiUrl(gsize, salt, contour, bSurface, url) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;

        let oReq = new XMLHttpRequest();
        oReq.open("GET", url, true);

        oReq.responseType = "text";

        oReq.onreadystatechange = async function() {
            if(this.readyState == 4) {
               if(this.status == 200) {
                   let data = oReq.response;

                   await thisClass.CalcPhi(gsize, salt, contour, bSurface, data);
                }
                else {
                    alert("The PQR file is unavailable...");
                }
            }
            else {
                ic.ParserUtilsCls.showLoading();
            }
        };

        oReq.send();
    }

    getPdbStr(bNode) { let ic = this.icn3d, me = ic.icn3dui;
       let ionHash = {};
       let atomHash = {};

       let atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);
       for(let i in atoms) {
           ic.atoms[i];

           if(ic.ions.hasOwnProperty(i)) {
             ionHash[i] = 1;
           }
           else {
             atomHash[i] = 1;
           }
       }

       let atomCnt = Object.keys(atomHash).length;
       let bCalphaOnly = me.utilsCls.isCalphaPhosOnly(me.hashUtilsCls.hash2Atoms(atomHash, ic.atoms));
       if(bCalphaOnly) {
           if(!bNode) {
               alert("The potential will not be shown because the side chains are missing in the structure...");
           }
           else {
               console.log("The potential will not be shown because the side chains are missing in the structure...");
           }

           return;
       }

       if(atomCnt > 30000) {
           if(!bNode) {
               alert("The maximum number of allowed atoms is 30,000. Please try it again with selected chains...");
           }
           else {
               console.log("The maximum number of allowed atoms is 30,000. Please try it again with selected chains...");
           }

           return;
       }

       let pdbstr = '';
///       pdbstr += ic.saveFileCls.getPDBHeader();

       pdbstr +=(me.cfg.cid) ? ic.saveFileCls.getAtomPDB(atomHash, true) : ic.saveFileCls.getAtomPDB(atomHash);
       pdbstr += ic.saveFileCls.getAtomPDB(ionHash, true, undefined, true);

       return pdbstr;
    }

    async CalcPhi(gsize, salt, contour, bSurface, data) { let ic = this.icn3d; ic.icn3dui;
        let phidata = await this.CalcPhiPrms(gsize, salt, contour, bSurface, data);

        this.loadPhiData(phidata, contour, bSurface);
    
        ic.bAjaxPhi = true;

        if(bSurface) {
            ic.setOptionCls.setOption('phisurface', 'phi');
        }
        else {
            ic.setOptionCls.setOption('phimap', 'phi');
        }

        /// if(ic.deferredDelphi !== undefined) ic.deferredDelphi.resolve();
        /// if(ic.deferredPhi !== undefined) ic.deferredPhi.resolve();
    }

    CalcPhiPrms(gsize, salt, contour, bSurface, data) { let ic = this.icn3d, me = ic.icn3dui;
        ic.loadPhiFrom = 'delphi';
 
        let url = me.htmlCls.baseUrl + "delphi/delphi.cgi";
        let pdbid =(me.cfg.cid) ? me.cfg.cid : Object.keys(ic.structures).toString();
        let dataObj = {};
 
        if(data) {
            dataObj = {'pqr2phi': data, 'gsize': gsize, 'salt': salt, 'pdbid': pdbid};
        }
        else {
            let pdbstr = this.getPdbStr();
 
            dataObj = {'pdb2phi': pdbstr, 'gsize': gsize, 'salt': salt, 'pdbid': pdbid};
        }

        return new Promise(function(resolve, reject) {
            // see full_ui.js for ajaxTransport
            $.ajax({
                url: url,
                type: 'POST',
                data : dataObj,
                dataType: 'binary',
                responseType: 'arraybuffer',
                cache: true,
                beforeSend: function() {
                    ic.ParserUtilsCls.showLoading();
                },
                complete: function() {
                    ic.ParserUtilsCls.hideLoading();
                },
                success: function(phidata) {
                    resolve(phidata);
                },
                error : function(xhr, textStatus, errorThrown ) {
                    return;
                }
            });
        });
    }

    PhiParser(url, type, contour, bSurface) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;
        //var dataType;

        //var bCid = undefined;

        //https://stackoverflow.com/questions/33902299/using-jquery-ajax-to-download-a-binary-file
    /*
        if(type == '2fofc' && ic.bAjax2fofc) {
            ic.mapData.contour2 = contour;
            ic.setOptionCls.setOption('map', type);
        }
        else if(type == 'fofc' && ic.bAjaxfofc) {
            ic.mapData.contour = contour;
            ic.setOptionCls.setOption('map', type);
        }
        else {
    */
            let oReq = new XMLHttpRequest();
            oReq.open("GET", url, true);

            if(type == 'phiurl' || type == 'phiurl2') {
                oReq.responseType = "arraybuffer";
            }
            else {
                oReq.responseType = "text";
            }

            oReq.onreadystatechange = function() {
                if(this.readyState == 4) {
                   if(this.status == 200) {
                       let data = oReq.response;

                       if(type == 'phiurl' || type == 'phiurl2') {
                           thisClass.loadPhiData(data, contour, bSurface);
                       }
                       else {
                           thisClass.loadCubeData(data, contour, bSurface);
                       }

                       ic.bAjaxPhi = true;

                       if(bSurface) {
                         ic.setOptionCls.setOption('phisurface', 'phi');
                       }
                       else {
                         ic.setOptionCls.setOption('phimap', 'phi');
                       }
                    }
                    else {
                        alert("The potential file is unavailable...");
                    }

                    /// if(ic.deferredPhi !== undefined) ic.deferredPhi.resolve();
                }
                else {
                    ic.ParserUtilsCls.showLoading();
                }
            };

            oReq.send();
    //    }
    }

    loadPhiData(data, contour, bSurface) { let ic = this.icn3d; ic.icn3dui;
        // http://compbio.clemson.edu/downloadDir/delphi/delphi_manual8.pdf
        // Delphi phi map is almost the same as GRASP potential map except the last line in Delphi phi map
        //   has five float values and the last value is the grid size.

        let header = {};
        header.filetype = 'phi';

        let bin =(data.buffer && data.buffer instanceof ArrayBuffer) ? data.buffer : data;
    //var byteView = new Uint8Array(bin);

        // skip 4 bytes before and after each line
        //http://compbio.clemson.edu/downloadDir/delphi/delphi_manual8.pdf
        //character*20 uplbl
        //character*10 nxtlbl,character*60 toplbl
        //real*4 phi(65,65,65)
        //character*16 botlbl
        //real*4 scale,oldmid(3)

    //var headStr = String.fromCharCode.apply(null, byteView.subarray(0, 106));
    //var uplbl = headStr.substr(4, 20); // 20 chars, 0-28, skip 4 bytes at both ends
    //var nxtlbl = headStr.substr(32, 70); // 70 chars, 28-106, skip 4 bytes at both ends

        // 16 chars, bin.byteLength-52 : bin.byteLength-28, skip 4 bytes at both ends
    //var botlbl = String.fromCharCode.apply(null, byteView.subarray(byteView.length - 48, byteView.length - 32));

        // 20 chars, bin.byteLength-28 : bin.byteLength, skip 4 bytes at both ends
        let scale_center = new Float32Array(bin.slice(bin.byteLength-24, bin.byteLength-8) ); // 4 values
        header.scale = scale_center[0];
        let cx = scale_center[1], cy = scale_center[2], cz = scale_center[3];

        // gridSize
        header.n = new Int32Array(bin.slice(bin.byteLength-8, bin.byteLength-4) ); // 1 value, skip the last 4 bytes

        header.xExtent = header.yExtent = header.zExtent = header.n;

        let step = 1.0/header.scale;
        let half_size = step *((header.n - 1) / 2);
        header.ori = new THREE.Vector3(cx - half_size, cy - half_size, cz - half_size);

        // matrix: n*n*n*4 chars, 106 : bin.byteLength-52, skip 4 bytes at both ends
        // In .phi file, correctly loop x, then y, then z
        let floatView = new Float32Array(bin.slice(110, bin.byteLength-56) ); // 4 values

        header.bSurface = bSurface;

        ic.mapData.headerPhi = header;
        ic.mapData.dataPhi = floatView;
        ic.mapData.contourPhi = contour;

        let matrix = new THREE.Matrix4();
        matrix.identity();
        matrix.multiply(new THREE.Matrix4().makeTranslation(
          header.ori.x, header.ori.y, header.ori.z
        ));
        ic.mapData.matrixPhi = matrix;
    }

    loadCubeData(data, contour, bSurface) { let ic = this.icn3d; ic.icn3dui;
        // http://compbio.clemson.edu/downloadDir/delphi/delphi_manual8.pdf
    //  2.000000   117 22.724000 42.148000  8.968000 // scale, grid size, center x, y, z
    //Gaussian cube format phimap
    //    1    -11.859921     24.846119    -37.854994
    //  117      0.944863      0.000000      0.000000
    //  117      0.000000      0.944863      0.000000
    //  117      0.000000      0.000000      0.944863
    //    1      0.000000      0.000000      0.000000      0.000000
    // -2.89368e+00 -2.91154e+00 -2.92951e+00 -2.94753e+00 -2.96562e+00 -2.98375e+00 // each section contains 117 values, loops z, then y, then x

        let header = {};
        header.filetype = 'cube';

        let lines = data.split('\n');

        let paraArray = [];

    /*
        let tmpArray = lines[0].split(/\s+/);
        for(let i = 0; i < tmpArray.length; ++i) {
            let value = parseFloat(tmpArray[i]);
            if(!isNaN(value)) paraArray.push(value);
        }
    */
        paraArray.push(parseFloat( lines[0].substr(0, 10) ) );
        paraArray.push(parseFloat( lines[0].substr(10, 6) ) );
        paraArray.push(parseFloat( lines[0].substr(16, 10) ) );
        paraArray.push(parseFloat( lines[0].substr(26, 10) ) );
        paraArray.push(parseFloat( lines[0].substr(36, 10) ) );

        header.scale = paraArray[0];
        let cx = paraArray[2], cy = paraArray[3], cz = paraArray[4];

        // gridSize
        header.n = paraArray[1];

        header.xExtent = header.yExtent = header.zExtent = header.n;

        let step = 1.0/header.scale;
        let half_size = step *((header.n - 1) / 2);
        header.ori = new THREE.Vector3(cx - half_size, cy - half_size, cz - half_size);

        let dataPhi = [];
        for(let i = 7, il = lines.length; i < il; ++i) {
            let valueArray = lines[i].split(/\s+/);
            for(let j = 0, jl = valueArray.length; j < jl; ++j) {
                let value = parseFloat(valueArray[j]);
                if(!isNaN(value)) dataPhi.push(value);
            }
        }

        if(dataPhi.length != header.n * header.n * header.n) {
            console.log("the data array size " + dataPhi.length + " didn't match the grid size " + header.n * header.n * header.n + "...");
        }

        header.bSurface = bSurface;

        ic.mapData.headerPhi = header;
        ic.mapData.dataPhi = dataPhi;
        ic.mapData.contourPhi = contour;

        let matrix = new THREE.Matrix4();
        matrix.identity();
        matrix.multiply(new THREE.Matrix4().makeTranslation(
          header.ori.x, header.ori.y, header.ori.z
        ));
        ic.mapData.matrixPhi = matrix;
    }

    async applyCommandPhi(command) { let ic = this.icn3d; ic.icn3dui;
      let thisClass = this;
      // chain functions together
    //   ic.deferredPhi = $.Deferred(function() { let ic = thisClass.icn3d;
          //me.htmlCls.clickMenuCls.setLogCmd('set phi phiurl2/cubeurl2 | contour ' + contour + ' | url ' + encodeURIComponent(url)
          //       + ' | gsize ' + gsize + ' | salt ' + salt
          //       + ' | surface ' + ic.phisurftype + ' | opacity ' + ic.phisurfop + ' | wireframe ' + ic.phisurfwf, true);
          //me.htmlCls.clickMenuCls.setLogCmd('set phi phiurl/cubeurl | contour ' + contour + ' | url ' + encodeURIComponent(url)
          //       + ' | gsize ' + gsize + ' | salt ' + salt, true);
          let paraArray = command.split(" | ");

          let typeArray = paraArray[0].split(" ");
          let contourArray = paraArray[1].split(" ");
          let urlArray = paraArray[2].split(" ");
          let gsizeArray = paraArray[3].split(" ");
          let saltArray = paraArray[4].split(" ");

          let type = typeArray[2];
          let contour = parseFloat(contourArray[1]);
          let url = urlArray[1];
          let gsize = gsizeArray[1];
          let salt = saltArray[1];

          //var pdbid = Object.keys(ic.structures)[0];
          //url = url.replace(/!/g, pdbid + '_');

          if(paraArray.length == 8) {
              let surfaceArray = paraArray[5].split(" ");
              let opacityArray = paraArray[6].split(" ");
              let wireframeArray = paraArray[7].split(" ");

              ic.phisurftype = surfaceArray[1];
              ic.phisurfop = parseFloat(opacityArray[1]);
              ic.phisurfwf = wireframeArray[1];

              $("#" + ic.pre + "delphi" + "surftype").val(ic.phisurftype);
              $("#" + ic.pre + "delphi" + "surfop").val(ic.phisurfop);
              $("#" + ic.pre + "delphi" + "surfwf").val(ic.phisurfwf);
          }

          let bSurface =(type == 'pqrurl2' || type == 'phiurl2' || type == 'cubeurl2') ? true : false;

          if(type == 'pqrurl' || type == 'pqrurl2') {
              await thisClass.CalcPhiUrl(gsize, salt, contour, bSurface, url);
          }
          else {
              thisClass.PhiParser(url, type, contour, bSurface);
          }
    //   }); // end of me.deferred = $.Deferred(function() {

    //   return ic.deferredPhi.promise();
    }

    async applyCommandDelphi(command) { let ic = this.icn3d; ic.icn3dui;
      let thisClass = this;

      // chain functions together
    //   ic.deferredDelphi = $.Deferred(function() { let ic = thisClass.icn3d;
           //me.htmlCls.clickMenuCls.setLogCmd('set delphi surface | contour ' + contour + ' | gsize ' + gsize + ' | salt ' + salt
           //  + ' | surface ' + ic.phisurftype + ' | opacity ' + ic.phisurfop + ' | wireframe ' + ic.phisurfwf, true);

           //me.htmlCls.clickMenuCls.setLogCmd('set delphi map | contour ' + contour + ' | gsize ' + gsize + ' | salt ' + salt, true);

          let paraArray = command.split(" | ");

          let typeArray = paraArray[0].split(" ");
          let contourArray = paraArray[1].split(" ");
          let gsizeArray = paraArray[2].split(" ");
          let saltArray = paraArray[3].split(" ");

          let type = typeArray[2];
          let contour = contourArray[1]; //parseFloat(contourArray[1]);
          let gsize = gsizeArray[1]; //parseInt(gsizeArray[1]);
          let salt = saltArray[1]; //parseFloat(saltArray[1]);

          // The values should be string
          $("#" + ic.pre + "delphigsize").val(gsize);
          $("#" + ic.pre + "delphisalt").val(salt);

          if(paraArray.length == 7) {
              let surfaceArray = paraArray[4].split(" ");
              let opacityArray = paraArray[5].split(" ");
              let wireframeArray = paraArray[6].split(" ");

              ic.phisurftype = surfaceArray[1];
              ic.phisurfop = opacityArray[1]; //parseFloat(opacityArray[1]);
              ic.phisurfwf = wireframeArray[1];

              $("#" + ic.pre + "delphi" + "surftype").val(ic.phisurftype);
              $("#" + ic.pre + "delphi" + "surfop").val(ic.phisurfop);
              $("#" + ic.pre + "delphi" + "surfwf").val(ic.phisurfwf);
          }

          let bSurface =(type == 'surface') ? true : false;

          await thisClass.CalcPhi(gsize, salt, contour, bSurface);
    //   }); // end of me.deferred = $.Deferred(function() {

    //   return ic.deferredDelphi.promise();
    }

    async loadDelphiFile(type) { let ic = this.icn3d, me = ic.icn3dui;
       let gsize = $("#" + ic.pre + "delphigsize").val();
       let salt = $("#" + ic.pre + "delphisalt").val();
       let contour =(type == 'delphi2') ? $("#" + ic.pre + "delphicontour2").val() : $("#" + ic.pre + "delphicontour").val();

       let bSurface =(type == 'delphi2') ? true: false;

       await this.CalcPhi(gsize, salt, contour, bSurface);

       let displayType =(type == 'delphi2') ? 'surface' : 'map';

       if(bSurface) {
           me.htmlCls.clickMenuCls.setLogCmd('set delphi ' + displayType + ' | contour ' + contour + ' | gsize ' + gsize + ' | salt ' + salt
             + ' | surface ' + ic.phisurftype + ' | opacity ' + ic.phisurfop + ' | wireframe ' + ic.phisurfwf, true);
       }
       else {
           me.htmlCls.clickMenuCls.setLogCmd('set delphi ' + displayType + ' | contour ' + contour + ' | gsize ' + gsize + ' | salt ' + salt, true);
       }
    }

    loadPhiFile(type) { let ic = this.icn3d, me = ic.icn3dui;
       let thisClass = this;

       let file;
       if(type == 'pqr' || type == 'phi' || type == 'cube') {
           file = $("#" + ic.pre + type + "file")[0].files[0];
       }
       else if(type == 'pqr2') {
           file = $("#" + ic.pre + "pqrfile2")[0].files[0];
       }
       else if(type == 'phi2') {
           file = $("#" + ic.pre + "phifile2")[0].files[0];
       }
       else if(type == 'cube2') {
           file = $("#" + ic.pre + "cubefile2")[0].files[0];
       }

       let contour =(type == 'pqr' || type == 'phi' || type == 'cube') ? $("#" + ic.pre + "phicontour").val() : $("#" + ic.pre + "phicontour2").val();
       if(!file) {
         alert("Please select a file before clicking 'Load'");
       }
       else {
         me.utilsCls.checkFileAPI();
         let reader = new FileReader();
         reader.onload = async function(e) { let ic = thisClass.icn3d;
           let data = e.target.result; // or = reader.result;

           let gsize = 0, salt = 0;
           if(type == 'pqr' || type == 'pqr2') {
             let bSurface =(type == 'pqr2') ? true: false;

             gsize = $("#" + ic.pre + type + "gsize").val();
             salt = $("#" + ic.pre + type + "salt").val();
             await thisClass.CalcPhi(gsize, salt, contour, bSurface, data);
           }
           else if(type == 'phi' || type == 'phi2') {
             let bSurface =(type == 'phi2') ? true: false;
             thisClass.loadPhiData(data, contour, bSurface);
           }
           else if(type == 'cube' || type == 'cube2') {
             let bSurface =(type == 'cube2') ? true: false;
             thisClass.loadCubeData(data, contour, bSurface);
           }

           ic.bAjaxPhi = true;

           if(bSurface) {
             ic.setOptionCls.setOption('phisurface', 'phi');
           }
           else {
             ic.setOptionCls.setOption('phimap', 'phi');
           }

           if(bSurface) {
               me.htmlCls.clickMenuCls.setLogCmd('load phi ' + type + ' | contour ' + contour + ' | file ' + $("#" + ic.pre + type + "file").val()
                 + ' | gsize ' + gsize + ' | salt ' + salt
                 + ' | surface ' + ic.phisurftype + ' | opacity ' + ic.phisurfop + ' | wireframe ' + ic.phisurfwf, false);
           }
           else {
               me.htmlCls.clickMenuCls.setLogCmd('load phi ' + type + ' | contour ' + contour + ' | file ' + $("#" + ic.pre + type + "file").val()
                 + ' | gsize ' + gsize + ' | salt ' + salt, false);
           }
         };
         if(type == 'phi' || type == 'phi2') {
             reader.readAsArrayBuffer(file);
         }
         else {
             reader.readAsText(file);
         }
       }
    }
    async loadPhiFileUrl(type) { let ic = this.icn3d, me = ic.icn3dui;
       let url;
       if(type == 'pqrurl' || type == 'phiurl' || type == 'cubeurl') {
           url = $("#" + ic.pre + type + "file").val();
       }
       else if(type == 'pqrurl2') {
           url = $("#" + ic.pre + "pqrurlfile2").val();
       }
       else if(type == 'phiurl2') {
           url = $("#" + ic.pre + "phiurlfile2").val();
       }
       else if(type == 'cubeurl2') {
           url = $("#" + ic.pre + "cubeurlfile2").val();
       }

       let contour =(type == 'pqrurl' || type == 'phiurl' || type == 'cubeurl') ? $("#" + ic.pre + "phiurlcontour").val() :  $("#" + ic.pre + "phiurlcontour2").val();
       if(!url) {
            alert("Please input the file URL before clicking 'Load'");
       }
       else {
           let bSurface =(type == 'pqrurl2' || type == 'phiurl2' || type == 'cubeurl2') ? true: false;

           let gsize = 0, salt = 0;

           if(type == 'pqrurl' || type == 'pqrurl2') {
               gsize = $("#" + ic.pre + type + "gsize").val();
               salt = $("#" + ic.pre + type + "salt").val();
               await this.CalcPhiUrl(gsize, salt, contour, bSurface, url);
           }
           else {
               this.PhiParser(url, type, contour, bSurface);
           }

           if(bSurface) {
               me.htmlCls.clickMenuCls.setLogCmd('set phi ' + type + ' | contour ' + contour + ' | url ' + encodeURIComponent(url)
                 + ' | gsize ' + gsize + ' | salt ' + salt
                 + ' | surface ' + ic.phisurftype + ' | opacity ' + ic.phisurfop + ' | wireframe ' + ic.phisurfwf, true);
           }
           else {
               me.htmlCls.clickMenuCls.setLogCmd('set phi ' + type + ' | contour ' + contour + ' | url ' + encodeURIComponent(url)
                 + ' | gsize ' + gsize + ' | salt ' + salt, true);
           }
       }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Dssp {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async applyDssp(bCalphaOnly, bAppend) { let ic = this.icn3d, me = ic.icn3dui;
      let thisClass = this;

      let calphaonly =(bCalphaOnly) ? '1' : '0';

      // make it work for concatenated multiple PDB files
      let struArray = Object.keys(ic.structures);

      let ajaxArray = [];

      let url = (window && window.location && window.location.hostname.indexOf('ncbi.nlm.nih.gov') != -1) ? "/Structure/mmcifparser/mmcifparser.cgi" :
        me.htmlCls.baseUrl + "mmcifparser/mmcifparser.cgi";
        
      for(let i = 0, il = struArray.length; i < il; ++i) {
           let pdbStr = '';

           let atomHash = {};
           let chainidArray = ic.structures[struArray[i]];

           for(let j = 0, jl = chainidArray.length; j < jl; ++j) {
             atomHash = me.hashUtilsCls.unionHash(atomHash, ic.chains[chainidArray[j]]);
           }

           pdbStr += ic.saveFileCls.getAtomPDB(atomHash, undefined, true);

           let dataObj = {'dssp':'t', 'calphaonly': calphaonly, 'pdbfile': pdbStr};
           let dssp = me.getAjaxPostPromise(url, dataObj);

           ajaxArray.push(dssp);
      }

        let allPromise = Promise.allSettled(ajaxArray);
        try {
            let dataArray = await allPromise;
            await thisClass.parseDsspData(dataArray, struArray, bAppend);
            
            await ic.ParserUtilsCls.checkMemProteinAndRotate();
        }
        catch(err) {
            console.log("DSSP calculation had a problem with this structure " + struArray[0] + "...");

            await ic.pdbParserCls.loadPdbDataRender(bAppend);

            /// if(ic.deferredOpm !== undefined) ic.deferredOpm.resolve();
            /// if(ic.deferredSecondary !== undefined) ic.deferredSecondary.resolve();
        }
    }

    async parseDsspData(dataArray, struArray, bAppend) { let ic = this.icn3d; ic.icn3dui;
        //var dataArray =(struArray.length == 1) ? [data] : data;

        // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
        //var data2 = v2[0];
        for(let index = 0, indexl = dataArray.length; index < indexl; ++index) {
            //let ssHash = dataArray[index][0];
            let ssHash = dataArray[index].value;

            if(ssHash !== undefined && JSON.stringify(ssHash).indexOf('Oops there was a problem') === -1) {
              for(let chainNum in ic.chainsSeq) {
                  let pos = chainNum.indexOf('_');

                  // one structure at a time
                  if(chainNum.substr(0, pos) != struArray[index]) continue;

                  let chain = chainNum.substr(pos + 1);

                  let residueObjectArray = ic.chainsSeq[chainNum];
                  let prevSS = 'coil', prevResi;

                  for(let i = 0, il = residueObjectArray.length; i < il; ++i) {
                    let resi = residueObjectArray[i].resi;
                    let chain_resi = chain + '_' + resi;

                    let ssOneLetter = 'c';
                    if(ssHash.hasOwnProperty(chain_resi)) {
                        ssOneLetter = ssHash[chain_resi];
                    }
                    else if(ssHash.hasOwnProperty(' _' + resi)) {
                        ssOneLetter = ssHash[' _' + resi];
                    }
                    else if(ssHash.hasOwnProperty('_' + resi)) {
                        ssOneLetter = ssHash['_' + resi];
                    }

                    let ss;
                    if(ssOneLetter === 'H') {
                        ss = 'helix';
                    }
                    else if(ssOneLetter === 'E') {
                        ss = 'sheet';
                    }
                    else {
                        ss = 'coil';
                    }

                    // update ss in sequence window
                    //ic.chainsAn[chainNum][1][i] = ssOneLetter;

                    // assign atom ss, ssbegin, and ssend
                    let resid = chainNum + '_' + resi;

                    ic.secondaries[resid] = ssOneLetter;

                    // no residue can be both ssbegin and ssend in DSSP calculated secondary structures
                    let bSetPrevResidue = 0; // 0: no need to reset, 1: reset previous residue to "ssbegin = true", 2: reset previous residue to "ssend = true"

                    let ssbegin, ssend;
                    if(ss !== prevSS) {
                        if(prevSS === 'coil') {
                            ssbegin = true;
                            ssend = false;
                        }
                        else if(ss === 'coil') {
                            bSetPrevResidue = 2;
                            ssbegin = false;
                            ssend = false;
                        }
                        else if((prevSS === 'sheet' && ss === 'helix') ||(prevSS === 'helix' && ss === 'sheet')) {
                            //bSetPrevResidue = 1;
                            bSetPrevResidue = 2;
                            ssbegin = true;
                            ssend = false;
                        }
                    }
                    else {
                            ssbegin = false;
                            ssend = false;
                    }

                    if(bSetPrevResidue == 1) { //1: reset previous residue to "ssbegin = true"
                        let prevResid = chainNum + '_' + prevResi; //(resi - 1).toString();
                        for(let j in ic.residues[prevResid]) {
                            ic.atoms[j].ssbegin = true;
                            ic.atoms[j].ssend = false;
                        }
                    }
                    else if(bSetPrevResidue == 2) { //2: reset previous residue to "ssend = true"
                        let prevResid = chainNum + '_' + prevResi; //(resi - 1).toString();
                        for(let j in ic.residues[prevResid]) {
                            ic.atoms[j].ssbegin = false;
                            ic.atoms[j].ssend = true;
                        }
                    }

                    // set the current residue
                    for(let j in ic.residues[resid]) {
                        ic.atoms[j].ss = ss;
                        ic.atoms[j].ssbegin = ssbegin;
                        ic.atoms[j].ssend = ssend;
                    }

                    prevSS = ss;
                    prevResi = resi;
                  } // for each residue
              } // for each chain
            } // if no error
            else {
                console.log("DSSP calculation had a problem with this structure " + struArray[index] + "...");
            }
        }

        await ic.pdbParserCls.loadPdbDataRender(bAppend);

        ///// if(ic.deferredMmdbaf !== undefined) ic.deferredMmdbaf.resolve();
        /// if(ic.deferredSecondary !== undefined) ic.deferredSecondary.resolve();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

 class Refnum {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async hideIgRefNum() { let ic = this.icn3d; ic.icn3dui;
        ic.bShowRefnum = false;

        ic.hAtoms = {};
        ic.bResetAnno = true;
        await ic.showAnnoCls.showAnnotations();
        ic.hlUpdateCls.updateHlAll();
    }
 
    async showIgRefNum() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        if(Object.keys(ic.resid2refnum).length > 0) {
            ic.bShowRefnum = true;

            // open sequence view
            ic.hAtomsRefnum = {};
            await ic.showAnnoCls.showAnnotations();
            ic.annotationCls.setAnnoViewAndDisplay('detailed view');
        }
        else {
            ic.refpdbArray = ['1bqu_fn3', '1cd8_igv', '1t6v_vnar', '1wio_c2', '1wio_igv', '2atp_a', '2atp_b', '2dm3_iset', '5esv_vh', '5esv_vl', '6al5_cd19', '7bz5_cl1', '7bz5_vh', '7bz5_vl'];

            if(ic.pdbDataArray) {
                await thisClass.parseRefPdbData(ic.pdbDataArray);
            }
            else {

                let pdbAjaxArray = [];
                for(let k = 0, kl = ic.refpdbArray.length; k < kl; ++k) {
                    //let urlpdb = me.htmlCls.baseUrl + "icn3d/refpdb/" + ic.refpdbArray[k] + ".pdb";
                    let urlpdb = me.htmlCls.baseUrl + "mmcifparser/mmcifparser.cgi?refpdbid=" + ic.refpdbArray[k];

                    let pdbAjax = me.getAjaxPromise(urlpdb, 'text');

                    pdbAjaxArray.push(pdbAjax);
                }

                let allPromise = Promise.allSettled(pdbAjaxArray);
                try {
                    ic.pdbDataArray = await allPromise;
                    await thisClass.parseRefPdbData(ic.pdbDataArray);
                }
                catch(err) {
                    alert("Error in retrieveing reference PDB data...");
                    return;
                }                
            }
        }
    }

    async parseRefPdbData(dataArray) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        let struArray = Object.keys(ic.structures);
 
        let ajaxArray = [];
        let domainidpairArray = [];

        me.htmlCls.baseUrl + "tmalign/tmalign.cgi";

        ic.resid2domainid = {};

        for(let i = 0, il = struArray.length; i < il; ++i) {
            let struct = struArray[i];
            let chainidArray = ic.structures[struct];
 
            for(let j = 0, jl = chainidArray.length; j < jl; ++j) {
                let chainid = chainidArray[j];
                if(!ic.proteins.hasOwnProperty(ic.firstAtomObjCls.getFirstAtomObj(ic.chains[chainid]).serial)) continue;
                if(ic.chainsSeq[chainid].length < 50) continue; // peptide

                // align each 3D domain with reference structure
                let result = ic.domain3dCls.c2b_NewSplitChain(ic.chains[chainid]);
                let subdomains = result.subdomains;
                
                let domainAtomsArray = [];
                if(subdomains.length <= 1) {
                    domainAtomsArray.push(ic.chains[chainid]);

                    for(let n = 0, nl = ic.chainsSeq[chainid].length; n < nl; ++n) {
                        let resid = chainid + '_' + ic.chainsSeq[chainid][n].resi;
                        ic.resid2domainid[resid] = chainid + '-0';
                    }             
                }
                else {
                    for(let k = 0, kl = subdomains.length; k < kl; ++k) {
                        let domainAtoms = {};
                        let segArray = subdomains[k];

                        for(let m = 0, ml = segArray.length; m < ml; m += 2) {
                            let startResi = segArray[m];
                            let endResi = segArray[m+1];
                            for(let n = parseInt(startResi); n <= parseInt(endResi); ++n) {
                                let resid = chainid + '_' + n;
                                domainAtoms = me.hashUtilsCls.unionHash(domainAtoms, ic.residues[resid]);
                                ic.resid2domainid[resid] = chainid + '-' + k;
                            }
                        }

                        domainAtomsArray.push(domainAtoms);
                    }
                }
                
                for(let k = 0, kl = domainAtomsArray.length; k < kl; ++k) {
                    let pdb_target = ic.saveFileCls.getAtomPDB(domainAtomsArray[k], undefined, undefined, undefined, undefined, struct);
                    let domainid = chainid + '-' + k;
                    for(let index = 0, indexl = dataArray.length; index < indexl; ++index) {
                        let struct2 = "stru" + index;
                        let pdb_query = dataArray[index].value; //[0];

                        let header = 'HEADER                                                        ' + struct2 + '\n';
                        pdb_query = header + pdb_query;

                        let dataObj = {'pdb_query': pdb_query, 'pdb_target': pdb_target, "queryid": ic.refpdbArray[index]};
                        let alignAjax = me.getAjaxPostPromise(url, dataObj);

                        ajaxArray.push(alignAjax);
                        
                        domainidpairArray.push(domainid + "," + index);
                    }
                }
            }
       }

        let allPromise = Promise.allSettled(ajaxArray);
        try {
            let dataArray = await allPromise;
            await thisClass.parseAlignData(dataArray, domainidpairArray);

            /// if(ic.deferredRefnum !== undefined) ic.deferredRefnum.resolve();
        }
        catch(err) {
            console.log("Error in aligning with TM-align...");
            return;
        }         
    }

    async parseAlignData(dataArray, domainidpairArray) { let ic = this.icn3d; ic.icn3dui;
        let thisClass = this;

        let tmscoreThreshold = 0.4; //0.5;

        // find the best alignment for each chain
        let domainid2score = {}, domainid2segs = {}, chainid2segs = {};
        ic.chainid2index = {};
        ic.domainid2ig2kabat = {};
        for(let i = 0, il = domainidpairArray.length; i < il; ++i) {
            let queryData = dataArray[i].value; //[0];
            if(queryData.length == 0) continue;
            if(queryData[0].score < tmscoreThreshold || queryData[0].num_res < 50) continue;

            let domainid_index = domainidpairArray[i].split(',');
            let domainid = domainid_index[0];
            let chainid = domainid.split('-')[0];

            // Ig-like domains: B (2050, 2050a, 2050b), C (3050), E (5050), F (6050) strands
            // Ig domain may require G (7050). But we'll leave that out for now.
            let bBstrand = false, bCstrand = false, bEstrand = false, bFstrand = false;
            for(let i = 0, il = queryData[0].segs.length; i < il; ++i) {
                let seg = queryData[0].segs[i];
                if(seg.q_start.indexOf('2050') != -1) {
                    bBstrand = true;
                }
                else if(seg.q_start.indexOf('3050') != -1) {
                    bCstrand = true;
                }
                else if(seg.q_start.indexOf('5050') != -1) {
                    bEstrand = true;
                }
                else if(seg.q_start.indexOf('6050') != -1) {
                    bFstrand = true;
                }
                // else if(seg.q_start.indexOf('7050') != -1) {
                //     bGstrand = true;
                // }

                //if(bBstrand && bCstrand && bEstrand && bFstrand && bGstrand) break;
                if(bBstrand && bCstrand && bEstrand && bFstrand) break;
            }
            //if(!(bBstrand && bCstrand && bEstrand && bFstrand && bGstrand)) continue;
            if(!(bBstrand && bCstrand && bEstrand && bFstrand)) continue;

            if(!domainid2score.hasOwnProperty(domainid) || queryData[0].score > domainid2score[domainid]) {
                domainid2score[domainid] = queryData[0].score;
console.log(domainid + ' TM-score: ' + domainid2score[domainid] + ' matched ' + ic.refpdbArray[domainid_index[1]]);                
                ic.chainid2index[chainid] = domainid_index[1]; // could be several, just take the recent one for simplicity
                domainid2segs[domainid] = queryData[0].segs;
                ic.domainid2ig2kabat[domainid] = queryData[0].ig2kabat;
            }
        }
        
        // combine domainid into chainid
        for(let domainid in domainid2segs) {
            let chainid = domainid.split('-')[0];
            if(!chainid2segs[chainid]) chainid2segs[chainid] = [];
            chainid2segs[chainid] = chainid2segs[chainid].concat(domainid2segs[domainid]);
        }
        
        // assign ic.resid2refnum, ic.refnum2residArray, ic.chainsMapping
        if(!ic.resid2refnum) ic.resid2refnum = {};
        if(!ic.refnum2residArray) ic.refnum2residArray = {};
        if(!ic.chainsMapping) ic.chainsMapping = {};
        for(let chainid in chainid2segs) {
            let segArray = chainid2segs[chainid];
console.log("One of the reference PDBs for chain chainid: " + ic.refpdbArray[ic.chainid2index[chainid]]);

            for(let i = 0, il = segArray.length; i < il; ++i) {
                let seg = segArray[i];
                let qStartInt = parseInt(seg.q_start);
                let postfix = '';
                if(isNaN(seg.q_start)) postfix = seg.q_start.substr(seg.q_start.length - 1, 1);

                for(let j = 0; j <= parseInt(seg.t_end) - parseInt(seg.t_start); ++j) {
                    let resid = chainid + '_' + (j + parseInt(seg.t_start)).toString();
                    //let refnum = j + seg.q_start;
                    let refnum = (j + qStartInt).toString() + postfix;

                    let refnumLabel = thisClass.getLabelFromRefnum(refnum);

                    ic.resid2refnum[resid] = refnumLabel;

                    if(!ic.refnum2residArray.hasOwnProperty(refnum)) {
                        ic.refnum2residArray[refnum] = [resid];
                    }
                    else {
                        ic.refnum2residArray[refnum].push(resid);
                    }

                    if(!ic.chainsMapping.hasOwnProperty(chainid)) {
                        ic.chainsMapping[chainid] = {};
                    }
                    ic.chainsMapping[chainid][resid] = refnumLabel;
                }
            }
        }

        if(Object.keys(ic.resid2refnum).length > 0) {
            ic.bShowRefnum = true;

            // open sequence view
            ic.hAtomsRefnum = {};
            await ic.showAnnoCls.showAnnotations();
            ic.annotationCls.setAnnoViewAndDisplay('detailed view');
        }
        else {
            alert("No Ig reference numbers are assigned based on the reference structures in iCn3D...");
        }
    }

    getLabelFromRefnum(oriRefnum) { let ic = this.icn3d; ic.icn3dui;
        let refnum = parseInt(oriRefnum);

        if(refnum >= 1000 && refnum < 1200) return "A" + oriRefnum;
        else if(refnum >= 1200 && refnum < 2000) return "A'" + oriRefnum;
        else if(refnum >= 2000 && refnum < 3000) return "B" + oriRefnum;
        else if(refnum >= 3000 && refnum < 3200) return "C" + oriRefnum;
        else if(refnum >= 3200 && refnum < 3700) return "C'" + oriRefnum;
        else if(refnum >= 3700 && refnum < 4000) return "C''" + oriRefnum;
        else if(refnum >= 4000 && refnum < 5000) return "D" + oriRefnum;
        else if(refnum >= 5000 && refnum < 6000) return "E" + oriRefnum;
        else if(refnum >= 6000 && refnum < 7000) return "F" + oriRefnum;
        else if(refnum >= 7000 && refnum < 7200) return "G" + oriRefnum;
        else if(refnum >= 7200 && refnum < 8000) return "G'" + oriRefnum;
    }

    async parseCustomRefFile(data) { let ic = this.icn3d; ic.icn3dui;
        ic.bShowCustomRefnum = true;

        //refnum,11,12,,21,22
        //1TUP_A,100,101,,,132
        //1TUP_B,110,111,,141,142

        let lineArray = data.split('\n');

        if(!ic.resid2refnum) ic.resid2refnum = {};
        if(!ic.refnum2residArray) ic.refnum2residArray = {};
        if(!ic.chainsMapping) ic.chainsMapping = {};

        let refAA = [];
        for(let i = 0, il = lineArray.length; i < il; ++i) {
            let numArray = lineArray[i].split(',');
            refAA.push(numArray);
        }

        // assign ic.refnum2residArray
        let refI = 0;
        for(let j = 1, jl = refAA[refI].length; j < jl; ++j) {
            if(!refAA[refI][j]) continue;

            let refnum = refAA[refI][j].trim();
            if(refnum) {
                for(let i = 1, il = refAA.length; i < il; ++i) {
                    if(!refAA[i][j]) continue;
                    let chainid = refAA[i][0].trim();
                    let resid = chainid + '_' + refAA[i][j].trim();
                    if(!ic.refnum2residArray[refnum]) {
                        ic.refnum2residArray[refnum] = [resid];
                    }
                    else {
                        ic.refnum2residArray[refnum].push(resid);
                    }
                }
            }
        }

        // assign ic.resid2refnum and ic.chainsMapping
        for(let i = 1, il = refAA.length; i < il; ++i) {
            let chainid = refAA[i][0].trim();

            for(let j = 1, jl = refAA[i].length; j < jl; ++j) {
                if(!refAA[i][j] || !refAA[refI][j]) continue;

                let resi = refAA[i][j].trim();
                let refnum = refAA[refI][j].trim();
                if(resi && refnum) {
                    let resid = chainid + '_' + resi;
                    ic.resid2refnum[resid] = refnum;

                    if(!ic.chainsMapping.hasOwnProperty(chainid)) {
                        ic.chainsMapping[chainid] = {};
                    }
                    ic.chainsMapping[chainid][resid] = refnum;
                }
            }
        }

        // open sequence view
        await ic.showAnnoCls.showAnnotations();
        ic.annotationCls.setAnnoViewAndDisplay('detailed view');
    }
 }

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Scap {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async applyCommandScap(command) { let ic = this.icn3d; ic.icn3dui;
        let snp = command.substr(command.lastIndexOf(' ') + 1);

        if(command.indexOf('scap 3d') == 0) {
          await this.retrieveScap(snp);
        }
        else if(command.indexOf('scap interaction') == 0) {
          await this.retrieveScap(snp, true);
        }
        else if(command.indexOf('scap pdb') == 0) {
          await this.retrieveScap(snp, undefined, true);
        }
    }

    adjust2DWidth(id) { let ic = this.icn3d, me = ic.icn3dui;
        let halfWidth = 125;
        id = ic.pre + id;

        let height =($("#" + ic.pre + 'dl_selectannotations').hasClass("ui-dialog-content")) ? $("#" + ic.pre + 'dl_selectannotations').dialog( "option", "height") : me.htmlCls.HEIGHT;
        let width =($("#" + ic.pre + 'dl_selectannotations').hasClass("ui-dialog-content")) ? halfWidth * 2 : me.htmlCls.WIDTH * 0.5;

        $("#" + id).dialog( "option", "width", width );
        $("#" + id).dialog( "option", "height", height);
        let position = { my: "left-" + halfWidth + " top+" + me.htmlCls.MENU_HEIGHT, at: "right top", of: "#" + ic.pre + "viewer", collision: "none" };

         $("#" + id).dialog( "option", "position", position );
    }

    async retrieveScap(snp, bInteraction, bPdb) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        //snp: 6M0J_E_484_K,6M0J_E_501_Y,6M0J_E_417_N
        let snpStr = '';
        let snpArray = snp.split(','); //stru_chain_resi_snp
        let atomHash = {}, snpResidArray = [], chainResi2pdb = {};
        for(let i = 0, il = snpArray.length; i < il; ++i) {
            let idArray = snpArray[i].split('_'); //stru_chain_resi_snp

            let resid = idArray[0] + '_' + idArray[1] + '_' + idArray[2];
            atomHash = me.hashUtilsCls.unionHash(atomHash, ic.residues[resid]);
            snpResidArray.push(resid);
            chainResi2pdb[idArray[1] + '_' + idArray[2]] = '';

            snpStr += idArray[1] + '_' + idArray[2] + '_' + idArray[3];
            if(i != il -1) snpStr += ',';
        }

        let selectSpec = ic.resid2specCls.residueids2spec(snpResidArray);
        let select = "select " + selectSpec;

        let bGetPairs = false;
        let radius = 10; //4;
        // find neighboring residues
        let result = ic.showInterCls.pickCustomSphere_base(radius, atomHash, ic.atoms, false, false, undefined, select, bGetPairs);


        let residArray = Object.keys(result.residues);
        ic.hAtoms = {};
        for(let index = 0, indexl = residArray.length; index < indexl; ++index) {
          let residueid = residArray[index];
          for(let i in ic.residues[residueid]) {
            ic.hAtoms[i] = 1;
          }
        }

    //    ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid]);
        ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, atomHash);
        ic.hAtoms = me.hashUtilsCls.exclHash(ic.hAtoms, ic.chemicals);

        // the displayed atoms are for each SNP only
        //var atomHash = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);

///        let pdbStr = ic.saveFileCls.getPDBHeader() + ic.saveFileCls.getAtomPDB(ic.hAtoms);
        let pdbStr = ic.saveFileCls.getAtomPDB(ic.hAtoms);

        let url = me.htmlCls.baseUrl + "scap/scap.cgi";

        let pdbid = Object.keys(ic.structures)[0]; //Object.keys(ic.structures).toString();
        let dataObj = {'pdb': pdbStr, 'snp': snpStr, 'pdbid': pdbid, 'v': '2'};

        let data;
         
        try {
          data = await me.getAjaxPostPromise(url, dataObj, true, undefined, undefined, undefined, 'text');

          let pos = data.indexOf('\n');
          let energy = data.substr(0, pos);
          let pdbData = data.substr(pos + 1);
console.log("free energy: " + energy + " kcal/mol");

          let bAddition = true;
          let hAtom1 = me.hashUtilsCls.cloneHash(ic.hAtoms);

          // the wild type is the reference
          for(let serial in hAtom1) {
              let atom = ic.atoms[serial];
              let chainid = atom.structure + '_' + atom.chain;
              let resid = chainid + '_' + atom.resi;

              if(!ic.chainsMapping.hasOwnProperty(chainid)) {
                ic.chainsMapping[chainid] = {};
              }
              ic.chainsMapping[chainid][resid] = me.utilsCls.residueName2Abbr(atom.resn) + atom.resi;
          }

          //ic.hAtoms = {};
          //ic.loadPDBCls.loadPDB(pdbData, pdbid, false, false, bAddition);
          //let hAtom2 = me.hashUtilsCls.cloneHash(ic.hAtoms);

          // get the mutant pdb
          let lines = pdbData.split('\n');
          let allChainResiHash = {};
          for (let i in lines) {
              let line = lines[i];
              let record = line.substr(0, 6);
              
              if (record === 'ATOM  ' || record === 'HETATM') {
                  let chain = line.substr(20, 2).trim();
                  if(chain === '') chain = 'A';
  
                  let resi = line.substr(22, 5).trim();
                  let chainResi = chain + '_' + resi;
                  
                  if(chainResi2pdb.hasOwnProperty(chainResi)) {
                      chainResi2pdb[chainResi] += line + '\n';
                  }  

                  allChainResiHash[chainResi] = 1;
              }
          }

          // get the full mutatnt PDB
          let pdbDataMutant = ic.saveFileCls.getAtomPDB(ic.atoms, false, false, false, chainResi2pdb);
          ic.hAtoms = {};
          let bMutation = true;
          ic.loadPDBCls.loadPDB(pdbDataMutant, pdbid, false, false, bMutation, bAddition);
          //let allAtoms2 = me.hashUtilsCls.cloneHash(ic.hAtoms);
          
          ic.setStyleCls.setAtomStyleByOptions(ic.opts);
          ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

          // get the mutant residues in the sphere
          let hAtom2 = {};
          for(let serial in ic.hAtoms) {
            let atom = ic.atoms[serial];
            let chainResi = atom.chain + '_' + atom.resi;
            if(allChainResiHash.hasOwnProperty(chainResi)) {
              hAtom2[serial] = 1;
            }
          }

          ic.hAtoms = me.hashUtilsCls.unionHash(hAtom1, hAtom2);
          //ic.hAtoms = me.hashUtilsCls.unionHash(hAtom1, allAtoms2);
          ic.dAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
          //ic.dAtoms = ic.hAtoms;

          ic.transformCls.zoominSelection();
          ic.setOptionCls.setStyle('proteins', 'stick');

          //ic.opts['color'] = 'chain';
          //ic.setColorCls.setColorByOptions(ic.opts, ic.dAtoms);
          for(let serial in hAtom2) {
          //for(let serial in allAtoms2) {
              let atom = ic.atoms[serial];

              if(!atom.het) {
                  // use the same color as the wild type
                  let resid = atom.structure.substr(0, atom.structure.length - 1) + '_' + atom.chain + '_' + atom.resi;

                  let atomWT = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]);

                  ic.atoms[serial].color = atomWT.color;
                  ic.atomPrevColors[serial] = atomWT.color;
              }

              let chainid = atom.structure + '_' + atom.chain;
              let resid = chainid + '_' + atom.resi;
              let residWT = atom.structure.substr(0, atom.structure.length - 1) + '_' + atom.chain + '_' + atom.resi;

              if(!ic.chainsMapping.hasOwnProperty(chainid)) {
                ic.chainsMapping[chainid] = {};
              }
              ic.chainsMapping[chainid][resid] = me.utilsCls.residueName2Abbr(atom.resn) + atom.resi;
              // use the wild type as reference

              if(snpResidArray.indexOf(residWT) != -1) {
                  let atomWT = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[residWT]);
                  ic.chainsMapping[chainid][resid] = me.utilsCls.residueName2Abbr(atomWT.resn) + atomWT.resi;
              }
          }

          if(bPdb) {
              //let pdbStr = '';
              //pdbStr += ic.saveFileCls.getAtomPDB(ic.hAtoms);

              let file_pref =(ic.inputid) ? ic.inputid : "custom";
              ic.saveFileCls.saveFile(file_pref + '_' + snpStr + '.pdb', 'text', [pdbDataMutant]);

              ic.drawCls.draw();
          }
          else {
              //var select = '.' + idArray[1] + ':' + idArray[2];
              //var name = 'snp_' + idArray[1] + '_' + idArray[2];
              let select = selectSpec;

              let name = 'snp_' + snpStr;
              await ic.selByCommCls.selectByCommand(select, name, name);
              ic.opts['color'] = 'atom';
              ic.setColorCls.setColorByOptions(ic.opts, ic.hAtoms);

              ic.viewInterPairsCls.clearInteractions();

              if(bInteraction) {
                //me.htmlCls.clickMenuCls.setLogCmd("select " + select + " | name " + name, true);

                let type = 'linegraph';
                await ic.viewInterPairsCls.viewInteractionPairs(['selected'], ['non-selected'], false, type, true, true, true, true, true, true);
                //me.htmlCls.clickMenuCls.setLogCmd("line graph interaction pairs | selected non-selected | hbonds,salt bridge,interactions,halogen,pi-cation,pi-stacking | false | threshold 3.8 6 4 3.8 6 5.5", true);

                thisClass.adjust2DWidth('dl_linegraph');
              }

              ic.hAtoms = ic.dAtoms;
              //me.htmlCls.clickMenuCls.setLogCmd("select displayed set", true);

              ic.drawCls.draw();

              if(!me.alertAlt) {
                me.alertAlt = true;

                //if(ic.bRender) alert('Please press the letter "a" to alternate between wild type and mutant.');
                alert('Please press the letter "a" to alternate between wild type and mutant.');
              }
          }

          $("#" + ic.pre + "mn2_alternateWrap").show();
          // expand the toolbar
          let id = ic.pre + 'selection';
          $("#" + id).show();
        }
        catch(err) {
            alert("There are some problems in predicting the side chain of the mutant...");

            ic.ParserUtilsCls.hideLoading();

            /// if(ic.deferredScap !== undefined) ic.deferredScap.resolve();
            return;
        }    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Symd {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async applyCommandSymd(command) { let ic = this.icn3d; ic.icn3dui;
        await this.retrieveSymd();
    }

    async retrieveSymd() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        //var url = "https://data.rcsb.org/rest/v1/core/assembly/" + pdbid + "/1";
        let url = me.htmlCls.baseUrl + "symd/symd.cgi";

        let atomHash = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);

        // just output C-alpha atoms
        // the number of residues matters
        //   atomHash = me.hashUtilsCls.intHash(atomHash, ic.calphas);
        // just output proteins
        atomHash = me.hashUtilsCls.intHash(atomHash, ic.proteins);

        let atomCnt = Object.keys(atomHash).length;

        let residHash = {};
        for(let serial in atomHash) {
            let atom = ic.atoms[serial];
            let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;
            residHash[resid] = 1;
        }

        // the cgi took too long for structures with more than 10000 atoms
        if(atomCnt > 10000) {
            alert("The maximum number of allowed atoms is 10,000. Please try it again with smaller sets...");
            return;
        }

        let pdbstr = '';
        pdbstr += ic.saveFileCls.getAtomPDB(atomHash);

        let dataObj = {'pdb': pdbstr, 'pdbid': Object.keys(ic.structures).toString()};
        let data;
        try {
            data = await me.getAjaxPostPromise(url, dataObj, true);

            let symmetryArray = data.rcsb_struct_symmetry;
            let rot, centerFrom, centerTo;

            let title = 'none';

            if(symmetryArray !== undefined) {
                if(ic.rmsd_supr !== undefined && ic.rmsd_supr.rot !== undefined) {
                    rot = ic.rmsd_supr.rot;
                    centerFrom = ic.rmsd_supr.trans1;
                    centerTo = ic.rmsd_supr.trans2;
                }

                //ic.symdHash = {}
                if(ic.symdArray === undefined) ic.symdArray = [];
                let order;
                for(let i = 0, il = symmetryArray.length; i < il; ++i) {
                    if(symmetryArray[i].symbol == 'C1') continue;
                    title = symmetryArray[i].symbol + " ";
                    if(symmetryArray[i].kind == "Pseudo Symmetry") {
                        title = symmetryArray[i].symbol + ' (pseudo)';
                    }
                    else if(symmetryArray[i].kind == "Global Symmetry") {
                        title = symmetryArray[i].symbol + ' (global)';
                    }
                    else if(symmetryArray[i].kind == "Local Symmetry") {
                        title = symmetryArray[i].symbol + ' (local)';
                    }

                    let rotation_axes = symmetryArray[i].rotation_axes;
                    let axesArray = [];
                    for(let j = 0, jl = rotation_axes.length; j < jl; ++j) {
                        let tmpArray = [];
                        let start = new THREE.Vector3(rotation_axes[j].start[0], rotation_axes[j].start[1], rotation_axes[j].start[2]);
                        let end = new THREE.Vector3(rotation_axes[j].end[0], rotation_axes[j].end[1], rotation_axes[j].end[2]);

                        order = rotation_axes[j].order;

                        // apply matrix for each atom
                        //if(ic.rmsd_supr !== undefined && ic.rmsd_supr.rot !== undefined) {
                        //    start = ic.surfaceCls.transformMemPro(start, rot, centerFrom, centerTo);
                        //    end = ic.surfaceCls.transformMemPro(end, rot, centerFrom, centerTo);
                        //}

                        tmpArray.push(start);
                        tmpArray.push(end);

                        // https://www.rcsb.org/pages/help/viewers/jmol_symmetry_view
                        let colorAxis = thisClass.getAxisColor(symmetryArray[i].symbol, rotation_axes[j].order);
                        let colorPolygon = thisClass.getPolygonColor(symmetryArray[i].symbol);
                        tmpArray.push(colorAxis);
                        tmpArray.push(colorPolygon);

                        tmpArray.push(rotation_axes[j].order);

                        // selected chain
                        tmpArray.push('selection');

                        axesArray.push(tmpArray);
                    }
                    let symdHash = {};
                    symdHash[title] = axesArray;
                    ic.symdArray.push(symdHash);
                }

                if(ic.symdArray.length == 0) {
                    $("#" + ic.pre + "dl_symd").html("<br>The selected residues have no detected symmetry with a Z score of " + data.zscore + " from the program <a href='https://symd.nci.nih.gov/' target='_blank'>SymD</a>.");
                    me.htmlCls.dialogCls.openDlg('dl_symd', 'Dynamically Calculated Symmetry Using SymD');
                }
                else {
                    let ori_permSeq = data.seqalign.replace(/ /g, '').split(','); //oriSeq,permSeq
                    let nres = data.nres;
                    let shift = data.shift;
                    let rmsd = data.rmsd;

                    let oriResidArray = Object.keys(residHash);
                    let residArrayHash1 = {}, residArrayHash2 = {};
                    let residArray1 = [], residArray2 = [];
                    let index1 = 0, index2 = 0;
                    let chainCntHash = {};
                    for(let i = 0, il = ori_permSeq[0].length; i < il; ++i) {
                        let resn1 = ori_permSeq[0][i];
                        let resn2 = ori_permSeq[1][i];

                        if(resn1 != '-') {
                            if(resn1 == resn1.toUpperCase()) { // aligned
                                residArrayHash1[oriResidArray[index1]] = 1;

                                let idArray1 = me.utilsCls.getIdArray(oriResidArray[index1]);
                                residArray1.push(resn1 + ' $' + idArray1[0] + '.' + idArray1[1] + ':' + idArray1[2]);

                                let chainid = idArray1[0] + '_' + idArray1[1];
                                if(!chainCntHash.hasOwnProperty(chainid)) {
                                    chainCntHash[chainid] = [];
                                }

                                chainCntHash[chainid].push(residArray1.length - 1); // the position in the array
                            }
                            ++index1;
                        }

                        if(resn2 != '-') {
                            if(resn2 == resn2.toUpperCase()) { // aligned
                                let oriIndex =(index2 + shift + nres) % nres;
                                residArrayHash2[oriResidArray[oriIndex]] = 1;

                                let idArray2 = me.utilsCls.getIdArray(oriResidArray[oriIndex]);
                                residArray2.push(resn2 + ' $' + idArray2[0] + '.' + idArray2[1] + ':' + idArray2[2]);
                            }
                            ++index2;
                        }
                    }

                    let residArrayHashFinal1 = {}, residArrayHashFinal2 = {};
                    let residArrayFinal1 = [], residArrayFinal2 = [];

                    let bOnechain = false;
                    if(Object.keys(chainCntHash).length == 1) {
                        bOnechain = true;
                        let nResUnit = parseInt(residArray1.length / order + 0.5);
                        let residArrayFromHash1 = Object.keys(residArrayHash1), residArrayFromHash2 = Object.keys(residArrayHash2);
                        for(let i = 0; i < nResUnit; ++i) {
                        if(!residArrayHashFinal1.hasOwnProperty(residArrayFromHash2[i])) { // do not appear in both original and permuted
                            residArrayFinal1.push(residArray1[i]);
                            residArrayFinal2.push(residArray2[i]);
                            residArrayHashFinal1[residArrayFromHash1[i]] = 1;
                            residArrayHashFinal2[residArrayFromHash2[i]] = 1;
                        }
                        }
                    }
                    else {
                        let selChainid, selCnt = 0;
                        for(let chainid in chainCntHash) {
                            if(chainCntHash[chainid].length > selCnt) {
                                selCnt = chainCntHash[chainid].length;
                                selChainid = chainid;
                            }
                        }

                        let residArrayFromHash1 = Object.keys(residArrayHash1), residArrayFromHash2 = Object.keys(residArrayHash2);
                        for(let i = 0, il = chainCntHash[selChainid].length; i < il; ++i) {
                        let pos = chainCntHash[selChainid][i];
                        if(!residArrayHashFinal1.hasOwnProperty(residArrayFromHash2[pos])) { // do not appear in both original and permuted
                            residArrayFinal1.push(residArray1[pos]);
                            residArrayFinal2.push(residArray2[pos]);
                            residArrayHashFinal1[residArrayFromHash1[pos]] = 1;
                            residArrayHashFinal2[residArrayFromHash2[pos]] = 1;
                        }
                        }
                    }

                    let html = '<br>';
                    html += "The symmetry " + symmetryArray[0].symbol + " was calculated dynamically using the program <a href='https://symd.nci.nih.gov/' target='_blank'>SymD</a>. The Z score " + data.zscore + " is greater than the threshold Z score 8. The RMSD is " + rmsd + " angstrom. <br><br>The following sequence alignment shows the residue mapping of the best aligned sets: \"symOri\" and \"symPerm\", which are also available in the menu \"Analysis > Defined Sets\".<br>";

                    $("#" + ic.pre + "symd_info").html(html);

                    thisClass.setSeqAlignForSymmetry(residArrayFinal1, residArrayFinal2, bOnechain);

                    let bShowHighlight = false;
                    let seqObj = me.htmlCls.alignSeqCls.getAlignSequencesAnnotations(Object.keys(ic.alnChains), undefined, undefined, bShowHighlight, bOnechain);

                    html = $("#" + ic.pre + "dl_sequence2").html() + seqObj.sequencesHtml;

                    $("#" + ic.pre + "dl_sequence2").html(html);
                    $("#" + ic.pre + "dl_sequence2").width(me.htmlCls.RESIDUE_WIDTH * seqObj.maxSeqCnt + 200);

                    me.htmlCls.dialogCls.openDlg('dl_alignment', 'Select residues in aligned sequences from SymD');

                    let numDef = Object.keys(ic.defNames2Residues).length + Object.keys(ic.defNames2Atoms).length;

                    let name = 'symOri' + numDef;
                    ic.selectionCls.selectResidueList(residArrayHashFinal1, name, name);
                    ic.selectionCls.updateSelectionNameDesc();
                    me.htmlCls.clickMenuCls.setLogCmd('select ' + ic.resid2specCls.residueids2spec(Object.keys(residArrayHashFinal1)) + ' | name ' + name, false);

                    name = 'symPerm' + numDef;
                    ic.selectionCls.selectResidueList(residArrayHashFinal2, name, name);
                    ic.selectionCls.updateSelectionNameDesc();
                    me.htmlCls.clickMenuCls.setLogCmd('select ' + ic.resid2specCls.residueids2spec(Object.keys(residArrayHashFinal2)) + ' | name ' + name, false);

                    name = 'symBoth' + numDef;
                    residArrayHashFinal1 = me.hashUtilsCls.unionHash(residArrayHashFinal1, residArrayHashFinal2);
                    ic.selectionCls.selectResidueList(residArrayHashFinal1, name, name);
                    ic.selectionCls.updateSelectionNameDesc();
                    me.htmlCls.clickMenuCls.setLogCmd('select ' + ic.resid2specCls.residueids2spec(Object.keys(residArrayHashFinal1)) + ' | name ' + name, false);

                    //ic.hlUpdateCls.toggleHighlight();
                }
            }
            else {
                $("#" + ic.pre + "dl_symd").html("<br>The selected residues have no detected symmetry with a Z score of " + data.zscore + " from the program <a href='https://symd.nci.nih.gov/' target='_blank'>SymD</a>.");
                me.htmlCls.dialogCls.openDlg('dl_symd', 'Dynamically Calculated Symmetry Using SymD');
            }

            //var title = $("#" + ic.pre + "selectSymd" ).val();
            ic.symdtitle =(title === 'none') ? undefined : title;
            ic.drawCls.draw();

            /// if(ic.deferredSymd !== undefined) ic.deferredSymd.resolve();
        }
        catch(err) {
        $("#" + ic.pre + "dl_symd").html("<br>The web service can not determine the symmetry of the input set.");

        me.htmlCls.dialogCls.openDlg('dl_symd', 'Dynamically Calculated Symmetry Using SymD');

        ic.ParserUtilsCls.hideLoading();

        /// if(ic.deferredSymd !== undefined) ic.deferredSymd.resolve();
        return;
        }
    }

    getResObj(resn_resid) { let ic = this.icn3d; ic.icn3dui;
        // K $1KQ2.A:2

        let resn = resn_resid.substr(0, resn_resid.indexOf(' '));
        let pos1 = resn_resid.indexOf('$');
        let pos2 = resn_resid.indexOf('.');
        let pos3 = resn_resid.indexOf(':');

        let structure = resn_resid.substr(pos1 + 1, pos2 - pos1 - 1);
        let chain = resn_resid.substr(pos2 + 1, pos3 - pos2 - 1);
        let resi = resn_resid.substr(pos3 + 1);
        let resid = structure + '_' + chain + '_' + resi;

        let resObject = {'resn': resn, 'resid': resid, 'resi': resi, 'aligned': true};

        return resObject;
    }

    setSeqAlignForSymmetry(residArray1, residArray2, bOnechain) { let ic = this.icn3d, me = ic.icn3dui;
          //var structureArray = Object.keys(ic.structures);
          //var structure1 = structureArray[0];
          //var structure2 = structureArray[1];

          ic.conservedName1 = 'symOri_cons'; //structure1 + '_cons';
          ic.conservedName2 = 'symPerm_cons'; //structure2 + '_cons';

          ic.consHash1 = {};
          ic.consHash2 = {};

          ic.alnChainsAnTtl = {};
          ic.alnChainsAnno = {};

          ic.alnChainsSeq = {};
          ic.alnChains = {};

          ic.alnChainsSeq = {};

          let residuesHash = {};

          for(let i = 0, il = residArray1.length; i < il; ++i) { // K $1KQ2.A:2
              let resObject1 = this.getResObj(residArray1[i]);
              let resObject2 = this.getResObj(residArray2[i]);

              let chainid1 = resObject1.resid.substr(0, resObject1.resid.lastIndexOf('_'));
              let chainid2Ori = resObject2.resid.substr(0, resObject2.resid.lastIndexOf('_'));
              let chainid2 = chainid2Ori;
              // if one chain, separate it into two chains to show seq alignment
              if(bOnechain) {
                  let stucture = chainid2Ori.substr(0, chainid2Ori.indexOf('_'));
                  chainid2 = stucture + '2' + chainid2Ori.substr(chainid2Ori.indexOf('_'));
              }

              residuesHash[resObject1.resid] = 1;
              residuesHash[resObject2.resid] = 1;

              let color;
              if(resObject1.resn == resObject2.resn) {
                  color = "#FF0000";
              }
              else {
                  color = "#0000FF";
              }
              let color2 = '#' + ic.showAnnoCls.getColorhexFromBlosum62(resObject1.resn, resObject2.resn);

              resObject1.color = color;
              resObject2.color = color;

              resObject1.color2 = color2;
              resObject2.color2 = color2;

              for(let j in ic.residues[resObject1.resid]) {
                  ic.atoms[j].color = me.parasCls.thr(color);
                  ic.atomPrevColors[j] = me.parasCls.thr(color);
              }
              for(let j in ic.residues[resObject2.resid]) {
                  ic.atoms[j].color = me.parasCls.thr(color);
                  ic.atomPrevColors[j] = me.parasCls.thr(color);
              }

              // annoation title for the master seq only
              if(ic.alnChainsAnTtl[chainid1] === undefined ) ic.alnChainsAnTtl[chainid1] = [];

              for(let j = 0; j < 3; ++j) {
                  if(ic.alnChainsAnTtl[chainid1][j] === undefined ) ic.alnChainsAnTtl[chainid1][j] = [];
              }

              // two annotations without titles
              for(let j = 0; j < 3; ++j) {
                  ic.alnChainsAnTtl[chainid1][j].push("");
              }

              if(ic.alnChainsSeq[chainid1] === undefined) ic.alnChainsSeq[chainid1] = [];
              if(ic.alnChainsSeq[chainid2] === undefined) ic.alnChainsSeq[chainid2] = [];

              ic.alnChainsSeq[chainid1].push(resObject1);
              ic.alnChainsSeq[chainid2].push(resObject2);

              if(ic.alnChains[chainid1] === undefined) ic.alnChains[chainid1] = {};
              if(ic.alnChains[chainid2] === undefined) ic.alnChains[chainid2] = {};
              $.extend(ic.alnChains[chainid1], ic.residues[chainid1 + '_' + resObject1.resi] );
              $.extend(ic.alnChains[chainid2], ic.residues[chainid2 + '_' + resObject2.resi] );

              ic.consHash1[chainid1 + '_' + resObject1.resi] = 1;
              ic.consHash2[chainid2 + '_' + resObject2.resi] = 1;

              // annotation is for the master seq only
              if(ic.alnChainsAnno[chainid1] === undefined ) ic.alnChainsAnno[chainid1] = [];
              //if(ic.alnChainsAnno[chainid2] === undefined ) ic.alnChainsAnno[chainid2] = [];

              for(let j = 0; j < 3; ++j) {
                  if(ic.alnChainsAnno[chainid1][j] === undefined ) ic.alnChainsAnno[chainid1][j] = [];
              }

              let symbol = '.';
              if(i % 5 === 0) symbol = '*';
              if(i % 10 === 0) symbol = '|';
              ic.alnChainsAnno[chainid1][0].push(symbol); // symbol: | for 10th, * for 5th, . for rest

              let numberStr = '';
              if(i % 10 === 0) numberStr = i.toString();
              ic.alnChainsAnno[chainid1][1].push(numberStr); // symbol: 10, 20, etc, empty for rest
          }

    /*
            let commandname = 'symBoth_aligned'; //'protein_aligned';
            let commanddescr = 'symBoth aligned'; //'protein aligned';
            let select = "select " + ic.resid2specCls.residueids2spec(Object.keys(residuesHash));

            ic.selectionCls.addCustomSelection(Object.keys(residuesHash), commandname, commanddescr, select, true);
    */
    }

    async retrieveSymmetry(pdbid) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass =this;
        
        let data;
        let url = "https://data.rcsb.org/rest/v1/core/assembly/" + pdbid + "/1";
        try {
            data = await me.getAjaxPromise(url, 'json', false);
        }
        catch(err) {
            $("#" + ic.pre + "dl_symmetry").html("<br>This structure has no symmetry.");

            me.htmlCls.dialogCls.openDlg('dl_symmetry', 'Symmetry');

            /// if(ic.deferredSymmetry !== undefined) ic.deferredSymmetry.resolve();
            return;
        }

        let symmetryArray = data.rcsb_struct_symmetry;
        let rot, centerFrom, centerTo;

        if(symmetryArray !== undefined) {
            if(ic.rmsd_supr !== undefined && ic.rmsd_supr.rot !== undefined) {
                rot = ic.rmsd_supr.rot;
                centerFrom = ic.rmsd_supr.trans1;
                centerTo = ic.rmsd_supr.trans2;
            }

            ic.symmetryHash = {};
            for(let i = 0, il = symmetryArray.length; i < il; ++i) {
                if(symmetryArray[i].symbol == 'C1') continue;
                let title = 'no title';
                if(symmetryArray[i].kind == "Pseudo Symmetry") {
                    title = symmetryArray[i].symbol + ' (pseudo)';
                }
                else if(symmetryArray[i].kind == "Global Symmetry") {
                    title = symmetryArray[i].symbol + ' (global)';
                }
                else if(symmetryArray[i].kind == "Local Symmetry") {
                    title = symmetryArray[i].symbol + ' (local)';
                }

                let rotation_axes = symmetryArray[i].rotation_axes;
                let axesArray = [];
                for(let j = 0, jl = rotation_axes.length; j < jl; ++j) {
                    let tmpArray = [];
                    let start = new THREE.Vector3(rotation_axes[j].start[0], rotation_axes[j].start[1], rotation_axes[j].start[2]);
                    let end = new THREE.Vector3(rotation_axes[j].end[0], rotation_axes[j].end[1], rotation_axes[j].end[2]);

                    // apply matrix for each atom
                    if(ic.rmsd_supr !== undefined && ic.rmsd_supr.rot !== undefined) {
                        start = ic.surfaceCls.transformMemPro(start, rot, centerFrom, centerTo);
                        end = ic.surfaceCls.transformMemPro(end, rot, centerFrom, centerTo);
                    }

                    tmpArray.push(start);
                    tmpArray.push(end);

                    // https://www.rcsb.org/pages/help/viewers/jmol_symmetry_view
                    let colorAxis = thisClass.getAxisColor(symmetryArray[i].symbol, rotation_axes[j].order);
                    let colorPolygon = thisClass.getPolygonColor(symmetryArray[i].symbol);
                    tmpArray.push(colorAxis);
                    tmpArray.push(colorPolygon);

                    tmpArray.push(rotation_axes[j].order);

                    // selected chain
                    tmpArray.push(symmetryArray[i].clusters[0].members[0].asym_id);

                    axesArray.push(tmpArray);
                }

                ic.symmetryHash[title] = axesArray;
            }

            if(Object.keys(ic.symmetryHash).length == 0) {
                $("#" + ic.pre + "dl_symmetry").html("<br>This structure has no symmetry.");
            }
            else {
                let html = "<option value='none'>None</option>", index = 0;
                for(let title in ic.symmetryHash) {
                    let selected =(index == 0) ? 'selected' : '';
                    html += "<option value=" + "'" + title + "' " + selected + ">" + title + "</option>";
                    ++index;
                }

                $("#" + ic.pre + "selectSymmetry").html(html);
            }
        }
        else {
            $("#" + ic.pre + "dl_symmetry").html("<br>This structure has no symmetry.");
        }

        me.htmlCls.dialogCls.openDlg('dl_symmetry', 'Symmetry');

        /// if(ic.deferredSymmetry !== undefined) ic.deferredSymmetry.resolve();
    }

    getPolygonColor(symbol) { let ic = this.icn3d, me = ic.icn3dui;
        let type = symbol.substr(0, 1);

        //https://www.rcsb.org/pages/help/viewers/jmol_symmetry_view
        if(type == 'C') { // Cyclic Cn
            return me.parasCls.thr(0xFF8C00); // dark orange
        }
        else if(type == 'D') { // Dihedral Dn
            return me.parasCls.thr(0x00FFFF); // cyan
        }
        else if(type == 'T') { // Tetrahedral T
            return me.parasCls.thr(0xEE82EE); //0x800080); // purple
        }
        else if(type == 'O') { // Octahedral O
            return me.parasCls.thr(0xFFA500); // orange
        }
        else if(type == 'I') { // Icosahedral I
            return me.parasCls.thr(0x00FF00); // green
        }
        else { // Helical H, etc
            return me.parasCls.thr(0xA9A9A9); // dark grey
        }
    }

    getAxisColor(symbol, order) { let ic = this.icn3d, me = ic.icn3dui;
        let type = symbol.substr(0, 1);

        //https://www.rcsb.org/pages/help/viewers/jmol_symmetry_view
        if(type == 'C') { // Cyclic Cn
            return me.parasCls.thr(0xFF0000); // red
        }
        else if(type == 'D') { // Dihedral Dn
            if(order == 2) {
                return me.parasCls.thr(0x00FFFF); // cyan
            }
            else {
                return me.parasCls.thr(0xFF0000); // red
            }
        }
        else if(type == 'T') { // Tetrahedral T
            if(order == 2) {
                return me.parasCls.thr(0x00FFFF); // cyan
            }
            else {
                return me.parasCls.thr(0x00FF00); // green
            }
        }
        else if(type == 'O') { // Octahedral O
            if(order == 2) {
                return me.parasCls.thr(0x00FFFF); // cyan
            }
            else if(order == 3) {
                return me.parasCls.thr(0x00FF00); // green
            }
            else {
                return me.parasCls.thr(0xFF0000); // red
            }
        }
        else if(type == 'I') { // Icosahedral I
            if(order == 2) {
                return me.parasCls.thr(0x00FFFF); // cyan
            }
            else if(order == 3) {
                return me.parasCls.thr(0x00FF00); // green
            }
            else {
                return me.parasCls.thr(0xFF0000); // red
            }
        }
        else { // Helical H, etc
            return me.parasCls.thr(0xFF0000); // red
        }
    }
}

/**
 * @author Jack Lin, modified from https://github.com/lh3/bioseq-js/blob/master/bioseq.js
 */

class AlignSW {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    alignSW(target, query, match_score, mismatch, gap, extension, is_local) { let ic = this.icn3d; ic.icn3dui;
        //let time_start = new Date().getTime();

        let rst = this.bsa_align(is_local, target, query, [match_score, mismatch], [gap, extension]);
        let str = 'score: ' + rst[0] + '\n';
        str += 'start: ' + rst[1] + '\n';
        str += 'cigar: ' + this.bsa_cigar2str(rst[2]) + '\n\n';
        str += 'alignment:\n\n';
        let fmt = this.bsa_cigar2gaps(target, query, rst[1], rst[2]);

        let algn = {};
        algn.score = rst[0];
        algn.start = rst[1];
        algn.cigar = this.bsa_cigar2str(rst[2]);
        algn.target = fmt[0];
        algn.query = fmt[1];

        return algn;
    }

    /**
     * Encode a sequence string with table
     *
     * @param seq    sequence
     * @param table  encoding table; must be of size 256
     *
     * @return an integer array
     */

    bsg_enc_seq(seq, table) { let ic = this.icn3d; ic.icn3dui;
        if (table == null) return null;
        let s = [];
        s.length = seq.length;
        for (let i = 0; i < seq.length; ++i)
            s[i] = table[seq.charCodeAt(i)];
        return s;
    }

    /**************************
     *** Pairwise alignment ***
        **************************/

    /*
        * The following implements local and global pairwise alignment with affine gap
        * penalties. There are two formulations: the Durbin formulation as is
        * described in his book and the Green formulation as is implemented in phrap.
        * The Durbin formulation is easier to understand, while the Green formulation
        * is simpler to code and probably faster in practice.
        *
        * The Durbin formulation is:
        *
        *   M(i,j) = max{M(i-1,j-1)+S(i,j), E(i-1,j-1), F(i-1,j-1)}
        *   E(i,j) = max{M(i-1,j)-q-r, F(i-1,j)-q-r, E(i-1,j)-r}
        *   F(i,j) = max{M(i,j-1)-q-r, F(i,j-1)-r, E(i,j-1)-q-r}
        *
        * where q is the gap open penalty, r the gap extension penalty and S(i,j) is
        * the score between the i-th residue in the row sequence and the j-th residue
        * in the column sequence. Note that the original Durbin formulation disallows
        * transitions between between E and F states, but we allow them here.
        *
        * In the Green formulation, we introduce:
        *
        *   H(i,j) = max{M(i,j), E(i,j), F(i,j)}
        *
        * The recursion becomes:
        *
        *   H(i,j) = max{H(i-1,j-1)+S(i,j), E(i,j), F(i,j)}
        *   E(i,j) = max{H(i-1,j)-q, E(i-1,j)} - r
        *   F(i,j) = max{H(i,j-1)-q, F(i,j-1)} - r
        *
        * It is in fact equivalent to the Durbin formulation. In implementation, we
        * calculate the scores in a different order:
        *
        *   H(i,j)   = max{H(i-1,j-1)+S(i,j), E(i,j), F(i,j)}
        *   E(i+1,j) = max{H(i,j)-q, E(i,j)} - r
        *   F(i,j+1) = max{H(i,j)-q, F(i,j)} - r
        *
        * i.e. at cell (i,j), we compute E for the next row and F for the next column.
        * Please see inline comments below for details.
        *
        *
        * The following implementation is ported from klib/ksw.c. The original C
        * implementation has a few bugs which have been fixed here. Like the C
        * version, this implementation should be very efficient. It could be made more
        * efficient if we use typed integer arrays such as Uint8Array. In addition,
        * I mixed the local and global alignments together. For performance,
        * it would be preferred to separate them out.
        */

    /**
     * Generate scoring matrix from match/mismatch score
     *
     * @param n     size of the alphabet
     * @param a     match score, positive
     * @param b     mismatch score, negative
     *
     * @return sqaure scoring matrix. The last row and column are zero, for
     * matching an ambiguous residue.
     */
    bsa_gen_score_matrix(n, a, b) { let ic = this.icn3d; ic.icn3dui;
        let m = [];
        if (b > 0) b = -b; // mismatch score b should be non-positive
        let i, j;
        for (i = 0; i < n - 1; ++i) {
            m[i] = [];
            for (j = 0; j < n - 1; ++j)
                m[i][j] = i == j ? a : b;
            m[i][j] = 0;
        }
        m[n - 1] = [];
        for (let j = 0; j < n; ++j) m[n - 1][j] = 0;
        return m;
    }

    /**
     * Generate query profile (a preprocessing step)
     *
     * @param _s      sequence in string or post bsg_enc_seq()
     * @param _m      score matrix or [match,mismatch] array
     * @param table   encoding table; must be consistent with _s and _m
     *
     * @return query profile. It is a two-dimensional integer matrix.
     */
    bsa_gen_query_profile(_s, _m, table) { let ic = this.icn3d; ic.icn3dui;
        let s = typeof _s == 'string' ? this.bsg_enc_seq(_s, table) : _s;
        let qp = [],
            matrix;
        if (_m.length >= 2 && typeof _m[0] == 'number' && typeof _m[1] == 'number') { // match/mismatch score
            if (table == null) return null;
            let n = typeof table == 'number' ? table : table[table.length - 1] + 1;
            matrix = this.bsa_gen_score_matrix(n, _m[0], _m[1]);
        } else matrix = _m; // _m is already a matrix; FIXME: check if it is really a square matrix!
        for (let j = 0; j < matrix.length; ++j) {
            let qpj, mj = matrix[j];
            qpj = qp[j] = [];
            for (let i = 0; i < s.length; ++i)
                qpj[i] = mj[s[i]];
        }
        return qp;
    }

    /**
     * Local or global pairwise alignemnt
     *
     * @param is_local  perform local alignment
     * @param target    target string
     * @param query     query string or query profile
     * @param matrix    square score matrix or [match,mismatch] array
     * @param gapsc     [gap_open,gap_ext] array; k-length gap costs gap_open+gap_ext*k
     * @param w         bandwidth, disabled by default
     * @param table     encoding table. It defaults to bst_nt5.
     *
     * @return [score,target_start,cigar]. cigar is encoded in the BAM way, where
     * higher 28 bits keeps the length and lower 4 bits the operation in order of
     * "MIDNSH". See bsa_cigar2str() for converting cigar to string.
     */
    bsa_align(is_local, target, query, matrix, gapsc, w, table) { let ic = this.icn3d; ic.icn3dui;
        let bst_nt5 = [
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 0, 4, 1, 4, 4, 4, 2, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 0, 4, 1, 4, 4, 4, 2, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,

            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
        ];

        // convert bases to integers
        if (table == null) table = bst_nt5;
        let t = this.bsg_enc_seq(target, table);
        let qp = this.bsa_gen_query_profile(query, matrix, table);
        let qlen = qp[0].length;

        // adjust band width
        let max_len = qlen > t.length ? qlen : t.length;
        w = w == null || w < 0 ? max_len : w;
        let len_diff = t.target > qlen ? t.target - qlen : qlen - t.target;
        w = w > len_diff ? w : len_diff;

        // set gap score
        let gapo, gape; // these are penalties which should be non-negative
        if (typeof gapsc == 'number') gapo = 0, gape = gapsc > 0 ? gapsc : -gapsc;
        else gapo = gapsc[0] > 0 ? gapsc[0] : -gapsc[0], gape = gapsc[1] > 0 ? gapsc[1] : -gapsc[1];
        let gapoe = gapo + gape; // penalty for opening the first gap

        // initial values
        let NEG_INF = -0x40000000;
        let H = [],
            E = [],
            z = [],
            score, max = 0,
            end_i = -1,
            end_j = -1;
        if (is_local) {
            for (let j = 0; j <= qlen; ++j) H[j] = E[j] = 0;
        } else {
            H[0] = 0;
            E[0] = -gapoe - gapoe;
            for (let j = 1; j <= qlen; ++j) {
                if (j >= w) H[j] = E[j] = NEG_INF; // everything is -inf outside the band
                else H[j] = -(gapoe + gape * (j - 1)), E[j] = -(gapoe + gapoe + gape * j);
            }
        }

        // the DP loop
        for (let i = 0; i < t.length; ++i) {
            let h1 = 0,
                f = 0,
                m = 0,
                mj = -1;
            let zi, qpi = qp[t[i]];
            zi = z[i] = [];
            let beg = i > w ? i - w : 0;
            let end = i + w + 1 < qlen ? i + w + 1 : qlen; // only loop through [beg,end) of the query sequence
            if (!is_local) {
                h1 = beg > 0 ? NEG_INF : -(gapoe + gape * i);
                f = beg > 0 ? NEG_INF : -(gapoe + gapoe + gape * i);
            }
            for (let j = beg; j < end; ++j) {
                // At the beginning of the loop: h=H[j]=H(i-1,j-1), e=E[j]=E(i,j), f=F(i,j) and h1=H(i,j-1)
                // If we only want to compute the max score, delete all lines involving direction "d".
                let e = E[j],
                    h = H[j],
                    d;
                H[j] = h1; // set H(i,j-1) for the next row
                h += qpi[j]; // h = H(i-1,j-1) + S(i,j)
                d = h >= e ? 0 : 1;
                h = h >= e ? h : e;
                d = h >= f ? d : 2;
                h = h >= f ? h : f; // h = H(i,j) = max{H(i-1,j-1)+S(i,j), E(i,j), F(i,j)}
                d = !is_local || h > 0 ? d : 1 << 6;
                h1 = h; // save H(i,j) to h1 for the next column
                mj = m > h ? mj : j;
                m = m > h ? m : h; // update the max score in this row
                h -= gapoe;
                h = !is_local || h > 0 ? h : 0;
                e -= gape;
                d |= e > h ? 1 << 2 : 0;
                e = e > h ? e : h; // e = E(i+1,j)
                E[j] = e; // save E(i+1,j) for the next row
                f -= gape;
                d |= f > h ? 2 << 4 : 0;
                f = f > h ? f : h; // f = F(i,j+1)
                zi[j] = d; // z[i,j] keeps h for the current cell and e/f for the next cell
            }
            H[end] = h1, E[end] = is_local ? 0 : NEG_INF;
            if (m > max) max = m, end_i = i, end_j = mj;
        }
        if (is_local && max == 0) return null;
        score = is_local ? max : H[qlen];

        let cigar = [],
            tmp, which = 0,
            i, k, start_i = 0;
        if (is_local) {
            i = end_i, k = end_j;
            if (end_j != qlen - 1) // then add soft cliping
                this.push_cigar(cigar, 4, qlen - 1 - end_j);
        } else i = t.length - 1, k = (i + w + 1 < qlen ? i + w + 1 : qlen) - 1; // (i,k) points to the last cell
        while (i >= 0 && k >= 0) {
            tmp = z[i][k - (i > w ? i - w : 0)];
            which = tmp >> (which << 1) & 3;
            if (which == 0 && tmp >> 6) break;
            if (which == 0) which = tmp & 3;
            if (which == 0) { this.push_cigar(cigar, 0, 1);--i, --k; } // match
            else if (which == 1) { this.push_cigar(cigar, 2, 1);--i; } // deletion
            else { this.push_cigar(cigar, 1, 1), --k; } // insertion
        }
        if (is_local) {
            if (k >= 0) this.push_cigar(cigar, 4, k + 1); // add soft clipping
            start_i = i + 1;
        } else { // add the first insertion or deletion
            if (i >= 0) this.push_cigar(cigar, 2, i + 1);
            if (k >= 0) this.push_cigar(cigar, 1, k + 1);
        }
        for (let i = 0; i < cigar.length >> 1; ++i) // reverse CIGAR
            tmp = cigar[i], cigar[i] = cigar[cigar.length - 1 - i], cigar[cigar.length - 1 - i] = tmp;
        return [score, start_i, cigar];
    }

    // backtrack to recover the alignment/cigar
    push_cigar(ci, op, len) { let ic = this.icn3d; ic.icn3dui;
        if (ci.length == 0 || op != (ci[ci.length - 1] & 0xf))
            ci.push(len << 4 | op);
        else ci[ci.length - 1] += len << 4;
    }

    bsa_cigar2gaps(target, query, start, cigar) { let ic = this.icn3d; ic.icn3dui;
        let oq = '',
            ot = '',
            mid = '',
            lq = 0,
            lt = start;
        for (let k = 0; k < cigar.length; ++k) {
            let op = cigar[k] & 0xf,
                len = cigar[k] >> 4;
            if (op == 0) { // match
                oq += query.substr(lq, len);
                ot += target.substr(lt, len);
                lq += len, lt += len;
            } else if (op == 1) { // insertion
                oq += query.substr(lq, len);
                ot += Array(len + 1).join("-");
                lq += len;
            } else if (op == 2) { // deletion
                oq += Array(len + 1).join("-");
                ot += target.substr(lt, len);
                lt += len;
            } else if (op == 4) { // soft clip
                lq += len;
            }
        }
        let ut = ot.toUpperCase();
        let uq = oq.toUpperCase();
        for (let k = 0; k < ut.length; ++k)
            mid += ut.charAt(k) == uq.charAt(k) ? '|' : ' ';
        return [ot, oq, mid];
    }

    bsa_cigar2str(cigar) { let ic = this.icn3d; ic.icn3dui;
        let s = [];
        for (let k = 0; k < cigar.length; ++k)
            s.push((cigar[k] >> 4).toString() + "MIDNSHP=XB".charAt(cigar[k] & 0xf));
        return s.join("");
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Analysis {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    calculateArea() {var ic = this.icn3d, me = ic.icn3dui;
       ic.bCalcArea = true;
       ic.opts.surface = 'solvent accessible surface';
       ic.applyMapCls.applySurfaceOptions();
       $("#" + ic.pre + "areavalue").val(ic.areavalue);
       $("#" + ic.pre + "areatable").html(ic.areahtml);
       me.htmlCls.dialogCls.openDlg('dl_area', 'Surface area calculation');
       ic.bCalcArea = false;
    }

    calcBuriedSurface(nameArray2, nameArray) {var ic = this.icn3d, me = ic.icn3dui;
       if(nameArray2.length == 0) {
           alert("Please select the first set");
       }
       else {
           let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
           let atomSet2 = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);
           let atomSet1 = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
           ic.bCalcArea = true;
           ic.opts.surface = 'solvent accessible surface';
           ic.hAtoms = me.hashUtilsCls.cloneHash(atomSet2);
           ic.applyMapCls.applySurfaceOptions();
           let area2 = ic.areavalue;
           let resid2area2 = me.hashUtilsCls.cloneHash(ic.resid2area);
           ic.hAtoms = me.hashUtilsCls.cloneHash(atomSet1);
           ic.applyMapCls.applySurfaceOptions();
           let area1 = ic.areavalue;
           let resid2area1 = me.hashUtilsCls.cloneHash(ic.resid2area);

           ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, atomSet2);
           ic.applyMapCls.applySurfaceOptions();
           let areaTotal = ic.areavalue;
           let resid2areaTotal = me.hashUtilsCls.cloneHash(ic.resid2area);

           let buriedArea1 = 0, buriedArea2 = 0;
           let areaSum1 = 0, areaSum2 = 0;
           // set 1 buried
           for(let resid in resid2area2) {
               if(resid2areaTotal.hasOwnProperty(resid)) {
                   areaSum2 += parseFloat(resid2areaTotal[resid]);
               }
           }
           buriedArea2 = (area2 - areaSum2).toFixed(2);

           // set 2 buried
           for(let resid in resid2area1) {
               if(resid2areaTotal.hasOwnProperty(resid)) {
                   areaSum1 += parseFloat(resid2areaTotal[resid]);
               }
           }
           buriedArea1 = (area1 - areaSum1).toFixed(2);

           ic.bCalcArea = false;
           ic.hAtoms = me.hashUtilsCls.cloneHash(prevHAtoms);
           let buriedArea =(parseFloat(area1) + parseFloat(area2) - parseFloat(areaTotal)).toFixed(2);
           let html = '<br>Calculate solvent accessible surface area in the interface:<br><br>';
           html += 'Set 1: ' + nameArray2 + ', Surface: ' +  area2 + ' &#8491;<sup>2</sup><br>';
           html += 'Set 2: ' + nameArray + ', Surface: ' +  area1 + ' &#8491;<sup>2</sup><br>';
           html += 'Total Surface: ' +  areaTotal + ' &#8491;<sup>2</sup><br>';
           //html += '<b>Buried Surface for both Sets</b>: ' +  buriedArea + ' &#8491;<sup>2</sup><br>';
           html += '<b>Buried Surface for Set 1</b>: ' +  buriedArea2 + ' &#8491;<sup>2</sup><br>';
           html += '<b>Buried Surface for Set 2</b>: ' +  buriedArea1 + ' &#8491;<sup>2</sup><br><br>';
           $("#" + ic.pre + "dl_buriedarea").html(html);
           me.htmlCls.dialogCls.openDlg('dl_buriedarea', 'Buried solvent accessible surface area in the interface');
           me.htmlCls.clickMenuCls.setLogCmd('buried surface ' + buriedArea, false);
       }
    }

    measureDistTwoSets(nameArray, nameArray2) {var ic = this.icn3d, me = ic.icn3dui;
       if(nameArray.length == 0 || nameArray2.length == 0) {
           alert("Please select two sets");
       }
       else {
           let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);
           let atomSet1 = ic.definedSetsCls.getAtomsFromNameArray(nameArray);
           let atomSet2 = ic.definedSetsCls.getAtomsFromNameArray(nameArray2);

           let posArray1 = ic.contactCls.getExtent(atomSet1);
           let posArray2 = ic.contactCls.getExtent(atomSet2);

           let pos1 = new THREE.Vector3(posArray1[2][0], posArray1[2][1], posArray1[2][2]);
           let pos2 = new THREE.Vector3(posArray2[2][0], posArray2[2][1], posArray2[2][2]);

           ic.hAtoms = me.hashUtilsCls.cloneHash(prevHAtoms);

           if(ic.distPnts === undefined) ic.distPnts = [];
           ic.distPnts.push(pos1);
           ic.distPnts.push(pos2);

           let color = $("#" + ic.pre + "distancecolor2" ).val();

           this.addLine(pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, color, true, 'distance');

           let size = 0, background = 0;
           let labelPos = pos1.clone().add(pos2).multiplyScalar(0.5);
           let distance = parseInt(pos1.distanceTo(pos2) * 10) / 10;
           let text = distance.toString() + " A";
           this.addLabel(text, labelPos.x, labelPos.y, labelPos.z, size, color, background, 'distance');
           ic.drawCls.draw();
       }
    }

    measureDistManySets(nameArray, nameArray2) {var ic = this.icn3d, me = ic.icn3dui;
        if(nameArray.length == 0 || nameArray2.length == 0) {
            alert("Please select sets for distance calculation...");
        }
        else {

            let prevHAtoms = me.hashUtilsCls.cloneHash(ic.hAtoms);

            let distHash = {};

            for(let i = 0, il = nameArray.length; i < il; ++i) {
                let set1 = nameArray[i];
                let array1 = [set1];
                distHash[set1] = {};

                for(let j = 0, jl = nameArray2.length; j < jl; ++j) {
                    let set2 = nameArray2[j];
                    let array2 = [set2];

                    if(set1 == set2) continue;

                    let atomSet1 = ic.definedSetsCls.getAtomsFromNameArray(array1);
                    let atomSet2 = ic.definedSetsCls.getAtomsFromNameArray(array2);

                    let posArray1 = ic.contactCls.getExtent(atomSet1);
                    let posArray2 = ic.contactCls.getExtent(atomSet2);
        
                    let pos1 = new THREE.Vector3(posArray1[2][0], posArray1[2][1], posArray1[2][2]);
                    let pos2 = new THREE.Vector3(posArray2[2][0], posArray2[2][1], posArray2[2][2]);
        
                    let distance = pos1.distanceTo(pos2);

                    distHash[set1][set2] = distance.toFixed(2);
                }
            }

            ic.hAtoms = me.hashUtilsCls.cloneHash(prevHAtoms);

            let tableHtml = 'Note: Click on the distance to show a dashed line in 3D view.<br><br>';
            tableHtml += '<table align=center border=1 cellpadding=10 cellspacing=0><tr><th></th>';
            for(let j = 0, jl = nameArray2.length; j < jl; ++j) {
                let set2 = nameArray2[j];
                tableHtml += '<th><b>' + set2 + '</b> (&#8491;)</th>';
            }
            tableHtml += '</tr>';

            for(let i = 0, il = nameArray.length; i < il; ++i) {
                let set1 = nameArray[i];
                tableHtml += '<tr><th><b>' + set1 + '</b> (&#8491;)</th>';

                for(let j = 0, jl = nameArray2.length; j < jl; ++j) {
                    let set2 = nameArray2[j];

                    if(distHash[set1] && distHash[set1][set2]) {
                        tableHtml += '<td><span class="icn3d-distance" sets="' + set1 + '|' + set2 + '">' + distHash[set1][set2] + '</span></td>';
                    }
                    else {
                        tableHtml += '<td>0</td>';
                    }
                }

                tableHtml += '</tr>';
            }

            tableHtml += '</table><br><br>';

            $("#" + me.pre + "dl_disttable").html(tableHtml);
        }
     }

    //Add a line between the position (x1, y1, z1) and the position (x2, y2, z2) with the input "color".
    //The line can be dashed if "dashed" is set true.
    addLine(x1, y1, z1, x2, y2, z2, color, dashed, type, radius, opacity) {var ic = this.icn3d; ic.icn3dui;
        let line = {}; // Each line contains 'position1', 'position2', 'color', and a boolean of 'dashed'
        line.position1 = new THREE.Vector3(x1, y1, z1);
        line.position2 = new THREE.Vector3(x2, y2, z2);
        line.color = color;
        line.dashed = dashed;
        line.radius = radius;
        line.opacity = opacity;
        if(ic.lines[type] === undefined) ic.lines[type] = [];
        if(type !== undefined) {
            ic.lines[type].push(line);
        }
        else {
            if(ic.lines['custom'] === undefined) ic.lines['custom'] = [];
            ic.lines['custom'].push(line);
        }
        ic.hlObjectsCls.removeHlObjects();
        //ic.drawCls.draw();
    }

    addLineFromPicking(type) {var ic = this.icn3d, me = ic.icn3dui;
        let color = $("#" + ic.pre + type + "color" ).val();
        (ic.pAtom.coord.x + ic.pAtom2.coord.x) / 2;
        (ic.pAtom.coord.y + ic.pAtom2.coord.y) / 2;
        (ic.pAtom.coord.z + ic.pAtom2.coord.z) / 2;
        let dashed =(type == 'stabilizer') ? false : true;
        me.htmlCls.clickMenuCls.setLogCmd('add line | x1 ' + ic.pAtom.coord.x.toPrecision(4)  + ' y1 ' + ic.pAtom.coord.y.toPrecision(4) + ' z1 ' + ic.pAtom.coord.z.toPrecision(4) + ' | x2 ' + ic.pAtom2.coord.x.toPrecision(4)  + ' y2 ' + ic.pAtom2.coord.y.toPrecision(4) + ' z2 ' + ic.pAtom2.coord.z.toPrecision(4) + ' | color ' + color + ' | dashed ' + dashed + ' | type ' + type, true);
        this.addLine(ic.pAtom.coord.x, ic.pAtom.coord.y, ic.pAtom.coord.z, ic.pAtom2.coord.x, ic.pAtom2.coord.y, ic.pAtom2.coord.z, color, dashed, type);
        ic.pickpair = false;
    }

    //Add a "text" at the position (x, y, z) with the input "size", "color", and "background".
    addLabel(text, x, y, z, size, color, background, type) {var ic = this.icn3d; ic.icn3dui;
        let label = {}; // Each label contains 'position', 'text', 'color', 'background'

        if(size === '0' || size === '' || size === 'undefined') size = undefined;
        if(color === '0' || color === '' || color === 'undefined') color = undefined;
        if(background === '0' || background === '' || background === 'undefined') background = undefined;

        let position = new THREE.Vector3();
        position.x = x;
        position.y = y;
        position.z = z;

        label.position = position;

        label.text = text;
        label.size = size;
        label.color = color;
        label.background = background;

        if(ic.labels[type] === undefined) ic.labels[type] = [];

        if(type !== undefined) {
            ic.labels[type].push(label);
        }
        else {
            if(ic.labels['custom'] === undefined) ic.labels['custom'] = [];
            ic.labels['custom'].push(label);
        }

        ic.hlObjectsCls.removeHlObjects();

        //ic.drawCls.draw();
    }

    //Display chain name in the 3D structure display for the chains intersecting with the atoms in "atomHash".
    addChainLabels(atoms) {var ic = this.icn3d, me = ic.icn3dui;
        let size = 18;
        let background = "#FFFFFF"; //"#CCCCCC";
        let atomsHash = me.hashUtilsCls.intHash(ic.hAtoms, atoms);
        if(ic.labels['chain'] === undefined) ic.labels['chain'] = [];
        let chainHash = ic.firstAtomObjCls.getChainsFromAtoms(atomsHash);
        for(let chainid in chainHash) {
            let label = {};
            label.position = ic.applyCenterCls.centerAtoms(ic.chains[chainid]).center;
            let pos = chainid.indexOf('_');
            let chainName = chainid.substr(pos + 1);
            let proteinName = ic.showSeqCls.getProteinName(chainid);
            if(proteinName.length > 20) proteinName = proteinName.substr(0, 20) + '...';
            label.text = 'Chain ' + chainName + ': ' + proteinName;
            label.size = size;
            ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chainid]).color.getHexString().toUpperCase();
            label.color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd; //(atomColorStr === "CCCCCC" || atomColorStr === "C8C8C8") ? "#888888" : "#" + atomColorStr;
            label.background = background;
            ic.labels['chain'].push(label);
        }
        ic.hlObjectsCls.removeHlObjects();
    }
    //Display the terminal labels for the atoms in "atomHash". The termini of proteins are labeld
    //as "N-" and "C-". The termini of nucleotides are labeled as "5'" and "3'".
    addTerminiLabels(atoms) {var ic = this.icn3d, me = ic.icn3dui;
        let size = 18;
        let background = "#FFFFFF"; //"#CCCCCC";
        let protNucl;
        protNucl = me.hashUtilsCls.unionHash(protNucl, ic.proteins);
        protNucl = me.hashUtilsCls.unionHash(protNucl, ic.nucleotides);
        let hlProtNucl = me.hashUtilsCls.intHash(ic.dAtoms, protNucl);
        let atomsHash = me.hashUtilsCls.intHash(hlProtNucl, atoms);
        if(ic.labels['chain'] === undefined) ic.labels['chain'] = [];
        let chainHash = ic.firstAtomObjCls.getChainsFromAtoms(atomsHash);
        for(let chainid in chainHash) {
            let chainAtomsHash = me.hashUtilsCls.intHash(hlProtNucl, ic.chains[chainid]);
            let serialArray = Object.keys(chainAtomsHash);
            let firstAtom = ic.atoms[serialArray[0]];
            let lastAtom = ic.atoms[serialArray[serialArray.length - 1]];
            let labelN = {}, labelC = {};
            labelN.position = firstAtom.coord;
            labelC.position = lastAtom.coord;
            labelN.text = 'N-';
            labelC.text = 'C-';
            if(ic.nucleotides.hasOwnProperty(firstAtom.serial)) {
                labelN.text = "5'";
                labelC.text = "3'";
            }
            labelN.size = size;
            labelC.size = size;
            firstAtom.color.getHexString().toUpperCase();
            lastAtom.color.getHexString().toUpperCase();
            labelN.color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd; //(atomNColorStr === "CCCCCC" || atomNColorStr === "C8C8C8") ? "#888888" : "#" + atomNColorStr;
            labelC.color = (ic.opts.background != 'black') ? ic.colorWhitebkgd : ic.colorBlackbkgd; //(atomCColorStr === "CCCCCC" || atomCColorStr === "C8C8C8") ? "#888888" : "#" + atomCColorStr;
            labelN.background = background;
            labelC.background = background;
            ic.labels['chain'].push(labelN);
            ic.labels['chain'].push(labelC);
        }
        ic.hlObjectsCls.removeHlObjects();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Diagram2d {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    // draw 2D dgm for MMDB ID
    // Used as a reference the work at 2016 ISMB hackathon: https://github.com/NCBI-Hackathons/3D_2D_Rep_Structure
    // bUpdate: redraw 2Ddiagramfor the displayed structure
    draw2Ddgm(data, mmdbid, structureIndex, bUpdate) { let ic = this.icn3d, me = ic.icn3dui;
        // only show the 2D diagrams for displayed structures

///        mmdbid = mmdbid.substr(0, 4);

        // reduce the size from 300 to 200 (150)
        let factor = 0.667;

        // set molid2chain
        let molid2chain = {}, molid2color = {}, molid2name = {}, chainid2molid = {};
        let chainNameHash = {};

        if(data === undefined) return '';

        for(let molid in data.moleculeInfor) {
              let color = '#' +( '000000' + data.moleculeInfor[molid].color.toString( 16 ) ).slice( - 6 );
              let chainName = data.moleculeInfor[molid].chain.trim();
              if(chainNameHash[chainName] === undefined) {
                  chainNameHash[chainName] = 1;
              }
              else {
                  ++chainNameHash[chainName];
              }

              let chainNameFinal =(chainNameHash[chainName] === 1) ? chainName : chainName + chainNameHash[chainName].toString();
              let chainid = mmdbid + '_' + chainNameFinal;
              if(ic.mmdbid_q !== undefined && ic.mmdbid_q === ic.mmdbid_t && structureIndex === 0) ;

              molid2chain[molid] = chainid;
              molid2color[molid] = color;
              molid2name[molid] = data.moleculeInfor[molid].name;

              chainid2molid[chainid] = molid;
        }

        // save the interacting residues
        if(bUpdate === undefined || !bUpdate) {
            for(let i = 0, il = data['intracResidues'].length; i < il; ++i) {
                let pair = data['intracResidues'][i];

                let index = 0;
                let chainid1, chainid2;

                for(let molid in pair) {
                    //molid = parseInt(molid);

                    let chainid;

                    chainid = molid2chain[molid];
                    if(index === 0) {
                        chainid1 = chainid;
                    }
                    else {
                        chainid2 = chainid;
                    }

                    ++index;
                }

                if(chainid1 === undefined || chainid2 === undefined) continue;

                index = 0;
                for(let molid in pair) {
                    let resArray = pair[molid];

                    let fisrtChainid, secondChainid;
                    if(index === 0) {
                        fisrtChainid = chainid1;
                        secondChainid = chainid2;
                    }
                    else {
                        fisrtChainid = chainid2;
                        secondChainid = chainid1;
                    }

                    if(ic.chainids2resids[fisrtChainid] === undefined) {
                        ic.chainids2resids[fisrtChainid] = {};
                    }

                    if(ic.chainids2resids[fisrtChainid][secondChainid] === undefined) {
                        ic.chainids2resids[fisrtChainid][secondChainid] = [];
                    }

                    for(let j = 0, jl = resArray.length; j < jl; ++j) {
                        let res = resArray[j];
                        let resid = ic.mmdbMolidResid2mmdbChainResi[mmdbid.toUpperCase() + '_' + molid + '_' + res];

                        ic.chainids2resids[fisrtChainid][secondChainid].push(resid);
                    }

                    // update ic.chainname2residues
                    if(ic.chainname2residues === undefined) ic.chainname2residues = {};

                    chainid2 = secondChainid;

                    if(!ic.chains.hasOwnProperty(chainid2)) continue;

                    let atom2 = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.chains[chainid2]);
                    //if(ic.chainname2residues[chainid2] === undefined) ic.chainname2residues[chainid2] = {}

                    let type2;
                    if(ic.chemicals.hasOwnProperty(atom2.serial)) { // 1. chemical interacting with proteins
                        type2 = 'chemical';
                    }
                    else if(ic.nucleotides.hasOwnProperty(atom2.serial)) { // 2. DNA interacting with proteins
                        type2 = 'nucleotide';
                    }
                    else if(ic.ions.hasOwnProperty(atom2.serial)) { // 3. ions interacting with proteins
                        type2 = 'ion';
                    }
                    else if(ic.proteins.hasOwnProperty(atom2.serial)) { // 4. protein interacting with proteins
                        type2 = 'protein';
                    }
                    else if(ic.water.hasOwnProperty(atom2.serial)) { // 5. water interacting with proteins
                        type2 = 'water';
                    }

                    let name = chainid2.substr(chainid2.indexOf('_') + 1) + "(" + type2 + ")";

                    if(ic.chainname2residues[fisrtChainid] === undefined) ic.chainname2residues[fisrtChainid] = {};

                    ic.chainname2residues[fisrtChainid][name] = ic.chainids2resids[fisrtChainid][secondChainid];


                    ++index;
                }
            }
        }

        let html = "<div id='#" + ic.pre + mmdbid + "'>";

        html += "<b>" + mmdbid.toUpperCase() + "</b><br/>";

        html += "<svg viewBox='0,0," + me.htmlCls.width2d + "," + me.htmlCls.width2d + "'>";
        let strokecolor = '#000000';
        let linestrokewidth = '2';

        let posHash = {};
        let lines = [];

        let nodeHtml = "", chemNodeHtml = "";

        let displayedMolids = {};
        if(bUpdate) {
            // get all displayed chains
            for(let i in ic.dAtoms) {
                let atom = ic.atoms[i];
                let chainid = atom.structure + '_' + atom.chain;
                let molid = chainid2molid[chainid];

                displayedMolids[molid] = 1;
            }
        }

        let allMolidArray = Object.keys(data.moleculeInfor);
        let intracMolidArray = Object.keys(data.intrac);

        let missingMolidArray = [];
        for(let i = 0, il = allMolidArray.length; i < il; ++i) {
            if(intracMolidArray.indexOf(allMolidArray[i]) === -1) missingMolidArray.push(allMolidArray[i]);
        }

        let missingMolid2intrac = {}; // biopolymer

        if(missingMolidArray.length > 0) {
            for(let molid in data.intrac) {
                let dgm = data.intrac[molid];
                for(let i = 0, il = dgm.intrac.length; i < il; ++i) {
                    let intracMolid = dgm.intrac[i].toString();
                    if(missingMolidArray.indexOf(intracMolid) !== -1) {
                        if(missingMolid2intrac[intracMolid] === undefined) missingMolid2intrac[intracMolid] = [];
                        missingMolid2intrac[intracMolid].push(molid);
                        lines.push([intracMolid, molid]);
                    }
                }

                if(dgm.shape === 'rect') {
                    let x = dgm.coords[0] * factor;
                    let y = dgm.coords[1] * factor;
                    let width = dgm.coords[2] * factor - x;
                    let height = dgm.coords[3] * factor - y;

                    posHash[molid] = [x + width/2, y + height/2];
                }
                else if(dgm.shape === 'circle') {
                    let x = dgm.coords[0] * factor;
                    let y = dgm.coords[1] * factor;
                    dgm.coords[2] * factor;

                    posHash[molid] = [x, y];
                }
                else if(dgm.shape === 'poly') {
                    let x0 = dgm.coords[0] * factor;
                    dgm.coords[1] * factor;
                    dgm.coords[2] * factor;
                    let y1 = dgm.coords[3] * factor;
                    dgm.coords[4] * factor;
                    dgm.coords[5] * factor;
                    dgm.coords[6] * factor;
                    dgm.coords[7] * factor;

                    posHash[molid] = [x0, y1];
                }
            }
        }

        let cntNointeraction = 0;
        //for(let molid in data.intrac) {
        for(let index = 0, indexl = allMolidArray.length; index < indexl; ++index) {
            let molid = allMolidArray[index];

            let chainid = molid2chain[molid];

            // if redraw2d diagram and the molid is not displayed, skip
            if(bUpdate && !displayedMolids.hasOwnProperty(molid)) continue;

            let dgm = data.intrac[molid];
            let color = "#FFFFFF";
            let oricolor = molid2color[molid];
            if(chainid !== undefined && ic.chains[chainid] !== undefined) {
                let atomArray = Object.keys(ic.chains[chainid]);
                if(atomArray.length > 0) {
                    oricolor = "#" + ic.atoms[atomArray[0]].color.getHexString().toUpperCase();
                }
            }

            let alignNum = "";
            if(ic.bInitial && structureIndex !== undefined) {
                if(ic.alignmolid2color !== undefined && ic.alignmolid2color[structureIndex].hasOwnProperty(molid)) {
                    alignNum = ic.alignmolid2color[structureIndex][molid];
                    oricolor = "#FF0000";
                }
                else {
                    oricolor = "#FFFFFF";
                }
            }

            let chainname = molid2name[molid];

            let chain = ' ', oriChain = ' ';
            if(chainid !== undefined) {
                let pos = chainid.indexOf('_');
                oriChain = chainid.substr(pos + 1);

                if(oriChain.length > 1) {
                    chain = oriChain.substr(0, 1) + '..';
                }
                else {
                    chain = oriChain;
                }
            }
            else {
                chainid = 'Misc';
            }

            if(oricolor === undefined) {
                oricolor = '#FFFFFF';
            }

            let ratio = 1.0;
            if(ic.bInitial && ic.alnChains[chainid] !== undefined) {
                //ratio = 1.0 * Object.keys(ic.alnChains[chainid]).length / Object.keys(ic.chains[chainid]).length;
                let alignedAtomCnt = 0;
                for(let i in ic.alnChains[chainid]) {
                    let colorStr = ic.atoms[i].color.getHexString().toUpperCase();
                    if(colorStr === 'FF0000' || colorStr === '00FF00') {
                        ++alignedAtomCnt;
                    }
                }
                ratio = 1.0 * alignedAtomCnt / Object.keys(ic.chains[chainid]).length;
            }
            if(ratio < 0.2) ratio = 0.2;

            if(missingMolidArray.indexOf(molid) === -1) {
                for(let i = 0, il = dgm.intrac.length; i < il; ++i) {
                    // show the interactin line once
                    if(parseInt(molid) < parseInt(dgm.intrac[i])) lines.push([molid, dgm.intrac[i] ]);
                }

                if(dgm.shape === 'rect') {
                    let x = dgm.coords[0] * factor;
                    let y = dgm.coords[1] * factor;
                    let width = dgm.coords[2] * factor - x;
                    let height = dgm.coords[3] * factor - y;

                    nodeHtml += this.draw2DNucleotide(x + 0.5 * width, y + 0.5 * height, chainid, oriChain, chain, chainname, alignNum, color, oricolor, factor, ratio);

                    posHash[molid] = [x + width/2, y + height/2];
                }
                else if(dgm.shape === 'circle') {
                    let x = dgm.coords[0] * factor;
                    let y = dgm.coords[1] * factor;

                    nodeHtml += this.draw2DProtein(x, y, chainid, oriChain, chain, chainname, alignNum, color, oricolor, factor, ratio);

                    posHash[molid] = [x, y];
                }
                else if(dgm.shape === 'poly') {
                  let x0 = dgm.coords[0] * factor;
                  dgm.coords[1] * factor;
                  dgm.coords[2] * factor;
                  let y1 = dgm.coords[3] * factor;
                  dgm.coords[4] * factor;
                  dgm.coords[5] * factor;
                  dgm.coords[6] * factor;
                  dgm.coords[7] * factor;

                  let x = x0, y = y1;

                  ic.firstAtomObjCls.getFirstAtomObj(ic.chains[chainid]);

                  chemNodeHtml += this.draw2DChemical(x, y, chainid, oriChain, chain, chainname, alignNum, color, oricolor, factor, ratio);

                  posHash[molid] = [x0, y1];
                }
            }
            else { // missing biopolymer
                // max x and y value: 300
                let maxSize = 300;
                let step = 50;

                let xCenter, yCenter;
                if(missingMolid2intrac[molid] !== undefined && missingMolid2intrac[molid].length > 1) { // has interactions
                    // find its position
                    let xSum = 0, ySum = 0;

                    for(let j = 0, jl = missingMolid2intrac[molid].length; j < jl; ++j) {
                        let intracMolid = missingMolid2intrac[molid][j];
                        if(posHash.hasOwnProperty(intracMolid)) {
                            let node = posHash[intracMolid];
                            xSum += node[0];
                            ySum += node[1];
                        }
                    }

                    xCenter = xSum / missingMolid2intrac[molid].length;
                    yCenter = ySum / missingMolid2intrac[molid].length;
                }
                else { // has NO interactions or just one interaction
                    let nSteps = maxSize / step;

                    if(cntNointeraction < nSteps - 1) {
                        xCenter =(cntNointeraction + 1) * step * factor;
                        yCenter = 0.1 * maxSize * factor;
                    }
                    else if(cntNointeraction -(nSteps - 1) < nSteps - 1) {
                        xCenter = 0.1 * maxSize * factor;
                        yCenter =(cntNointeraction -(nSteps - 1) + 1) * step * factor;
                    }
                    else {
                        xCenter = 0.25 * maxSize * factor;
                        yCenter = xCenter;
                    }

                    ++cntNointeraction;

                }

                let x = xCenter, y = yCenter;

                ic.firstAtomObjCls.getFirstAtomObj(ic.chains[chainid]);

                let bBiopolymer = true;
                chemNodeHtml += this.draw2DChemical(x, y, chainid, oriChain, chain, chainname, alignNum, color, oricolor, factor, ratio, bBiopolymer);

                posHash[molid] = [x, y];
            }
        }

        for(let i = 0, il = lines.length; i < il; ++i) {
            let pair = lines[i];

            // if redraw2d diagram and the molid is not displayed, skip
            if(bUpdate &&(!displayedMolids.hasOwnProperty(pair[0]) || !displayedMolids.hasOwnProperty(pair[1])) ) continue;

            let node1 = posHash[parseInt(pair[0])];
            let node2 = posHash[parseInt(pair[1])];

            if(node1 === undefined || node2 === undefined) continue;

            let chainid1, chainid2;

            chainid1 = molid2chain[pair[0]];
            chainid2 = molid2chain[pair[1]];

            let pos1 = chainid1.indexOf('_');
            let pos2 = chainid2.indexOf('_');

            let chain1 = chainid1.substr(pos1 + 1);
            let chain2 = chainid2.substr(pos2 + 1);

            let x1 = node1[0], y1 = node1[1], x2 = node2[0], y2 = node2[1], xMiddle =(x1 + x2) * 0.5, yMiddle =(y1 + y2) * 0.5;

            html += "<g class='icn3d-interaction' chainid1='" + chainid1 + "' chainid2='" + chainid2 + "' >";
            html += "<title>Interaction of chain " + chain1 + " with chain " + chain2 + "</title>";
            html += "<line x1='" + x1 + "' y1='" + y1 + "' x2='" + xMiddle + "' y2='" + yMiddle + "' stroke='" + strokecolor + "' stroke-width='" + linestrokewidth + "' /></g>";

            html += "<g class='icn3d-interaction' chainid1='" + chainid2 + "' chainid2='" + chainid1 + "' >";
            html += "<title>Interaction of chain " + chain2 + " with chain " + chain1 + "</title>";
            html += "<line x1='" + xMiddle + "' y1='" + yMiddle + "' x2='" + x2 + "' y2='" + y2 + "' stroke='" + strokecolor + "' stroke-width='" + linestrokewidth + "' /></g>";
        }

        html += chemNodeHtml + nodeHtml; // draw chemicals at the bottom layer

        html += "</svg>";
        html += "</div>";

        ic.html2ddgm += html;

        $("#" + ic.pre + "dl_2ddgm").html(ic.html2ddgm);

        return html;
    }

    set2DdgmNote(bAlign) { let ic = this.icn3d, me = ic.icn3dui;
        let html = "<div style='width:150px'><b>Nodes</b>:<br>";

        if(me.utilsCls.isMac()) {
            html += "<span style='margin-right:18px;'>&#9711;</span>Protein<br>";
            html += "<span style='margin-right:18px;'>&#9634;</span>Nucleotide<br>";
            html += "<span style='margin-right:18px;'>&#9826;</span>Chemical<br>";
            html += "<span style='margin-right:18px;display: inline-block;transform: skew(-25deg);'>&#9634;</span>Biopolymer<br>";
        }
        else {
            html += "<span style='margin-right:18px;'>O</span>Protein<br>";
            html += "<span style='margin-right:18px;'>&#9634;</span>Nucleotide<br>";
            html += "<span style='margin-right:18px;'>&#9671;</span>Chemical<br>";
            html += "<span style='margin-right:18px;display: inline-block;transform: skew(-25deg);'>&#9634;</span>Biopolymer<br>";
        }

        html += "<br><b>Lines</b>:<br> Interactions at 4 &#197;<br>";
        if(bAlign) html += "<b>Numbers in red</b>:<br> Aligned chains";
        html += "</div><br/>";

        return html;
    }

    highlightNode(type, highlight, base, ratio) { let ic = this.icn3d, me = ic.icn3dui;
        if(ratio < 0.2) ratio = 0.2;
        let strokeWidth = 3; // default 1

        if(type === 'rect') {
            $(highlight).attr('stroke', me.htmlCls.ORANGE);
            $(highlight).attr('stroke-width', strokeWidth);

            let x = Number($(base).attr('x'));
            let y = Number($(base).attr('y'));
            let width = Number($(base).attr('width'));
            let height = Number($(base).attr('height'));
            $(highlight).attr('x', x + width / 2.0 *(1 - ratio));
            $(highlight).attr('y', y + height / 2.0 *(1 - ratio));
            $(highlight).attr('width', width * ratio);
            $(highlight).attr('height', height * ratio);
        }
        else if(type === 'circle') {
            $(highlight).attr('stroke', me.htmlCls.ORANGE);
            $(highlight).attr('stroke-width', strokeWidth);

            $(highlight).attr('r', Number($(base).attr('r')) * ratio);
        }
        else if(type === 'polygon') {
            $(highlight).attr('stroke', me.htmlCls.ORANGE);
            $(highlight).attr('stroke-width', strokeWidth);

            let x = Number($(base).attr('x'));
            let y = Number($(base).attr('y'));

            let x0diff = Number($(base).attr('x0d'));
            let y0diff = Number($(base).attr('y0d'));
            let x1diff = Number($(base).attr('x1d'));
            let y1diff = Number($(base).attr('y1d'));
            let x2diff = Number($(base).attr('x2d'));
            let y2diff = Number($(base).attr('y2d'));
            let x3diff = Number($(base).attr('x3d'));
            let y3diff = Number($(base).attr('y3d'));

            $(highlight).attr('points',(x+x0diff*ratio).toString() + ", " +(y+y0diff*ratio).toString() + ", " +(x+x1diff*ratio).toString() + ", " +(y+y1diff*ratio).toString() + ", " +(x+x2diff*ratio).toString() + ", " +(y+y2diff*ratio).toString() + ", " +(x+x3diff*ratio).toString() + ", " +(y+y3diff*ratio).toString());
        }
    }

    removeLineGraphSelection() { let ic = this.icn3d; ic.icn3dui;
          $("#" + ic.pre + "dl_linegraph circle").attr('stroke', '#000000');
          $("#" + ic.pre + "dl_linegraph circle").attr('stroke-width', 1);

          $("#" + ic.pre + "dl_linegraph svg line.icn3d-hlline").attr('stroke', '#FFF');
          //$("#" + ic.pre + "dl_linegraph svg line .icn3d-hlline").attr('stroke-width', 1);
    }

    removeScatterplotSelection() { let ic = this.icn3d; ic.icn3dui;
          $("#" + ic.pre + "dl_scatterplot circle").attr('stroke', '#000000');
          $("#" + ic.pre + "dl_scatterplot circle").attr('stroke-width', 1);

          $("#" + ic.pre + "dl_scatterplot rect").attr('stroke', '#000000');
          $("#" + ic.pre + "dl_scatterplot rect").attr('stroke-width', 1);
    }

    click2Ddgm() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        //$("#" + ic.pre + "dl_2ddgm .icn3d-node", "click", function(e) { let ic = this.icn3d, me = ic.icn3dui;
        $(document).on("click", "#" + ic.pre + "dl_2ddgm .icn3d-node", function(e) { let ic = thisClass.icn3d;
            e.stopImmediatePropagation();
            if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) ic.definedSetsCls.setMode('selection');

            //ic.bClickInteraction = false;

            let chainid = $(this).attr('chainid');

            // clear all nodes
            if(!ic.bCtrl && !ic.bShift) {
                ic.selectionCls.removeSelection();

                // ic.lineArray2d is used to highlight lines in 2D diagram
                ic.lineArray2d = [];
            }

            let ratio = 1.0;
            if(ic.alnChains[chainid] !== undefined) ratio = 1.0 * Object.keys(ic.alnChains[chainid]).length / Object.keys(ic.chains[chainid]).length;

            let target = $(this).find("rect[class='icn3d-hlnode']");
            let base = $(this).find("rect[class='icn3d-basenode']");
            thisClass.highlightNode('rect', target, base, ratio);

            target = $(this).find("circle[class='icn3d-hlnode']");
            base = $(this).find("circle[class='icn3d-basenode']");
            thisClass.highlightNode('circle', target, base, ratio);

            target = $(this).find("polygon[class='icn3d-hlnode']");
            base = $(this).find("polygon[class='icn3d-basenode']");
            thisClass.highlightNode('polygon', target, base, ratio);

            if(!ic.bCtrl && !ic.bShift) {
                ic.hAtoms = me.hashUtilsCls.cloneHash(ic.chains[chainid]);
            }
            else {
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.chains[chainid]);
            }

            // get the name array
            if(!ic.bCtrl && !ic.bShift) {
                ic.chainArray2d = [chainid];
            }
            else {
                if(ic.chainArray2d === undefined) ic.chainArray2d = [];
                ic.chainArray2d.push(chainid);
            }

            ic.hlUpdateCls.updateHlAll(ic.chainArray2d);

            // show selected chains in annotation window
            ic.annotationCls.showAnnoSelectedChains();

            let select = "select chain " + chainid;
            me.htmlCls.clickMenuCls.setLogCmd(select, true);

            ic.bSelectResidue = false;
        });

        //$("#" + ic.pre + "dl_2ddgm .icn3d-interaction", "click", function(e) { let ic = thisClass.icn3d;
        $(document).on("click", "#" + ic.pre + "dl_2ddgm .icn3d-interaction", function(e) { let ic = thisClass.icn3d;
            e.stopImmediatePropagation();
            if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) ic.definedSetsCls.setMode('selection');

            ic.bClickInteraction = true;

            let chainid1 = $(this).attr('chainid1');
            let chainid2 = $(this).attr('chainid2');

            $(this).find('line').attr('stroke', me.htmlCls.ORANGE);

            // interaction of chain1 with chain2, only show the part of chain1 interacting with chain2
            thisClass.selectInteraction(chainid1, chainid2);

            // show selected chains in annotation window
            ic.annotationCls.showAnnoSelectedChains();

            let select = "select interaction " + chainid1 + "," + chainid2;
            me.htmlCls.clickMenuCls.setLogCmd(select, true);

            ic.bClickInteraction = false;
        });

        //$("#" + ic.pre + "dl_linegraph .icn3d-node", "click", function(e) { let ic = this.icn3d, me = ic.icn3dui;
        $(document).on("click", "#" + ic.pre + "dl_linegraph .icn3d-node", function(e) { let ic = thisClass.icn3d;
            e.stopImmediatePropagation();
            if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) ic.definedSetsCls.setMode('selection');

            let resid = $(this).attr('resid');

            if(!ic.bCtrl && !ic.bShift) {
              ic.hAtoms = {};

              thisClass.removeLineGraphSelection();
            }

            let strokeWidth = 2;
            $(this).find('circle').attr('stroke', me.htmlCls.ORANGE);
            $(this).find('circle').attr('stroke-width', strokeWidth);

            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid]);

            let select = 'select ' + ic.resid2specCls.residueids2spec([resid]);

            ic.hlUpdateCls.updateHlAll();

            me.htmlCls.clickMenuCls.setLogCmd(select, true);

            ic.bSelectResidue = false;
        });

        //$("#" + ic.pre + "dl_scatterplot .icn3d-node", "click", function(e) { let ic = this.icn3d, me = ic.icn3dui;
        $(document).on("click", "#" + ic.pre + "dl_scatterplot .icn3d-node", function(e) { thisClass.icn3d;
            e.stopImmediatePropagation();

            thisClass.clickNode(this);
        });

        //$("#" + ic.pre + "dl_linegraph .icn3d-interaction", "click", function(e) { let ic = this.icn3d, me = ic.icn3dui;
        $(document).on("click", "#" + ic.pre + "dl_linegraph .icn3d-interaction", function(e) { let ic = thisClass.icn3d;
              e.stopImmediatePropagation();
            if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) ic.definedSetsCls.setMode('selection');

            let resid1 = $(this).attr('resid1');
            let resid2 = $(this).attr('resid2');

            if(!ic.bCtrl && !ic.bShift) {
              ic.hAtoms = {};

              thisClass.removeLineGraphSelection();
            }

            $(this).find('line.icn3d-hlline').attr('stroke', me.htmlCls.ORANGE);

            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid1]);
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid2]);

            let select = 'select ' + ic.resid2specCls.residueids2spec([resid1, resid2]);

            ic.hlUpdateCls.updateHlAll();

            me.htmlCls.clickMenuCls.setLogCmd(select, true);
        });

        //$("#" + ic.pre + "dl_scatterplot .icn3d-interaction", "click", function(e) { let ic = this.icn3d, me = ic.icn3dui;
        $(document).on("click", "#" + ic.pre + "dl_scatterplot .icn3d-interaction", function(e) { thisClass.icn3d;
            e.stopImmediatePropagation();

            thisClass.clickInteraction(this);
        });

        $(document).on("click", "#" + ic.pre + "dl_contactmap .icn3d-interaction", function(e) { thisClass.icn3d;
            e.stopImmediatePropagation();

            thisClass.clickInteraction(this);
        });

        $(document).on("click", "#" + ic.pre + "dl_contactmap .icn3d-node", function(e) { thisClass.icn3d;
            e.stopImmediatePropagation();

            thisClass.clickNode(this);
        });

        $(document).on("click", "#" + ic.pre + "dl_alignerrormap .icn3d-interaction", function(e) { thisClass.icn3d;
            e.stopImmediatePropagation();

            thisClass.clickInteraction(this);
        });

        $(document).on("click", "#" + ic.pre + "dl_alignerrormap .icn3d-node", function(e) { thisClass.icn3d;
            e.stopImmediatePropagation();

            thisClass.clickNode(this);
        });
    }

    clickNode(node) {  let ic = this.icn3d, me = ic.icn3dui;
        if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) ic.definedSetsCls.setMode('selection');

        let resid = $(node).attr('resid');

        if(!ic.bCtrl && !ic.bShift) {
          ic.hAtoms = {};

          this.removeScatterplotSelection();
        }

        let strokeWidth = 2;
        $(node).find('circle').attr('stroke', me.htmlCls.ORANGE);
        $(node).find('circle').attr('stroke-width', strokeWidth);

        ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid]);

        let select = 'select ' + ic.resid2specCls.residueids2spec([resid]);

        ic.hlUpdateCls.updateHlAll();

        me.htmlCls.clickMenuCls.setLogCmd(select, true);

        ic.bSelectResidue = false;
    }

    clickInteraction(node) {  let ic = this.icn3d, me = ic.icn3dui;
        if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) ic.definedSetsCls.setMode('selection');

        let resid1 = $(node).attr('resid1');
        let resid2 = $(node).attr('resid2');

        if(!ic.bCtrl && !ic.bShift) {
          ic.hAtoms = {};

          this.removeScatterplotSelection();
        }

        let strokeWidth = 2;
        $(node).find('rect').attr('stroke', me.htmlCls.ORANGE);
        $(node).find('rect').attr('stroke-width', strokeWidth);

        ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid1]);
        ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[resid2]);

        let select = 'select ' + ic.resid2specCls.residueids2spec([resid1, resid2]);

        ic.hlUpdateCls.updateHlAll();

        me.htmlCls.clickMenuCls.setLogCmd(select, true);
    }

    selectInteraction(chainid1, chainid2) {  let ic = this.icn3d; ic.icn3dui;
            ic.hlUpdateCls.removeHl2D();
            ic.hlObjectsCls.removeHlObjects();

            if(!ic.bCtrl && !ic.bShift) {
                // ic.lineArray2d is used to highlight lines in 2D diagram
                ic.lineArray2d = [chainid1, chainid2];
            }
            else {
                if(ic.lineArray2d === undefined) ic.lineArray2d = [];
                ic.lineArray2d.push(chainid1);
                ic.lineArray2d.push(chainid2);
            }

            this.selectInteractionAtoms(chainid1, chainid2);

            ic.hlObjectsCls.addHlObjects();

            ic.hlUpdateCls.updateHlAll();
    }

    selectInteractionAtoms(chainid1, chainid2) {  let ic = this.icn3d, me = ic.icn3dui;  // ic.pAtom is set already
        let radius = 4;

        // method 2. Retrieved from the cgi(This previously had problems in sharelink where the data from ajax is async. Now the data is from the same cgi as the atom data and there is no problem.)
        let residueArray = ic.chainids2resids[chainid1][chainid2];

        if(!ic.bCtrl && !ic.bShift) ic.hAtoms = {};

        for(let i = 0, il = residueArray.length; i < il; ++i) {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[residueArray[i]]);
        }

        let commandname, commanddesc;
        if(Object.keys(ic.structures).length > 1) {
            commandname = "inter_" + chainid1 + "_" + chainid2;
        }
        else {
            let pos1 = chainid1.indexOf('_');
            let pos2 = chainid2.indexOf('_');

            commandname = "inter_" + chainid1.substr(pos1 + 1) + "_" + chainid2.substr(pos2 + 1);
        }

        commanddesc = "select the atoms in chain " + chainid1 + " interacting with chain " + chainid2 + " in a distance of " + radius + " angstrom";

        let select = "select interaction " + chainid1 + "," + chainid2;

        ic.selectionCls.addCustomSelection(residueArray, commandname, commanddesc, select, true);
    }

    draw2DProtein(x, y, chainid, oriChain, chain, chainname, alignNum, color, oricolor, factor, ratio) { let ic = this.icn3d; ic.icn3dui;
        let strokecolor = '#000000';
        let strokewidth = '1';
        let textcolor = '#000000';
        let fontsize = '10';
        let smallfontsize = '8';
        let adjustx = 0, adjusty = 4, halfLetHigh = 6;

        let r = 20 * factor;

        let html = "<g class='icn3d-node' chainid='" + chainid + "' >";
        html += "<title>Chain " + oriChain + ": " + chainname + "</title>";
        html += "<circle class='icn3d-basenode' cx='" + x + "' cy='" + y + "' r='" + r + "' fill='" + color + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' class='icn3d-node' chainid='" + chainid + "' />";

        html += "<circle class='icn3d-hlnode' cx='" + x + "' cy='" + y + "' r='" +(r * ratio).toString() + "' fill='" + oricolor + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' />";

        html += "<text x='" +(x - adjustx).toString() + "' y='" +(y + adjusty).toString() + "' style='fill:" + textcolor + "; font-size:" + fontsize + "; text-anchor:middle' >" + chain + "</text>";

        if(alignNum !== "") html += "<text x='" +(x - adjustx).toString() + "' y='" +(y + r + adjusty + halfLetHigh).toString() + "' style='fill:" + oricolor + "; font-size:" + smallfontsize + "; font-weight:bold; text-anchor:middle' >" + alignNum + "</text>";

        html += "</g>";

        return html;
    }

    draw2DNucleotide(x, y, chainid, oriChain, chain, chainname, alignNum, color, oricolor, factor, ratio) { let ic = this.icn3d; ic.icn3dui;
        let strokecolor = '#000000';
        let strokewidth = '1';
        let textcolor = '#000000';
        let fontsize = '10';
        let smallfontsize = '8';
        let adjustx = 0, adjusty = 4, halfLetHigh = 6;

        let width = 30 * factor;
        let height = 30 * factor;

        x -= 0.5 * width;
        y -= 0.5 * height;

        let html = "<g class='icn3d-node' chainid='" + chainid + "' >";
        html += "<title>Chain " + oriChain + ": " + chainname + "</title>";
        // place holder
        html += "<rect class='icn3d-basenode' x='" + x + "' y='" + y + "' width='" + width + "' height='" + height + "' fill='" + color + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' />";
        // highlight
        html += "<rect class='icn3d-hlnode' x='" +(x + width / 2.0 *(1 - ratio)).toString() + "' y='" +(y + height / 2.0 *(1 - ratio)).toString() + "' width='" +(width * ratio).toString() + "' height='" +(height * ratio).toString() + "' fill='" + oricolor + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' />";

        html += "<text x='" +(x + width / 2 - adjustx).toString() + "' y='" +(y + height / 2 + adjusty).toString() + "' style='fill:" + textcolor + "; font-size:" + fontsize + "; text-anchor:middle' >" + chain + "</text>";

        if(alignNum !== "") html += "<text x='" +(x + width / 2 - adjustx).toString() + "' y='" +(y + height + adjusty + halfLetHigh).toString() + "' style='fill:" + oricolor + "; font-size:" + smallfontsize + "; font-weight:bold; text-anchor:middle' >" + alignNum + "</text>";

        html += "</g>";

        return html;
    }

    draw2DChemical(x, y, chainid, oriChain, chain, chainname, alignNum, color, oricolor, factor, ratio, bBiopolymer) { let ic = this.icn3d; ic.icn3dui;
        let strokecolor = '#000000';
        let strokewidth = '1';
        let textcolor = '#000000';
        let smallfontsize = '8';
        let smalladjustx = 1, smalladjusty = 2, halfLetHigh = 6;

        let bpsize = 30 * factor;

        let x0, y0, x1, y1, x2, y2, x3, y3;
        if(bBiopolymer) {
            // biopolymer
            let xOffset = 0.5 * bpsize / Math.sqrt(3);
            let yOffset = 0.5 * bpsize;

            x0 = x - xOffset;
            y0 = y - yOffset;
            x1 = x + 3 * xOffset;
            y1 = y - yOffset;
            x2 = x + xOffset;
            y2 = y + yOffset;
            x3 = x - 3 * xOffset;
            y3 = y + yOffset;
        }
        else {
            // diamond
            let xOffset = 0.5 * bpsize;
            let yOffset = 0.5 * bpsize;

            x0 = x - xOffset;
            y0 = y;
            x1 = x;
            y1 = y + yOffset;
            x2 = x + xOffset;
            y2 = y;
            x3 = x;
            y3 = y - yOffset;
        }

        let x0diff = x0 - x;
        let y0diff = y0 - y;
        let x1diff = x1 - x;
        let y1diff = y1 - y;
        let x2diff = x2 - x;
        let y2diff = y2 - y;
        let x3diff = x3 - x;
        let y3diff = y3 - y;

        let html = "<g class='icn3d-node' chainid='" + chainid + "' >";
        html += "<title>Chain " + oriChain + ": " + chainname + "</title>";
        html += "<polygon class='icn3d-basenode' points='" + x0 + ", " + y0 + "," + x1 + ", " + y1 + "," + x2 + ", " + y2 + "," + x3 + ", " + y3 + "' x='" + x + "' y='" + y + "' x0d='" + x0diff + "' y0d='" + y0diff + "' x1d='" + x1diff + "' y1d='" + y1diff + "' x2d='" + x2diff + "' y2d='" + y2diff + "' x3d='" + x3diff + "' y3d='" + y3diff + "' fill='" + color + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' />";

        html += "<polygon class='icn3d-hlnode' points='" +(x+x0diff*ratio).toString() + ", " +(y+y0diff*ratio).toString() + "," +(x+x1diff*ratio).toString() + ", " +(y+y1diff*ratio).toString() + "," +(x+x2diff*ratio).toString() + ", " +(y+y2diff*ratio).toString() + "," +(x+x3diff*ratio).toString() + ", " +(y+y3diff*ratio).toString() + "' fill='" + oricolor + "' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' />";

        html += "<text x='" +(x + smalladjustx).toString() + "' y='" +(y + smalladjusty).toString() + "' style='fill:" + textcolor + "; font-size:" + smallfontsize + "; text-anchor:middle' >" + chain + "</text>";

        if(alignNum !== "") html += "<text x='" +(x + smalladjustx).toString() + "' y='" +(y + smalladjusty + halfLetHigh).toString() + "' style='fill:" + oricolor + "; font-size:" + smallfontsize + "; font-weight:bold; text-anchor:middle' >" + alignNum + "</text>";

        html += "</g>";

        return html;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Cartoon2d {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    async draw2Dcartoon(type, bResize) { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        me.htmlCls.clickMenuCls.setLogCmd("cartoon 2d " + type, true);

        ic.cartoon2dType = type;

        //ic.bGraph = false; // differentiate from force-directed graph for interactions

        if(bResize) {
            let html = thisClass.getCartoonSvg(type, ic.graphStr);
            $("#" + me.svgid_ct).html(html);
        }
        else {
/*            
            if(type == 'domain' && !ic.chainid2pssmid) {
                //$.when(thisClass.getNodesLinksForSetCartoon(type)).then(function() {
                    await thisClass.getNodesLinksForSetCartoon(type);

                    ic.graphStr = thisClass.getCartoonData(type, ic.node_link);
                    //ic.viewInterPairsCls.drawGraphWrapper(ic.graphStr, ic.deferredCartoon2d, true);
                    let html = thisClass.getCartoonSvg(type, ic.graphStr);
                    $("#" + me.svgid_ct).html(html);
                    thisClass.setEventsForCartoon2d();

                    me.htmlCls.dialogCls.openDlg('dl_2dctn', '2D Cartoon');

                    /// if(ic.deferredCartoon2d !== undefined) ic.deferredCartoon2d.resolve();
                //});
            }
            else {
*/               
                //await this.getNodesLinksForSetCartoonBase(type);
                await this.getNodesLinksForSetCartoon(type);

                ic.graphStr = thisClass.getCartoonData(type, ic.node_link);

                //ic.viewInterPairsCls.drawGraphWrapper(ic.graphStr, ic.deferredCartoon2d, true);
                let html = thisClass.getCartoonSvg(type, ic.graphStr);

                $("#" + me.svgid_ct).html(html);
                thisClass.setEventsForCartoon2d();

                me.htmlCls.dialogCls.openDlg('dl_2dctn', '2D Cartoon');
//            }
        }
    }

    getCartoonSvg(type, graphStr) { let ic = this.icn3d, me = ic.icn3dui;
        //let html = "<svg id='" + me.svgid_ct + "' viewBox='" + "0,0," + me.htmlCls.width2d + "," + me.htmlCls.width2d + "'>";
        let html = "";

        let strokecolor = '#bbbbbb';
        let linestrokewidth = '1';

        let nodeHtml = "";

        let graph = JSON.parse(graphStr);
        ic.ctnNodeHash = {};
        for(let i = 0, il = graph.nodes.length; i < il; ++i) {
            let node = graph.nodes[i];
            ic.ctnNodeHash[node.id] = node;

            if(type == 'secondary') {
                nodeHtml += this.drawHelix(type, node.id, node.ss, node.x, node.y, node.x1, node.y1, node.x2, node.y2, node.len, node.ang, node.c);
            }
            else {
                nodeHtml += this.drawOval(type, node.id, node.x, node.y, node.rx, node.ry, node.ang, node.c, node.from, node.to);
            }
        }

        ic.nodeid2lineid = {};
        for(let i = 0, il = graph.links.length; i < il; ++i) {
            let id1 = graph.links[i].source;
            let id2 = graph.links[i].target;

            let x1 = ic.ctnNodeHash[id1].x, y1 = me.htmlCls.width2d - ic.ctnNodeHash[id1].y, x2 = ic.ctnNodeHash[id2].x, y2 = me.htmlCls.width2d - ic.ctnNodeHash[id2].y;

            if(type == 'chain') {
                html += "<g class='icn3d-ctinteraction' chainid1='" + ic.ctnNodeHash[id1].id + "' chainid2='" + ic.ctnNodeHash[id2].id + "' >";
            }
            else if(type == 'domain') {
                html += "<g class='icn3d-ctinteraction' from1='" + ic.ctnNodeHash[id1].from + "' to1='" + ic.ctnNodeHash[id1].to
                    + "' from2='" + ic.ctnNodeHash[id2].from + "' to2='" + ic.ctnNodeHash[id2].to + "' >";
            }
            else if(type == 'secondary') {
                x1 = ic.ctnNodeHash[id1].x2, y1 = me.htmlCls.width2d - ic.ctnNodeHash[id1].y2, x2 = ic.ctnNodeHash[id2].x1, y2 = me.htmlCls.width2d - ic.ctnNodeHash[id2].y1;

                html += "<g class='icn3d-ctinteraction' range1='" + ic.ctnNodeHash[id1].range + "' range2='" + ic.ctnNodeHash[id2].range + "' >";
            }

            let idStr1 = this.getLabelFromId(id1, type);
            let idStr2 = this.getLabelFromId(id2, type);
            let idpair = id1 + "--" + id2;

            html += "<title>Interaction of " + type + " " + idStr1 + " with " + type + " " + idStr2 + "</title>";
            html += "<line class='icn3d-edge' id='" + idpair + "' x1='" + x1 + "' y1='" + y1 + "' x2='" + x2 + "' y2='" + y2 + "' stroke='" + strokecolor + "' stroke-width='" + linestrokewidth + "' /></g>";

            if(!ic.nodeid2lineid.hasOwnProperty(id1)) {
                ic.nodeid2lineid[id1] = [];
            }
            if(!ic.nodeid2lineid.hasOwnProperty(id2)) {
                ic.nodeid2lineid[id2] = [];
            }
            ic.nodeid2lineid[id1].push(idpair);
            ic.nodeid2lineid[id2].push(idpair);
        }

        html += nodeHtml; // draw chemicals at the bottom layer

        //html += "</svg>";

        return html;
    }

    setEventsForCartoon2d() {  let ic = this.icn3d, me = ic.icn3dui;
        //https://stackoverflow.com/questions/1108480/svg-draggable-using-jquery-and-jquery-svg
        $("#" + me.svgid_ct + " .icn3d-ctnode")
        .draggable({
            start: function( e, ui ) {
                let oriCx = parseFloat(e.target.getAttribute('cx'));
                let oriCy = parseFloat(e.target.getAttribute('cy'));

                e.target.setAttribute('cx', oriCx);
                e.target.setAttribute('cy', oriCy);

                let angle = e.target.getAttribute('ang');

                if(angle) {
                    // update coordinates manually, since top/left style props don't work on SVG
                    e.target.setAttribute('transform', "rotate(" + angle + "," + oriCx + "," + oriCy + ")");
                }
                else {
                    let x1 = parseFloat(e.target.getAttribute('x1'));
                    let y1 = parseFloat(e.target.getAttribute('y1'));

                    let x2 = parseFloat(e.target.getAttribute('x2'));
                    let y2 = parseFloat(e.target.getAttribute('y2'));

                    e.target.setAttribute('x1', x1);
                    e.target.setAttribute('y1', y1);
                    e.target.setAttribute('x2', x2);
                    e.target.setAttribute('y2', y2);
                }
            },
            drag: function( e, ui ) {
                let offsetX = $("#" + me.svgid_ct).offset().left;
                let offsetY = $("#" + me.svgid_ct).offset().top;

                let id = e.target.getAttribute('id');
                let angle = e.target.getAttribute('ang');

                //let cx = ui.position.left - offsetX;
                //let cy = ui.position.top - offsetY;
                let cx = (e.clientX - offsetX);
                let cy = (e.clientY - offsetY);

                let oriCx = parseFloat(e.target.getAttribute('cx'));
                let oriCy = parseFloat(e.target.getAttribute('cy'));

                // change for each step
                let dx = (cx - oriCx) / ic.resizeRatioX;
                let dy = (cy - oriCy) / ic.resizeRatioY;

                // move the text label
                let oriX = parseFloat($("#" + id + "_text").attr('x'));
                let oriY = parseFloat($("#" + id + "_text").attr('y'));

                $("#" + id + "_text").attr('x', oriX + dx);
                $("#" + id + "_text").attr('y', oriY + dy);

                // update the center
                e.target.setAttribute('cx', cx);
                e.target.setAttribute('cy', cy);

                if(angle) {
                    // update coordinates manually, since top/left style props don't work on SVG
                    e.target.setAttribute('transform', "rotate(" + angle + "," + cx + "," + cy + ")");
                }
                else {
                    let x1 = parseFloat(e.target.getAttribute('x1'));
                    let y1 = parseFloat(e.target.getAttribute('y1'));

                    let x2 = parseFloat(e.target.getAttribute('x2'));
                    let y2 = parseFloat(e.target.getAttribute('y2'));

                    e.target.setAttribute('x1', x1 + dx);
                    e.target.setAttribute('y1', y1 + dy);
                    e.target.setAttribute('x2', x2 + dx);
                    e.target.setAttribute('y2', y2 + dy);

                    // move the outer box for sheets
                    if(id.substr(0, 1) == 'S') {
                        let oriX1 = parseFloat($("#" + id + "_box").attr('x1'));
                        let oriY1 = parseFloat($("#" + id + "_box").attr('y1'));
                        let oriX2 = parseFloat($("#" + id + "_box").attr('x2'));
                        let oriY2 = parseFloat($("#" + id + "_box").attr('y2'));

                        $("#" + id + "_box").attr('x1', oriX1 + dx);
                        $("#" + id + "_box").attr('y1', oriY1 + dy);
                        $("#" + id + "_box").attr('x2', oriX2 + dx);
                        $("#" + id + "_box").attr('y2', oriY2 + dy);
                    }
                }

                // update the edges
                if(ic.nodeid2lineid[id]) {
                    for(let i = 0, il = ic.nodeid2lineid[id].length; i < il; ++i) {
                        let idpair = ic.nodeid2lineid[id][i];

                        updateEdges(idpair, id, angle);
                    }
                }

                function updateEdges(idpair, id, angle) {
                    if(idpair && idpair.indexOf(id) != -1) {
                        let idArray = idpair.split('--');
                        if(idArray.length == 2) {
                            let id1, id2;

                            id1 = idArray[1];
                            id2 = idArray[0];

                            let posX1 = (angle) ? 'cx' : 'x1';
                            let posY1 = (angle) ? 'cy' : 'y1';

                            let x1 = $("#" + id1).attr(posX1);
                            let y1 = $("#" + id1).attr(posY1);

                            $("#" + idpair).attr('x1', x1);
                            $("#" + idpair).attr('y1', y1);

                            let posX2 = (angle) ? 'cx' : 'x2';
                            let posY2 = (angle) ? 'cy' : 'y2';

                            let x2 = $("#" + id2).attr(posX2);
                            let y2 = $("#" + id2).attr(posY2);

                            $("#" + idpair).attr('x2', x2);
                            $("#" + idpair).attr('y2', y2);
                        }
                    } // if
                } // function
            }
        });
    }

    getLabelFromId(id, type) {
        let idStr = id;
        let pos = idStr.indexOf('__');
        if (pos !== -1) idStr = idStr.substr(0, pos);
        if(type == 'secondary') {
            idStr = idStr.substr(0, idStr.indexOf('-'));
        }
        else {
            idStr = idStr.substr(idStr.lastIndexOf('_') + 1);
        }

        return idStr;
    }

    drawHelix(type, id, ss, x, y, x1, y1, x2, y2, length, angle, color) { let ic = this.icn3d, me = ic.icn3dui;
        let helixstrokewidth = '3';
        let helixstrokewidth2 = '1';
        let textcolor = '#000000';
        let adjustx = 0, adjusty = 4;

        let idStr = this.getLabelFromId(id, type);
        y = me.htmlCls.width2d - y; // flip
        y1 = me.htmlCls.width2d - y1; // flip
        y2 = me.htmlCls.width2d - y2; // flip

        let range = idStr.substr(1);
        //let html = "<g class='icn3d-node' range='" + range + "' >";
        let html = "<g range='" + range + "' >";
        html += "<title>" + type + " " + idStr + "</title>";

        if(id.substr(0,1) == 'H') {
            html += "<line id='" + id + "' class='icn3d-ctnode' x1='" + x1 + "' y1='" + y1 + "' x2='" + x2 + "' y2='" + y2 + "' cx='" + 0.5*(x1+x2).toFixed(1) + "' cy='" + 0.5*(y1+y2).toFixed(1) + "' stroke='#" + color + "' stroke-width='" + helixstrokewidth + "' stroke-linecap='round' />";
        }
        else {
            html += "<line id='" + id + "_box' x1='" + x1 + "' y1='" + y1 + "' x2='" + x2 + "' y2='" + y2 + "' stroke='#" + color + "' stroke-width='" + helixstrokewidth + "' stroke-linecap='square' />";
            html += "<line id='" + id + "' class='icn3d-ctnode' x1='" + x1 + "' y1='" + y1 + "' x2='" + x2 + "' y2='" + y2 + "' cx='" + 0.5*(x1+x2).toFixed(1) + "' cy='" + 0.5*(y1+y2).toFixed(1) + "' stroke='#FFF' stroke-width='" + helixstrokewidth2 + "' stroke-linecap='square' />";
        }

        html += "<text id='" + id + "_text' x='" +(x - adjustx).toString() + "' y='" +(y + adjusty).toString() + "' style='fill:" + textcolor + "; text-anchor:middle' class='icn3d-node-text8' >" + idStr + "</text>";

        html += "</g>";

        return html;
    }

    drawOval(type, id, x, y, rx, ry, angle, color, from, to) { let ic = this.icn3d, me = ic.icn3dui;
        let strokecolor = 'none';
        let strokewidth = '1';
        let textcolor = '#000000';
        let adjustx = 0, adjusty = 4;

        let idStr = this.getLabelFromId(id, type);
        y = me.htmlCls.width2d - y; // flip
        angle = 180 - angle; // flip

        let html = (type == 'chain') ? "<g chainid='" + id + "' >"
            : "<g from='" + from + "' to='" + to + "' >";
        html += "<title>" + type + " " + idStr + "</title>";

        html += "<defs>";
        html += "<linearGradient id='" + id + "_g_obj' x1='0%' y1='0%' x2='100%' y2='0%'>";
        html += "  <stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1' />";
        html += "  <stop offset='100%' style='stop-color:#" + color + ";stop-opacity:1' />";
        html += "</linearGradient>";
        html += "</defs>";

        html += "<ellipse id='" + id + "' class='icn3d-ctnode' cx='" + x.toFixed(0) + "' cy='" + y.toFixed(0) + "' rx='" + rx.toFixed(0) + "' ry='" + ry.toFixed(0) + "' fill='url(#" + id + "_g_obj)' stroke-width='" + strokewidth + "' stroke='" + strokecolor + "' ";
        html += " ang='" + angle + "' transform='rotate(" + angle + "," + x.toFixed(0) + "," + y.toFixed(0) + ")'";
        html += (type == 'chain') ? " chainid='" + id + "' />" : " from='" + from + "' to='" + to + "' />";

        html += "<text id='" + id + "_text' x='" +(x - adjustx).toString() + "' y='" +(y + adjusty).toString() + "' style='fill:" + textcolor + "; text-anchor:middle' class='icn3d-node-text12' >" + idStr + "</text>";

        html += "</g>";

        return html;
    }

    getCartoonData(type, node_link) { let ic = this.icn3d; ic.icn3dui;
       // get the nodes and links data
       let nodeArray = [], linkArray = [];
       let nodeStr, linkStr;

       nodeArray = node_link.node;

       // removed duplicated nodes
       let nodeJsonArray = [];
       let checkedNodeidHash = {};
       let cnt = 0;
       for(let i = 0, il = nodeArray.length; i < il; ++i) {
           let node = nodeArray[i];
           let nodeJson = JSON.parse(node);
           if(!checkedNodeidHash.hasOwnProperty(nodeJson.id)) {
               nodeJsonArray.push(nodeJson);
               checkedNodeidHash[nodeJson.id] = cnt;
               ++cnt;
           }
       }
       let nodeStrArray = [];
       for(let i = 0, il = nodeJsonArray.length; i < il; ++i) {
           let nodeJson = nodeJsonArray[i];
           nodeStrArray.push(JSON.stringify(nodeJson));
       }
       nodeStr = nodeStrArray.join(', ');
       // linkStr
       linkArray = node_link.link;
       linkStr = linkArray.join(', ');

       ic.hAtoms;
       let chemicalNodeStr = '';
       let hBondLinkStr = '', ionicLinkStr = '', halogenpiLinkStr = '', contactLinkStr = '',
         disulfideLinkStr = '', crossLinkStr = '';

//       contactLinkStr += ic.getGraphCls.getContactLinksForSet(ic.hAtoms, 'chain', true);

       let resStr = '{"nodes": [' + nodeStr + chemicalNodeStr + '], "links": [';
       resStr += linkStr + disulfideLinkStr + crossLinkStr + contactLinkStr + hBondLinkStr + ionicLinkStr + halogenpiLinkStr;

       let level = (node_link.level) ? node_link.level : '';
       resStr += '], "level": "' + level + '"}';
       return resStr;
    }

    // async getNodesLinksForSetCartoon(type) { let ic = this.icn3d, me = ic.icn3dui;
    //   await this.getNodesLinksForSetCartoonBase(type);
    // }

    projectTo2d(v3) { let ic = this.icn3d, me = ic.icn3dui;
        let v2 = v3.project( ic.cam );

        var realV3 = new THREE.Vector3();
        realV3.x = Math.round((v2.x + 1) * me.htmlCls.width2d * 0.5);
        realV3.y = Math.round((-v2.y) * me.htmlCls.width2d * 0.5);
        realV3.z = 0;

        if(realV3.y > 0) {
            realV3.y = me.htmlCls.width2d - realV3.y;
        }
        else {
            realV3.y = -realV3.y;
        }

        return realV3;
    }

    //async getNodesLinksForSetCartoonBase(type) { let ic = this.icn3d, me = ic.icn3dui;
    async getNodesLinksForSetCartoon(type) { let ic = this.icn3d, me = ic.icn3dui;
       let thisClass = this;

       let nodeArray = [], linkArray = [];
       let cnt = 0;
       let thickness = me.htmlCls.defaultValue; // 1

       let prevChain = '', prevResName = '', prevAtom, lastChain = '';
       let x, y;
       let bBegin = false, bEnd = true;
       let resName, residLabel;

       if(type == 'chain') {
           let chainidHash = {};
           for(let i in ic.hAtoms) {
               let atom = ic.atoms[i];
               if(atom.chain == 'DUM') continue;

               let chainid = atom.structure + '_' + atom.chain;

               if(ic.proteins.hasOwnProperty(i) || ic.nucleotides.hasOwnProperty(i)) {
                   if(!chainidHash.hasOwnProperty(chainid)) {
                       chainidHash[chainid] = {};
                   }
                   chainidHash[chainid][atom.serial] = atom;
               }
           }

           let min_max_center = ic.contactCls.getExtent(ic.atoms);

           let minX=9999, minY=9999, maxX=-9999, maxY=-9999, maxR = -9999;
           let itemArray = [];
           for(let chainid in chainidHash) {
               ic.hAtom = {};
               ic.hAtoms = me.hashUtilsCls.cloneHash(ic.chains[chainid]);

               let center_x_y_z = ic.axesCls.setPc1Axes();
               let center = center_x_y_z[0];
               let rx = center_x_y_z[1].distanceTo(center_x_y_z[0]);
               let ry = center_x_y_z[2].distanceTo(center_x_y_z[0]);
               let angle = new THREE.Vector2(center_x_y_z[1].x - center_x_y_z[0].x, center_x_y_z[1].y - center_x_y_z[0].y).angle() * 180 / Math.PI;
               if(angle > 180) angle -= 180;

               let serial = Object.keys(ic.hAtoms)[0];
               let atom = ic.atoms[serial];

               residLabel = chainid; //.substr(chainid.lastIndexOf('_') + 1); //chainid;
               //let shapeid = 0;

               center = this.projectTo2d(center);
               let x = center.x;
               let y = center.y;

               if(x < minX) minX = x;
               if(x > maxX) maxX = x;
               if(y < minY) minY = y;
               if(y > maxY) maxY = y;

               //let x = me.htmlCls.width2d * (center.x - min_max_center[0][0]) / (min_max_center[1][0] - min_max_center[0][0]);
               //let y = me.htmlCls.width2d * (center.y - min_max_center[0][1]) / (min_max_center[1][1] - min_max_center[0][1]);

               let factor = 0.5;
               rx = factor * me.htmlCls.width2d * rx / (min_max_center[1][0] - min_max_center[0][0]);
               ry = factor * me.htmlCls.width2d * ry / (min_max_center[1][1] - min_max_center[0][1]);

               if(rx > maxR) maxR = rx;
               if(ry > maxR) maxR = ry;

               itemArray.push({"id":chainid, "r":residLabel, "x":x, "y":y, "rx":rx, "ry":ry,
                 "ang":angle, "c":atom.color.getHexString()});
           }

           let offset = maxR + 2;
           let rangeX = maxX - minX, rangeY = maxY - minY;

           for(let i = 0, il = itemArray.length; i < il; ++i) {
               let item = itemArray[i];
               let x = (rangeX < 1) ? 0.5 * me.htmlCls.width2d : (item.x - minX) / rangeX * (me.htmlCls.width2d - 2 * offset) + offset;
               let y = (rangeY < 1) ? 0.5 * me.htmlCls.width2d : (item.y - minY) / rangeY * (me.htmlCls.width2d - 2 * offset) + offset;

               nodeArray.push('{"id": "' + item.id + '", "r": "' + item.r //+ '", "s": "' + setName
                   + '", "x": ' + x.toFixed(0) + ', "y": ' + y.toFixed(0)
                   + ', "rx": ' + item.rx.toFixed(0) + ', "ry": ' + item.ry.toFixed(0)
                   + ', "ang": ' + item.ang.toFixed(0) //+ ', "shape": ' + shapeid
                   + ', "c": "' + item.c.toUpperCase() + '"}');
           }

           ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);

           ic.node_link = {"node": nodeArray, "link":linkArray, "level": "chain"};
       }
       else if(type == 'domain') {
/*
           if(!ic.chainid2pssmid) { // mmtf data do NOT have the missing residues
                //$.when(ic.loadScriptCls.applyCommandAnnotationsAndCddSite('view annotations')).then(function() {
                    await ic.loadScriptCls.applyCommandAnnotationsAndCddSite('view annotations');
                    thisClass.getNodesLinksForDomains(ic.chainid2pssmid);
                    /// if(ic.deferredCartoonData !== undefined) ic.deferredCartoonData.resolve();
                    //return;
                //});
           }
           else {
               thisClass.getNodesLinksForDomains(ic.chainid2pssmid);
               /// if(ic.deferredCartoonData !== undefined) ic.deferredCartoonData.resolve();
               //return;
           }
*/

            if(!ic.chainid2pssmid) { // mmtf data do NOT have the missing residues
                await ic.loadScriptCls.applyCommandAnnotationsAndCddSite('view annotations');
            }

            thisClass.getNodesLinksForDomains(ic.chainid2pssmid);
       }
       else if(type == 'secondary') {
           ic.resi2resirange = {};
           let resiArray = [], tmpResName;

           ic.contactCls.getExtent(ic.atoms);

           let ss = '';

           let minX=9999, minY=9999, maxX=-9999, maxY=-9999, maxR = 2;
           let itemArray = [];
           for(let i in ic.hAtoms) {
               let atom = ic.atoms[i];
               if(atom.chain == 'DUM') continue;

               if((atom.ssbegin || atom.ssend) && atom.name == "CA" && atom.elem == "C") {
                   let resid = atom.structure + '_' + atom.chain + '_' + atom.resi;

                   //if((prevChain === '' || prevChain == atom.chain) && bEnd && atom.ssbegin) {
                   if(bEnd && atom.ssbegin) {
                       bBegin = true;
                       bEnd = false;

                       prevAtom = atom;

                       ss = (atom.ss == 'helix') ? 'H' : 'S';

                       resName = ss + atom.resi;
                       // add 1_1_ to match other conventionssuch as seq_div0_1KQ2_A_50
                       residLabel = '1_1_' + resid;

                       lastChain = atom.chain;
                   }

                   if(bBegin) {
                       tmpResName = me.utilsCls.residueName2Abbr(atom.resn) + atom.resi;
                       tmpResName += '__' + atom.chain;
                       if(Object.keys(ic.structures).length > 1) tmpResName += '__' + atom.structure;

                       resiArray.push(tmpResName);
                   }

                   if(lastChain == atom.chain && bBegin && atom.ssend) {
                       let v2a = this.projectTo2d(prevAtom.coord.clone());
                       let x1 = v2a.x;
                       let y1 = v2a.y;

                       let v2b = this.projectTo2d(atom.coord.clone());
                       let x2 = v2b.x;
                       let y2 = v2b.y;

                       x = 0.5 * (x1 + x2);
                       y = 0.5 * (y1 + y2);

                       // use half length of the helix or sheet to make the display clear
                       x1 = 0.5 * (x + x1);
                       y1 = 0.5 * (y + y1);
                       x2 = 0.5 * (x + x2);
                       y2 = 0.5 * (y + y2);

                       if(x1 < minX) minX = x1;
                       if(x1 > maxX) maxX = x1;
                       if(y1 < minY) minY = y1;
                       if(y1 > maxY) maxY = y1;

                       if(x2 < minX) minX = x2;
                       if(x2 > maxX) maxX = x2;
                       if(y2 < minY) minY = y2;
                       if(y2 > maxY) maxY = y2;

                       bBegin = false;
                       bEnd = true;

                       resName += '-' + atom.resi;
                       residLabel += '-' + atom.resi;

                       resName += '__' + atom.chain;
                       if(Object.keys(ic.structures).length > 1) resName += '__' + atom.structure;

                       for(let j = 0, jl = resiArray.length; j < jl; ++j) {
                           tmpResName = resiArray[j];
                           ic.resi2resirange[tmpResName] = resName;
                       }
                       resiArray = [];

                       if(cnt > 0 && prevChain == atom.chain) {
                           linkArray.push('{"source": "' + prevResName + '", "target": "' + resName
                               + '", "v": ' + thickness + ', "c": "' + prevAtom.color.getHexString().toUpperCase() + '"}');
                       }

                       itemArray.push({"id":resName, "r":residLabel, "ss":ss, "x":x, "y":y,
                         "x1":x1, "y1":y1, "x2":x2, "y2":y2, "c":atom.color.getHexString()});

                       prevChain = atom.chain;
                       prevResName = resName;
                       ++cnt;
                   }
               }
           } //end for

           let offset = maxR + 2;
           let rangeX = maxX - minX, rangeY = maxY - minY;

           for(let i = 0, il = itemArray.length; i < il; ++i) {
               let item = itemArray[i];
               let x = (rangeX < 1) ? 0.5 * me.htmlCls.width2d : (item.x - minX) / rangeX * (me.htmlCls.width2d - 2 * offset) + offset;
               let y = (rangeY < 1) ? 0.5 * me.htmlCls.width2d : (item.y - minY) / rangeY * (me.htmlCls.width2d - 2 * offset) + offset;
               let x1 = (rangeX < 1) ? 0.5 * me.htmlCls.width2d : (item.x1 - minX) / rangeX * (me.htmlCls.width2d - 2 * offset) + offset;
               let y1 = (rangeY < 1) ? 0.5 * me.htmlCls.width2d : (item.y1 - minY) / rangeY * (me.htmlCls.width2d - 2 * offset) + offset;
               let x2 = (rangeX < 1) ? 0.5 * me.htmlCls.width2d : (item.x2 - minX) / rangeX * (me.htmlCls.width2d - 2 * offset) + offset;
               let y2 = (rangeY < 1) ? 0.5 * me.htmlCls.width2d : (item.y2 - minY) / rangeY * (me.htmlCls.width2d - 2 * offset) + offset;

               nodeArray.push('{"id": "' + item.id + '", "r": "' + item.r
                   + '", "x": ' + x.toFixed(0) + ', "y": ' + y.toFixed(0)
                   + ', "x1": ' + x1.toFixed(0) + ', "y1": ' + y1.toFixed(0)
                   + ', "x2": ' + x2.toFixed(0) + ', "y2": ' + y2.toFixed(0)
                   + ', "c": "' + item.c.toUpperCase() + '"}');
           }

           ic.node_link = {"node": nodeArray, "link":linkArray, "level": "secondary"};
       }

       /// if(ic.deferredCartoonData !== undefined) ic.deferredCartoonData.resolve();
    }

    getNodesLinksForDomains(chainid2pssmid) { let ic = this.icn3d, me = ic.icn3dui;
       let nodeArray = [], linkArray = [];
       let thickness = me.htmlCls.defaultValue; // 1

       ic.resi2resirange = {};

       // find the chainids
       let chainidHash = {};
       for(let i in ic.hAtoms) {
           let atom = ic.atoms[i];
           if(atom.chain == 'DUM') continue;

           chainidHash[atom.structure + '_' + atom.chain] = 1;
       }

       let min_max_center = ic.contactCls.getExtent(ic.atoms);

       let minX=9999, minY=9999, maxX=-9999, maxY=-9999, maxR = -9999;
       let itemArray = [];

       // show domains for each chain
       for(let chainid in chainidHash) {
           if(!chainid2pssmid.hasOwnProperty(chainid)) continue;

           let pssmid2name = chainid2pssmid[chainid].pssmid2name;
           let pssmid2fromArray = chainid2pssmid[chainid].pssmid2fromArray;
           let pssmid2toArray = chainid2pssmid[chainid].pssmid2toArray;

           // sort the domains according to the starting residue number
           let pssmid2start = {};
           for(let pssmid in pssmid2name) {
               let fromArray = pssmid2fromArray[pssmid];
               pssmid2start[pssmid] = fromArray[0];
           }

           var pssmidArray = Object.keys(pssmid2start);
           pssmidArray.sort(function(a, b) {
               return pssmid2start[a] - pssmid2start[b]
           });
           let prevDomainName, prevAtom;
           //for(let pssmid in pssmid2name) {
           for(let i = 0, il = pssmidArray.length; i < il; ++i) {
               let pssmid = pssmidArray[i];

               let domainName = pssmid2name[pssmid];
               domainName += '__' + chainid.substr(chainid.indexOf('_') + 1);
               if(Object.keys(ic.structures).length > 1) domainName += '__' + chainid.substr(0, chainid.indexOf('_'));

               let fromArray = pssmid2fromArray[pssmid];
               let toArray = pssmid2toArray[pssmid];

               ic.hAtoms = {};
               for(let j = 0, jl = fromArray.length; j < jl; ++j) {
                   let resiStart = parseInt(fromArray[j]) + 1;
                   let resiEnd = parseInt(toArray[j]) + 1;

                   for(let k = resiStart; k <= resiEnd; ++k) {
                       ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.residues[chainid + '_' + k]);
                   }
               }

               if(Object.keys(ic.hAtoms).length == 0) continue;

               //let extent = ic.contactCls.getExtent(atomSet);

               //let radiusSq = (extent[1][0] - extent[0][0]) * (extent[1][0] - extent[0][0]) + (extent[1][1] - extent[0][1]) * (extent[1][1] - extent[0][1]) + (extent[1][2] - extent[0][2]) * (extent[1][2] - extent[0][2]);
               //let radius = Math.sqrt(radiusSq);

               let center_x_y_z = ic.axesCls.setPc1Axes();
               let center = center_x_y_z[0];
               let rx = center_x_y_z[1].distanceTo(center_x_y_z[0]);
               let ry = center_x_y_z[2].distanceTo(center_x_y_z[0]);
               let angle = new THREE.Vector2(center_x_y_z[1].x - center_x_y_z[0].x, center_x_y_z[1].y - center_x_y_z[0].y).angle() * 180 / Math.PI;
               if(angle > 180) angle -= 180;

               let serial = Object.keys(ic.hAtoms)[0];
               let atom = ic.atoms[serial];
               //let shapeid = 0;

               //let x = me.htmlCls.width2d * (center.x - min_max_center[0][0]) / (min_max_center[1][0] - min_max_center[0][0]);
               //let y = me.htmlCls.width2d * (center.y - min_max_center[0][1]) / (min_max_center[1][1] - min_max_center[0][1]);
               center = this.projectTo2d(center);
               let x = center.x;
               let y = center.y;

               if(x < minX) minX = x;
               if(x > maxX) maxX = x;
               if(y < minY) minY = y;
               if(y > maxY) maxY = y;

               let factor = 0.5;
               rx = factor * me.htmlCls.width2d * rx / (min_max_center[1][0] - min_max_center[0][0]);
               ry = factor * me.htmlCls.width2d * ry / (min_max_center[1][1] - min_max_center[0][1]);

               if(rx > maxR) maxR = rx;
               if(ry > maxR) maxR = ry;

               if(prevDomainName !== undefined) {
                   linkArray.push('{"source": "' + prevDomainName + '", "target": "' + domainName
                       + '", "v": ' + thickness + ', "c": "' + prevAtom.color.getHexString().toUpperCase() + '"}');
               }

               itemArray.push({"id":domainName, "from":fromArray + '', "to":toArray + '', "x":x, "y":y, "rx":rx, "ry":ry,
                 "ang":angle, "c":atom.color.getHexString()});

               prevDomainName = domainName;
               prevAtom = atom;
           }
       }

       let offset = maxR + 2;
       let rangeX = maxX - minX, rangeY = maxY - minY;

       for(let i = 0, il = itemArray.length; i < il; ++i) {
           let item = itemArray[i];
           let x = (rangeX < 1) ? 0.5 * me.htmlCls.width2d : (item.x - minX) / rangeX * (me.htmlCls.width2d - 2 * offset) + offset;
           let y = (rangeY < 1) ? 0.5 * me.htmlCls.width2d : (item.y - minY) / rangeY * (me.htmlCls.width2d - 2 * offset) + offset;

           nodeArray.push('{"id": "' + item.id
               + '", "from": "' + item.from + '", "to": "' + item.to
               + '", "x": ' + x.toFixed(0) + ', "y": ' + y.toFixed(0)
               + ', "rx": ' + item.rx.toFixed(0) + ', "ry": ' + item.ry.toFixed(0)
               + ', "ang": ' + item.ang.toFixed(0)
               + ', "c": "' + item.c.toUpperCase() + '"}');
       }

       ic.hAtoms = me.hashUtilsCls.cloneHash(ic.dAtoms);

       ic.node_link = {"node": nodeArray, "link":linkArray, "level": "domain"};

       //return {"node": nodeArray, "link":linkArray};
    }

    getSelection(idArray, from, to) { let ic = this.icn3d, me = ic.icn3dui;
        let atomSet = {};
        let residArray = [];

        let fromArray = from.toString().split(',');
        let toArray = to.toString().split(',');

        let structure = (idArray.length == 3) ? idArray[2] : Object.keys(ic.structures)[0];
        let chainidTmp = (idArray.length >= 2) ? structure + '_' + idArray[1] : Object.keys(ic.chains)[0];

        for(let i = 0, il = fromArray.length; i < il; ++i) {
            let from = parseInt(fromArray[i]) + 1;
            let to = parseInt(toArray[i]) + 1;
            for(let j = from; j <= to; ++j) {
                let resid = chainidTmp + '_' + j;
                atomSet = me.hashUtilsCls.unionHash(atomSet, ic.residues[resid]);
                residArray.push(resid);
            }
        }

        return {"atomSet": atomSet, "residArray": residArray};
    }

    click2Dcartoon() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        me.myEventCls.onIds("#" + me.pre + "2dctn_chain", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           thisClass.initCartoonSvg();

           //if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.cartoon2dCls.draw2Dcartoon('chain');
        });

        me.myEventCls.onIds("#" + me.pre + "2dctn_domain", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           thisClass.initCartoonSvg();

           //if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.cartoon2dCls.draw2Dcartoon('domain');
        });

        me.myEventCls.onIds("#" + me.pre + "2dctn_secondary", "click", async function(e) { let ic = me.icn3d;
           e.preventDefault();
           thisClass.initCartoonSvg();

           //if(!me.cfg.notebook) dialog.dialog( "close" );
           await ic.cartoon2dCls.draw2Dcartoon('secondary');
        });

        $(document).on("click", "#" + ic.pre + "dl_2dctn .icn3d-ctnode", function(e) { let ic = thisClass.icn3d;
            e.stopImmediatePropagation();
            if(Object.keys(ic.hAtoms).length < Object.keys(ic.atoms).length) ic.definedSetsCls.setMode('selection');

            //ic.bClickInteraction = false;

            let atomSet = {}, residArray = [], type;

            let id = $(this).attr('id');
            let chainid = $(this).attr('chainid');
            let from = $(this).attr('from');
            let to = $(this).attr('to');
            let x1 = $(this).attr('x1');

            if(chainid !== undefined) {
                type = 'chain';
                atomSet = ic.chains[chainid];
            }
            else if(from !== undefined) {
                type = 'domain';

                let idArray = id.split('__');
                let result = thisClass.getSelection(idArray, from, to);
                atomSet = result.atomSet;
                residArray = result.residArray;
            }
            else if(x1 !== undefined) {
                type = 'secondary';

                let idArray = id.split('__');
                let from_to = idArray[0].substr(1).split('-');
                let from = parseInt(from_to[0]) - 1; // 0-based
                let to = parseInt(from_to[1]) - 1;
                let result = thisClass.getSelection(idArray, from, to);
                atomSet = result.atomSet;
                residArray = result.residArray;
            }

            // clear all nodes
            if(!ic.bCtrl && !ic.bShift) {
                ic.selectionCls.removeSelection();

                // ic.lineArray2d is used to highlight lines in 2D diagram
                ic.lineArray2d = [];
            }
            if(ic.alnChains[chainid] !== undefined) 1.0 * Object.keys(ic.alnChains[chainid]).length / Object.keys(ic.chains[chainid]).length;

            if(!ic.bCtrl && !ic.bShift) {
                ic.hAtoms = me.hashUtilsCls.cloneHash(atomSet); //ic.chains[chainid]);
            }
            else {
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, atomSet); //ic.chains[chainid]);
            }

            // get the name array
            if(type == 'chain') {
                if(!ic.bCtrl && !ic.bShift) {
                    ic.chainArray2d = [chainid];
                }
                else {
                    if(ic.chainArray2d === undefined) ic.chainArray2d = [];
                    ic.chainArray2d.push(chainid);
                }

                ic.hlUpdateCls.updateHlAll(ic.chainArray2d);
            }
            else {
                ic.hlUpdateCls.updateHlAll();
            }

            // show selected chains in annotation window
            ic.annotationCls.showAnnoSelectedChains();

            let select = (type == 'chain') ? "select chain " + chainid : "select " + ic.resid2specCls.residueids2spec(residArray);
            me.htmlCls.clickMenuCls.setLogCmd(select, true);

            ic.bSelectResidue = false;
        });
    }

    initCartoonSvg() { let ic = this.icn3d, me = ic.icn3dui;
       ic.resizeRatioX = 1.0;
       ic.resizeRatioY = 1.0;
       $("#" + me.svgid_ct).empty();
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ResizeCanvas {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Resize the canvas with the defined "width" and "height".
    resizeCanvas(width, height, bForceResize, bDraw) {var ic = this.icn3d, me = ic.icn3dui;
      if( bForceResize || me.cfg.resize ) {
        //var heightTmp = parseInt(height) - me.htmlCls.EXTRAHEIGHT;
        let heightTmp = height;
        $("#" + ic.pre + "canvas").width(width).height(heightTmp);
        $("#" + ic.pre + "viewer").width(width).height(height);

        //$("div:has(#" + ic.pre + "canvas)").width(width).height(heightTmp);
        $("#" + ic.divid + " div:has(#" + ic.pre + "canvas)").width(width).height(heightTmp);

        ic.applyCenterCls.setWidthHeight(width, heightTmp);

        if(bDraw === undefined || bDraw) {
            ic.drawCls.draw();
        }
      }
    }

    windowResize() { let ic = this.icn3d, me = ic.icn3dui;
        let thisClass = this;

        if(me.cfg.resize && !me.utilsCls.isMobile() ) {
            $(window).resize(function() { let ic = thisClass.icn3d;
                //me.htmlCls.WIDTH = $( window ).width();
                //me.htmlCls.HEIGHT = $( window ).height();
                me.utilsCls.setViewerWidthHeight(ic.icn3dui);

                let width = me.htmlCls.WIDTH; // - me.htmlCls.LESSWIDTH_RESIZE;
                let height = me.htmlCls.HEIGHT; // - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT;

                if(ic !== undefined && !ic.bFullscreen) thisClass.resizeCanvas(width, height);
            });
        }
    }

    openFullscreen(elem) {var ic = this.icn3d, me = ic.icn3dui;
      if(me.bNode) return;

      if(!document.fullscreenElement && !document.mozFullScreenElement &&
        !document.webkitFullscreenElement && !document.msFullscreenElement) {
          if(elem.requestFullscreen) {
            elem.requestFullscreen();
          } else if(elem.mozRequestFullScreen) { /* Firefox */
            elem.mozRequestFullScreen();
          } else if(elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
            elem.webkitRequestFullscreen();
          } else if(elem.msRequestFullscreen) { /* IE/Edge */
            elem.msRequestFullscreen();
          }
      }
    }

    //Rotate the structure in one of the directions: "left", "right", "up", and "down".
    rotStruc(direction, bInitial) {var ic = this.icn3d; ic.icn3dui;
        let thisClass = this;

        if(ic.bStopRotate) return false;
        if(ic.transformCls.rotateCount > ic.transformCls.rotateCountMax) {
            // back to the original orientation
            ic.transformCls.resetOrientation();

            return false;
        }
        ++ic.transformCls.rotateCount;

        if(bInitial) {
            if(direction === 'left') {
              ic.ROT_DIR = 'left';
            }
            else if(direction === 'right') {
              ic.ROT_DIR = 'right';
            }
            else if(direction === 'up') {
              ic.ROT_DIR = 'up';
            }
            else if(direction === 'down') {
              ic.ROT_DIR = 'down';
            }
            else {
              return false;
            }
        }

        if(direction === 'left' && ic.ROT_DIR === 'left') {
          ic.transformCls.rotateLeft(1);
        }
        else if(direction === 'right' && ic.ROT_DIR === 'right') {
          ic.transformCls.rotateRight(1);
        }
        else if(direction === 'up' && ic.ROT_DIR === 'up') {
          ic.transformCls.rotateUp(1);
        }
        else if(direction === 'down' && ic.ROT_DIR === 'down') {
          ic.transformCls.rotateDown(1);
        }
        else {
          return false;
        }

        setTimeout(function(){ thisClass.rotStruc(direction); }, 100);
    }

    //Go back one step. Basically the commands are sequentially executed, but with one less step.
    async back() {var ic = this.icn3d; ic.icn3dui;
      ic.backForward = true;
      ic.STATENUMBER--;
      // do not add to the array ic.commands
      ic.bAddCommands = false;
      ic.bAddLogs = false; // turn off log
      ic.bNotLoadStructure = true;
      if(ic.STATENUMBER < 1) {
        ic.STATENUMBER = 1;
      }
      else {
        await ic.loadScriptCls.execCommands(0, ic.STATENUMBER-1, ic.STATENUMBER, true);
      }
      ic.setStyleCls.adjustIcon();
      ic.bAddCommands = true;
      ic.bAddLogs = true;
    }

    //Go forward one step. Basically the commands are sequentially executed, but with one more step.
    async forward() {var ic = this.icn3d; ic.icn3dui;
      ic.backForward = true;
      ic.STATENUMBER++;
      // do not add to the array ic.commands
      ic.bAddCommands = false;
      ic.bAddLogs = false; // turn off log
      ic.bNotLoadStructure = true;
      if(ic.STATENUMBER > ic.commands.length) {
        ic.STATENUMBER = ic.commands.length;
      }
      else {
        await ic.loadScriptCls.execCommands(0, ic.STATENUMBER-1, ic.STATENUMBER, true);
      }
      ic.setStyleCls.adjustIcon();
      ic.bAddCommands = true;
      ic.bAddLogs = true;
    }

    async replayon() {var ic = this.icn3d; ic.icn3dui;
      ic.CURRENTNUMBER = 0;
      ic.bReplay = 1;
      $("#" + ic.pre + "replay").show();

      if(ic.commands.length > 0) {
          await ic.loadScriptCls.replayFirstStep(ic.CURRENTNUMBER);

          //ic.resizeCanvasCls.closeDialogs();
      }
    }
    async replayoff() {var ic = this.icn3d; ic.icn3dui;
        ic.bReplay = 0;
        $("#" + ic.pre + "replay").hide();
        // replay all steps
        ++ic.CURRENTNUMBER;
        await ic.loadScriptCls.execCommands(ic.CURRENTNUMBER, ic.STATENUMBER-1, ic.STATENUMBER);
    }

    closeDialogs() {var ic = this.icn3d, me = ic.icn3dui;
        //let itemArray = ['dl_selectannotations', 'dl_alignment', 'dl_2ddgm', 'dl_definedsets', 'dl_graph',
        //    'dl_linegraph', 'dl_scatterplot', 'dl_contactmap', 'dl_allinteraction', 'dl_copyurl',
        //    'dl_symmetry', 'dl_symd', 'dl_rmsd', 'dl_legend', 'dl_disttable'];
        let itemArray = ['dl_2ddgm', 'dl_2dctn', 'dl_alignment', 'dl_sequence2', 'dl_definedsets', 'dl_setsmenu', 'dl_command', 'dl_setoperations', 'dl_vast', 'dl_foldseek', 'dl_mmtfid', 'dl_pdbid', 'dl_afid', 'dl_opmid', 'dl_pdbfile', 'dl_pdbfile_app', 'dl_rescolorfile', 'dl_customcolor', 'dl_align', 'dl_alignaf', 'dl_chainalign', 'dl_chainalign2', 'dl_chainalign3', 'dl_mutation', 'dl_mol2file', 'dl_sdffile', 'dl_xyzfile', 'dl_afmapfile', 'dl_urlfile', 'dl_mmciffile', 'dl_mmcifid', 'dl_mmdbid', 'dl_mmdbafid', 'dl_blast_rep_id', 'dl_yournote', 'dl_gi', 'dl_refseqid', 'dl_cid', 'dl_pngimage', 'dl_state', 'dl_fixedversion', 'dl_selection', 'dl_dsn6', 'dl_dsn6url', 'dl_clr', 'dl_symmetry', 'dl_symd', 'dl_contact', 'dl_hbonds', 'dl_realign', 'dl_realignbystruct', 'dl_allinteracton', 'dl_interactionsorted', 'dl_linegraph', 'dl_linegraphcolor', 'dl_scatterplot', 'dl_scatterploitcolor', 'dl_contactmap', 'dl_alignerrormap', 'dl_elecmap2fofc', 'dl_elecmapfofc', 'dl_emmap', 'dl_aroundsphere', 'dl_adjustmem', 'dl_selectplane', 'dl_addlabel', 'dl_addlabelselection', 'dl_labelColor', 'dl_distance', 'dl_stabilizer', 'dl_disttwosets', 'dl_distmanysets', 'dl_stabilizer_rm', 'dl_thickness', 'dl_thickness2', 'dl_addtrack', 'dl_addtrack_tabs', 'dl_saveselection', 'dl_copyurl', 'dl_selectannotations', 'dl_annotations_tabs', 'dl_anno_view_tabs', 'dl_annotations', 'dl_graph', 'dl_svgcolor', 'dl_area', 'dl_colorbyarea', 'dl_rmsd', 'dl_buriedarea', 'dl_propbypercentout', 'dl_propbybfactor', 'dl_legend', 'dl_disttable'];

        for(let i in itemArray) {
            let item = itemArray[i];
            if(!me.cfg.notebook) {
                if($('#' + ic.pre + item).hasClass('ui-dialog-content') && $('#' + ic.pre + item).dialog( 'isOpen' )) {
                    $('#' + ic.pre + item).dialog( 'close' ).remove();
                }
            }
            else {
                $('#' + ic.pre + item).hide();
            }
        }
        if(!me.cfg.notebook) this.resizeCanvas(me.htmlCls.WIDTH, me.htmlCls.HEIGHT, true);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Transform {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Set the orientation to the original one, but leave the style, color, etc alone.
    resetOrientation() { let ic = this.icn3d; ic.icn3dui;
        let bSet = false;
        if(ic.commands.length > 0) {
            let commandTransformation = ic.commands[0].split('|||');

            if(commandTransformation.length == 2) {
                let transformation = JSON.parse(commandTransformation[1]);

                ic._zoomFactor = transformation.factor;

                ic.mouseChange.x = transformation.mouseChange.x;
                ic.mouseChange.y = transformation.mouseChange.y;

                ic.quaternion._x = transformation.quaternion._x;
                ic.quaternion._y = transformation.quaternion._y;
                ic.quaternion._z = transformation.quaternion._z;
                ic.quaternion._w = transformation.quaternion._w;

                bSet = true;
            }
        }

        if(!bSet) {
            ic._zoomFactor = 1.0;
            ic.mouseChange = new THREE.Vector2(0,0);
            ic.quaternion = new THREE.Quaternion(0,0,0,1);
        }

        //reset ic.maxD
        ic.maxD = ic.oriMaxD;
        ic.center = ic.oriCenter.clone();

        if(ic.ori_chemicalbinding == 'show') {
            ic.bSkipChemicalbinding = false;
        }
        else if(ic.ori_chemicalbinding == 'hide') {
            ic.bSkipChemicalbinding = true;
        }
    }

    //Rotate the structure certain degree to the left, e.g., 5 degree.
    rotateLeft (degree) { let ic = this.icn3d, me = ic.icn3dui;
      let axis = new THREE.Vector3(0,1,0);
      let angle = -degree / 180.0 * Math.PI;

      if(ic.bControlGl && !me.bNode) {
          axis.applyQuaternion( window.cam.quaternion ).normalize();
      }
      else {
          axis.applyQuaternion( ic.cam.quaternion ).normalize();
      }

      let quaternion = new THREE.Quaternion();
      quaternion.setFromAxisAngle( axis, -angle );

      let para = {};
      para.quaternion = quaternion;
      para.update = true;

      if(ic.bControlGl && !me.bNode) {
          window.controls.update(para);
      }
      else {
          ic.controls.update(para);
      }

      if(ic.bRender) ic.drawCls.render();
    }

    //Rotate the structure certain degree to the right, e.g., 5 degree.
    rotateRight (degree) { let ic = this.icn3d, me = ic.icn3dui;
      let axis = new THREE.Vector3(0,1,0);
      let angle = degree / 180.0 * Math.PI;

      if(ic.bControlGl && !me.bNode) {
          axis.applyQuaternion( window.cam.quaternion ).normalize();
      }
      else {
          axis.applyQuaternion( ic.cam.quaternion ).normalize();
      }

      let quaternion = new THREE.Quaternion();
      quaternion.setFromAxisAngle( axis, -angle );

      let para = {};
      para.quaternion = quaternion;
      para.update = true;

      if(ic.bControlGl && !me.bNode) {
          window.controls.update(para);
      }
      else {
          ic.controls.update(para);
      }

      if(ic.bRender) ic.drawCls.render();
    }

    rotateUp (degree) { let ic = this.icn3d; ic.icn3dui;
        this.rotate_base(-degree);
    }

    //Rotate the structure certain degree to the bottom, e.g., 5 degree.
    rotateDown (degree) { let ic = this.icn3d; ic.icn3dui;
        this.rotate_base(degree);
    }

    //Rotate the structure certain degree to the top, e.g., 5 degree.
    rotate_base (degree) { let ic = this.icn3d, me = ic.icn3dui;
      let axis = new THREE.Vector3(1,0,0);
      let angle = degree / 180.0 * Math.PI;

      if(ic.bControlGl && !me.bNode) {
          axis.applyQuaternion( window.cam.quaternion ).normalize();
      }
      else {
          axis.applyQuaternion( ic.cam.quaternion ).normalize();
      }

      let quaternion = new THREE.Quaternion();
      quaternion.setFromAxisAngle( axis, -angle );

      let para = {};
      para.quaternion = quaternion;
      para.update = true;

      if(ic.bControlGl && !me.bNode) {
          window.controls.update(para);
      }
      else {
          ic.controls.update(para);
      }

      if(ic.bRender) ic.drawCls.render();
    }

    setRotation(axis, angle) { let ic = this.icn3d, me = ic.icn3dui;
      if(ic.bControlGl && !me.bNode && window.cam) {
          axis.applyQuaternion( window.cam.quaternion ).normalize();
      }
      else if(ic.cam) {
          axis.applyQuaternion( ic.cam.quaternion ).normalize();
      }

      let quaternion = new THREE.Quaternion();
      quaternion.setFromAxisAngle( axis, -angle );

      let para = {};
      para.quaternion = quaternion;
      para.update = true;

      if(ic.bControlGl && !me.bNode && window.controls) {
        window.controls.update(para);
      }
      else if(ic.controls) {
          ic.controls.update(para);
      }

      if(ic.bRender) ic.drawCls.render();
    }

    //Translate the structure certain distance to the left, e.g., "percentScreenSize" 1 means 1% of the screen width.
    translateLeft(percentScreenSize) {  let ic = this.icn3d; ic.icn3dui;
        this.translate_base(-percentScreenSize, 0);
    }

    //Translate the structure certain distance to the right, e.g., "percentScreenSize" 1 means 1% of the screen width.
    translateRight(percentScreenSize) {  let ic = this.icn3d; ic.icn3dui;
        this.translate_base(percentScreenSize, 0);
    }

    //Translate the structure certain distance to the top, e.g., "percentScreenSize" 1 means 1% of the screen height.
    translateUp(percentScreenSize) {  let ic = this.icn3d; ic.icn3dui;
        this.translate_base(0, -percentScreenSize);
    }

    //Translate the structure certain distance to the bottom, e.g., "percentScreenSize" 1 means 1% of the screen height.
    translateDown(percentScreenSize) {  let ic = this.icn3d; ic.icn3dui;
        this.translate_base(0, percentScreenSize);
    }

    translate_base(x, y) {  let ic = this.icn3d, me = ic.icn3dui;
      let mouseChange = new THREE.Vector2(0,0);

      mouseChange.x += x / 100.0;
      mouseChange.y += y / 100.0;

      let para = {};
      para.mouseChange = mouseChange;
      para.update = true;

      if(ic.bControlGl && !me.bNode) {
          window.controls.update(para);
      }
      else {
          ic.controls.update(para);
      }

      if(ic.bRender) ic.drawCls.render();
    }

    //Center on the selected atoms and zoom in.
    zoominSelection(atoms) { let ic = this.icn3d, me = ic.icn3dui;
       let para = {};

       para._zoomFactor = 1.0 / ic._zoomFactor;
       para.update = true;

       if(ic.bControlGl && !me.bNode) {
          if(window.controls) window.controls.update(para);
       }
       else {
          if(ic.controls) ic.controls.update(para);
       }

       if(atoms === undefined) {
           atoms = me.hashUtilsCls.hash2Atoms(ic.hAtoms, ic.atoms);
       }

       // center on the hAtoms if more than one residue is selected
       if(Object.keys(atoms).length > 1) {
               let centerAtomsResults = ic.applyCenterCls.centerAtoms(atoms);

               ic.maxD = centerAtomsResults.maxD;
               if (ic.maxD < 5) ic.maxD = 5;

               ic.center = centerAtomsResults.center;
               ic.applyCenterCls.setCenter(ic.center);

               // reset cameara
               ic.cameraCls.setCamera();
       }
    }

    getTransformationStr(transformation) {var ic = this.icn3d; ic.icn3dui;
        let transformation2 = {"factor": 1.0, "mouseChange": {"x": 0, "y": 0}, "quaternion": {"_x": 0, "_y": 0, "_z": 0, "_w": 1} };
        transformation2.factor = parseFloat(transformation.factor).toPrecision(4);
        transformation2.mouseChange.x = parseFloat(transformation.mouseChange.x).toPrecision(4);
        transformation2.mouseChange.y = parseFloat(transformation.mouseChange.y).toPrecision(4);
        transformation2.quaternion._x = parseFloat(transformation.quaternion._x).toPrecision(4);
        transformation2.quaternion._y = parseFloat(transformation.quaternion._y).toPrecision(4);
        transformation2.quaternion._z = parseFloat(transformation.quaternion._z).toPrecision(4);
        transformation2.quaternion._w = parseFloat(transformation.quaternion._w).toPrecision(4);

        if(transformation2.factor == '1.0000') transformation2.factor = 1;
        if(transformation2.mouseChange.x == '0.0000') transformation2.mouseChange.x = 0;
        if(transformation2.mouseChange.y == '0.0000') transformation2.mouseChange.y = 0;

        if(transformation2.quaternion._x == '0.0000') transformation2.quaternion._x = 0;
        if(transformation2.quaternion._y == '0.0000') transformation2.quaternion._y = 0;
        if(transformation2.quaternion._z == '0.0000') transformation2.quaternion._z = 0;
        if(transformation2.quaternion._w == '1.0000') transformation2.quaternion._w = 1;

        return JSON.stringify(transformation2);
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class SaveFile {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Save the state file or the image file with "filename". "type" is either "text" for state file or "png" for image file.

    //Five types are used: command, png, html, text, and binary. The type "command" is used to save the statefile.
    //The type "png" is used to save the current canvas image. The type "html" is used to save html file with the
    //"data". This can be used to save any text. The type "text" is used to save an array of text, where "data" is
    //actually an array. The type "binary" is used to save an array of binary, where "data" is actually an array.
    saveFile(filename, type, text) { let ic = this.icn3d, me = ic.icn3dui;
        //Save file
        let blob;

        if(type === 'command') {
            let dataStr =(ic.loadCmd) ? ic.loadCmd + '\n' : '';
            for(let i = 0, il = ic.commands.length; i < il; ++i) {
                let command = ic.commands[i].trim();
                if(i == il - 1) {
                   let command_tf = command.split('|||');

                   let transformation = {};
                   transformation.factor = ic._zoomFactor;
                   transformation.mouseChange = ic.mouseChange;
                   transformation.quaternion = ic.quaternion;

                   command = command_tf[0] + '|||' + ic.transformCls.getTransformationStr(transformation);
                }

                dataStr += command + '\n';
            }
            let data = decodeURIComponent(dataStr);

            blob = new Blob([data],{ type: "text;charset=utf-8;"});
        }
        else if(type === 'png') {
            //ic.scaleFactor = 1.0;
            let width = $("#" + ic.pre + "canvas").width();
            let height = $("#" + ic.pre + "canvas").height();
            ic.applyCenterCls.setWidthHeight(width, height);

            if(ic.bRender) ic.drawCls.render();

            let bAddURL = true;
            if(!window.File || !window.FileReader || !window.FileList || !window.Blob) {
                bAddURL = false;
            }

            if(me.utilsCls.isIE()) {
                blob = ic.renderer.domElement.msToBlob();

                if(bAddURL) {
                    let reader = new FileReader();
                    reader.onload = function(e) {
                        let arrayBuffer = e.target.result; // or = reader.result;

                        let text = ic.shareLinkCls.getPngText();

                        blob = me.convertTypeCls.getBlobFromBufferAndText(arrayBuffer, text);

                        //if(window.navigator.msSaveBlob) navigator.msSaveBlob(blob, filename);
                        saveAs(blob, filename);

                        return;
                    };

                    reader.readAsArrayBuffer(blob);
                }
                else {
                    //ic.createLinkForBlob(blob, filename);
                    saveAs(blob, filename);

                    return;
                }
            }
            else {
                ic.renderer.domElement.toBlob(function(data) {
                    if(bAddURL) {
                        let reader = new FileReader();
                        reader.onload = function(e) {
                            let arrayBuffer = e.target.result; // or = reader.result;

                            let text = ic.shareLinkCls.getPngText();

                            blob = me.convertTypeCls.getBlobFromBufferAndText(arrayBuffer, text);

                            //ic.createLinkForBlob(blob, filename);
                            saveAs(blob, filename);

                            return;
                        };

                        reader.readAsArrayBuffer(data);
                    }
                    else {
                        blob = data;

                        //ic.createLinkForBlob(blob, filename);
                        saveAs(blob, filename);

                        return;
                    }
                });
            }

            // reset the image size
            ic.scaleFactor = 1.0;
            ic.applyCenterCls.setWidthHeight(width, height);

            if(ic.bRender) ic.drawCls.render();
        }
        else if(type === 'html') {
            let dataStr = text;
            let data = decodeURIComponent(dataStr);

            blob = new Blob([data],{ type: "text/html;charset=utf-8;"});
        }
        else if(type === 'text') {
            //var dataStr = text;
            //var data = decodeURIComponent(dataStr);

            //blob = new Blob([data],{ type: "text;charset=utf-8;"});

            let data = text; // here text is an array of text

            blob = new Blob(data,{ type: "text;charset=utf-8;"});
        }
        else if(type === 'binary') {
            let data = text; // here text is an array of blobs

            //blob = new Blob([data],{ type: "application/octet-stream"});
            blob = new Blob(data,{ type: "application/octet-stream"});
        }

        if(type !== 'png') {
            //https://github.com/eligrey/FileSaver.js/
            saveAs(blob, filename);
        }
    }

    saveSvg(id, filename, bContactmap) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return '';
        
        let width = $("#" + id).width();
        let height = $("#" + id).height();

        if(bContactmap) height = width;

        let svgXml = this.getSvgXml(id, width, height, bContactmap);

        let blob = new Blob([svgXml], {type: "image/svg+xml"});
        saveAs(blob, filename);
    }

    getSvgXml(id, width, height, bContactmap) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return '';

        // font is not good
        let svg_data = document.getElementById(id).innerHTML; //put id of your svg element here

        let viewbox = (width && height) ? "<svg viewBox=\"0 0 " + width + " " + height + "\"" : "<svg";
        //let head = viewbox + " title=\"graph\" version=\"1.1\" xmlns:xl=\"http://www.w3.org/1999/xlink\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">";
        let head = viewbox + " title=\"graph\" xmlns:xl=\"http://www.w3.org/1999/xlink\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">";

        //if you have some additional styling like graph edges put them inside <style> tag
        let style = "<style>text {font-family: sans-serif; font-weight: bold; font-size: 18px;}</style>";

        let full_svg = head +  style + svg_data + "</svg>";

        return full_svg;
    }

    savePng(id, filename, bContactmap) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return '';

        let width = $("#" + id).width();
        let height = $("#" + id).height();

        if(bContactmap) height = width;

        // https://stackoverflow.com/questions/3975499/convert-svg-to-image-jpeg-png-etc-in-the-browser
        let svg = document.getElementById(id);
        let bbox = svg.getBBox();

        let copy = svg.cloneNode(true);
        ic.lineGraphCls.copyStylesInline(copy, svg);
        let canvas = document.createElement("CANVAS");
        canvas.width = width;
        canvas.height = height;

        let ctx = canvas.getContext("2d");
        ctx.clearRect(0, 0, bbox.width, bbox.height);

        let data = this.getSvgXml(id, width, height, bContactmap); //(new XMLSerializer()).serializeToString(copy); //ic.saveFileCls.getSvgXml();
        let DOMURL = window.URL || window.webkitURL || window;
        let svgBlob = new Blob([data], {type: "image/svg+xml;charset=utf-8"});

        let img = new Image();
        img.src = DOMURL.createObjectURL(svgBlob);

        img.onload = function() {
            ctx.drawImage(img, 0, 0);
            DOMURL.revokeObjectURL(this.src);

            if(me.utilsCls.isIE()) {
                let blob = canvas.msToBlob();

                if(blob) {
                    saveAs(blob, filename);

                    canvas.remove();
                }

                return;
            }
            else {
                canvas.toBlob(function(data) {
                    let blob = data;

                    if(blob) {
                        saveAs(blob, filename);

                        canvas.remove();
                    }

                    return;
                });
            }
        };
    }

    exportCustomAtoms(bDetails) {var ic = this.icn3d; ic.icn3dui;
       let html = "";
       let nameArray =(ic.defNames2Residues !== undefined) ? Object.keys(ic.defNames2Residues).sort() : [];
       for(let i = 0, il = nameArray.length; i < il; ++i) {
         let name = nameArray[i];
         let residueArray = ic.defNames2Residues[name];
         ic.defNames2Descr[name];
         let command = ic.defNames2Command[name];
         command = command.replace(/,/g, ', ');

         html += this.exportResidues(name, residueArray, bDetails);
       } // outer for
       nameArray =(ic.defNames2Atoms !== undefined) ? Object.keys(ic.defNames2Atoms).sort() : [];
       for(let i = 0, il = nameArray.length; i < il; ++i) {
         let name = nameArray[i];
         let atomArray = ic.defNames2Atoms[name];
         ic.defNames2Descr[name];
         let command = ic.defNames2Command[name];
         command = command.replace(/,/g, ', ');
         let residueArray = ic.resid2specCls.atoms2residues(atomArray);

         html += this.exportResidues(name, residueArray, bDetails);
       } // outer for
       return html;
    }

    exportResidues(name, residueArray, bDetails) {var ic = this.icn3d, me = ic.icn3dui;
         let html = '';

         if(residueArray.length > 0) {
             if(bDetails) {
                 let chainidHash = {};
                 for(let i = 0, il = residueArray.length; i < il; ++i) {
                     let resid = residueArray[i];
                     let atom = ic.firstAtomObjCls.getFirstAtomObj(ic.residues[resid]);
                     let chainid = atom.structure + '_' + atom.chain;
                     let resnAbbr = me.utilsCls.residueName2Abbr(atom.resn);
                     let resName = resnAbbr + atom.resi;

                     if(!chainidHash.hasOwnProperty(chainid)) {
                         chainidHash[chainid] = [];
                     }

                     chainidHash[chainid].push(resName);
                 }

                 html += name + ":\n";
                 for(let chainid in chainidHash) {
                     let resStr = (chainidHash[chainid].length == 1) ? "residue" : "residues";
                     html += chainid + " (" + chainidHash[chainid].length + " " + resStr + "): ";
                     html += chainidHash[chainid].join(", ");
                     html += "\n";
                 }
                 html += "\n";
             }
             else {
                 html += name + "\tselect ";
                 html += ic.resid2specCls.residueids2spec(residueArray);
                 html += "\n";
             }
         }

         return html;
    }

    //getAtomPDB: function(atomHash, bPqr, bPdb, bNoChem) { let ic = this.icn3d, me = ic.icn3dui;
    getAtomPDB(atomHash, bPqr, bNoChem, bNoHeader, chainResi2pdb, pdbid) { let ic = this.icn3d, me = ic.icn3dui;
        let pdbStr = '';

        // get all phosphate groups in lipids
        let phosPHash = {}, phosOHash = {};
        for(let i in ic.chemicals) {
            let atom = ic.atoms[i];
            if(atom.elem == 'P') {
                phosPHash[i] = 1;

                for(let j = 0, jl = atom.bonds.length; j < jl; ++j) {
                    let serial = atom.bonds[j];
                    if(serial && ic.atoms[serial].elem == 'O') { // could be null
                        phosOHash[serial] = 1;
                    }
                }
            }
        }
    /*
    HELIX    1  NT MET A    3  ALA A   12  1                                  10
            let startChain =(line.substr(19, 1) == ' ') ? 'A' : line.substr(19, 1);
            let startResi = parseInt(line.substr(21, 4));
            let endResi = parseInt(line.substr(33, 4));
    SHEET    1  B1 2 GLY A  35  THR A  39  0
            let startChain =(line.substr(21, 1) == ' ') ? 'A' : line.substr(21, 1);
            let startResi = parseInt(line.substr(22, 4));
            let endResi = parseInt(line.substr(33, 4));
    */

        let calphaHash = me.hashUtilsCls.intHash(atomHash, ic.calphas);
        let helixStr = 'HELIX', sheetStr = 'SHEET';
        let bHelixBegin = false, bHelixEnd = true;
        let bSheetBegin = false, bSheetEnd = true;

        let stru2header = {};
        for(let stru in ic.structures) {
            stru2header[stru] = '';
        }

//        if(!bNoSs) {
            let prevResi, stru;
            for(let i in calphaHash) {
                let atom = ic.atoms[i];
                stru = atom.structure;

                if(atom.ssbegin) {
                    if(atom.ss == 'helix') {
                        bHelixBegin = true;
                        if(bHelixEnd) stru2header[stru] += helixStr.padEnd(15, ' ') + atom.resn.padStart(3, ' ') + atom.chain.replace(/_/gi, '').substr(0, 2).padStart(2, ' ')
                            + atom.resi.toString().padStart(5, ' ');
                        bHelixEnd = false;
                        prevResi = atom.resi;
                    }
                    else if(atom.ss == 'sheet') {
                        bSheetBegin = true;
                        if(bSheetEnd) stru2header[stru] += sheetStr.padEnd(17, ' ') + atom.resn.padStart(3, ' ') + atom.chain.replace(/_/gi, '').substr(0, 2).padStart(2, ' ')
                            + atom.resi.toString().padStart(4, ' ');
                        bSheetEnd = false;
                    }
                }

                if(atom.ssend) {
                    if(atom.ss == 'helix') {
                        bHelixEnd = true;
                        let helixLen = ic.resid2ncbi[atom.resi] - ic.resid2ncbi[prevResi];
                        let helixType = 1;
                        if(bHelixBegin) stru2header[stru] += atom.resn.padStart(5, ' ') + atom.chain.replace(/_/gi, '').substr(0, 2).padStart(2, ' ')
                            + atom.resi.toString().padStart(5, ' ') + '  ' + helixType + helixLen.toString().padStart(36, ' ') + '\n';
                        bHelixBegin = false;
                    }
                    else if(atom.ss == 'sheet') {
                        bSheetEnd = true;
                        let sense = 0;
                        if(bSheetBegin) stru2header[stru] += atom.resn.padStart(5, ' ') + atom.chain.replace(/_/gi, '').substr(0, 2).padStart(2, ' ')
                            + atom.resi.toString().padStart(4, ' ') + '  ' + sense + '\n';
                        bSheetBegin = false;
                    }
                }
            }
            // add a new line in case the structure is a subset
            stru2header[stru] += '\n';
//        }

        // export assembly symmetry matrix "BIOMT"
        if(ic.biomtMatrices) {
            let stru = Object.keys(ic.structures)[0];
            for(let m = 0, ml = ic.biomtMatrices.length; m < ml; ++m) {
                let mNum = m + 1;
                for(let n = 0; n < 3; ++n) {
                    let nNum = n + 1;
                    stru2header[stru] += "REMARK 350   BIOMT" + nNum.toString() + "  " + mNum.toString().padStart(2, ' ')
                        + " " + ic.biomtMatrices[m].elements[n + 0].toString().padStart(9, ' ')
                        + " " + ic.biomtMatrices[m].elements[n + 4].toString().padStart(9, ' ')
                        + " " + ic.biomtMatrices[m].elements[n + 8].toString().padStart(9, ' ')
                        + " " + ic.biomtMatrices[m].elements[n + 12].toString().padStart(14, ' ') + "\n";
                }
            }
        }

        // add missing residues "REMARK 465..."
        for(let chainid in ic.chainMissingResidueArray) {
            let pos = chainid.indexOf('_');
            let chain = chainid.substr(pos + 1, 2);
            let stru = chainid.substr(0, pos);

            for(let i = 0, il = ic.chainMissingResidueArray[chainid].length; i < il; ++i) {
                let resi = ic.chainMissingResidueArray[chainid][i].resi;
                let resn = me.utilsCls.residueAbbr2Name(ic.chainMissingResidueArray[chainid][i].name);

                stru2header[stru] += "REMARK 465     " + resn.padStart(3, " ") + chain.padStart(2, " ") + " " + resi.toString().padStart(5, " ") + "\n";
            }
        }

        let connStr = '';
        let struArray = Object.keys(ic.structures);
        let bMulStruc =(struArray.length > 1) ? true : false;

        let molNum = 1, prevStru = '';
        //pdbStr += '\n';

        let addedChainResiHash = {};
        for(let i in atomHash) {
            let atom = ic.atoms[i];

            // remove chemicals
            if(bNoChem && atom.het) continue;

            //if(bMulStruc && atom.structure != prevStru) {
            if(atom.structure != prevStru) {
                pdbStr += connStr;
                connStr = '';

                if(molNum > 1)  pdbStr += '\nENDMDL\n';

                if(bMulStruc) pdbStr += 'MODEL        ' + molNum + '\n';

                // add header            
                let mutantInfo = (chainResi2pdb) ? "Mutated chain_residue " + Object.keys(chainResi2pdb) + '; ' : '';
                if(!bNoHeader) pdbStr += this.getPDBHeader(molNum - 1, stru2header, mutantInfo, pdbid);

                prevStru = atom.structure;
                ++molNum;
            }

            let chainResi = atom.chain + '_' + atom.resi;
            if(chainResi2pdb && chainResi2pdb.hasOwnProperty(chainResi)) {
                if(!addedChainResiHash.hasOwnProperty(chainResi)) {
                    pdbStr += chainResi2pdb[chainResi];
                    addedChainResiHash[chainResi] = 1;
                }
                continue;
            }

            let line = '';
    /*
    1 - 6 Record name "ATOM "
    7 - 11 Integer serial Atom serial number.
    13 - 16 Atom name Atom name.
    17 Character altLoc Alternate location indicator.
    18 - 20 Residue name resName Residue name.
    22 Character chainID Chain identifier.
    23 - 26 Integer resSeq Residue sequence number.
    27 AChar iCode Code for insertion of residues.
    31 - 38 Real(8.3) x Orthogonal coordinates for X in
    Angstroms.
    39 - 46 Real(8.3) y Orthogonal coordinates for Y in
    Angstroms.
    47 - 54 Real(8.3) z Orthogonal coordinates for Z in
    Angstroms.
    55 - 60 Real(6.2) occupancy Occupancy.
    61 - 66 Real(6.2) tempFactor Temperature factor.
    73 - 76 LString(4) segID Segment identifier, left-justified.
    77 - 78 LString(2) element Element symbol, right-justified.
    79 - 80 LString(2) charge Charge on the atom.
    */
            line +=(atom.het) ? 'HETATM' : 'ATOM  ';
            line += i.toString().padStart(5, ' ');
            line += ' ';

            let atomName = atom.name.trim();
            if(!isNaN(atomName.substr(0, 1)) ) atomName = atomName.substr(1) + atomName.substr(0, 1);

            if(atomName.length == 4) {
                line += atomName;
            }
            else {
                line += ' ';
                atomName = atomName.replace(/\*/g, "'");
                if(atomName == 'O1P') atomName = 'OP1';
                else if(atomName == 'O2P') atomName = 'OP2';
                else if(atomName == 'C5M') atomName = 'C7 ';
                line += atomName.padEnd(3, ' ');
            }

            line += ' ';
            let resn = atom.resn;
    /*
            // add "D" in front of nucleotide residue names
            if(resn == 'A') resn = 'DA';
            else if(resn == 'T') resn = 'DT';
            else if(resn == 'C') resn = 'DC';
            else if(resn == 'G') resn = 'DG';
            else if(resn == 'U') resn = 'DU';
    */

            line +=(resn.length <= 3) ? resn.padStart(3, ' ') : resn.substr(0, 3);
            //line += ' ';
            //line +=(atom.chain.length <= 1) ? atom.chain.padStart(1, ' ') : atom.chain.substr(0, 1);
            if(atom.chain.length >= 2) {
                let chainTmp = atom.chain.replace(/_/gi, '').substr(0, 2);
                line += chainTmp;
            }
            else if(atom.chain.length == 1) {
                line += ' ' + atom.chain.substr(0, 1);
            }
            else if(atom.chain.length == 0) {
                line += ' A';
            }

            let resi = atom.resi;
            if(!isNaN(resi) && atom.chain.length > 3 && !isNaN(atom.chain.substr(3)) ) { // such as: chain = NAG2, resi=1 => chain = NAG, resi=2
                resi = resi - 1 + parseInt(atom.chain.substr(3));
            }
            let resiInt = parseInt(resi);
            line +=(resiInt.toString().length <= 4) ? resiInt.toString().padStart(4, ' ') : resiInt.toString().substr(0, 4);
            //line += ' '.padStart(4, ' ');
            // insert
            let lastChar = resi.toString().substr(resi.toString().length - 1, 1);
            if(isNaN(lastChar)) {
                line += lastChar;
            }
            else {
                line += ' ';
            }
            line += ' '.padStart(3, ' ');

            line += atom.coord.x.toFixed(3).toString().padStart(8, ' ');
            line += atom.coord.y.toFixed(3).toString().padStart(8, ' ');
            line += atom.coord.z.toFixed(3).toString().padStart(8, ' ');

            //if((bPqr && atom.het) ||(phosPHash.hasOwnProperty(i) && !bPdb) ||(phosOHash.hasOwnProperty(i) && !bPdb) ) {
            //if((bPqr && atom.het) ||(phosPHash.hasOwnProperty(i)) ||(phosOHash.hasOwnProperty(i)) ) {
            if(bPqr && atom.het) {
                let size = 1.5, charge = 0;

    /*
                // use antechamber atom size
                if(atom.elem == 'C') size = 1.7; //1.9080;
                else if(atom.elem == 'N') size = 1.55; //1.8240;
                else if(atom.elem == 'O') size = 1.52; //1.6612;
                else if(atom.elem == 'H') size = 1.2; //1.2500;
                else if(atom.elem == 'S') size = 1.8; //2.0000;
                else if(atom.elem == 'P') size = 1.8; //2.1000;
                else if(me.parasCls.vdwRadii.hasOwnProperty(atom.elem)) {
                    size = me.parasCls.vdwRadii[atom.elem];
                }
    */

                // use amber atom size
                if(atom.elem == 'C') size = 1.9080;
                else if(atom.elem == 'N') size = 1.8240;
                else if(atom.elem == 'O') size = 1.6612;
                else if(atom.elem == 'H') size = 1.2500;
                else if(atom.elem == 'S') size = 2.0000;
                else if(atom.elem == 'P') size = 2.1000;
                else if(me.parasCls.vdwRadii.hasOwnProperty(atom.elem)) {
                    size = me.parasCls.vdwRadii[atom.elem];
                }

                if(me.cfg.cid !== undefined && atom.crg !== undefined) {
                    charge = atom.crg;
                }
                else if(phosPHash.hasOwnProperty(i)) {
                    charge = 1.3800; // P in phosphate
                }
                else if(phosOHash.hasOwnProperty(i)) {
                    charge = -0.5950; // O in phosphate
                }
                else if(me.parasCls.ionCharges.hasOwnProperty(atom.elem)) {
                    charge = me.parasCls.ionCharges[atom.elem];
                }

                line += charge.toFixed(4).toString().padStart(8, ' ');
                line += size.toFixed(4).toString().padStart(7, ' ');
            }
            else {
                line += "1.00".padStart(6, ' ');
                line +=(atom.b) ? parseFloat(atom.b).toFixed(2).toString().padStart(6, ' ') : ' '.padStart(6, ' ');
                line += ' '.padStart(10, ' ');
                line += atom.elem.padStart(2, ' ');
                line += ' '.padStart(2, ' ');
            }

            // connection info
            if(atom.het && atom.bonds.length > 0) {
                connStr += 'CONECT' + i.toString().padStart(5, ' ');
                let bondHash = {};
                for(let j = 0, jl = atom.bonds.length; j < jl; ++j) {
                    if(atom.bonds[j] && !bondHash.hasOwnProperty(atom.bonds[j])) { // could be null
                        connStr += atom.bonds[j].toString().padStart(5, ' ');
                        bondHash[atom.bonds[j]] = 1;
                    }
                }
                connStr += '\n';
            }

            pdbStr += line + '\n';
        }

        pdbStr += connStr;

        if(bMulStruc) pdbStr += '\nENDMDL\n';

        return pdbStr;
    }

    getSecondary(atomHash) { let ic = this.icn3d, me = ic.icn3dui;
        let json = '{"data": [\n';

        let prevChainid = '', prevResi = '';
        let data = {};
        for(let i in atomHash) {
            let atom = ic.atoms[i];

            let chainid = atom.structure + '_' + atom.chain;
            let resi = atom.resi;
            let resn = me.utilsCls.residueName2Abbr(atom.resn);
            let ss = this.secondary2Abbr(atom.ss);

            if(chainid != prevChainid) {
                data[chainid] = {"resi": [], "resn": [], "secondary": []};
            }

            if(chainid != prevChainid || resi != prevResi) {
                data[chainid]["resi"].push(resi);
                data[chainid]["resn"].push(resn);
                data[chainid]["secondary"].push(ss);
            }

            prevChainid = chainid;
            prevResi = resi;
        }

        let chainidArray = Object.keys(data);
        let cnt = chainidArray.length;
        for(let i = 0; i < cnt; ++i) {
            let chainid = chainidArray[i];
            json += '{"chain": "' + chainid + '",\n';

            json += '"resi": "' + data[chainid]["resi"].join(',') + '",\n';
            json += '"resn": "' + data[chainid]["resn"].join('') + '",\n';
            json += '"secondary": "' + data[chainid]["secondary"].join('') + '"';

            if(i < cnt - 1) {
                json += '},\n';
            }
            else {
                json += '}\n';
            }
        }

        json += ']}\n';

        return json;
    }

    secondary2Abbr(ss) { let ic = this.icn3d; ic.icn3dui;
        if(ss == 'helix') {
            return 'H';
        }
        else if(ss == 'sheet') {
            return 'E';
        }
        else {
            return 'c';
        }
    }

    getSelectedResiduePDB() { let ic = this.icn3d, me = ic.icn3dui;
       let pdbStr = '';
///       pdbStr += this.getPDBHeader();

       let atoms = me.hashUtilsCls.intHash(ic.dAtoms, ic.hAtoms);
       pdbStr += this.getAtomPDB(atoms);

       return pdbStr;
    }
    getPDBHeader(struNum, stru2header, mutantInfo, pdbid) { let ic = this.icn3d; ic.icn3dui;
       if(struNum === undefined) struNum = 0;

       let pdbStr = '';
       let stru = (pdbid) ? pdbid : Object.keys(ic.structures)[struNum];
       let id = (mutantInfo) ? stru + '2' : stru;
       pdbStr += 'HEADER    PDB From iCn3D'.padEnd(62, ' ') + id + '\n';

       if(struNum == 0) {
           let title =(ic.molTitle.length > 50) ? ic.molTitle.substr(0,47) + '...' : ic.molTitle;
           // remove quotes
           if(title.indexOf('"') != -1) title = '';
           if(mutantInfo) {
               title = mutantInfo + title;
           }
           pdbStr += 'TITLE     ' + title + '\n';
       }

       if(stru2header && stru2header[stru]) {
           pdbStr += stru2header[stru];
       }

       return pdbStr;
    }

    //Show the title and PDB ID of the PDB structure at the beginning of the viewer.
    showTitle() {var ic = this.icn3d, me = ic.icn3dui;
        if(ic.molTitle !== undefined && ic.molTitle !== '') {
            let title = ic.molTitle;

            let titlelinkColor =(ic.opts['background'] == 'black') ?  me.htmlCls.GREYD : 'black';

            if(ic.inputid === undefined) {
                if(ic.molTitle.length > 40) title = ic.molTitle.substr(0, 40) + "...";

                $("#" + ic.pre + "title").html(title);
            }
            else if(me.cfg.cid !== undefined) {
                let url = this.getLinkToStructureSummary();

                $("#" + ic.pre + "title").html("PubChem CID <a id='" + ic.pre + "titlelink' href='" + url + "' style='color:" + titlelinkColor + "' target='_blank'>" + ic.inputid.toUpperCase() + "</a>: " + title);
            }
            else if(me.cfg.align !== undefined) {
                title = 'VAST+ alignment of ' + Object.keys(ic.structures);

                $("#" + ic.pre + "title").html(title);
            }
            else if(me.cfg.chainalign !== undefined) {
                let chainidArray = me.cfg.chainalign.split(',');
                title = 'Dynamic Structure Alignment of Chain ' + chainidArray[0] + ' to Chain ' + chainidArray[1];

                $("#" + ic.pre + "title").html(title);
            }
            else if(me.cfg.mmdbafid !== undefined) {
                let structureArray = me.cfg.mmdbafid.split(',');
                if(structureArray.length > 1) {
                    title = 'Multiple structures: ' + structureArray;
                    $("#" + ic.pre + "title").html(title);
                }
                else if(structureArray.length == 1) {
                    let url = this.getLinkToStructureSummary();
                    this.setStructureTitle(url, title, titlelinkColor);
                }
            }
            else {
                let url = this.getLinkToStructureSummary();
                this.setStructureTitle(url, title, titlelinkColor);
            }
        }
        else {
            $("#" + ic.pre + "title").html("");
        }
    }

    setStructureTitle(url, title, titlelinkColor) {var ic = this.icn3d, me = ic.icn3dui;
        if(ic.molTitle.length > 40) title = ic.molTitle.substr(0, 40) + "...";

        //var asymmetricStr =(ic.bAssemblyUseAsu) ? "(Asymmetric Unit)" : "";
        let asymmetricStr = "";

        let idName = (isNaN(ic.inputid) && ic.inputid.length > 5) ? "AlphaFold ID" : "PDB ID";
        if(me.cfg.refseqid) idName = 'NCBI Protein Acc.';

        $("#" + ic.pre + "title").html(idName + " <a id='" + ic.pre + "titlelink' href='" + url + "' style='color:" + titlelinkColor + "' target='_blank'>" + ic.inputid.toUpperCase() + "</a>" + asymmetricStr + ": " + title);
    }

    getLinkToStructureSummary(bLog) {var ic = this.icn3d, me = ic.icn3dui;
       let url = "https://www.ncbi.nlm.nih.gov/structure/?term=";

       if(me.cfg.cid !== undefined) {
           url = "https://www.ncbi.nlm.nih.gov/pccompound/?term=";
       }
       else if(me.cfg.refseqid !== undefined) {
        url = "https://www.ncbi.nlm.nih.gov/protein/";
       }
       else if(me.cfg.afid !== undefined) {
           url = "https://alphafold.ebi.ac.uk/search/text/";
       }
       else {
           //if(ic.inputid.indexOf(",") !== -1) {
           if(Object.keys(ic.structures).length > 1) {
               url = "https://www.ncbi.nlm.nih.gov/structure/?term=";
           }
           else {
               //url = "https://www.ncbi.nlm.nih.gov/Structure/mmdb/mmdbsrv.cgi?uid=";
               url = me.htmlCls.baseUrl + "pdb/";
           }
       }

       if(ic.inputid === undefined) {
           url = "https://www.ncbi.nlm.nih.gov/pccompound/?term=" + ic.molTitle;
       }
       else {
           let idArray = ic.inputid.split('_');

           if(idArray.length === 1) {
               url += ic.inputid;
               if(bLog) me.htmlCls.clickMenuCls.setLogCmd("link to " + ic.inputid + ": " + url, false);
           }
           else if(idArray.length === 2) {
                if(me.cfg.afid) {
                    url += idArray[0] + " " + idArray[1];
                }
                else {
                    url += idArray[0] + " OR " + idArray[1];
                }

                if(bLog) me.htmlCls.clickMenuCls.setLogCmd("link to structures " + idArray[0] + " and " + idArray[1] + ": " + url, false);
           }
       }

       return url;
    }

    setEntrezLinks(db) {var ic = this.icn3d, me = ic.icn3dui;
      let structArray = Object.keys(ic.structures);
      let url;
      if(structArray.length === 1) {
          url = "https://www.ncbi.nlm.nih.gov/" + db + "/?term=" + structArray[0];
          me.htmlCls.clickMenuCls.setLogCmd("Entrez " + db + " about PDB " + structArray[0] + ": " + url, false);
          let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
          window.open(url, urlTarget);
      }
      else if(structArray.length === 2) {
          url = "https://www.ncbi.nlm.nih.gov/" + db + "/?term=" + structArray[0] + " OR " + structArray[1];
          me.htmlCls.clickMenuCls.setLogCmd("Entrez " + db + " about PDB " + structArray[0] + " OR " + structArray[1] + ": " + url, false);
          let urlTarget = (ic.structures && Object.keys(ic.structures).length > 0) ? '_blank' : '_self';
          window.open(url, urlTarget);
      }
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ShareLink {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Generate a URL to capture the current state and open it in a new window. Basically the state
    //file (the comand history) is concatenated in the URL to show the current state.
    async shareLink(bPngHtml, bPngOnly) { let ic = this.icn3d, me = ic.icn3dui;
        let url = this.shareLinkUrl();
        let bTooLong =(url.length > 4000 || url.indexOf('http') !== 0) ? true : false;
        if(bPngHtml) url += "&random=" + parseInt(Math.random() * 1000); // generate a new shorten URL and thus image name everytime
        //var inputid =(ic.inputid) ? ic.inputid : "custom";
        let inputid = Object.keys(ic.structures).join('_');
        if(inputid == 'stru') {
            if(ic.filename) {
                inputid = ic.filename;
            }
            else if(ic.inputid) {
                inputid = ic.inputid;
            }
        }

        if(!bPngHtml) {
            if(ic.bInputfile && !ic.bInputUrlfile) {
                alert("Share Link does NOT work when the data are from custom files. Please save 'iCn3D PNG Image' in the File menu and open it in iCn3D.");
                return;
            }
            if(bTooLong) {
                alert("The url is more than 4000 characters and may not work. Please save 'iCn3D PNG Image' or 'State File' and open them in iCn3D.");
                return;
            }
            me.htmlCls.clickMenuCls.setLogCmd("share link: " + url, false);
        }
        else {
            if(bPngOnly || ic.bInputfile || bTooLong) {
                ic.saveFileCls.saveFile(inputid + '_icn3d_loadable.png', 'png');
                return;
            }
        }

        let data = await this.getShareLinkPrms(url, bPngHtml);

        let shorturl = 'Problem in getting shortened URL';
        if(data.shortLink !== undefined) {
            shorturl = data.shortLink;
            if(bPngHtml) { // save png and corresponding html
                let strArray = shorturl.split("/");
                let shortName = strArray[strArray.length - 1];
                ic.saveFileCls.saveFile(inputid + '-' + shortName + '.png', 'png');
                let text = '<div style="float:left; border: solid 1px #0000ff; padding: 5px; margin: 10px; text-align:center;">';
                text += '<a href="https://structure.ncbi.nlm.nih.gov/icn3d/share.html?' + shortName + '" target="_blank">';
                text += '<img style="height:300px" src ="' + inputid + '-' + shortName + '.png"><br>\n';
                text += '<!--Start of your comments==================-->\n';
                let yournote =(ic.yournote) ? ': ' + ic.yournote.replace(/\n/g, "<br>").replace(/; /g, ", ") : '';
                text += 'PDB ' + inputid.toUpperCase() + yournote + '\n';
                text += '<!--End of your comments====================-->\n';
                text += '</a>';
                text += '</div>\n\n';
                ic.saveFileCls.saveFile(inputid + '-' + shortName + '.html', 'html', text);
            }
        }
        if(bPngHtml && data.shortLink === undefined) {
            ic.saveFileCls.saveFile(inputid + '_icn3d_loadable.png', 'png');
        }
        //shorturl: https://icn3d.page.link/NvbAh1Vmiwc4bgX87
        let urlArray = shorturl.split('page.link/');
        //if(urlArray.length == 2) shorturl = me.htmlCls.baseUrl + 'icn3d/share.html?' + urlArray[1];
        // When the baseURL is structure.ncbi.nlm.nih.gov, mmcifparser.cgi has a problem to past posted data in Mac/iphone
        // So the base URL is still www.ncbi.nlm.nih.gov/Structure,just use short URL here
        if(urlArray.length == 2) shorturl = 'https://structure.ncbi.nlm.nih.gov/icn3d/share.html?' + urlArray[1];
        $("#" + ic.pre + "ori_url").val(url);
        $("#" + ic.pre + "short_url").val(shorturl);
        $("#" + ic.pre + "short_url_title").val(shorturl + '&t=' + ic.yournote);
        if(!bPngHtml) me.htmlCls.dialogCls.openDlg('dl_copyurl', 'Copy a Share Link URL');
    }

    getShareLinkPrms(url, bPngHtml) { let ic = this.icn3d, me = ic.icn3dui;
        //https://firebase.google.com/docs/dynamic-links/rest
        //Web API Key: AIzaSyBxl9CgM0dY5lagHL4UOhEpLWE1fuwdnvc
        let fdlUrl = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=AIzaSyBxl9CgM0dY5lagHL4UOhEpLWE1fuwdnvc";
        return new Promise(function(resolve, reject) {
            $.ajax({
                url: fdlUrl,
                type: 'POST',
                //data : {'longDynamicLink': 'https://d55qc.app.goo.gl/?link=' + url, "suffix": {"option": "SHORT"}},
                //data : {'longDynamicLink': 'https://d55qc.app.goo.gl/?link=' + encodeURIComponent(url)},
                data : {'longDynamicLink': 'https://icn3d.page.link/?link=' + encodeURIComponent(url)},
                dataType: 'json',
                success: function(data) {
                    resolve(data);
                },
                error : function(xhr, textStatus, errorThrown ) {
                    let shorturl = 'Problem in getting shortened URL';
                    $("#" + ic.pre + "ori_url").val(url);
                    $("#" + ic.pre + "short_url").val(shorturl);
                    $("#" + ic.pre + "short_url_title").val(shorturl + '&t=' + ic.yournote);
                    if(!bPngHtml) me.htmlCls.dialogCls.openDlg('dl_copyurl', 'Copy a Share Link URL');
                }
            });
        });
    }

    shareLinkUrl(bAllCommands) { let ic = this.icn3d, me = ic.icn3dui;
           let url = me.htmlCls.baseUrl + "icn3d/full.html?";
           if(me.cfg.bSidebyside) url = me.htmlCls.baseUrl + "icn3d/full2.html?";

           if(ic.bInputUrlfile) {
               let urlArray = window.location.href.split('?');
               url = urlArray[0] + '?' + ic.inputurl + '&';
           }

           let paraHash = {};
           for(let key in ic.cfg) {
               let value = ic.cfg[key];
               //if(key === 'inpara' || ic.key === 'command' || value === undefined) continue;
               if(key === 'inpara' || key === 'command' || key === 'usepdbnum'
                 || key === 'date' || key === 'v' || value === undefined) continue;

                // check the default values as defined at the beginning of full_ui.js
                //if(key === 'command' && value === '') continue;

                if(key === 'width' && value === '100%') continue;
                if(key === 'height' && value === '100%') continue;

                if(key === 'resize' && value === true) continue;
                if(key === 'showmenu' && value === true) continue;
                if(key === 'showtitle' && value === true) continue;
                if(key === 'showcommand' && value === true) continue;

                //if(key === 'simplemenu' && value === false) continue;
                if(key === 'mobilemenu' && value === false) continue;
                //if(key === 'closepopup' && value === false) continue;
                if(key === 'showanno' && value === false) continue;
                if(key === 'showseq' && value === false) continue;
                if(key === 'showalignseq' && value === false) continue;
                if(key === 'show2d' && value === false) continue;
                if(key === 'showsets' && value === false) continue;

                if(key === 'rotate' && value === 'right') continue;

                // commands will be added in the for loop below: for(let il = ic.commands...
                if(key === 'command') continue;

               if(key === 'options') {
                   if(Object.keys(value).length > 0) {
                       //url += key + '=' + JSON.stringify(value) + '&';
                       paraHash[key] = JSON.stringify(value);
                   }
               }
               else if(value === true) {
                   //url += key + '=1&';
                   paraHash[key] = 1;
               }
               else if(value === false) {
                   //url += key + '=0&';
                   paraHash[key] = 0;
               }
               else if(value !== '') {
                   //url += key + '=' + value + '&';
                   paraHash[key] = value;
               }
           }

           if(ic.bAfMem) {
            paraHash['afmem'] = 'on';
           }
           //else {
           else if(me.cfg.afid || (Object.keys(ic.structures).length == 1 && Object.keys(ic.structures)[0].length > 5) ) {
            paraHash['afmem'] = 'off';
           }

           let inparaWithoutCommand;
           let pos = -1;
           if(me.cfg.inpara !== undefined) pos = me.cfg.inpara.indexOf('&command=');
           inparaWithoutCommand =(pos !== -1 ) ? me.cfg.inpara.substr(0, pos) : me.cfg.inpara;

           let bPrevDate = false;
           if(!ic.bInputUrlfile) {
               let inparaArray =(inparaWithoutCommand && inparaWithoutCommand.substr(1)) ? inparaWithoutCommand.substr(1).split('&') : [];
               for(let i = 0, il = inparaArray.length; i < il; ++i) {
                   let key_value = inparaArray[i].split('=');
                   if(key_value.length == 2) paraHash[key_value[0]] = key_value[1];
               }

               for(let key in paraHash) {
                   if(key === 'v') continue;

                   if(key === 'date') bPrevDate = true;
                   url += key + '=' + paraHash[key] + '&';
               }
           }

           // add time stamp
           let date = new Date();
           let monthStr =(date.getMonth() + 1).toString();
           if(date.getMonth() + 1 < 10) monthStr = '0' + monthStr;

           let dateStr = date.getDate().toString();
           if(date.getDate() < 10) dateStr = '0' + dateStr;

           let dateAllStr = date.getFullYear().toString() + monthStr + dateStr;
           if(!bPrevDate) url += 'date=' + dateAllStr + '&';
           url += 'v=' + me.REVISION + '&';

           url += 'command=';

           //let start =(inparaWithoutCommand !== undefined) ? 1 : 0;
           let start = 0;

           if(bAllCommands || ic.bInputUrlfile) start = 0;

           let transformation = {};
           transformation.factor = ic._zoomFactor;
           transformation.mouseChange = ic.mouseChange;
           transformation.quaternion = ic.quaternion;

           let statefile = "";
           let prevCommandStr = undefined;

           let toggleStr = 'toggle highlight';
           let cntToggle = 0;

           if(ic.commands.length > start) {
               let command_tf = ic.commands[start].split('|||');
               let command_tf2 = command_tf[0].split('&command=');
               prevCommandStr = command_tf2[0].trim();

               //statefile += ic.commands[start] + "\n";

               if(prevCommandStr.indexOf(toggleStr) !== -1) ++cntToggle;
           }

           let i = start + 1;
           let tmpUrl = '';
           for(let il = ic.commands.length; i < il; ++i) {
               let command_tf = ic.commands[i].split('|||');
               let command_tf2 = command_tf[0].split('&command=');
               let commandStr = command_tf2[0].trim();

               // only one load command
               //if(prevCommandStr.substr(0, 5) == 'load ' && commandStr.substr(0, 5) == 'load ') {
               //    continue;
               //}

               //statefile += ic.commands[i] + "\n";

               // only output the most recent 'select saved atoms...' without " | name ..."
               if(((prevCommandStr.indexOf('select saved atoms') !== -1 || prevCommandStr.indexOf('select sets') !== -1)
                 &&(commandStr.indexOf('select') === 0 || commandStr.indexOf('select') === 0)
                 && prevCommandStr.indexOf(' name ') === -1)
                 ||(prevCommandStr.indexOf('pickatom') !== -1 && commandStr.indexOf('pickatom') !== -1)
                 ) ;
               // remove all "show selection" except the last one
               else if(prevCommandStr == 'show selection' && ic.commands.slice(i).toString().indexOf('show selection') != -1) ;
               else if(prevCommandStr.indexOf(toggleStr) !== -1) {
                   ++cntToggle;
               }
               else if(i === start + 1) {
                   //tmpUrl += prevCommandStr;
                   
                   //if(!(inparaWithoutCommand !== undefined && ic.inputid)) {
                   if(prevCommandStr.substr(0, 4) !== 'load') {
                       tmpUrl += prevCommandStr;
                   }

                   //statefile += prevCommandStr + "\n";
               }
               else {
                   tmpUrl += (tmpUrl) ? '; ' + prevCommandStr : prevCommandStr;
                   //statefile += prevCommandStr + "\n";
               }

               // keep all commands in statefile
               statefile += prevCommandStr + "\n";

               prevCommandStr = commandStr;
           }

           url += tmpUrl;

           // last command
           if(prevCommandStr) {
               if(tmpUrl) url += '; ';
               if(cntToggle > 0 && cntToggle %2 == 0 && prevCommandStr !== toggleStr) url += toggleStr + '; ';

               url += prevCommandStr + '|||' + ic.transformCls.getTransformationStr(transformation);
               statefile += prevCommandStr + '|||' + ic.transformCls.getTransformationStr(transformation) + '\n';
           }

           statefile = statefile.replace(/!/g, Object.keys(ic.structures)[0] + '_');
           if((ic.bInputfile && !ic.bInputUrlfile) || (ic.bInputUrlfile && ic.bAppend) || url.length > 4000) url = statefile;
           let id;
           if(ic.structures !== undefined && Object.keys(ic.structures).length == 1 && ic.inputid !== undefined) {
               id = Object.keys(ic.structures)[0];
               url = url.replace(new RegExp(id + '_','g'), '!');
           }

           if(me.cfg.blast_rep_id !== undefined) {
               url = url.replace(new RegExp('blast_rep_id=!','g'), 'blast_rep_id=' + id + '_');
           }

           return url;
    }

    getPngText() { let ic = this.icn3d; ic.icn3dui;
        let url; // output state file if ic.bInputfile is true or the URL is mor than 4000 chars
        let bAllCommands = true;

        let text = "";
        if(ic.bInputfile) {
            url = this.shareLinkUrl(bAllCommands); // output state file if ic.bInputfile is true or the URL is more than 4000 chars

            if(url.substr(0,4) == 'http') {
                text += "\nShare Link: " + url;
            }
            else {
                text += "\nStart of type file======\n";
                text += ic.InputfileType + "\n";
                text += "End of type file======\n";

                text += "Start of data file======\n";
                //text += ic.InputfileData;
    ///            text += ic.saveFileCls.getPDBHeader();
                text += ic.saveFileCls.getAtomPDB(ic.atoms);

                text += "End of data file======\n";

                text += "Start of state file======\n";
                text += url + "\n";
                text += "End of state file======\n";
            }
        }
        else {
            url = this.shareLinkUrl();
            let bTooLong =(url.length > 4000 || url.indexOf('http') !== 0) ? true : false;
            if(bTooLong) {
                url = this.shareLinkUrl(bAllCommands); // output state file if ic.bInputfile is true or the URL is mor than 4000 chars

                text += "\nStart of state file======\n";

                text += url + "\n";
                text += "End of state file======\n";
            }
            else {
                text += "\nShare Link: " + url;
            }
        }

        text = text.replace(/!/g, Object.keys(ic.structures)[0] + '_');

        return text;
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class ThreeDPrint {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    setThichknessFor3Dprint(  ){ let ic = this.icn3d, me = ic.icn3dui;
        ic.lineRadius = 1; //0.1; // hbonds, distance lines
        ic.coilWidth = 1.2; //0.3; // style cartoon-coil
        ic.cylinderRadius = 0.8; //0.4; // style stick
        ic.crosslinkRadius = 0.8; //0.4; // cross-linkage
        ic.traceRadius = 1; //0.4; // style c alpha trace, nucleotide stick
        ic.dotSphereScale = 0.6; //0.3; // style ball and stick, dot

        ic.sphereRadius = 1.5; // style sphere
        //ic.cylinderHelixRadius = 1.6; // style sylinder and plate

        ic.ribbonthickness = 1.0; //0.2; // style ribbon, nucleotide cartoon, stand thickness
        ic.helixSheetWidth = 2.0; //1.3; // style ribbon, stand thickness
        ic.nucleicAcidWidth = 1.4; //0.8; // nucleotide cartoon

        me.htmlCls.setHtmlCls.setCookieForThickness();
    }

    //Prepare for 3D printing by changing dashed lines to solid lines, changing the thickness of the model.
    prepareFor3Dprint(  ){ let ic = this.icn3d, me = ic.icn3dui;
        // turn off hilight
        ic.bShowHighlight = false;
        ic.hlObjectsCls.removeHlObjects();

        ic.bDashedLines = false;

        if(!ic.bSetThickness && me.cfg.cid === undefined) {
            this.setThichknessFor3Dprint();
        }

        // change hbond and distance lines from dashed to solid for 3d printing
        if(ic.lines['hbond'] !== undefined) {
            for(let i = 0, il = ic.lines['hbond'].length; i < il; ++i) {
                let line = ic.lines['hbond'][i];
                line.dashed = false;

                ic.bDashedLines = true;
            }
        }

        if(ic.lines['distance'] !== undefined) {
            for(let i = 0, il = ic.lines['distance'].length; i < il; ++i) {
                let line = ic.lines['distance'][i];
                line.dashed = false;

                ic.bDashedLines = true;
            }
        }

        ic.drawCls.draw();
    }

    //Reset the hydrogen bonds, distance lines to dashed lines. Reset the thickness to the default values.
    resetAfter3Dprint(){ let ic = this.icn3d, me = ic.icn3dui;
        // change hbond and distance lines from dashed to solid for 3d printing
        //if(ic.bDashedLines) {
          if(ic.lines['hbond'] !== undefined) {
            for(let i = 0, il = ic.lines['hbond'].length; i < il; ++i) {
                let line = ic.lines['hbond'][i];
                line.dashed = true;
            }
          }

          if(ic.lines['distance'] !== undefined) {
            for(let i = 0, il = ic.lines['distance'].length; i < il; ++i) {
                let line = ic.lines['distance'][i];
                line.dashed = true;
            }
          }

          ic.lineRadius = 0.1; // hbonds, distance lines
          ic.coilWidth = 0.3; // style cartoon-coil
          ic.cylinderRadius = 0.4; // style stick
          ic.crosslinkRadius = 0.4; // cross-linkage
          ic.traceRadius = 0.4; // style c alpha trace, nucleotide stick
          ic.dotSphereScale = 0.3; // style ball and stick, dot
          ic.sphereRadius = 1.5; // style sphere
          ic.cylinderHelixRadius = 1.6; // style sylinder and plate

          ic.ribbonthickness = 0.2; // style ribbon, nucleotide cartoon, stand thickness
          ic.helixSheetWidth = 1.3; // style ribbon, nucleotide cartoon, stand thickness
          ic.nucleicAcidWidth = 0.8; // nucleotide cartoon

          me.htmlCls.setHtmlCls.setCookieForThickness();

          //ic.drawCls.draw();
        //}
    }

    removeOneStabilizer(rmLineArray) { let ic = this.icn3d; ic.icn3dui;
        let index;
        for(let i = 0, il = ic.pairArray.length; i < il; i += 2) {
            let atom1 = this.getResidueRepAtom(ic.pairArray[i]);
            let atom2 = this.getResidueRepAtom(ic.pairArray[i+1]);

            if(rmLineArray != undefined) {
                for(let j = 0, jl = rmLineArray.length; j < jl; j += 2) {
                    let atomb1 = this.getResidueRepAtom(rmLineArray[j]);
                    let atomb2 = this.getResidueRepAtom(rmLineArray[j+1]);
                    if((atom1.serial == atomb1.serial && atom2.serial == atomb2.serial)
                      ||(atom1.serial == atomb2.serial && atom2.serial == atomb1.serial)
                      ) {
                        index = i;
                        break;
                    }
                }
            }

            if(index !== undefined) break;
        }

        if(index !== undefined) {
            ic.pairArray.splice(index, 2); // removetwoelements at index i
        }
    }

    //Output the selected residues in the residue dialog.
    outputSelection() { let ic = this.icn3d; ic.icn3dui;
        let residues = {};
        for(let i in ic.hAtoms) {
            let residueId = ic.atoms[i].structure + '_' + ic.atoms[i].chain + '_' + ic.atoms[i].resi;
            residues[residueId] = 1;
        }

        let residueArray = Object.keys(residues).sort(function(a, b) {
                    if(a !== '' && !isNaN(a)) {
                        return parseInt(a) - parseInt(b);
                    }
                    else {
                        let lastPosA = a.lastIndexOf('_');
                        let lastPosB = b.lastIndexOf('_');
                        if(a.substr(0, lastPosA) < b.substr(0, lastPosA)) return -1;
                        else if(a.substr(0, lastPosA) > b.substr(0, lastPosA)) return 1;
                        else if(a.substr(0, lastPosA) == b.substr(0, lastPosA)) {
                            if(parseInt(a.substr(lastPosA + 1)) < parseInt(b.substr(lastPosB + 1)) ) return -1;
                            else if(parseInt(a.substr(lastPosA + 1)) > parseInt(b.substr(lastPosB + 1)) ) return 1;
                            else if(parseInt(a.substr(lastPosA + 1)) == parseInt(b.substr(lastPosB + 1)) ) return 0;
                        }
                    }
                });

        let output = "<table><tr><th>Structure</th><th>Chain</th><th>Residue Number</th></tr>";
        for(let i = 0, il = residueArray.length; i < il; ++i) {
            //if(typeof(residueArray[i]) === 'function') continue;

            let firstPos = residueArray[i].indexOf('_');
            let lastPos = residueArray[i].lastIndexOf('_');
            let structure = residueArray[i].substr(0, firstPos);
            let chain = residueArray[i].substr(firstPos + 1, lastPos - firstPos - 1);
            let resi = residueArray[i].substr(lastPos + 1);

            output += "<tr><td>" + structure + "</td><td>" + chain + "</td><td>" + resi + "</td></tr>";
        }

        let file_pref =(ic.inputid) ? ic.inputid : "custom";
        ic.saveFileCls.saveFile(file_pref + '_residues.txt', 'html', output);

    }

    // within the display atoms, show the bonds between C alpha or nucleotide N3
    // 1. add hbonds in protein and nucleotide
    // 2. add stabilizer between chemicals/ions and proteins

    //Add stabilizers in the model for 3D printing. This is especially important for the cartoon display such as ribbons.
    addStabilizer() { let ic = this.icn3d, me = ic.icn3dui;
        let threshold = 3.5; //between 3.2 and 4.0

        let minHbondLen = 3.2;

        //ic.opts["water"] = "dot";

        if(Object.keys(ic.dAtoms).length > 0) {

            // 1. add hbonds in nucleotide
            let atomHbond = {};
            let chain_resi_atom;

            let maxlengthSq = threshold * threshold;
            let minlengthSq = minHbondLen * minHbondLen;

            for(let i in ic.dAtoms) {
              let atom = ic.atoms[i];

              // protein: N, O
              // DNA: C: O2, N3, N4; G: N1, N2, O6; A: N1, N6; T: N1, N6
              if(ic.nucleotides.hasOwnProperty(atom.serial) &&(atom.name === "N1" || atom.name === "N2"
                  || atom.name === "N3" || atom.name === "N4" || atom.name === "N6" || atom.name === "O2" || atom.name === "O6")
                  ) { // calculate hydrogen bond in residue backbone
                chain_resi_atom = atom.structure + "_" + atom.chain + "_" + atom.resi + "_" + atom.name;

                atomHbond[chain_resi_atom] = atom;
              }
            } // end of for(let i in molecule) {

            let atomArray = Object.keys(atomHbond);
            let len = atomArray.length;

            if(ic.pairArray === undefined) ic.pairArray = [];
            for(let i = 0; i < len; ++i) {
                for(let j = i + 1; j < len; ++j) {
                  let atomid1 = atomArray[i];
                  let atomid2 = atomArray[j];

                  let xdiff = Math.abs(atomHbond[atomid1].coord.x - atomHbond[atomid2].coord.x);
                  if(xdiff > threshold) continue;

                  let ydiff = Math.abs(atomHbond[atomid1].coord.y - atomHbond[atomid2].coord.y);
                  if(ydiff > threshold) continue;

                  let zdiff = Math.abs(atomHbond[atomid1].coord.z - atomHbond[atomid2].coord.z);
                  if(zdiff > threshold) continue;

                  let dist = xdiff * xdiff + ydiff * ydiff + zdiff * zdiff;
                  if(dist > maxlengthSq || dist < minlengthSq) continue;

                  // output hydrogen bonds
                  ic.pairArray.push(atomHbond[atomid1].serial);
                  ic.pairArray.push(atomHbond[atomid2].serial);
                } // end of for(let j
            } // end of for(let i

            // 2. add stabilizer for chemicals/ions and proteins
            let maxDistance = 6; // connect within 6 angstrom, use 6 since some proteins such as 1FFK_A has large distance between residues

            //displayed residues
            let displayResidueHash = {};
            for(let i in ic.dAtoms) {
                let atom = ic.atoms[i];

                let residueid = atom.structure + "_" + atom.chain + "_" + atom.resi;
                displayResidueHash[residueid] = 1;
            }

            // connect chemicals, ions, and every third protein residues to neighbors(within 4 angstrom)
            let residueHash = {};
            //chemicals
            for(let i in ic.chemicals) {
                let atom = ic.atoms[i];

                let residueid = atom.structure + "_" + atom.chain + "_" + atom.resi;
                if(displayResidueHash.hasOwnProperty(residueid)) residueHash[residueid] = 1;
            }
            //ions
            for(let i in ic.ions) {
                let atom = ic.atoms[i];

                let residueid = atom.structure + "_" + atom.chain + "_" + atom.resi;
                if(displayResidueHash.hasOwnProperty(residueid)) residueHash[residueid] = 1;
            }

            //every third protein residues
            let chainArray = Object.keys(ic.chains);
            for(let i = 0, il = chainArray.length; i < il; ++i) {
                let chainid = chainArray[i];
                let coilCnt = 0;
                let residueid;
                let prevResi = 0;
                for(let j = 0, jl = ic.chainsSeq[chainid].length; j < jl; ++j) {
                    residueid = chainid + '_' + ic.chainsSeq[chainid][j].resi;
                    if(ic.secondaries[residueid] == 'c' || ic.secondaries[residueid] == 'E' || ic.secondaries[residueid] == 'H') {
                        // add every third residue
                        if(coilCnt % 3 == 0 || ic.resid2ncbi[ic.chainsSeq[chainid][j].resi] != ic.resid2ncbi[prevResi] + 1) {
                            if(displayResidueHash.hasOwnProperty(residueid)) residueHash[residueid] = 1;
                        }

                        ++coilCnt;

                        prevResi = ic.chainsSeq[chainid][j].resi;
                    }
                }

                // last residue
                if(ic.secondaries[residueid] == 'c' || ic.secondaries[residueid] == 'E' || ic.secondaries[residueid] == 'H') {
                    if(displayResidueHash.hasOwnProperty(residueid)) residueHash[residueid] = 1;
                }
            }

            let residueArray = Object.keys(residueHash);

            if(ic.pairArray === undefined) ic.pairArray = [];
            // displayed atoms except water
            let dAtomsNotWater = me.hashUtilsCls.exclHash(ic.dAtoms, ic.water);

            for(let i = 0, il = residueArray.length; i < il; ++i) {
                let residueid = residueArray[i];
                let ss = ic.secondaries[residueid];

                let sphere = ic.contactCls.getNeighboringAtoms(dAtomsNotWater, me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms), maxDistance);

                // original atoms
                let sphereArray = Object.keys(sphere).sort();
                let atomArray = Object.keys(ic.residues[residueid]).sort();

                let bProtein = false;
                if(ic.proteins.hasOwnProperty(atomArray[0])) { // protein
                    atomArray = [atomArray[0]]; // one atom from the residue

                    bProtein = true;

                    // remove the previous, current and the next residues, chemicals, and ions from "sphere"
                    //let resi = parseInt(residueid.substr(residueid.lastIndexOf('_') + 1));
                    let chainid = residueid.substr(0, residueid.lastIndexOf('_'));
                    let resi = ic.ParserUtilsCls.getResiNCBI(chainid, residueid.substr(residueid.lastIndexOf('_') + 1));

                    let simSphere = {};
                    for(let serial in sphere) {
                        if(ic.chemicals.hasOwnProperty(serial) || ic.ions.hasOwnProperty(serial)) continue;

                        let atom = ic.atoms[serial];
                        if(isNaN(atom.resi)) continue;
                        let atomResi = ic.ParserUtilsCls.getResiNCBI(chainid, atom.resi);

                        if((ss == 'c' &&(atomResi > resi + 1 || atomResi < resi - 1) )
                          ||(ss == 'E' &&(atomResi > resi + 2 || atomResi < resi - 2) )
                          ||(ss == 'H' &&(atomResi > resi + 4 || atomResi < resi - 4) )
                          ) {
                            simSphere[serial] = 1;
                        }
                    }

                    sphereArray = Object.keys(simSphere).sort();
                }

                // one line per each protein residue
                if(sphereArray.length > 0 && atomArray.length > 0) {
                    if(bProtein) {
                            let inter2 = parseInt((sphereArray.length + 0.5) / 2.0);
                            ic.pairArray.push(atomArray[0]);
                            ic.pairArray.push(sphereArray[inter2]);
                    }
                    else { // chemicals or ions
                        let n = 10;
                        let step = parseInt(sphereArray.length /(n+1));

                        for(let j = 0, jl = atomArray.length; j < jl; ++j) {
                            if(j % n == 0) { // make one line for every other 10 atoms
                                let sphereIndex = parseInt(j/n) * step;
                                let inter2 =(sphereIndex < sphereArray.length) ?  sphereIndex : sphereArray.length - 1;
                                ic.pairArray.push(atomArray[j]);
                                ic.pairArray.push(sphereArray[inter2]);

                                if(atomArray.length < n + 1) {
                                    ic.pairArray.push(atomArray[j]);
                                    ic.pairArray.push(sphereArray[sphereArray.length - 1]);
                                }
                            }
                        }
                    } // else
                } // if(sphereArray.length > 0) {
            } // for
        }
    }

    //Remove all the added stabilizers.
    hideStabilizer() { let ic = this.icn3d; ic.icn3dui;
        //ic.opts["stabilizer"] = "no";
        ic.pairArray = [];

        ic.lines['stabilizer'] = [];
        ic.stabilizerpnts = [];

        for(let i in ic.water) {
            ic.atoms[i].style = ic.opts["water"];
        }

        //ic.drawCls.draw();
    }

    getResidueRepAtom(serial) { let ic = this.icn3d; ic.icn3dui;
        let atomIn = ic.atoms[serial];
        let residueid = atomIn.structure + "_" + atomIn.chain + "_" + atomIn.resi;

        let foundAtom;
        if(!ic.proteins.hasOwnProperty(serial) && !ic.nucleotides.hasOwnProperty(serial)) { // chemicals or ions
            foundAtom = atomIn;
        }
        else {
            for(let i in ic.residues[residueid]) {
                let atom = ic.atoms[i];
                if(atom.name === 'CA' || atom.name === 'N3') { // protein: CA, nucleotide: N3
                    foundAtom = ic.atoms[i];
                    break;
                }
            }
        }

        if(foundAtom === undefined) foundAtom = atomIn;

        return foundAtom;
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Export3D {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    exportStlFile(postfix) { let ic = this.icn3d; ic.icn3dui;
       // assemblies
       if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1 && ic.bAssembly) {
            // use a smaller grid to build the surface for assembly
            ic.threshbox = 180 / Math.pow(ic.biomtMatrices.length, 0.33);
            ic.applyMapCls.removeSurfaces();
            ic.applyMapCls.applySurfaceOptions();
            ic.applyMapCls.removeMaps();
            ic.applyMapCls.applyMapOptions();
            ic.applyMapCls.removeEmmaps();
            ic.applyMapCls.applyEmmapOptions();
       }
       let text = this.saveStlFile();
       let file_pref =(ic.inputid) ? ic.inputid : "custom";
       ic.saveFileCls.saveFile(file_pref + postfix + '.stl', 'binary', text);
       // assemblies
       if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1 && ic.bAssembly
         && Object.keys(ic.dAtoms).length * ic.biomtMatrices.length > ic.maxAtoms3DMultiFile ) {
            alert(ic.biomtMatrices.length + " files will be generated for this assembly. Please merge these files using some software and 3D print the merged file.");
            let identity = new THREE.Matrix4();
            identity.identity();
            let index = 1;
            for(let i = 0; i < ic.biomtMatrices.length; i++) {  // skip itself
              let mat = ic.biomtMatrices[i];
              if(mat === undefined) continue;
              // skip itself
              if(mat.equals(identity)) continue;
              let time =(i + 1) * 100;
              //https://stackoverflow.com/questions/1190642/how-can-i-pass-a-parameter-to-a-settimeout-callback
              setTimeout(function(mat, index){
                  text = this.saveStlFile(mat);
                  ic.saveFileCls.saveFile(file_pref + postfix + index + '.stl', 'binary', text);
                  text = '';
              }.bind(this, mat, index), time);
              ++index;
            }
            // reset grid to build the surface for assembly
            ic.threshbox = 180;
       }
    }

    exportVrmlFile(postfix) { let ic = this.icn3d; ic.icn3dui;
       // assemblies
       if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1 && ic.bAssembly) {
            // use a smaller grid to build the surface for assembly
            ic.threshbox = 180 / Math.pow(ic.biomtMatrices.length, 0.33);
            ic.applyMapCls.removeSurfaces();
            ic.applyMapCls.applySurfaceOptions();
            ic.applyMapCls.removeMaps();
            ic.applyMapCls.applyMapOptions();
            ic.applyMapCls.removeEmmaps();
            ic.applyMapCls.applyEmmapOptions();
       }
       let text = this.saveVrmlFile();
       let file_pref =(ic.inputid) ? ic.inputid : "custom";
       ic.saveFileCls.saveFile(file_pref + postfix + '.wrl', 'text', text);
       //ic.saveFileCls.saveFile(file_pref + postfix + '.vrml', 'text', text);
       // assemblies
       if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1 && ic.bAssembly
         && Object.keys(ic.dAtoms).length * ic.biomtMatrices.length > ic.maxAtoms3DMultiFile ) {
            alert(ic.biomtMatrices.length + " files will be generated for this assembly. Please merge these files using some software and 3D print the merged file.");
            let identity = new THREE.Matrix4();
            identity.identity();
            let index = 1;
            for(let i = 0; i < ic.biomtMatrices.length; i++) {  // skip itself
              let mat = ic.biomtMatrices[i];
              if(mat === undefined) continue;
              // skip itself
              if(mat.equals(identity)) continue;
              let time =(i + 1) * 100;
              //https://stackoverflow.com/questions/1190642/how-can-i-pass-a-parameter-to-a-settimeout-callback
              setTimeout(function(mat, index){
                  text = this.saveVrmlFile(mat);
                  ic.saveFileCls.saveFile(ic.inputid + postfix + index + '.wrl', 'text', text);
                  //ic.saveFileCls.saveFile(file_pref + postfix + index + '.vrml', 'text', text);
                  text = '';
              }.bind(this, mat, index), time);
              ++index;
            }
            // reset grid to build the surface for assembly
            ic.threshbox = 180;
       }
    }

    // generate a binary STL file for 3D printing
    // https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL
    /*
    UINT8[80] � Header
    UINT32 � Number of triangles

    foreach triangle
    REAL32[3] � Normal vector
    REAL32[3] � Vertex 1
    REAL32[3] � Vertex 2
    REAL32[3] � Vertex 3
    UINT16 � Attribute byte count
    end
    */

    getFaceCnt( mdl ){ let ic = this.icn3d; ic.icn3dui;
        let cntFaces = 0;
        for(let i = 0, il = mdl.children.length; i < il; ++i) {
             let mesh = mdl.children[i];
             if(mesh.type === 'Sprite') continue;

             let geometry = mesh.geometry;

//             let faces = geometry.faces;
//             if(faces !== undefined) {
//                 for(let j = 0, jl = faces.length; j < jl; ++j) {
//                     ++cntFaces;
//                 }
//             }

             let indexArray = geometry.getIndex().array;
             cntFaces += indexArray.length / 3;

        }

        return cntFaces;
    }

    //Save the binary STL file for 3D monocolor printing.
    saveStlFile( mat ){ let ic = this.icn3d, me = ic.icn3dui;
        if(Object.keys(ic.dAtoms).length > 70000) {
            alert('Please display a subset of the structure to export 3D files. Then merge the files for 3D printing...');
            return [''];
        }

        ic.threeDPrintCls.prepareFor3Dprint();

        let cntFaces = 0;

        cntFaces += this.getFaceCnt(ic.mdl);
        cntFaces += this.getFaceCnt(ic.mdl_ghost);

        let blobArray = []; // hold blobs

        let stlArray = new Uint8Array(84);

        // UINT8[80] � Header
        let title = 'STL file for the structure(s) ';
        let structureArray = Object.keys(ic.structures);
        for(let i = 0, il = structureArray.length; i < il; ++i) {
            title += structureArray[i];
            if(i < il - 1) title += ', ';
        }

        if(title.length > 80) title = title.substr(0, 80);

        for(let i = 0; i < 80; ++i) {
            if(i < title.length) {
                stlArray[i] = me.convertTypeCls.passInt8([title.charCodeAt(i)])[0];
            }
            else {
                stlArray[i] = me.convertTypeCls.passInt8([' '.charCodeAt(0)])[0];
            }
        }

        // UINT32 � Number of triangles
        if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1 && ic.bAssembly
          && Object.keys(ic.dAtoms).length * ic.biomtMatrices.length <= ic.maxAtoms3DMultiFile ) {
            stlArray = this.updateArray( stlArray, me.convertTypeCls.passInt32([cntFaces * ic.biomtMatrices.length]), 80 );
        }
        else {
            stlArray = this.updateArray( stlArray, me.convertTypeCls.passInt32([cntFaces]), 80 );
        }

        blobArray.push(new Blob([stlArray],{ type: "application/octet-stream"}));

        blobArray = this.processStlMeshGroup( ic.mdl, blobArray, mat );

        blobArray = this.processStlMeshGroup( ic.mdl_ghost, blobArray, mat );

       // assemblies
       if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1 && ic.bAssembly
         && Object.keys(ic.dAtoms).length * ic.biomtMatrices.length <= ic.maxAtoms3DMultiFile ) {
            let identity = new THREE.Matrix4();
            identity.identity();

            for(let i = 0; i < ic.biomtMatrices.length; i++) {  // skip itself
              let mat1 = ic.biomtMatrices[i];
              if(mat1 === undefined) continue;

              // skip itself
              if(mat1.equals(identity)) continue;

              blobArray = this.processStlMeshGroup( ic.mdl, blobArray, mat1 );

              blobArray = this.processStlMeshGroup( ic.mdl_ghost, blobArray, mat1 );
            }
        }

        ic.threeDPrintCls.resetAfter3Dprint();

        return blobArray;
    }

    updateArray( array, inArray, indexBase ){ let ic = this.icn3d; ic.icn3dui;
        for( let i = 0, il = inArray.length; i < il; ++i ){
            array[indexBase + i] = inArray[i];
        }
        return array;
    }

    processStlMeshGroup( mdl, blobArray, mat ){ let ic = this.icn3d, me = ic.icn3dui;
        for(let i = 0, il = mdl.children.length; i < il; ++i) {
             let mesh = mdl.children[i];
             if(mesh.type === 'Sprite') continue;

             let geometry = mesh.geometry;

             let positionArray = geometry.getAttribute('position').array;
             let indexArray = geometry.getIndex().array;

             let position = mesh.position;
             let scale = mesh.scale;

             let matrix = mesh.matrix;

             let stlArray = new Uint8Array(indexArray.length / 3 * 50);

             let index = 0;

             for(let j = 0, jl = indexArray.length; j < jl; j += 3) {
                 let a = indexArray[j];
                 let b = indexArray[j+1];
                 let c = indexArray[j+2];

                 let va = new THREE.Vector3(positionArray[3*a], positionArray[3*a+1], positionArray[3*a+2]);
                 let vb = new THREE.Vector3(positionArray[3*b], positionArray[3*b+1], positionArray[3*b+2]);
                 let vc = new THREE.Vector3(positionArray[3*c], positionArray[3*c+1], positionArray[3*c+2]);

                 let v1, v2, v3;

                 if(geometry.type == 'SphereGeometry' || geometry.type == 'BoxGeometry') {
                     v1 = va.clone().multiply(scale).add(position);
                     v2 = vb.clone().multiply(scale).add(position);
                     v3 = vc.clone().multiply(scale).add(position);
                 }
                  else if(geometry.type == 'CylinderGeometry') {
                     v1 = va.clone().applyMatrix4(matrix);
                     v2 = vb.clone().applyMatrix4(matrix);
                     v3 = vc.clone().applyMatrix4(matrix);
                 }
                 else {
                     v1 = va.clone();
                     v2 = vb.clone();
                     v3 = vc.clone();
                 }

                 {
                     stlArray = this.updateArray( stlArray, me.convertTypeCls.passFloat32([0.0, 0.0, 0.0]), index );
                     index += 12;
                 }

                 if(mat !== undefined) {
                     v1.applyMatrix4(mat);
                     v2.applyMatrix4(mat);
                     v3.applyMatrix4(mat);
                 }

                 stlArray = this.updateArray( stlArray, me.convertTypeCls.passFloat32([v1.x, v1.y, v1.z]), index );
                 index += 12;
                 stlArray = this.updateArray( stlArray, me.convertTypeCls.passFloat32([v2.x, v2.y, v2.z]), index );
                 index += 12;
                 stlArray = this.updateArray( stlArray, me.convertTypeCls.passFloat32([v3.x, v3.y, v3.z]), index );
                 index += 12;

                 v1 = v2 = v3 = undefined;

                 stlArray = this.updateArray( stlArray, me.convertTypeCls.passInt16([0]), index );
                 index += 2;
             }

             blobArray.push(new Blob([stlArray],{ type: "application/octet-stream"}));
             stlArray = null;
        }

        return blobArray;
    }

    //http://gun.teipir.gr/VRML-amgem/spec/part1/examples.html
    //Save the VRML file for 3D color printing.
    saveVrmlFile( mat ){ let ic = this.icn3d; ic.icn3dui;
        if(Object.keys(ic.dAtoms).length > 50000) {
            alert('Please display a subset of the structure to export 3D files. Then merge the files for 3D printing...');
            return [''];
        }

        ic.threeDPrintCls.prepareFor3Dprint();

        let vrmlStrArray = [];
        vrmlStrArray.push('#VRML V2.0 utf8\n');

        let vertexCnt = 0;
        let result = this.processVrmlMeshGroup( ic.mdl, vrmlStrArray, vertexCnt, mat );
        vrmlStrArray = result.vrmlStrArray;
        vertexCnt = result.vertexCnt;

        result = this.processVrmlMeshGroup( ic.mdl_ghost, vrmlStrArray, vertexCnt, mat );
        vrmlStrArray = result.vrmlStrArray;
        vertexCnt = result.vertexCnt;

       // assemblies
       if(ic.biomtMatrices !== undefined && ic.biomtMatrices.length > 1 && ic.bAssembly
         && Object.keys(ic.dAtoms).length * ic.biomtMatrices.length <= ic.maxAtoms3DMultiFile ) {
            let identity = new THREE.Matrix4();
            identity.identity();

            for(let i = 0; i < ic.biomtMatrices.length; i++) {  // skip itself
              let mat1 = ic.biomtMatrices[i];
              if(mat1 === undefined) continue;

              // skip itself
              if(mat1.equals(identity)) continue;

                result = this.processVrmlMeshGroup( ic.mdl, vrmlStrArray, vertexCnt, mat1 );
                vrmlStrArray = result.vrmlStrArray;
                vertexCnt = result.vertexCnt;

                result = this.processVrmlMeshGroup( ic.mdl_ghost, vrmlStrArray, vertexCnt, mat1 );
                vrmlStrArray = result.vrmlStrArray;
                vertexCnt = result.vertexCnt;
            }
        }

        return vrmlStrArray;
    }

    // The file lost face color after being repaired by https://service.netfabb.com/. It only works with vertex color
    // convert face color to vertex color
    processVrmlMeshGroup( mdl, vrmlStrArray, vertexCnt, mat ) { let ic = this.icn3d, me = ic.icn3dui;
        for(let i = 0, il = mdl.children.length; i < il; ++i) {
            let mesh = mdl.children[i];
            if(mesh.type === 'Sprite') continue;

            let geometry = mesh.geometry;

            mesh.material.type;
            (geometry.type == 'Surface') ? true : false;

            let positionArray = geometry.getAttribute('position').array;
            let colorArray = (geometry.getAttribute('color')) ? geometry.getAttribute('color').array : [];
            let indexArray = geometry.getIndex().array;

            let position = mesh.position;
            let scale = mesh.scale;

            let matrix = mesh.matrix;

            let meshColor = me.parasCls.thr(1, 1, 1);
            if(geometry.type == 'SphereGeometry' || geometry.type == 'BoxGeometry' || geometry.type == 'CylinderGeometry') {
                if(mesh.material !== undefined) meshColor = mesh.material.color;
            }

            vrmlStrArray.push('Shape {\n');
            vrmlStrArray.push('geometry IndexedFaceSet {\n');

            vrmlStrArray.push('coord Coordinate { point [ ');

            let vertexColorStrArray = [];
            for(let j = 0, jl = positionArray.length; j < jl; j += 3) {
                let va = new THREE.Vector3(positionArray[j], positionArray[j+1], positionArray[j+2]);

                let vertex;
                if(geometry.type == 'SphereGeometry' || geometry.type == 'BoxGeometry') {
                    vertex = va.clone().multiply(scale).add(position);
                }
                else if(geometry.type == 'CylinderGeometry') {
                    vertex = va.clone().applyMatrix4(matrix);
                }
                else {
                    vertex = va.clone();
                }

                if(mat !== undefined) vertex.applyMatrix4(mat);

                vrmlStrArray.push(vertex.x.toPrecision(5) + ' ' + vertex.y.toPrecision(5) + ' ' + vertex.z.toPrecision(5));
                vertex = undefined;

                if(j < jl - 3) vrmlStrArray.push(', ');

                vertexColorStrArray.push(me.parasCls.thr(1, 1, 1));
            }
            vrmlStrArray.push(' ] }\n');

            let coordIndexStr = '', colorStr = '';

            for(let j = 0, jl = indexArray.length; j < jl; j += 3) {
                let a = indexArray[j];
                let b = indexArray[j+1];
                let c = indexArray[j+2];

                let color;
                if(geometry.type == 'SphereGeometry' || geometry.type == 'BoxGeometry' || geometry.type == 'CylinderGeometry') {
                    color = meshColor;
                }
                else {
                    color = new THREE.Color(colorArray[3*a], colorArray[3*a+1], colorArray[3*a+2]);
                }

                coordIndexStr += a + ' ' + b + ' ' + c;
                // http://www.lighthouse3d.com/vrml/tutorial/index.shtml?indfs
                // use -1 to separate polygons
                if(j < jl - 3) coordIndexStr += ', -1, ';

                // update vertexColorStrArray
                vertexColorStrArray[a] = color;
                vertexColorStrArray[b] = color;
                vertexColorStrArray[c] = color;
            }

            for(let j = 0, jl = vertexColorStrArray.length; j < jl; ++j) {
                let color = vertexColorStrArray[j];
                colorStr += color.r.toPrecision(3) + ' ' + color.g.toPrecision(3) + ' ' + color.b.toPrecision(3);
                if(j < jl - 1) colorStr += ', ';
            }

            vrmlStrArray.push('coordIndex [ ' + coordIndexStr + ' ]\n');
            vrmlStrArray.push('color Color { color [ ' + colorStr + ' ] } colorPerVertex TRUE\n');

            vrmlStrArray.push('  }\n');
            vrmlStrArray.push('}\n');
        }

        return {'vrmlStrArray': vrmlStrArray,'vertexCnt': vertexCnt};
    }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Ray {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    rayCaster(e, bClick) {
        this.rayCasterBase(e, bClick);
    }

    rayCasterBase(e, bClick) { let ic = this.icn3d; ic.icn3dui;
    // if(ic.bChainAlign) return; // no picking for chain alignment

        let x = e.pageX, y = e.pageY;
        if (e.originalEvent.targetTouches && e.originalEvent.targetTouches[0]) {
            x = e.originalEvent.targetTouches[0].pageX;
            y = e.originalEvent.targetTouches[0].pageY;
        }

        let left = ic.oriContainer.offset().left;
        let top = ic.oriContainer.offset().top;

        let containerWidth = ic.oriContainer.width();
        let containerHeight = ic.oriContainer.height();

        let popupX = x - left;
        let popupY = y - top;

        //ic.isDragging = true;

        // see ref http://soledadpenades.com/articles/three-js-tutorials/object-pk/
        //if(ic.pk && (e.altKey || e.ctrlKey || e.shiftKey || e.keyCode === 18 || e.keyCode === 16 || e.keyCode === 17 || e.keyCode === 224 || e.keyCode === 91) ) {
        //    ic.highlightlevel = ic.pk;

            ic.mouse.x = ( popupX / containerWidth ) * 2 - 1;
            ic.mouse.y = - ( popupY / containerHeight ) * 2 + 1;

            let mouse3 = new THREE.Vector3();
            mouse3.x = ic.mouse.x;
            mouse3.y = ic.mouse.y;
            //mouse3.z = 0.5;
            if(ic.cam_z > 0) {
              mouse3.z = -1.0; // between -1 to 1. The z positio of mouse in the real world should be between the camera and the target."-1" worked in our case.
            }
            else {
              mouse3.z = 1.0; // between -1 to 1. The z positio of mouse in the real world should be between the camera and the target."-1" worked in our case.
            }

            // similar to setFromCamera() except mouse3.z is the opposite sign from the value in setFromCamera()
            // use itsown camera for picking

            if(ic.cam === ic.perspectiveCamera) { // perspective
                if(ic.cam_z > 0) {
                  mouse3.z = -1.0;
                }
                else {
                  mouse3.z = 1.0;
                }
                //ic.projector.unprojectVector( mouse3, ic.cam );  // works for all versions
                mouse3.unproject(ic.cam );  // works for all versions
                ic.raycaster.set(ic.cam.position, mouse3.sub(ic.cam.position).normalize()); // works for all versions
            }
            else if(ic.cam === ic.orthographicCamera) {  // orthographics
                if(ic.cam_z > 0) {
                  mouse3.z = 1.0;
                }
                else {
                  mouse3.z = -1.0;
                }
                //ic.projector.unprojectVector( mouse3, ic.cam );  // works for all versions
                mouse3.unproject(ic.cam );  // works for all versions
                ic.raycaster.set(mouse3, new THREE.Vector3(0,0,-1).transformDirection( ic.cam.matrixWorld )); // works for all versions
            }

            let bFound = this.isIntersect(ic.objects, ic.mdl, bClick, popupX, popupY);

            if(!bFound) {
                bFound = this.isIntersect(ic.objects_ghost, ic.mdl_ghost, bClick, popupX, popupY);
            }
        //}
    }

    isIntersect(objects, mdl, bClick, popupX, popupY) { let ic = this.icn3d; ic.icn3dui;
        let intersects = ic.raycaster.intersectObjects( objects ); // not all "mdl" group will be used for pk

        let bFound = false;

        let position = mdl.position;
        if ( intersects.length > 0 ) {
            // the intersections are sorted so that the closest point is the first one.
            intersects[ 0 ].point.sub(position); // mdl.position was moved to the original (0,0,0) after reading the molecule coordinates. The raycasting was done based on the original. The position of the original should be substracted.

            let threshold = ic.rayThreshold; //0.5;
            let atom = this.getAtomsFromPosition(intersects[ 0 ].point, threshold); // the second parameter is the distance threshold. The first matched atom will be returned. Use 1 angstrom, not 2 angstrom. If it's 2 angstrom, other atom will be returned.

            while(!atom && threshold < 10) {
                threshold = threshold + 0.5;
                atom = this.getAtomsFromPosition(intersects[ 0 ].point, threshold);
            }

            if(atom) {
                bFound = true;
                if(ic.pickpair) {
                    if(bClick) {
                      if(ic.pAtomNum % 2 === 0) {
                        ic.pAtom = atom;
                      }
                      else {
                        ic.pAtom2 = atom;
                      }

                      ++ic.pAtomNum;
                    }
                }
                else {
                  ic.pAtom = atom;
                }

                if(bClick) {
                  ic.pickingCls.showPicking(atom);
                }
                else {
                  ic.pickingCls.showPicking(atom, popupX, popupY);
                }
            }
            else {
                console.log("No atoms were found in 10 andstrom range");
            }
        } // end if

        return bFound;
    }

     // from iview (http://istar.cse.cuhk.edu.hk/iview/)
     getAtomsFromPosition(point, threshold) { let ic = this.icn3d, me = ic.icn3dui;
        let i;

        if(threshold === undefined || threshold === null) {
          threshold = 1;
        }

        //for (i in ic.atoms) {
        for (i in ic.dAtoms) {
           let atom = ic.atoms[i];

           if(ic.ions.hasOwnProperty(i) && ic.opts['ions'] === 'sphere') {
               let adjust = me.parasCls.vdwRadii[atom.elem.toUpperCase()];

               if(Math.abs(atom.coord.x - point.x) - adjust > threshold) continue;
               if(Math.abs(atom.coord.y - point.y) - adjust > threshold) continue;
               if(Math.abs(atom.coord.z - point.z) - adjust > threshold) continue;
           }
           else {
               if(atom.coord.x < point.x - threshold || atom.coord.x > point.x + threshold) continue;
               if(atom.coord.y < point.y - threshold || atom.coord.y > point.y + threshold) continue;
               if(atom.coord.z < point.z - threshold || atom.coord.z > point.z + threshold) continue;
           }

           return atom;
        }

        return null;
     }
}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Control {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    setControl() { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        let thisClass = this;

        // adjust the size
        ic.WIDTH = ic.container.width(), ic.HEIGHT = ic.container.height();
        ic.applyCenterCls.setWidthHeight(ic.WIDTH, ic.HEIGHT);

        ic._zoomFactor = 1.0;
        ic.mouseChange = new THREE.Vector2(0,0);
        ic.quaternion = new THREE.Quaternion(0,0,0,1);

        ic.container.bind('contextmenu', function (e) {
        //document.getElementById(ic.id).addEventListener('contextmenu', function (e) {
            e.preventDefault();
        });

        // key event has to use the document because it requires the focus
        ic.typetext = false;

        //http://unixpapa.com/js/key.html
        $(document).bind('keyup', function (e) {
        //document.addEventListener('keyup', function (e) {
          if(e.keyCode === 16) { // shiftKey
              ic.bShift = false;
          }
          if(e.keyCode === 17 || e.keyCode === 224 || e.keyCode === 91) { // ctrlKey or apple command key
              ic.bCtrl = false;
          }
        });

        $('input[type=text], textarea').focus(function() {
            ic.typetext = true;
        });

        $('input[type=text], textarea').blur(function() {
            ic.typetext = false;
        });

        $(document).bind('keydown', function (e) {
        //document.addEventListener('keydown', function (e) {
          if(e.shiftKey || e.keyCode === 16) {
              ic.bShift = true;
          }
          if(e.ctrlKey || e.keyCode === 17 || e.keyCode === 224 || e.keyCode === 91) {
              ic.bCtrl = true;
          }

          if ((!ic.bControlGl && !ic.controls) || (ic.bControlGl && !window.controls)) return;

          ic.bStopRotate = true;

          let rotAngle = (ic.bShift) ? 90 : 5;

          if(!ic.typetext) {
            // zoom
            if(e.keyCode === 90 ) { // Z
              let para = {};

              if(ic.bControlGl && !me.bNode) {
                  if(window.cam === ic.perspectiveCamera) { // perspective
                    para._zoomFactor = 0.9;
                  }
                  else if(window.cam === ic.orthographicCamera) {  // orthographics
                    if(ic._zoomFactor < 0.1) {
                      ic._zoomFactor = 0.1;
                    }
                    else if(ic._zoomFactor > 1) {
                      ic._zoomFactor = 1;
                    }

                    para._zoomFactor = ic._zoomFactor * 0.8;
                    if(para._zoomFactor < 0.1) para._zoomFactor = 0.1;
                  }
              }
              else {
                  if(ic.cam === ic.perspectiveCamera) { // perspective
                    para._zoomFactor = 0.9;
                  }
                  else if(ic.cam === ic.orthographicCamera) {  // orthographics
                    if(ic._zoomFactor < 0.1) {
                      ic._zoomFactor = 0.1;
                    }
                    else if(ic._zoomFactor > 1) {
                      ic._zoomFactor = 1;
                    }

                    para._zoomFactor = ic._zoomFactor * 0.8;
                    if(para._zoomFactor < 0.1) para._zoomFactor = 0.1;
                  }
              }

              para.update = true;
              if(ic.bControlGl && !me.bNode) {
                  window.controls.update(para);
              }
              else {
                  ic.controls.update(para);
              }
              if(ic.bRender) ic.drawCls.render();
            }
            else if(e.keyCode === 88 ) { // X
              let para = {};

              if(ic.bControlGl && !me.bNode) {
                  if(window.cam === ic.perspectiveCamera) { // perspective
                    //para._zoomFactor = 1.1;
                    para._zoomFactor = 1.03;
                  }
                  else if(window.cam === ic.orthographicCamera) {  // orthographics
                    if(ic._zoomFactor > 10) {
                      ic._zoomFactor = 10;
                    }
                    else if(ic._zoomFactor < 1) {
                      ic._zoomFactor = 1;
                    }

                    para._zoomFactor = ic._zoomFactor * 1.01;
                    if(para._zoomFactor > 10) para._zoomFactor = 10;
                  }
              }
              else {
                  if(ic.cam === ic.perspectiveCamera) { // perspective
                    //para._zoomFactor = 1.1;
                    para._zoomFactor = 1.03;
                  }
                  else if(ic.cam === ic.orthographicCamera) {  // orthographics
                    if(ic._zoomFactor > 10) {
                      ic._zoomFactor = 10;
                    }
                    else if(ic._zoomFactor < 1) {
                      ic._zoomFactor = 1;
                    }

                    para._zoomFactor = ic._zoomFactor * 1.01;
                    if(para._zoomFactor > 10) para._zoomFactor = 10;
                  }
              }

              para.update = true;
              if(ic.bControlGl && !me.bNode) {
                  window.controls.update(para);
              }
              else {
                  ic.controls.update(para);
              }
              if(ic.bRender) ic.drawCls.render();
            }

            // rotate
            else if(e.keyCode === 76 ) { // L, rotate left
              let axis = new THREE.Vector3(0,1,0);
              let angle = -rotAngle / 180.0 * Math.PI;

              ic.transformCls.setRotation(axis, angle);
            }
            else if(e.keyCode === 74 ) { // J, rotate right
              let axis = new THREE.Vector3(0,1,0);
              let angle = rotAngle / 180.0 * Math.PI;

              ic.transformCls.setRotation(axis, angle);
            }
            else if(e.keyCode === 73 ) { // I, rotate up
              let axis = new THREE.Vector3(1,0,0);
              let angle = -rotAngle / 180.0 * Math.PI;

              ic.transformCls.setRotation(axis, angle);
            }
            else if(e.keyCode === 77 ) { // M, rotate down
              let axis = new THREE.Vector3(1,0,0);
              let angle = rotAngle / 180.0 * Math.PI;

              ic.transformCls.setRotation(axis, angle);
            }

            else if(e.keyCode === 65 ) { // A, alternate
               if(Object.keys(ic.structures).length > 1) {
                 ic.alternateCls.alternateWrapper();
               }
            }

          }
        });

        ic.container.bind('mouseup', function (e) {
        //document.getElementById(ic.id).addEventListener('mouseup', function (e) {
            ic.isDragging = false;
        });
        ic.container.bind('touchend', function (e) {
        //document.getElementById(ic.id).addEventListener('touchend', function (e) {
            ic.isDragging = false;
        });

        ic.container.bind('mousedown', function (e) {
        //document.getElementById(ic.id).addEventListener('mousedown', function (e) {
            //e.preventDefault();
            ic.isDragging = true;

            if (!ic.scene) return;

            ic.bStopRotate = true;

            if(ic.pk && (e.altKey || e.ctrlKey || e.shiftKey || e.keyCode === 18 || e.keyCode === 16 || e.keyCode === 17 || e.keyCode === 224 || e.keyCode === 91) ) {
                ic.highlightlevel = ic.pk;

                let bClick = true;
                ic.rayCls.rayCaster(e, bClick);
            }

            if(ic.bControlGl && !me.bNode) {
              window.controls.handleResize();
              window.controls.update();
            }
            else {
              ic.controls.handleResize();
              ic.controls.update();
            }

            if(ic.bRender) ic.drawCls.render();
        });

        ic.container.bind('touchstart', function (e) {
        //document.getElementById(ic.id).addEventListener('touchstart', function (e) {
            //e.preventDefault();
            e.preventDefault();
            ic.isDragging = true;

            if (!ic.scene) return;

            ic.bStopRotate = true;

            //$("[id$=popup]").hide();
            $("#" + ic.pre + "popup").hide();

            //var bClick = false;
            let bClick = true;
            ic.rayCls.rayCaster(e, bClick);

            if(ic.bControlGl && !me.bNode) {
              window.controls.handleResize();
              window.controls.update();
            }
            else {
              ic.controls.handleResize();
              ic.controls.update();
            }

            if(ic.bRender) ic.drawCls.render();
        });

        ic.container.bind('mousemove touchmove', function (e) {
            thisClass.mouseMove(e);
        });
/*
        document.getElementById(ic.id).addEventListener('mousemove', function (e) {
            thisClass.mouseMove(e);
        });
        document.getElementById(ic.id).addEventListener('touchmove', function (e) {
            thisClass.mouseMove(e);
        });
*/
        ic.container.bind('mousewheel', function (e) {
        //document.getElementById(ic.id).addEventListener('mousewheel', function (e) {
            //e.preventDefault();
            e.preventDefault();
            if (!ic.scene) return;

            ic.bStopRotate = true;

            if(ic.bControlGl && !me.bNode) {
              window.controls.handleResize();
              window.controls.update();
            }
            else {
              ic.controls.handleResize();
              ic.controls.update();
            }

            if(ic.bRender) ic.drawCls.render();
        });
        ic.container.bind('DOMMouseScroll', function (e) {
        //document.getElementById(ic.id).addEventListener('DOMMouseScroll', function (e) {
            //e.preventDefault();
            e.preventDefault();
            if (!ic.scene) return;

            ic.bStopRotate = true;

            if(ic.bControlGl && !me.bNode) {
              window.controls.handleResize();
              window.controls.update();
            }
            else {
              ic.controls.handleResize();
              ic.controls.update();
            }

            if(ic.bRender) ic.drawCls.render();
        });
    }

    mouseMove(e) { let ic = this.icn3d, me = ic.icn3dui;
        if(me.bNode) return;

        //e.preventDefault();
        e.preventDefault();
        if (!ic.scene) return;
        // no action when no mouse button is clicked and no key was down
        //if (!ic.isDragging) return;

        //$("[id$=popup]").hide();
        $("#" + ic.pre + "popup").hide();

        let bClick = false;
        ic.rayCls.rayCaster(e, bClick);

        if(ic.bControlGl && !me.bNode) {
          window.controls.handleResize();
          window.controls.update();

          for(let divid in window.icn3duiHash) {
              let icTmp = window.icn3duiHash[divid].icn3d;
              if(icTmp.bRender) icTmp.drawCls.render();
          }
        }
        else {
          ic.controls.handleResize();
          ic.controls.update();

          if(ic.bRender) ic.drawCls.render();
        }
    }

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class Picking {
    constructor(icn3d) {
        this.icn3d = icn3d;
    }

    //Define actions when an atom is picked. By default, the atom information
    //($[structure id].[chain id]:[residue number]@[atom name]) is displayed.
    showPicking(atom, x, y) { let ic = this.icn3d, me = ic.icn3dui;
      //me = ic.setIcn3dui(ic.id);
      if(me.cfg.cid !== undefined && ic.pk != 0) {
          ic.pk = 1; // atom
      }
      ic.highlightlevel = ic.pk;
      this.showPickingBase(atom, x, y);

      if(ic.pk != 0) {
          if(x !== undefined && y !== undefined) { // mouse over
            if(me.cfg.showmenu != undefined && me.cfg.showmenu == true) {
                y += me.htmlCls.MENU_HEIGHT;
            }
            let text =(ic.pk == 1) ? atom.resn + atom.resi + '@' + atom.name : atom.resn + atom.resi;
            if(ic.structures !== undefined && Object.keys(ic.structures).length > 1) {
                text = atom.structure + '_' + atom.chain + ' ' + text;
                $("#" + ic.pre + "popup").css("width", "160px");
            }
            else {
                $("#" + ic.pre + "popup").css("width", "80px");
            }
            $("#" + ic.pre + "popup").html(text);
            $("#" + ic.pre + "popup").css("top", y).css("left", x+20).show();
          }
          else {
              // highlight the sequence background
              ic.hlUpdateCls.updateHlAll();
              let transformation = {};
              transformation.factor = ic._zoomFactor;
              transformation.mouseChange = ic.mouseChange;
              //transformation.quaternion = ic.quaternion;
              transformation.quaternion = {};
              transformation.quaternion._x = parseFloat(ic.quaternion._x).toPrecision(5);
              transformation.quaternion._y = parseFloat(ic.quaternion._y).toPrecision(5);
              transformation.quaternion._z = parseFloat(ic.quaternion._z).toPrecision(5);
              transformation.quaternion._w = parseFloat(ic.quaternion._w).toPrecision(5);

              /*
              if(ic.bAddCommands) {
                  ic.commands.push('pickatom ' + atom.serial + '|||' + ic.transformCls.getTransformationStr(transformation));
                  ic.optsHistory.push(me.hashUtilsCls.cloneHash(ic.opts));
                  ic.optsHistory[ic.optsHistory.length - 1].hlatomcount = Object.keys(ic.hAtoms).length;
                  if(me.utilsCls.isSessionStorageSupported()) ic.setStyleCls.saveCommandsToSession();
                  ic.STATENUMBER = ic.commands.length;
              }
              ic.logs.push('pickatom ' + atom.serial + '(chain: ' + atom.structure + '_' + atom.chain + ', residue: ' + atom.resn + ', number: ' + atom.resi + ', atom: ' + atom.name + ')');
              if( $( "#" + ic.pre + "logtext" ).length )  {
                $("#" + ic.pre + "logtext").val("> " + ic.logs.join("\n> ") + "\n> ").scrollTop($("#" + ic.pre + "logtext")[0].scrollHeight);
              }
              */
              me.htmlCls.clickMenuCls.setLogCmd('pickatom ' + atom.serial, true);

              ic.selectionCls.saveSelInCommand();

              // update the interaction flag
              ic.bSphereCalc = false;
              //me.htmlCls.clickMenuCls.setLogCmd('set calculate sphere false', true);
              ic.bHbondCalc = false;
              //me.htmlCls.clickMenuCls.setLogCmd('set calculate hbond false', true);
          }
      }
    }

    showPickingBase(atom, x, y) { let ic = this.icn3d; ic.icn3dui;
      if(x === undefined && y === undefined) { // NOT mouse over
          this.showPickingHilight(atom); // including render step
      }
    }

    showPickingHilight(atom) {  let ic = this.icn3d, me = ic.icn3dui;
      if(!ic.bShift && !ic.bCtrl) ic.hlObjectsCls.removeHlObjects();

      ic.pickedAtomList = {};
      if(ic.pk === 1) {
        ic.pickedAtomList[atom.serial] = 1;
      }
      else if(ic.pk === 2) {
        let residueid = atom.structure + '_' + atom.chain + '_' + atom.resi;
        ic.pickedAtomList = ic.residues[residueid];
      }
      else if(ic.pk === 3) {
        ic.pickedAtomList = this.selectStrandHelixFromAtom(atom);
      }
      else if(ic.pk === 4) {
        ic.pickedAtomList = this.select3ddomainFromAtom(atom);
      }
      else if(ic.pk === 5) {
        let chainid = atom.structure + '_' + atom.chain;
        ic.pickedAtomList = ic.chains[chainid];
      }

      if(ic.pk === 0) {
          ic.bShowHighlight = false;
      }
      else {
          ic.bShowHighlight = true;
      }

      let intersectAtoms = (Object.keys(ic.hAtoms).length == Object.keys(ic.atoms).length) ? {} : me.hashUtilsCls.intHash(ic.hAtoms, ic.pickedAtomList);
      let intersectAtomsSize = Object.keys(intersectAtoms).length;

      if(!ic.bShift && !ic.bCtrl) {
          //if(intersectAtomsSize > 0) {
          //    ic.hAtoms = me.hashUtilsCls.exclHash(ic.hAtoms, ic.pickedAtomList);
          //}
          //else {
          //    ic.hAtoms = me.hashUtilsCls.cloneHash(ic.pickedAtomList);
          //}
          ic.hAtoms = me.hashUtilsCls.cloneHash(ic.pickedAtomList);
      }
      else if(ic.bShift) { // select a range

        if(ic.prevPickedAtomList === undefined) {
            ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.pickedAtomList);
        }
        else {
            let prevAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.prevPickedAtomList);
            let currAtom = ic.firstAtomObjCls.getFirstAtomObj(ic.pickedAtomList);

            let prevChainid = prevAtom.structure + '_' + prevAtom.chain;
            let currChainid = currAtom.structure + '_' + currAtom.chain;

            if(prevChainid != currChainid) {
                ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.pickedAtomList);
            }
            else { // range in the same chain only
                let combinedAtomList;
                combinedAtomList = me.hashUtilsCls.unionHash(combinedAtomList, ic.prevPickedAtomList);
                combinedAtomList = me.hashUtilsCls.unionHash(combinedAtomList, ic.pickedAtomList);

                let firstAtom = ic.firstAtomObjCls.getFirstAtomObj(combinedAtomList);
                let lastAtom = ic.firstAtomObjCls.getLastAtomObj(combinedAtomList);

                for(let i = firstAtom.serial; i <= lastAtom.serial; ++i) {
                    ic.hAtoms[i] = 1;
                }
            }
        }

        // remember this shift selection
        ic.prevPickedAtomList = me.hashUtilsCls.cloneHash(ic.pickedAtomList);
      }
      else if(ic.bCtrl) {
          if(intersectAtomsSize > 0) {
              ic.hAtoms = me.hashUtilsCls.exclHash(ic.hAtoms, ic.pickedAtomList);
          }
          else {
              ic.hAtoms = me.hashUtilsCls.unionHash(ic.hAtoms, ic.pickedAtomList);
          }
      }

      ic.hlObjectsCls.removeHlObjects();
      ic.hlObjectsCls.addHlObjects();
    }

    select3ddomainFromAtom(atom) { let ic = this.icn3d, me = ic.icn3dui;
        let chainid = atom.structure + '_' + atom.chain;
        let resid = chainid + '_' + atom.resi;

        let domainid;
        for(let id in ic.tddomains) { // 3GVU_A_3d_domain_1
            let pos = id.indexOf('_3d_domain');
            if(id.substr(0, pos) == chainid) {
                if(Object.keys(ic.tddomains[id]).indexOf(resid) !== -1) {
                    domainid = id;
                    break;
                }
            }
        }

        let atomList = {};
        for(let resid in ic.tddomains[domainid]) {
            atomList = me.hashUtilsCls.unionHash(atomList, ic.residues[resid]);
        }

        return atomList;
    }

    //For an "atom", select all atoms in the same strand, helix, or coil.
    selectStrandHelixFromAtom(atom) { let ic = this.icn3d, me = ic.icn3dui;
        let firstAtom = atom;
        let lastAtom = atom;

        let atomsHash = {};

        // fill the beginning
        let beginResi = firstAtom.resi;
        if(!firstAtom.ssbegin && !isNaN(firstAtom.resi)) {
            for(let i = firstAtom.resi - 1; i > 0; --i) {
                let residueid = firstAtom.structure + '_' + firstAtom.chain + '_' + i;
                if(!ic.residues.hasOwnProperty(residueid)) break;

                let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
                beginResi = atom.resi;

                if( (firstAtom.ss !== 'coil' && atom.ss === firstAtom.ss && atom.ssbegin)
                  || (firstAtom.ss === 'coil' && atom.ss !== firstAtom.ss) ) {
                    if(firstAtom.ss === 'coil' && atom.ss !== firstAtom.ss) {
                        beginResi = parseInt(atom.resi) + 1;
                    }
                    break;
                }
            }

            for(let i = beginResi; i <= firstAtom.resi; ++i) {
                let residueid = firstAtom.structure + '_' + firstAtom.chain + '_' + i;
                atomsHash = me.hashUtilsCls.unionHash(atomsHash, me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
            }
        }

        // fill the end
        let endResi = lastAtom.resi;
        let endChainResi = ic.firstAtomObjCls.getLastAtomObj(ic.chains[lastAtom.structure + '_' + lastAtom.chain]).resi;
        for(let i = parseInt(lastAtom.resi) + 1; i <= parseInt(endChainResi); ++i) {
            let residueid = lastAtom.structure + '_' + lastAtom.chain + '_' + i;
            if(!ic.residues.hasOwnProperty(residueid)) break;

            let atom = ic.firstAtomObjCls.getFirstCalphaAtomObj(ic.residues[residueid]);
            endResi = atom.resi;

            if( (lastAtom.ss !== 'coil' && atom.ss === lastAtom.ss && atom.ssend) || (lastAtom.ss === 'coil' && atom.ss !== lastAtom.ss) ) {
                if(lastAtom.ss === 'coil' && atom.ss !== lastAtom.ss && !isNaN(atom.resi)) {
                    endResi = atom.resi - 1;
                }
                break;
            }
        }

        for(let i = parseInt(lastAtom.resi) + 1; i <= parseInt(endResi); ++i) {
            let residueid = lastAtom.structure + '_' + lastAtom.chain + '_' + i;
            atomsHash = me.hashUtilsCls.unionHash(atomsHash, me.hashUtilsCls.hash2Atoms(ic.residues[residueid], ic.atoms));
        }

        return atomsHash;
    }
}

//https://github.com/mrdoob/three.js/blob/master/examples/webxr_vr_cubes.html

class VRButton {
    constructor(icn3d) {
        this.icn3d = icn3d;

        //static xrSessionIsGranted = false;
        this.xrSessionIsGranted = false;
    }

    //static createButton( renderer, options ) {
    createButton( renderer, options ) { let ic = this.icn3d, me = ic.icn3dui;

        if ( options ) {

            console.error( 'THREE.VRButton: The "options" parameter has been removed. Please set the reference space type via renderer.xr.setReferenceSpaceType() instead.' );

        }

        const button = document.createElement( 'button' );

        function showEnterVR( /*device*/ ) {

            let currentSession = null;

            async function onSessionStarted( session ) {

                session.addEventListener( 'end', onSessionEnded );

                await renderer.xr.setSession( session );
                button.textContent = 'EXIT VR';

                currentSession = session;

            }

            function onSessionEnded( /*event*/ ) {
                // reset orientation after VR
                ic.transformCls.resetOrientation();
                
                ic.bVr = false;
                //ic.mdl.scale.copy(new THREE.Vector3( 1, 1, 1 )); 

                ic.drawCls.draw();

                currentSession.removeEventListener( 'end', onSessionEnded );

                button.textContent = 'ENTER VR';

                currentSession = null;

            }

            //

            button.style.display = '';

            button.style.cursor = 'pointer';
            //button.style.left = 'calc(50% - 50px)';
            button.style.left = 'calc(33% - 50px)';
            button.style.width = '100px';

            button.textContent = 'ENTER VR';

            button.onmouseenter = function () {

                button.style.opacity = '1.0';

            };

            button.onmouseleave = function () {

                button.style.opacity = '0.8'; //'0.5';

            };

            button.onclick = function () {       
                // imposter didn't work well in VR
                ic.bImpo = false;
                //ic.bInstanced = false;
                
                ic.bVr = true;
                //ic.mdl.scale.copy(ic.mdl.scale.multiplyScalar(0.2));

                ic.drawCls.draw(ic.bVr);

                if ( currentSession === null ) {

                    // WebXR's requestReferenceSpace only works if the corresponding feature
                    // was requested at session creation time. For simplicity, just ask for
                    // the interesting ones as optional features, but be aware that the
                    // requestReferenceSpace call will fail if it turns out to be unavailable.
                    // ('local' is always available for immersive sessions and doesn't need to
                    // be requested separately.)

                    const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking', 'layers' ] };
                    navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );

                } else {

                    currentSession.end();

                }

            };

        }

        function disableButton() {

            button.style.display = '';

            button.style.cursor = 'auto';
            button.style.left = 'calc(33% - 75px)'; //'calc(50% - 75px)';
            button.style.width = '150px';

            button.onmouseenter = null;
            button.onmouseleave = null;

            button.onclick = null;

        }

        function showWebXRNotFound() {

            disableButton();

            //button.textContent = 'VR NOT SUPPORTED';
            button.style.display = 'none';

        }

        function showVRNotAllowed( exception ) {

            disableButton();

            console.warn( 'Exception when trying to call xr.isSessionSupported', exception );

            //button.textContent = 'VR NOT ALLOWED';
            button.style.display = 'none';

        }

        function stylizeElement( element ) {

            element.style.position = 'absolute';
            element.style.bottom = '20px';
            element.style.padding = '12px 6px';
            element.style.border = '1px solid #fff';
            element.style.borderRadius = '4px';
            element.style.background = '#000'; //'rgba(0,0,0,0.5)';
            element.style.color = '#f8b84e'; //'#1c94c4'; //'#fff';
            element.style.font = 'bold 13px sans-serif';
            element.style.textAlign = 'center';
            element.style.opacity = '0.8';
            element.style.outline = 'none';
            element.style.zIndex = '999';

        }

        let thisClass = this;

        if ( 'xr' in navigator ) {

            button.id = me.pre + 'VRButton'; //'VRButton';
            button.style.display = 'none';

            stylizeElement( button );

            navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) {

                supported ? showEnterVR() : showWebXRNotFound();
                
                //if ( supported && VRButton.xrSessionIsGranted ) {
                if ( supported && thisClass.xrSessionIsGranted ) {

                    button.click();

                }

            } ).catch( showVRNotAllowed );

            return button;

        } else {
            const message = document.createElement( 'span' );
            return message;
        }

    }

    //static xrSessionIsGranted = false;

    //static registerSessionGrantedListener() {
    registerSessionGrantedListener() {

        if ( 'xr' in navigator ) {

            navigator.xr.addEventListener( 'sessiongranted', () => {

                //VRButton.xrSessionIsGranted = true;
                this.xrSessionIsGranted = true;

            } );

        }

    }

}

//https://github.com/mrdoob/three.js/blob/master/examples/webxr_ar_cones.html

class ARButton {
    constructor(icn3d) {
        this.icn3d = icn3d;

        //static xrSessionIsGranted = false;
        this.xrSessionIsGranted = false;
    }

	//static createButton( renderer, sessionInit = {} ) {
    createButton( renderer, sessionInit = {} ) { let ic = this.icn3d, me = ic.icn3dui;

		const button = document.createElement( 'button' );

		function showStartAR( /*device*/ ) {

			if ( sessionInit.domOverlay === undefined ) {

				const overlay = document.createElement( 'div' );
				overlay.style.display = 'none';
				document.body.appendChild( overlay );

				const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
				svg.setAttribute( 'width', 38 );
				svg.setAttribute( 'height', 38 );
				svg.style.position = 'absolute';
				svg.style.right = '20px';
				svg.style.top = '20px';
				svg.addEventListener( 'click', function () {

					currentSession.end();

				} );
				overlay.appendChild( svg );

				const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
				path.setAttribute( 'd', 'M 12,12 L 28,28 M 28,12 12,28' );
				path.setAttribute( 'stroke', '#fff' );
				path.setAttribute( 'stroke-width', 2 );
				svg.appendChild( path );

				if ( sessionInit.optionalFeatures === undefined ) {

					sessionInit.optionalFeatures = [];

				}

				sessionInit.optionalFeatures.push( 'dom-overlay' );
				sessionInit.domOverlay = { root: overlay };

			}

			//

			let currentSession = null;

			async function onSessionStarted( session ) {

				session.addEventListener( 'end', onSessionEnded );

				renderer.xr.setReferenceSpaceType( 'local' );

				await renderer.xr.setSession( session );

				button.textContent = 'STOP AR';
				sessionInit.domOverlay.root.style.display = '';

				currentSession = session;

			}

			function onSessionEnded( /*event*/ ) {
				// reset orientation after VR
				ic.transformCls.resetOrientation();

				ic.bAr = false;
				//ic.mdl.scale.copy(new THREE.Vector3( 1, 1, 1 ));

				ic.drawCls.draw();

				currentSession.removeEventListener( 'end', onSessionEnded );

				button.textContent = 'START AR';
				sessionInit.domOverlay.root.style.display = 'none';

				currentSession = null;

			}

			//

			button.style.display = '';

			button.style.cursor = 'pointer';
			//button.style.left = 'calc(50% - 50px)';
			button.style.left = 'calc(66% - 50px)';
			button.style.width = '100px';

			button.textContent = 'START AR';

			button.onmouseenter = function () {

				button.style.opacity = '1.0';

			};

			button.onmouseleave = function () {

				button.style.opacity = '0.8'; //'0.5';

			};

			button.onclick = function () {
                // imposter didn't work well in AR
                ic.bImpo = false;

                // important to keet the background transparent
				ic.opts['background'] = 'transparent';
                
                ic.bAr = true;
				//ic.mdl.scale.copy(ic.mdl.scale.multiplyScalar(0.2));

                ic.drawCls.draw(ic.bAr);

				if ( currentSession === null ) {

					navigator.xr.requestSession( 'immersive-ar', sessionInit ).then( onSessionStarted );

				} else {

					currentSession.end();

				}

			};

		}

		function disableButton() {

			button.style.display = '';

			button.style.cursor = 'auto';
			button.style.left = 'calc(66% - 50px)'; //'calc(50% - 75px)';
			button.style.width = '150px';

			button.onmouseenter = null;
			button.onmouseleave = null;

			button.onclick = null;

		}

		function showARNotSupported() {

			disableButton();

			//button.textContent = 'AR NOT SUPPORTED';
            button.style.display = 'none';

		}

        function showARAndroidPhone() {

			disableButton();

			//button.textContent = 'Chrome in Android Required';
            button.style.display = 'none';

		}

		function showARNotAllowed( exception ) {

			disableButton();

			console.warn( 'Exception when trying to call xr.isSessionSupported', exception );

			//button.textContent = 'AR NOT ALLOWED';
            button.style.display = 'none';

		}

		function stylizeElement( element ) {

			element.style.position = 'absolute';
			element.style.bottom = '20px';
			element.style.padding = '12px 6px';
			element.style.border = '1px solid #fff';
			element.style.borderRadius = '4px';
			element.style.background = '#000'; //'rgba(0,0,0,0.1)';
			element.style.color = '#f8b84e'; //'#fff';
			element.style.font = 'bold 13px sans-serif';
			element.style.textAlign = 'center';
			element.style.opacity = '0.8'; //'0.5';
			element.style.outline = 'none';
			element.style.zIndex = '999';

		}

		if(!me.utilsCls.isAndroid() || !me.utilsCls.isChrome()) {
            button.id = me.pre + 'ARButton'; //'ARButton';
			button.style.display = 'none';

			stylizeElement( button );

            showARAndroidPhone();

            return button;
        }
        else if ( 'xr' in navigator ) {

			button.id = me.pre + 'ARButton'; //'ARButton';
			button.style.display = 'none';

			stylizeElement( button );

			navigator.xr.isSessionSupported( 'immersive-ar' ).then( function ( supported ) {

				supported ? showStartAR() : showARNotSupported();

			} ).catch( showARNotAllowed );

			return button;

		} else {
/*            
			const message = document.createElement( 'a' );

			if ( window.isSecureContext === false ) {

				message.href = document.location.href.replace( /^http:/, 'https:' );
				message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message

			} else {

				message.href = 'https://immersiveweb.dev/';
				message.innerHTML = 'WEBXR NOT AVAILABLE';

			}

			message.style.left = 'calc(66% - 90px)'; //'calc(50% - 90px)';
			message.style.width = '180px';
			message.style.textDecoration = 'none';

			stylizeElement( message );

			return message;
*/
            const message = document.createElement( 'span' );
            return message;
		}

	}

}

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class iCn3D {
  constructor(icn3dui) { let me = icn3dui;
    this.icn3dui = icn3dui;
    this.id = this.icn3dui.pre + 'canvas';

    //A prefix for all custom html element id. It ensures all html elements have specific ids,
    //even when multiple iCn3D viewers are shown together.
    this.pre = this.icn3dui.pre; //this.id.substr(0, this.id.indexOf('_') + 1);

    this.container = $('#' + this.id);
    this.oriContainer = $('#' + this.id);

    this.bControlGl = false;

    this.maxatomcnt = 100000; // for a biological assembly, use instancing when the total number of atomsis greater than "maxatomcnt"

    this.overdraw = 0;

    this.bDrawn = false;
    this.bOpm = false; // true if the PDB data is from OPM for transmembrane proteins
    this.crossstrucinter = 0;

    this.bSecondaryStructure = false;

    //If its value is 1, the selected atoms will be highlighted with outlines around the structure.
    //If its value is 2, the selected atoms will be highlighted with transparent 3D objects such as
    //boxes, ribbons, cylinders, etc. If its value is undefined, no highlight will be shown.
    this.bHighlight = 1; // undefined: no highlight, 1: highlight by outline, 2: highlight by 3D object
    this.renderOrderPicking = -1; // less than 0, the default order is 0

    this.bInitial = true; // first 3d display

    this.bDoublecolor = false;

    this.originSize = 1; // radius

    this.ALTERNATE_STRUCTURE = -1;

    this.bUsePdbNum = true;

    let bWebGL, bWebGL2, bVR;
    if(!this.icn3dui.bNode) {
        let canvas = document.createElement( 'canvas' );
        bWebGL = !! ( window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ) );
        canvas.remove();

        canvas = document.createElement( 'canvas' );
        bWebGL2 = !! ( window.WebGLRenderingContext && ( canvas.getContext( 'webgl2' ) ) );
        canvas.remove();

        bVR = ( 'xr' in navigator ); // possibly support VR

        if(bWebGL){
            //https://discourse.threejs.org/t/three-js-r128-ext-frag-depth-and-angle-instanced-arrays-extensions-are-not-supported/26037
            //this.renderer = new THREE.WebGL1Renderer({
            if ( bWebGL2 && bVR) {                
                this.renderer = new THREE.WebGLRenderer({
                    canvas: this.oriContainer.get(0), //this.container.get(0),
                    antialias: true,
                    preserveDrawingBuffer: true,
                    sortObjects: false,
                    alpha: true
                });
                // Enable VR
                this.renderer.xr.enabled = true;
            }
            else {
                this.renderer = new THREE.WebGL1Renderer({
                    canvas: this.oriContainer.get(0), //this.container.get(0),
                    antialias: true,
                    preserveDrawingBuffer: true,
                    sortObjects: false,
                    alpha: true
                });
            }

            this.overdraw = 0;
        }
        else {
            alert("Currently your web browser has a problem on WebGL. If you are using Chrome, open a new tab for the same URL and WebGL may work again.");
        }
    }

    this.frac = new THREE.Color(0.1, 0.1, 0.1);
    this.shininess = 40; //30
    this.emissive = 0x111111; //0x000000

    this.light1 = 0.6; //1
    this.light2 = 0.4;
    this.light3 = 0.2;

    //This is the line radius for stabilizers, hydrogen bonds, and distance lines. It's 0.1 by default.
    this.lineRadius = 0.1; // hbonds, distance lines
    //This is the coil radius for coils. It's 0.3 by default.
    this.coilWidth = 0.3; //0.4; // style cartoon-coil
    //This is the stick radius. It's 0.4 by default.
    this.cylinderRadius = 0.4; // style stick
    //This is the cross-linkage radius. It's 0.4 by default.
    this.crosslinkRadius = 0.4; // cross-linkage
    //This is the stick radius for C alpha trace and O3' trace. It's 0.4 by default.
    this.traceRadius = 0.4; //0.4; // c alpha trace, nucleotide stick
    //This is the ball scale for styles 'Ball and Stick' and 'Dot'. It's 0.3 by default.
    this.dotSphereScale = 0.3; // style ball and stick, dot
    //This is the sphere radius for the style 'Sphere'. It's 1.5 by default.
    this.sphereRadius = 1.5; // style sphere
    //This is the cylinder radius for the style 'Cylinder and Plate'. It's 1.6 by default.
    this.cylinderHelixRadius = 1.6; // style sylinder and plate

    //This is the ribbon thickness for helix and sheet ribbons, and nucleotide ribbons. It's 0.4 by default.
    this.ribbonthickness = 0.2; // 0.4; // style ribbon, nucleotide cartoon, stand thickness
    //This is the width of protein ribbons. It's 1.3 by default.
    this.helixSheetWidth = 1.3; // style ribbon, nucleotide cartoon, stand thickness
    //This is the width of nucleotide ribbons. It's 0.8 by default.
    this.nucleicAcidWidth = 0.8; // nucleotide cartoon

    // mobile has a problem when the scaleFactor is 2.0
    // the scaleFactor improve the image quality, but it has some centering and picking problems in some Mac when it is not 1
    this.scaleFactor = 1.0;

    // scale all labels
    this.labelScale = 1.0; //0.3; //1.0;

    this.resizeRatioX = 1;
    this.resizeRatioY = 1;

    // Impostor shaders
    // This is a flag to turn on the rendering of spheres and cylinders using shaders instead of geometries.
    // It's true by default if the browser supports the EXT_frag_depth extension.
    this.bImpo = true;
    this.bInstanced = true;

    this.chainMissingResidueArray = {};
    this._zoomFactor = 1.0;

    this.transparentRenderOrder = false; // false: regular transparency; true: expensive renderOrder for each face

    this.AFUniprotVersion = 'v4';

    if(!this.icn3dui.bNode) {
        if ( bWebGL2 && bVR) { 
            // if(bVR) { // Meta browser (VR) has problems with imposter. The positions are wrong.
            //     this.bExtFragDepth = false;
            //     this.bImpo = false; 
            // }
            // else { // WebGL2 supports EXT_frag_depth and ANGLE_instanced_arrays
                this.bExtFragDepth = true;
                this.bImpo = true; 
                //console.log('WebGL2 is supported. Thus EXT_frag_depth and ANGLE_instanced_arrays are supported. All spheres and cylinders are drawn using shaders. Assembly is drawn with one copy of the asymmetric unit using hardware instancing.');
            // }

            this.bInstanced = true;
        }
        else {
            this.bExtFragDepth = this.renderer.extensions.get( "EXT_frag_depth" );
            if(!this.bExtFragDepth) {
                this.bImpo = false;
                console.log('EXT_frag_depth is NOT supported. All spheres and cylinders are drawn using geometry.');
            }
            else {
                console.log('EXT_frag_depth is supported. All spheres and cylinders are drawn using shaders.');
            }

            this.bInstanced = this.renderer.extensions.get( "ANGLE_instanced_arrays" );
            if(!this.bInstanced) {
                console.log('ANGLE_instanced_arrays is NOT supported. Assembly is drawn by making copies of the asymmetric unit.');
            }
            else {
                console.log('ANGLE_instanced_arrays is supported. Assembly is drawn with one copy of the asymmetric unit using hardware instancing.');
            }
        }
    }

    // cylinder impostor
    this.posArray = new Array();
    this.colorArray = new Array();

    this.pos2Array = new Array();
    this.color2Array = new Array();

    this.radiusArray = new Array();

    // sphere impostor
    this.posArraySphere = new Array();
    this.colorArraySphere = new Array();
    this.radiusArraySphere = new Array();

    this.axis = false;  // used to turn on and off xyz axes

    // pk
    //If its value is 1, selecting an atom will select the atom. If its value is 2, selecting an atom
    //will select the residue containing this atom. If its value is 3, selecting an atom will select
    //the strand or helix or coil containing this atom. If its value is 0, no selecting will work.
    this.pk = 1; // 0: no pk, 1: pk on atoms, 2: pk on residues, 3: pk on strand/helix/coil, 4: pk on domain, 5: pk on chain, 6: structure
    this.highlightlevel = 1; // 1: highlight on atoms, 2: highlight on residues, 3: highlight on strand/helix/coil 4: highlight on chain 5: highlight on structure

    this.pickpair = false; // used for pk pair of atoms for label and distance
    this.pAtomNum = 0;

    //"pAtom" has the value of the atom index of the picked atom.
    this.pAtom = undefined;
    //When two atoms are required to be selected (e.g., for measuring distance),
    //"pAtom2" has the value of the atom index of the 2nd picked atom.
    this.pAtom2 = undefined;

    this.bCtrl = false; // if true, union selection on sequence window or on 3D structure
    this.bShift = false; // if true, select a range on 3D structure

    //Once clicked, this flag can be set as "true" to the automatic rotation. It's false by default.
    this.bStopRotate = false; // by default, do not stop the possible automatic rotation
    this.bCalphaOnly = false; // by default the input has both Calpha and O, used for drawing strands. If atoms have Calpha only, the orientation of the strands is random
//    this.bSSOnly = false; // a flag to turn on when only helix and bricks are available to draw 3D dgm

//    this.bAllAtoms = true; // no need to adjust atom for strand style

    this.bConsiderNeighbors = false; // a flag to show surface considering the neighboring atoms or not

    this.bShowCrossResidueBond = true;

    this.bExtrude = true;

    this.effects = {
        //'anaglyph': new THREE.AnaglyphEffect(this.renderer),
        //'parallax barrier': new THREE.ParallaxBarrierEffect(this.renderer),
        //'oculus rift': new THREE.OculusRiftEffect(this.renderer),
        //'stereo': new THREE.StereoEffect(this.renderer),
        'none': this.renderer
    };

    this.maxD = 500; // size of the molecule
    this.oriMaxD = this.maxD; // size of the molecule
    //this.cam_z = -150;

    this.cam_z = this.maxD * 2; // when zooming in, it gets dark if the camera is in front
    //this.cam_z = -this.maxD * 2;

    // these variables will not be cleared for each structure
    this.commands = []; // a list of commands, ordered by the operation steps. Each operation will be converted into a command. this command list can be used to go backward and forward.
    this.optsHistory = []; // a list of options corresponding to this.commands.
    this.logs = []; // a list of comands and other logs, ordered by the operation steps.

    //This is a flag to turn off the rendering part if a sequence of commands are executed. It's true by default.
    this.bRender = true; // a flag to turn off rendering when loading state file

    // Default values
    //This defines the highlight color.
//    this.hColor = new THREE.Color(0xFFFF00);
    this.hColor = new THREE.Color(0xFFFF33);

    this.sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
    this.boxGeometry = new THREE.BoxGeometry(1, 1, 1);
    this.cylinderGeometry = new THREE.CylinderGeometry(1, 1, 1, 32, 1);
    this.cylinderGeometryOutline = new THREE.CylinderGeometry(1, 1, 1, 32, 1, true);
    this.axisDIV = 5 * 3; //5; // 3;
    this.strandDIV = 6;
    this.tubeDIV = 8;
    this.nucleicAcidStrandDIV = 6; //4;

    this.linewidth = 1;
    this.hlLineRadius = 0.1; // style line, highlight
    //this.curveWidth = 3;

    this.threshbox = 180; // maximum possible boxsize, default 180
    this.maxAtoms3DMultiFile = 40000; // above the threshold, multiple files wil be output for 3D printing

    this.tsHbond = 3.8;
    this.tsIonic = 6;
    this.tsContact = 4;
    this.tsHalogen = 3.8;
    this.tsPication = 6;
    this.tsPistacking = 5.5;

    this.LABELSIZE = 30;

    this.rayThreshold = 0.5; // threadshold for raycast
    this.colorBlackbkgd = '#ffff00';
    this.colorWhitebkgd = '#000000';

    //The default display options
    this.optsOri = {};
    this.optsOri['camera']             = 'perspective';        //perspective, orthographic
    this.optsOri['background']         = 'black';              //transparent, black, grey, white
    this.optsOri['color']              = 'chain';              //spectrum, secondary structure, charge, hydrophobic, conserved, chain, residue, atom, b factor, red, green, blue, magenta, yellow, cyan, white, grey, custom
    this.optsOri['proteins']           = 'ribbon';             //ribbon, strand, cylinder and plate, schematic, c alpha trace, backbone, b factor tube, lines, stick, ball and stick, sphere, nothing
    this.optsOri['sidec']              = 'nothing';            //lines2, stick2, ball and stick2, sphere2, nothing
    this.optsOri['nucleotides']        = 'nucleotide cartoon'; //nucleotide cartoon, o3 trace, backbone, schematic, lines, stick,
                                                              // nucleotides ball and stick, sphere, nothing
    this.optsOri['ntbase']             = 'nothing';            //lines2, stick2, ball and stick2, sphere2, nothing

    this.optsOri['surface']            = 'nothing';            //Van der Waals surface, molecular surface, solvent accessible surface, nothing
    this.optsOri['opacity']            = '1.0';                //1.0, 0.9, 0.8, 0.7, 0.6, 0.5
    this.optsOri['wireframe']          = 'no';                 //yes, no
    this.optsOri['map']                = 'nothing';            //2fofc, fofc, nothing
    this.optsOri['mapwireframe']       = 'yes';                //yes, no
    this.optsOri['emmap']              = 'nothing';            //em, nothing
    this.optsOri['emmapwireframe']     = 'yes';                //yes, no
    this.optsOri['phimap']             = 'nothing';            //phi, nothing
    this.optsOri['phimapwireframe']    = 'yes';                //yes, no
    this.optsOri['phisurface']         = 'nothing';            //phi, nothing
    this.optsOri['phisurftype']        = 'nothing';            //Van der Waals surface, molecular surface, solvent accessible surface, nothing
    this.optsOri['phisurfop']          = '1.0';                //1.0, 0.9, 0.8, 0.7, 0.6, 0.5
    this.optsOri['phisurfwf']          = 'yes';                //yes, no
    this.optsOri['chemicals']          = 'stick';              //lines, stick, ball and stick, schematic, sphere, nothing
    this.optsOri['water']              = 'nothing';            //sphere, dot, nothing
    this.optsOri['ions']               = 'sphere';             //sphere, dot, nothing
    this.optsOri['hbonds']             = 'no';                 //yes, no
    this.optsOri['saltbridge']         = 'no';                 //yes, no
    this.optsOri['contact']            = 'no';                 //yes, no
    this.optsOri['halogen']            = 'no';                 //yes, no
    this.optsOri['pi-cation']          = 'no';                 //yes, no
    this.optsOri['pi-stacking']        = 'no';                 //yes, no
    //this.optsOri['stabilizer']         = 'no';                 //yes, no
    this.optsOri['ssbonds']            = 'yes';                 //yes, no
    this.optsOri['clbonds']            = 'yes';                 //yes, no
    this.optsOri['rotationcenter']     = 'molecule center';    //molecule center, pick center, display center
    this.optsOri['axis']               = 'no';                 //yes, no
    this.optsOri['fog']                = 'no';                 //yes, no
    this.optsOri['slab']               = 'no';                 //yes, no
    this.optsOri['pk']                 = 'residue';            //no, atom, residue, strand, chain
    this.optsOri['chemicalbinding']    = 'hide';               //show, hide

    this.opts = me.hashUtilsCls.cloneHash(this.optsOri);

    this.sheetcolor = 'green';
    this.bShowHighlight = true;
    this.mapData = {};

    // previously in iCn3DUI
    this.bFullUi = true;
    this.divid = this.icn3dui.cfg.divid;

    this.inputid = '';
    this.setOperation = 'or'; // by default the set operation is 'or'
    this.ROT_DIR = 'right';
    //this.prevCommands = "";
    this.currSelectedSets = []; // for selecting multiple sets in sequence & annotations
    this.selectedResidues = {};

    this.ncbi2resid = {}; // convert from NCBI residue ID (structure_chain_resi) to PDB residue ID (structure_chain_resi)
    this.resid2ncbi = {}; // convert from PDB residue ID (structure_chain_resi) to NCBI residue ID (structure_chain_resi) 

    this.shapeCmdHash = {}; // remember the spheres/cubes for sets

    this.bHideSelection = true;
    this.bSelectResidue = false;
    this.bSelectAlignResidue = false;
    //A flag to remember whether the annotation window was set.
    this.bAnnoShown = false;
    //A flag to remember whether the menu of defined sets was set.
    this.bSetChainsAdvancedMenu = false;
    //A flag to remember whether the 2D interaction diagram was set.
    this.b2DShown = false;
    this.bCrashed = false;
    //A flag to determine whether to add current step into the command history.
    this.bAddCommands = true;
    //A flag to determine whether to add current step into the log window.
    this.bAddLogs = true;
    //A flag to determine whether to load the coordinates of the structure. When resetting the view,
    //it is true so that the coordinates of the structure will not be loaded again.
    this.bNotLoadStructure = false;

    this.InputfileData = '';
    this.bVr = false; // cflag to indicate whether in VR state
    this.bAr = false; // cflag to indicate whether in VR state

    // default color range for Add Custom Color button in the Sequence & Annotation window
    this.startColor = 'blue';
    this.midColor = 'white';
    this.endColor = 'red';
    this.startValue = 0;
    this.midValue = 50;
    this.endValue = 100;

    this.crosslinkRadius = 0.4; 

    // classes
    this.sceneCls = new Scene(this);
    this.cameraCls = new Camera(this);
    this.fogCls = new Fog(this);

    this.boxCls = new Box(this);
    this.brickCls = new Brick(this);
    this.curveStripArrowCls = new CurveStripArrow(this);
    this.curveCls = new Curve(this);
    this.cylinderCls = new Cylinder(this);
    this.lineCls = new Line$1(this);
    this.reprSubCls = new ReprSub(this);
    this.sphereCls = new Sphere$1(this);
    this.stickCls = new Stick(this);
    this.strandCls = new Strand(this);
    this.stripCls = new Strip(this);
    this.tubeCls = new Tube(this);
    this.cartoonNuclCls = new CartoonNucl(this);
    this.surfaceCls = new Surface(this);
    this.labelCls = new Label(this);
    this.axesCls = new Axes(this);
    this.glycanCls = new Glycan(this);

    this.applyCenterCls = new ApplyCenter(this);
    this.applyClbondsCls = new ApplyClbonds(this);
    this.applyDisplayCls = new ApplyDisplay(this);
    this.applyMapCls = new ApplyMap(this);
    this.applyOtherCls = new ApplyOther(this);
    this.applySsbondsCls = new ApplySsbonds(this);
    this.applySymdCls = new ApplySymd(this);

    this.hlObjectsCls = new HlObjects(this);
    this.residueLabelsCls = new ResidueLabels(this);
    this.alternateCls = new Alternate(this);

    this.drawCls = new Draw(this);
    this.firstAtomObjCls = new FirstAtomObj(this);

    this.impostorCls = new Impostor(this);
    this.instancingCls = new Instancing(this);

    this.contactCls = new Contact(this);
    this.hBondCls = new HBond(this);
    this.piHalogenCls = new PiHalogen(this);
    this.saltbridgeCls = new Saltbridge(this);

    this.loadPDBCls = new LoadPDB(this);
    this.vastplusCls = new Vastplus(this);
    this.transformCls = new Transform(this);

    this.setStyleCls = new SetStyle(this);
    this.setColorCls = new SetColor(this);

    // classes from icn3dui
    this.threeDPrintCls = new ThreeDPrint(this);
    this.export3DCls = new Export3D(this);

    this.annoCddSiteCls = new AnnoCddSite(this);
    this.annoContactCls = new AnnoContact(this);
    this.annoPTMCls = new AnnoPTM(this);
    this.annoCrossLinkCls = new AnnoCrossLink(this);
    this.annoDomainCls = new AnnoDomain(this);
    this.annoSnpClinVarCls = new AnnoSnpClinVar(this);
    this.annoSsbondCls = new AnnoSsbond(this);
    this.annoTransMemCls = new AnnoTransMem(this);
    this.domain3dCls = new Domain3d(this);

    this.addTrackCls = new AddTrack(this);
    this.annotationCls = new Annotation(this);
    this.showAnnoCls = new ShowAnno(this);
    this.showSeqCls = new ShowSeq(this);

    this.hlSeqCls = new HlSeq(this);
    this.hlUpdateCls = new HlUpdate(this);

    this.lineGraphCls = new LineGraph(this);
    this.getGraphCls = new GetGraph(this);
    this.showInterCls = new ShowInter(this);
    this.viewInterPairsCls = new ViewInterPairs(this);
    this.drawGraphCls = new DrawGraph(this);
    this.contactMapCls = new ContactMap(this);

    this.alignParserCls = new AlignParser(this);
    this.chainalignParserCls = new ChainalignParser(this);
    this.dsn6ParserCls = new Dsn6Parser(this);
    this.mmcifParserCls = new MmcifParser(this);
    this.mmdbParserCls = new MmdbParser(this);
    this.mmtfParserCls = new MmtfParser(this);
    this.mol2ParserCls = new Mol2Parser(this);
    this.opmParserCls = new OpmParser(this);
    this.pdbParserCls = new PdbParser(this);
    this.sdfParserCls = new SdfParser(this);
    this.xyzParserCls = new XyzParser(this);
    this.realignParserCls = new RealignParser(this);
    this.densityCifParserCls = new DensityCifParser(this);
    this.ParserUtilsCls = new ParserUtils(this);
    this.loadAtomDataCls = new LoadAtomData(this);
    this.setSeqAlignCls = new SetSeqAlign(this);

    this.applyCommandCls = new ApplyCommand(this);
    this.definedSetsCls = new DefinedSets(this);
    this.legendTableCls = new LegendTable(this);
    this.loadScriptCls = new LoadScript(this);
    this.selByCommCls = new SelectByCommand(this);
    this.selectionCls = new Selection(this);
    this.resid2specCls = new Resid2spec(this);

    this.delphiCls = new Delphi(this);
    this.dsspCls = new Dssp(this);
    this.refnumCls = new Refnum(this);
    this.scapCls = new Scap(this);
    this.symdCls = new Symd(this);
    this.alignSWCls = new AlignSW(this);

    this.analysisCls = new Analysis(this);
    this.resizeCanvasCls = new ResizeCanvas(this);
    this.saveFileCls = new SaveFile(this);
    this.setOptionCls = new SetOption(this);
    this.shareLinkCls = new ShareLink(this);
    this.diagram2dCls = new Diagram2d(this);
    this.cartoon2dCls = new Cartoon2d(this);

    this.rayCls = new Ray(this);
    this.controlCls = new Control(this);
    this.pickingCls = new Picking(this);

    this.VRButtonCls = new VRButton(this);
    this.ARButtonCls = new ARButton(this);

    // set this.matShader
    //This defines the highlight color using the outline method. It can be defined using the function setOutlineColor().
    this.matShader = this.setColorCls.setOutlineColor('yellow');
  }
}
//When users first load a structure, call this function to empty previous settings.
iCn3D.prototype.init = function (bKeepCmd) {
    this.init_base();

    this.molTitle = "";

    this.ssbondpnts = {}; // disulfide bonds for each structure
    this.clbondpnts = {}; // cross-linkages for each structure

    //this.inputid = {"idtype": undefined, "id":undefined}; // support pdbid, mmdbid

    this.biomtMatrices = [];
    this.bAssembly = true; //false; 

    this.bDrawn = false;
    this.bSecondaryStructure = false;

    this.bHighlight = 1; // undefined: no highlight, 1: highlight by outline, 2: highlight by 3D object

    this.axes = [];
};

iCn3D.prototype.init_base = function (bKeepCmd) {
    this.resetConfig();
    
    this.structures = {}; // structure name -> array of chains
    this.chains = {}; // structure_chain name -> atom hash
    this.tddomains = {}; // structure_chain_3d_domain_# name -> residue id hash such as {'structure_chain_3d_domain_1': 1, ...}
    this.residues = {}; // structure_chain_resi name -> atom hash
    this.secondaries = {}; // structure_chain_resi name -> secondary structure: 'c', 'H', or 'E'
    this.alnChains = {}; // structure_chain name -> atom hash

    this.chainsSeq = {}; // structure_chain name -> array of sequence
    this.chainsColor = {}; // structure_chain name -> color, show chain color in sequence display for mmdbid and align input
    this.chainsGene = {}; // structure_chain name -> gene, show chain gene symbol in sequence display for mmdbid and align input
    this.chainsAn = {}; // structure_chain name -> array of annotations, such as residue number
    this.chainsAnTitle = {}; // structure_chain name -> array of annotation title

    this.chainsMapping = {}; // structure_chain name -> residue id hash such as {'structure_chain_resi1': 'reference residue such as K10', ...}
    this.resid2refnum = {}; // residue id -> reference number, e.g.,  {'1WIO_A_16': '2050', ...}
    this.refnum2residArray = {}; // reference number -> array of residue id, e.g.,  {'2050': ['1WIO_A_16', ...], ...}
    this.bShowRefnum = false;
    
    this.alnChainsSeq = {}; // structure_chain name -> array of residue object: {mmdbid, chain, resi, resn, aligned}
    this.alnChainsAnno = {}; // structure_chain name -> array of annotations, such as residue number
    this.alnChainsAnTtl = {}; // structure_chain name -> array of annotation title

    //this.dAtoms = {}; // show selected atoms
    //this.hAtoms = {}; // used to change color or dislay type for certain atoms

    this.pickedAtomList = {}; // used to switch among different highlight levels

    this.prevHighlightObjects = [];
    this.prevHighlightObjects_ghost = [];

    this.prevSurfaces = [];
    this.prevMaps = [];
    this.prevEmmaps = [];
    this.prevPhimaps = [];

    this.prevOtherMesh = [];

    this.defNames2Residues = {}; // custom defined selection name -> residue array
    this.defNames2Atoms = {}; // custom defined selection name -> atom array
    this.defNames2Descr = {}; // custom defined selection name -> description
    this.defNames2Command = {}; // custom defined selection name -> command

    this.residueId2Name = {}; // structure_chain_resi -> one letter abbreviation

    this.atoms = {};
    //This is a hash used to store all atoms to be displayed. The key is the atom index. Its value is set as 1.
    this.dAtoms = {};
    //This is a hash used to store all atoms to be highlighted. The key is the atom index. Its value is set as 1.
    this.hAtoms = {};
    this.proteins = {};
    this.sidec = {};
    this.ntbase = {};
    this.nucleotides = {};
    this.nucleotidesO3 = {};

    this.chemicals = {};
    this.ions = {};
    this.water = {};
    this.calphas = {};
    //this.mem = {}; // membrane for OPM pdb

    this.hbondpnts = [];
    this.saltbridgepnts = [];
    this.contactpnts = [];
    this.stabilizerpnts = [];

    this.halogenpnts = [];
    this.picationpnts = [];
    this.pistackingpnts = [];

    this.distPnts = [];

    this.doublebonds = {};
    this.triplebonds = {};
    this.aromaticbonds = {};

    this.atomPrevColors = {};

    this.style2atoms = {}; // style -> atom hash, 13 styles: ribbon, strand, cylinder and plate, nucleotide cartoon, o3 trace, schematic, c alpha trace, b factor tube, lines, stick, ball and stick, sphere, dot, nothing
    this.labels = {};     // hash of name -> a list of labels. Each label contains 'position', 'text', 'size', 'color', 'background'
                        // label name could be custom, residue, schmatic, distance
    this.lines = {};     // hash of name -> a list of solid or dashed lines. Each line contains 'position1', 'position2', 'color', and a boolean of 'dashed'
                        // line name could be custom, hbond, ssbond, distance

    // used for interactions
    this.resids2inter = {};
    this.resids2interAll = {};

    this.transformCls.rotateCount = 0;
    this.transformCls.rotateCountMax = 20;

    if(bKeepCmd) this.commands = [];

    this.axes = [];

    this.bGlycansCartoon = 0;
    this.bMembrane = 1;
    this.bCmdWindow = 0;

    //this.chainid2offset = {};

    this.chainMissingResidueArray = {};
    this.nTotalGap = 0;
};

//Reset parameters for displaying the loaded structure.
iCn3D.prototype.reinitAfterLoad = function () { let ic = this, me = ic.icn3dui;
    ic.resetConfig();

    ic.setStyleCls.setAtomStyleByOptions();
    ic.setColorCls.setColorByOptions(ic.opts, ic.atoms);

    ic.dAtoms = me.hashUtilsCls.cloneHash(ic.atoms); // show selected atoms
    ic.hAtoms = me.hashUtilsCls.cloneHash(ic.atoms); // used to change color or dislay type for certain atoms

    ic.prevHighlightObjects = [];
    ic.prevHighlightObjects_ghost = [];

    ic.prevSurfaces = [];
    ic.prevMaps = [];
    ic.prevEmmaps = [];
    ic.prevPhimaps = [];

    ic.prevOtherMesh = [];

    ic.labels = {};   // hash of name -> a list of labels. Each label contains 'position', 'text', 'size', 'color', 'background'
                        // label name could be custom, residue, schmatic, distance
    ic.lines = {};    // hash of name -> a list of solid or dashed lines. Each line contains 'position1', 'position2', 'color', and a boolean of 'dashed'
                        // line name could be custom, hbond, ssbond, distance

    ic.shapeCmdHash = {};

    ic.bAssembly = true; //false;
};

iCn3D.prototype.resetConfig = function () { let ic = this, me = ic.icn3dui;
    this.opts = me.hashUtilsCls.cloneHash(this.optsOri);

    if(me.cfg.align !== undefined || me.cfg.chainalign !== undefined) {
        this.opts['color'] = 'identity';
        this.opts['proteins'] = 'c alpha trace';
        this.opts['nucleotides'] = 'o3 trace';
    }

    if(me.cfg.cid !== undefined) {
        this.opts['color'] = 'atom';

        this.opts['pk'] = 'atom';
        this.opts['chemicals'] = 'ball and stick';
    }

    if(me.cfg.afid !== undefined) {
        this.opts['color'] = 'confidence';
    }

    if(me.cfg.blast_rep_id !== undefined) this.opts['color'] = 'conservation';
    if(me.cfg.mmdbafid !== undefined) {
        let idArray = me.cfg.mmdbafid.split(',');
        if(idArray.length > 1) {
            ic.opts['color'] = 'structure';
        }
        else if(idArray.length == 1) {
            let struct = idArray[0];
            if(isNaN(struct) && struct.length > 5) {
                this.opts['color'] = 'confidence';
            }
            else {
                ic.opts['color'] = 'chain';
            }
        }
    }

    if(me.cfg.options !== undefined) $.extend(this.opts, me.cfg.options);
};

/**
 * @author Jiyao Wang <wangjiy@ncbi.nlm.nih.gov> / https://github.com/ncbi/icn3d
 */

class iCn3DUI {
  constructor(cfg) {
    //A hash containing all input parameters.
    this.cfg = cfg;
    //A prefix for all custom html element id. It ensures all html elements have specific ids,
    //even when multiple iCn3D viewers are shown together.
    this.pre = this.cfg.divid + "_";

    this.REVISION = '3.21.1';

    // In nodejs, iCn3D defines "window = {navigator: {}}"
    this.bNode = (Object.keys(window).length < 2) ? true : false;

    if(this.cfg.command === undefined) this.cfg.command = '';
    if(this.cfg.width === undefined) this.cfg.width = '100%';
    if(this.cfg.height === undefined) this.cfg.height = '100%';
    if(this.cfg.resize === undefined) this.cfg.resize = true;
    if(this.cfg.showmenu === undefined) this.cfg.showmenu = true;
    if(this.cfg.showtitle === undefined) this.cfg.showtitle = true;
    if(this.cfg.showcommand === undefined) this.cfg.showcommand = true;
    //if(this.cfg.simplemenu === undefined) this.cfg.simplemenu = false;
    if(this.cfg.mobilemenu === undefined) this.cfg.mobilemenu = false;
    if(this.cfg.closepopup === undefined) this.cfg.closepopup = false;
    if(this.cfg.showanno === undefined) this.cfg.showanno = false;
    if(this.cfg.showseq === undefined) this.cfg.showseq = false;
    if(this.cfg.showalignseq === undefined) this.cfg.showalignseq = false;
    if(this.cfg.show2d === undefined) this.cfg.show2d = false;
    if(this.cfg.showsets === undefined) this.cfg.showsets = false;
    if(this.cfg.rotate === undefined) this.cfg.rotate = 'right';
    if(this.cfg.hidelicense === undefined) this.cfg.hidelicense = false;

    // classes
    this.hashUtilsCls = new HashUtilsCls(this);
    this.utilsCls = new UtilsCls(this);
    this.parasCls = new ParasCls(this);
    this.myEventCls = new MyEventCls(this);
    this.rmsdSuprCls = new RmsdSuprCls(this);
    this.subdivideCls = new SubdivideCls(this);
    this.convertTypeCls = new ConvertTypeCls(this);

    this.htmlCls = new Html(this);
  }

  //You can add your custom events in this function if you want to add new links in the function setTools.
  allCustomEvents() {
      // add custom events here
  }

}

// show3DStructure is the main function to show 3D structure
iCn3DUI.prototype.show3DStructure = async function(pdbStr) { let me = this;
  let thisClass = this;
//   me.deferred = $.Deferred(function() {
    if(me.cfg.menuicon) {
        me.htmlCls.wifiStr = '<i class="icn3d-wifi" title="requires internet">&nbsp;</i>';
        me.htmlCls.licenseStr = '<i class="icn3d-license" title="requires license">&nbsp;</i>';
    }
    else {
        me.htmlCls.wifiStr = '';
        me.htmlCls.licenseStr = '';
    }

    me.setIcn3d();
    let ic = me.icn3d;

    if(me.utilsCls.isSessionStorageSupported()) ic.setStyleCls.getCommandsBeforeCrash();

    let width = me.htmlCls.WIDTH; // - me.htmlCls.LESSWIDTH_RESIZE;
    let height = me.htmlCls.HEIGHT; // - me.htmlCls.LESSHEIGHT - me.htmlCls.EXTRAHEIGHT;
    me.oriWidth = width;
    me.oriHeight = height;

    me.htmlCls.eventsCls.allEventFunctions();
    thisClass.allCustomEvents();

    let extraHeight = 0;
    if(me.cfg.showmenu == undefined || me.cfg.showmenu) {
        //extraHeight += 2*me.htmlCls.MENU_HEIGHT;
        extraHeight += me.htmlCls.MENU_HEIGHT;
    }
    if(me.cfg.showcommand == undefined || me.cfg.showcommand) {
        extraHeight += me.htmlCls.CMD_HEIGHT;
    }
    if(me.cfg.showmenu != undefined && me.cfg.showmenu == false) {
      me.htmlCls.setMenuCls.hideMenu();
    }
    else {
      me.htmlCls.setMenuCls.showMenu();
    }
    if(me.cfg.showtitle != undefined && me.cfg.showtitle == false) {
      $("#" + ic.pre + "title").hide();
    }
    else {
      $("#" + ic.pre + "title").show();
    }
    $("#" + ic.pre + "viewer").width(width).height(parseInt(height) + extraHeight);
    $("#" + ic.pre + "canvas").width(width).height(parseInt(height));
    $("#" + ic.pre + "canvas").resizable({
      resize: function( event, ui ) {
        me.htmlCls.WIDTH = ui.size.width; //$("#" + ic.pre + "canvas").width();
        me.htmlCls.HEIGHT = ui.size.height; //$("#" + ic.pre + "canvas").height();
        if(ic !== undefined && !me.icn3d.bFullscreen) {
            ic.resizeCanvasCls.resizeCanvas(me.htmlCls.WIDTH, me.htmlCls.HEIGHT, true);
        }
      }
    });

    if(me.cfg.usepdbnum !== undefined) {
        me.icn3d.bUsePdbNum = me.cfg.usepdbnum;
    }
    else {
        if(me.cfg.date !== undefined) {
            me.icn3d.bUsePdbNum =(parseInt(me.cfg.date) >= 20201222) ? true : false;
        }
        else {
            // iCn3D paper
            if(me.cfg.mmdbid == '1tup' && me.cfg.showanno == 1 && me.cfg.show2d == 1 && me.cfg.showsets == 1) {
                me.icn3d.bUsePdbNum = false;
            }
            //https://link.springer.com/article/10.1007/s00239-020-09934-4/figures/1
            else if(me.cfg.mmdbid == '118496' && me.cfg.showanno == 0 && me.cfg.inpara.indexOf('bu=1') != -1) {
                me.icn3d.bUsePdbNum = false;
            }
            //https://link.springer.com/article/10.1007/s00239-020-09934-4/figures/6
            else if(me.cfg.align == '163605,1,91105,1,1,1' && me.cfg.inpara.indexOf('atype=1') != -1) {
                me.icn3d.bUsePdbNum = false;
            }
            else {
                me.icn3d.bUsePdbNum = true;
            }
        }
    }

    if(me.cfg.replay) {
        ic.bReplay = 1;
        $("#" + ic.pre + "replay").show();
    }
    else {
        ic.bReplay = 0;
        $("#" + ic.pre + "replay").hide();
    }
    if(me.utilsCls.isMobile()) ic.threshbox = 60;
    if(me.cfg.controlGl) {
        ic.bControlGl = true;
        ic.container =(ic.bControlGl && !me.bNode) ? $(document) : $('#' + ic.id);
    }
    //ic.controlCls.setControl(); // rotation, translation, zoom, etc
    ic.setStyleCls.handleContextLost();
    ic.applyCenterCls.setWidthHeight(width, height);
    ic.ori_chemicalbinding = ic.opts['chemicalbinding'];
    if(me.cfg.bCalphaOnly !== undefined) ic.bCalphaOnly = me.cfg.bCalphaOnly;
    ic.opts = me.hashUtilsCls.cloneHash(ic.opts);
    ic.STATENUMBER = ic.commands.length;
    // If previously crashed, recover it
    if(me.utilsCls.isSessionStorageSupported() && ic.bCrashed) {
        ic.bCrashed = false;
        let loadCommand = ic.commandsBeforeCrash.split('|||')[0];
        let id = loadCommand.substr(loadCommand.lastIndexOf(' ') + 1);
        // reload only if viewing the same structure
        if(id === me.cfg.mmtfid || id === me.cfg.pdbid || id === me.cfg.opmid || id === me.cfg.mmdbid || id === me.cfg.gi  || id === me.cfg.blast_rep_id
          || id === me.cfg.cid || id === me.cfg.mmcifid || id === me.cfg.align || id === me.cfg.chainalign || id === me.cfg.mmdbafid) {
            await ic.loadScriptCls.loadScript(ic.commandsBeforeCrash, true);
            return;
        }
    }
    ic.molTitle = '';
    ic.loadCmd;

    // set menus
    me.htmlCls.clickMenuCls.getHiddenMenusFromCache();
    me.htmlCls.clickMenuCls.applyShownMenus();

    if(pdbStr) { // input pdbStr
        ic.init();

        ic.bInputfile = true;
        ic.InputfileType = 'pdb';
        ic.InputfileData = (ic.InputfileData) ? ic.InputfileData + '\nENDMDL\n' + pdbStr : pdbStr;
        
        await ic.pdbParserCls.loadPdbData(pdbStr);

        if(me.cfg.resdef !== undefined && me.cfg.chains !== undefined) {
            let structureArray = Object.keys(ic.structures);
            let chainArray = me.cfg.chains.split(' | ');
            let chainidArray = [];
            if(structureArray.length == chainArray.length) {
                for(let i = 0, il = structureArray.length; i  < il; ++i) {
                    chainidArray.push(structureArray[i] + '_' + chainArray[i]);
                }
                
                chainidArray = ic.chainalignParserCls.addPostfixForChainids(chainidArray);
                
                let bRealign = true, bPredefined = true;
                await ic.realignParserCls.realignChainOnSeqAlign(undefined, chainidArray, bRealign, bPredefined);
            }
        }
        else if(me.cfg.resdef !== undefined && me.cfg.matchedchains !== undefined) {
            let stru_t = Object.keys(ic.structures)[0];
            let chain_t = stru_t + '_' + me.cfg.masterchain;
            let domainidArray = me.cfg.matchedchains.split(',');
            let chainidArray = [];
            for(let i = 0, il = domainidArray.length; i  < il; ++i) {
                let pos = domainidArray[i].lastIndexOf('_');
                let lastId = domainidArray[i].substr(pos + 1);
                if(!isNaN(lastId)) { // lastId is domain id
                    chainidArray.push(domainidArray[i].substr(0, pos));
                }
                else {
                    chainidArray.push(domainidArray[i]);
                }
            }

            let mmdbafid = '';
            for(let i = 0, il = chainidArray.length; i < il; ++i) {
                if(i > 0) mmdbafid += ',';
                mmdbafid += chainidArray[i].substr(0, chainidArray[i].indexOf('_'));
            }
            
            // realign
            ic.chainidArray = [chain_t].concat(chainidArray);
            ic.chainidArray = ic.chainalignParserCls.addPostfixForChainids(ic.chainidArray);

            me.htmlCls.clickMenuCls.setLogCmd('resdef ' + me.cfg.resdef, true);

            ic.loadCmd = 'vast_search_chainid ' + ic.chainidArray;
            me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);

            // load multiple PDBs
            // ic.bNCBI = true;
            ic.bMmdbafid = true;
            
            let bQuery = true;
            await ic.chainalignParserCls.downloadMmdbAf(mmdbafid, bQuery);
        }
    }
    else if(me.cfg.url !== undefined) {
        ic.bInputUrlfile = true;

        let type_url = me.cfg.url.split('|');
        let type = type_url[0];
        let url = type_url[1];
        ic.molTitle = "";
        ic.inputid = url;
        ic.inputurl = 'type=' + type + '&url=' + encodeURIComponent(url);

        ic.loadCmd = 'load url ' + url + ' | type ' + type;
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
        await ic.pdbParserCls.downloadUrl(url, type, me.cfg.command);
    }
    else if(me.cfg.mmtfid !== undefined) {
       ic.inputid = me.cfg.mmtfid;
       ic.loadCmd = 'load mmtf ' + me.cfg.mmtfid;
       me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
       await ic.mmtfParserCls.downloadMmtf(me.cfg.mmtfid);
    }
    else if(me.cfg.pdbid !== undefined) {
       ic.inputid = me.cfg.pdbid;
       ic.loadCmd = 'load pdb ' + me.cfg.pdbid;
       me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
       await ic.pdbParserCls.downloadPdb(me.cfg.pdbid);
    }
    else if(me.cfg.afid !== undefined) {
       ic.inputid = me.cfg.afid;
       ic.loadCmd = 'load af ' + me.cfg.afid;
       me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
       let bAf = true;

       //ic.pdbParserCls.downloadPdb(me.cfg.afid, bAf);
       await ic.pdbParserCls.downloadPdb(me.cfg.afid, bAf);
       await ic.loadScriptCls.loadScript(me.cfg.command, undefined, true);
    }
    else if(me.cfg.opmid !== undefined) {
       ic.inputid = me.cfg.opmid;
       ic.loadCmd = 'load opm ' + me.cfg.opmid;
       me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
       await ic.opmParserCls.downloadOpm(me.cfg.opmid);
    }
    else if(me.cfg.mmdbid !== undefined) {
       ic.inputid = me.cfg.mmdbid;
       // ic.bNCBI = true;
       ic.loadCmd = 'load mmdb ' + me.cfg.mmdbid + ' | parameters ' + me.cfg.inpara;
       me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
       await ic.mmdbParserCls.downloadMmdb(me.cfg.mmdbid);
    }
    else if(me.cfg.gi !== undefined) {
        // ic.bNCBI = true;
        ic.loadCmd = 'load gi ' + me.cfg.gi;
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
        await ic.mmdbParserCls.downloadGi(me.cfg.gi);
    }
    else if(me.cfg.refseqid !== undefined) {
        ic.inputid = me.cfg.refseqid;
        
        // ic.bNCBI = true;
        ic.loadCmd = 'load refseq ' + me.cfg.refseqid;
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
        await ic.mmdbParserCls.downloadRefseq(me.cfg.refseqid);
    }
    else if(me.cfg.blast_rep_id !== undefined) {
       // ic.bNCBI = true;

       // custom seqeunce has query_id such as "Query_78989" in BLAST
       if(me.cfg.query_id.substr(0,5) !== 'Query' && me.cfg.rid === undefined) {
            // make it backward compatible for  figure 2 in iCn3D paper: https://academic.oup.com/bioinformatics/article/36/1/131/5520951
            if(me.cfg.from == 'icn3d' && me.cfg.blast_rep_id == '1TSR_A' && me.cfg.query_id == 'NP_001108451.1') {
                me.cfg.command = 'view annotations; set annotation cdd; set annotation site; set view detailed view; select chain 1TSR_A; show selection';
            }

            ic.inputid = me.cfg.query_id + '_' + me.cfg.blast_rep_id;

            if(me.cfg.alg == 'smithwm') {
                ic.loadCmd = 'load seq_struct_ids_smithwm ' + me.cfg.query_id + ',' + me.cfg.blast_rep_id;
                ic.bSmithwm = true;
            }
            else if(me.cfg.alg == 'local_smithwm') {
                ic.loadCmd = 'load seq_struct_ids_local_smithwm ' + me.cfg.query_id + ',' + me.cfg.blast_rep_id;
                ic.bLocalSmithwm = true;
            }
            else {
                ic.loadCmd = 'load seq_struct_ids ' + me.cfg.query_id + ',' + me.cfg.blast_rep_id;
                ic.bSmithwm = false;
                ic.bLocalSmithwm = false;
            }
            
            me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
            await ic.mmdbParserCls.downloadBlast_rep_id(me.cfg.query_id + ',' + me.cfg.blast_rep_id);
       }
       else if(me.cfg.rid !== undefined) {
            let url = "https://blast.ncbi.nlm.nih.gov/Blast.cgi?RESULTS_FILE=on&FORMAT_TYPE=JSON2_S&FORMAT_OBJECT=Alignment&CMD=Get&RID=" + me.cfg.rid; // e.g., RID=EFTRU3W5014
            let data = await me.getAjaxPromise(url, 'json', false, 'The RID ' + me.cfg.rid + ' may have expired...');

            for(let q = 0, ql = data.BlastOutput2.length; q < ql; ++q) {
                if(data.BlastOutput2[q].report.results.search.query_id != me.cfg.query_id) continue;
                let hitArray = data.BlastOutput2[q].report.results.search.hits;
                let qseq = undefined;
                for(let i = 0, il = hitArray.length; i < il; ++i) {
                let hit = hitArray[i];
                let bFound = false;
                for(let j = 0, jl = hit.description.length; j < jl; ++j) {
                    let acc = hit.description[j].accession;
                    if(acc == me.cfg.blast_rep_id) {
                    bFound = true;
                    break;
                    }
                }
                if(bFound) {
                    qseq = hit.hsps[0].qseq;
                    //remove gap '-'
                    qseq = qseq.replace(/-/g, '');
                    break;
                }
                }
                if(qseq !== undefined) me.cfg.query_id = qseq;
                ic.inputid = me.cfg.query_id + '_' + me.cfg.blast_rep_id;
                ic.loadCmd = 'load seq_struct_ids ' + me.cfg.query_id + ',' + me.cfg.blast_rep_id;
                me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
                await ic.mmdbParserCls.downloadBlast_rep_id(me.cfg.query_id + ',' + me.cfg.blast_rep_id);
                break;
            }
       }
       else {
           alert('BLAST "RID" is a required parameter...');
       }
    }
    else if(me.cfg.cid !== undefined) {
        ic.inputid = me.cfg.cid;

        let url = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/" + ic.inputid + "/description/jsonp";

        let data = await me.getAjaxPromise(url, 'jsonp', false);

        if(data.InformationList !== undefined && data.InformationList.Information !== undefined) ic.molTitle = data.InformationList.Information[0].Title;

        ic.loadCmd = 'load cid ' + me.cfg.cid;
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
        await ic.sdfParserCls.downloadCid(me.cfg.cid);
    }
    else if(me.cfg.mmcifid !== undefined) {
        ic.inputid = me.cfg.mmcifid;
        ic.loadCmd = 'load mmcif ' + me.cfg.mmcifid;
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
        await ic.mmcifParserCls.downloadMmcif(me.cfg.mmcifid);
    }
    else if(me.cfg.align !== undefined) {
        // ic.bNCBI = true;

        let alignArray = me.cfg.align.split(','); // e.g., 6 IDs: 103701,1,4,68563,1,167 [mmdbid1,biounit,molecule,mmdbid2,biounit,molecule], or 2IDs: 103701,68563 [mmdbid1,mmdbid2]
        if(alignArray.length === 6) {
            ic.inputid = alignArray[0] + "_" + alignArray[3];
        }
        else if(alignArray.length === 2) {
            ic.inputid = alignArray[0] + "_" + alignArray[1];
        }

        ic.loadCmd = 'load alignment ' + me.cfg.align + ' | parameters ' + me.cfg.inpara;
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
        if(me.cfg.inpara.indexOf('atype=2') == -1) {
            await ic.alignParserCls.downloadAlignment(me.cfg.align);
        }
        else {
            let vastplusAtype = 2; // Tm-align
            await ic.chainalignParserCls.downloadMmdbAf(me.cfg.align, undefined, vastplusAtype);
        }
    }
    else if(me.cfg.chainalign !== undefined) {
        // ic.bNCBI = true;

        ic.bChainAlign = true;
        ic.inputid = me.cfg.chainalign;
        ic.loadCmd = 'load chainalignment ' + me.cfg.chainalign + ' | resnum ' + me.cfg.resnum + ' | resdef ' + me.cfg.resdef + ' | aligntool ' + me.cfg.aligntool + ' | parameters ' + me.cfg.inpara;
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);
        await ic.chainalignParserCls.downloadChainalignment(me.cfg.chainalign, me.cfg.resnum, me.cfg.resdef);
    }
    else if(me.cfg.mmdbafid !== undefined) {
        // ic.bNCBI = true;

        // remove space
        me.cfg.mmdbafid = me.cfg.mmdbafid.replace(/\s+/g, '');

        ic.bMmdbafid = true;
        ic.inputid = me.cfg.mmdbafid;
        if(me.cfg.bu == 1) {
            ic.loadCmd = 'load mmdbaf1 ' + me.cfg.mmdbafid + ' | parameters ' + me.cfg.inpara;
        }
        else {
            ic.loadCmd = 'load mmdbaf0 ' + me.cfg.mmdbafid + ' | parameters ' + me.cfg.inpara;
        }
        me.htmlCls.clickMenuCls.setLogCmd(ic.loadCmd, true);

        await ic.chainalignParserCls.downloadMmdbAf(me.cfg.mmdbafid);        
        await ic.loadScriptCls.loadScript(me.cfg.command, undefined, true);
    }
    else if(me.cfg.command !== undefined && me.cfg.command !== '') {
        if(me.cfg.command.indexOf('url=') !== -1) ic.bInputUrlfile = true;
        await ic.loadScriptCls.loadScript(me.cfg.command, undefined, true);
    }
    else {
        //alert("Please use the \"File\" menu to retrieve a structure of interest or to display a local file.");
        //me.htmlCls.dialogCls.openDlg('dl_mmdbid', 'Please input MMDB or PDB ID');
        me.htmlCls.dialogCls.openDlg('dl_mmdbafid', 'Please input PDB/MMDB/AlphaFold UniProt IDs');
    }
//   });
//   return me.deferred.promise();
};

iCn3DUI.prototype.setIcn3d = function() { let me = this;
    let str1 = "<label class='icn3d-switch'><input id='" + me.pre + "modeswitch' type='checkbox'><div class='icn3d-slider icn3d-round' style='width:34px; height:18px; margin: 6px 0px 0px 3px;' title='Left(\"All atoms\"): Style and color menu options will be applied to all atoms in the structure&#13;Right(\"Selection\"): Style and color menu options will be applied only to selected atoms'></div></label>";
    let str2 = "<span id='" + me.pre + "modeall' title='Style and color menu options will be applied to all atoms in the structure'>All atoms&nbsp;&nbsp;</span><span id='" + me.pre + "modeselection' class='icn3d-modeselection' style='display:none;' title='Style and color menu options will be applied only to selected atoms'>Selection&nbsp;&nbsp;</span></div></div></td>";

    //me.htmlCls.WIDTH = $( window ).width() - me.htmlCls.LESSWIDTH;
    //me.htmlCls.HEIGHT = $( window ).height() - me.htmlCls.EXTRAHEIGHT - me.htmlCls.LESSHEIGHT;

    me.utilsCls.setViewerWidthHeight(me);

    if(me.utilsCls.isMobile() || me.cfg.mobilemenu) {
        me.htmlCls.setMenuCls.setTopMenusHtmlMobile(me.cfg.divid, str1, str2);
    }
    else {
        me.htmlCls.setMenuCls.setTopMenusHtml(me.cfg.divid, str1, str2);
    }

    me.icn3d = new iCn3D(me); // (ic.pre + 'canvas');

    me.icn3d.controlCls.setControl(); // rotation, translation, zoom, etc

    me.setDialogAjax();
};

iCn3DUI.prototype.getAjaxPromise = function(url, dataType, beforeSend, alertMess, logMess, complete) { let me = this;
    return new Promise(function(resolve, reject) {
        $.ajax({
            url: url,
            dataType: dataType,
            cache: true,
            beforeSend: function() {
                if(beforeSend) me.icn3d.ParserUtilsCls.showLoading();
            },
            complete: function() {
                if(complete) ic.ParserUtilsCls.hideLoading();
            },
            success: function(data) {
                resolve(data);
            },
            error : function() {
                if(alertMess) alert(alertMess);
                if(logMess) console.log(logMess);
                
                reject('error');
            }
        });
    });
};

iCn3DUI.prototype.getAjaxPostPromise = function(url, data, beforeSend, alertMess, logMess, complete, dataType) { let me = this;
    dataType = (dataType) ? dataType : 'json';

    return new Promise(function(resolve, reject) {
        $.ajax({
            url: url,
            type: 'POST',
            data : data,
            dataType: dataType,
            cache: true,
            beforeSend: function() {
                if(beforeSend) me.icn3d.ParserUtilsCls.showLoading();
            },
            complete: function() {
                if(complete) ic.ParserUtilsCls.hideLoading();
            },
            success: function(data) {
                resolve(data);
            },
            error : function() {
                if(alertMess) alert(alertMess);
                if(logMess) console.log(logMess);
                
                reject('error');
            }
        });
    });
};

iCn3DUI.prototype.setDialogAjax = function() { let me = this;
    // make dialog movable outside of the window
    // http://stackoverflow.com/questions/6696461/jquery-ui-dialog-drag-question
    if(!me.bNode && !$.ui.dialog.prototype._makeDraggableBase) {
        $.ui.dialog.prototype._makeDraggableBase = $.ui.dialog.prototype._makeDraggable;
        $.ui.dialog.prototype._makeDraggable = function() {
            this._makeDraggableBase();
            this.uiDialog.draggable("option", "containment", false);
        };
    }

    // https://gist.github.com/Artistan/c8d9d439c70117c8b9dd3e9bd8822d2c
    $.ajaxTransport("+binary", function(options, originalOptions, jqXHR) {
        // check for conditions and support for blob / arraybuffer response type
        if(window.FormData &&((options.dataType &&(options.dataType == 'binary')) ||(options.data &&((window.ArrayBuffer && options.data instanceof ArrayBuffer) ||(window.Blob && options.data instanceof Blob))))) {
            return {
                // create new XMLHttpRequest
                send: function(headers, callback) {
                    // setup all variables
                    let xhr = new XMLHttpRequest(),
                        url = options.url,
                        type = options.type,
                        async = options.async || true,
                        // blob or arraybuffer. Default is blob
                        responseType = options.responseType || "blob",
                        data = options.data || null;

                    xhr.addEventListener('load', function() {
                        let data = {};
                        data[options.dataType] = xhr.response;
                        // make callback and send data
                        callback(xhr.status, xhr.statusText, data, xhr.getAllResponseHeaders());
                    });

                    xhr.open(type, url, async);

                    // setup custom headers
                    for(let i in headers) {
                        xhr.setRequestHeader(i, headers[i]);
                    }

                    xhr.responseType = responseType;
                    xhr.send(data);
                },
                abort: function() {
                    jqXHR.abort();
                }
            }
        }
    });
};

/*
iCn3DUI.prototype.setIcn3dui = function(id) { let me = this;
    let idArray = id.split('_'); // id: div0_reload_pdbfile
    ic.pre = idArray[0] + "_";
    if(window.icn3duiHash !== undefined && window.icn3duiHash.hasOwnProperty(idArray[0])) { // for multiple 3D display
       me = window.icn3duiHash[idArray[0]];
    }
    return me;
};
*/


// required by npm
class printMsg {
  constructor() {
    console.log("This is a message from the icn3d package");
  }
}

export { ARButton, AddTrack, AlignParser, AlignSW, AlignSeq, Alternate, Analysis, AnnoCddSite, AnnoContact, AnnoCrossLink, AnnoDomain, AnnoSnpClinVar, AnnoSsbond, AnnoTransMem, Annotation, ApplyCenter, ApplyClbonds, ApplyCommand, ApplyDisplay, ApplyMap, ApplyOther, ApplySsbonds, ApplySymd, Axes, Box, Brick, Camera, CartoonNucl, ChainalignParser, ClickMenu, Contact, Control, ConvertTypeCls, Curve, CurveStripArrow, Cylinder, DefinedSets, Delphi, DensityCifParser, Diagram2d, Dialog, Domain3d, Draw, DrawGraph, Dsn6Parser, Dssp, ElectronMap, Events, Export3D, FirstAtomObj, Fog, GetGraph, Glycan, HBond, HashUtilsCls, HlObjects, HlSeq, HlUpdate, Html, Impostor, Instancing, Label, Line$1 as Line, LineGraph, LoadAtomData, LoadPDB, LoadScript, MarchingCube, MmcifParser, MmdbParser, MmtfParser, Mol2Parser, MyEventCls, OpmParser, ParasCls, ParserUtils, PdbParser, PiHalogen, Picking, ProteinSurface, Ray, RealignParser, Refnum, ReprSub, Resid2spec, ResidueLabels, ResizeCanvas, RmsdSuprCls, Saltbridge, SaveFile, Scap, Scene, SdfParser, SelectByCommand, Selection, SetColor, SetDialog, SetHtml, SetMenu, SetOption, SetSeqAlign, SetStyle, ShareLink, ShowAnno, ShowInter, ShowSeq, Sphere$1 as Sphere, Stick, Strand, Strip, SubdivideCls, Surface, Symd, ThreeDPrint, Transform, Tube, UtilsCls, VRButton, Vastplus, ViewInterPairs, XyzParser, iCn3D, iCn3DUI, printMsg };
