NGL@1.0.0-beta.7 Home Manual Reference Source Gallery

src/representation/measurement-representation.js

/**
 * @file Measurement Representation
 * @private
 */
import { Color } from '../../lib/three.es6.js'
import Selection from '../selection/selection.js'
import { Browser } from '../globals.js'
import { defaults } from '../utils.js'
import StructureRepresentation from './structure-representation.js'
import { uniformArray, uniformArray3 } from '../math/array-utils.js'

/**
 * Measurement representation parameter object.
 * @typedef {Object} MeasurementRepresentationParameters - measurement representation parameters
 * @mixes RepresentationParameters
 * @mixes StructureRepresentationParameters
 *
 * @property {Float} labelSize - size of the distance label
 * @property {Color} labelColor - color of the distance label
 * @property {Boolean} labelVisible - visibility of the distance label
 * @property {Float} labelZOffset - offset in z-direction (i.e. in camera direction)
 */

/**
 * Measurement representation
 */
class MeasurementRepresentation extends StructureRepresentation {
  /**
   * Handles common label settings and position logic for
   * distance and angle representations
   *
   */
  constructor (structure, viewer, params) {
    super(structure, viewer, params)

    this.n = 0 // Subclass create sets value
    this.parameters = Object.assign({

      labelVisible: {
        type: 'boolean'
      },
      labelSize: {
        type: 'number', precision: 3, max: 10.0, min: 0.001
      },
      labelColor: {
        type: 'color'
      },
      labelFontFamily: {
        type: 'select',
        options: {
          'sans-serif': 'sans-serif',
          'monospace': 'monospace',
          'serif': 'serif'
        },
        buffer: 'fontFamily'
      },
      labelFontStyle: {
        type: 'select',
        options: {
          'normal': 'normal',
          'italic': 'italic'
        },
        buffer: 'fontStyle'
      },
      labelFontWeight: {
        type: 'select',
        options: {
          'normal': 'normal',
          'bold': 'bold'
        },
        buffer: 'fontWeight'
      },
      labelsdf: {
        type: 'boolean', buffer: 'sdf'
      },
      labelXOffset: {
        type: 'number', precision: 1, max: 20, min: -20, buffer: 'xOffset'
      },
      labelYOffset: {
        type: 'number', precision: 1, max: 20, min: -20, buffer: 'yOffset'
      },
      labelZOffset: {
        type: 'number', precision: 1, max: 20, min: -20, buffer: 'zOffset'
      },
      labelAttachment: {
        type: 'select',
        options: {
          'bottom-left': 'bottom-left',
          'bottom-center': 'bottom-center',
          'bottom-right': 'bottom-right',
          'middle-left': 'middle-left',
          'middle-center': 'middle-center',
          'middle-right': 'middle-right',
          'top-left': 'top-left',
          'top-center': 'top-center',
          'top-right': 'top-right'
        },
        rebuild: true
      },
      labelBorder: {
        type: 'boolean', buffer: 'showBorder'
      },
      labelBorderColor: {
        type: 'color', buffer: 'borderColor'
      },
      labelBorderWidth: {
        type: 'number', precision: 2, max: 0.3, min: 0, buffer: 'borderWidth'
      },
      labelBackground: {
        type: 'boolean', rebuild: true
      },
      labelBackgroundColor: {
        type: 'color', buffer: 'backgroundColor'
      },
      labelBackgroundMargin: {
        type: 'number', precision: 2, max: 2, min: 0, rebuild: true
      },
      labelBackgroundOpacity: {
        type: 'range', step: 0.01, max: 1, min: 0, buffer: 'backgroundOpacity'
      }
    }, this.parameters, {
      flatShaded: null,
      assembly: null
    })
  }

  init (params) {
    var p = params || {}
    this.labelVisible = defaults(p.labelVisible, true)
    this.labelSize = defaults(p.labelSize, 2.0)
    this.labelColor = defaults(p.labelColor, 0xFFFFFF)
    this.labelFontFamily = defaults(p.labelFontFamily, 'sans-serif')
    this.labelFontStyle = defaults(p.labelFontstyle, 'normal')
    this.labelFontWeight = defaults(p.labelFontWeight, 'bold')
    this.labelsdf = defaults(p.labelsdf, Browser === 'Chrome')
    this.labelXOffset = defaults(p.labelXOffset, 0.0)
    this.labelYOffset = defaults(p.labelYOffset, 0.0)
    this.labelZOffset = defaults(p.labelZOffset, 0.5)
    this.labelAttachment = defaults(p.labelAttachment, 'bottom-left')
    this.labelBorder = defaults(p.labelBorder, false)
    this.labelBorderColor = defaults(p.labelBorderColor, 'lightgrey')
    this.labelBorderWidth = defaults(p.labelBorderWidth, 0.15)
    this.labelBackground = defaults(p.labelBackground, false)
    this.labelBackgroundColor = defaults(p.labelBackgroundColor, 'lightgrey')
    this.labelBackgroundMargin = defaults(p.labelBackgroundMargin, 0.5)
    this.labelBackgroundOpacity = defaults(p.labelBackgroundOpacity, 1.0)

    super.init(p)
  }

  // All measurements need to rebuild on position change
  update (what) {
    if (what.position) {
      this.build()
    } else {
      super.update(what)
    }
  }

  updateData (what, data) {
    const textData = {}
    if (what.labelSize) {
      textData.size = uniformArray(this.n, this.labelSize)
    }

    if (what.labelColor) {
      const c = new Color(this.labelColor)
      textData.color = uniformArray3(this.n, c.r, c.g, c.b)
    }

    this.textBuffer.setAttributes(textData)
  }

  setParameters (params) {
    var rebuild = false
    var what = {}

    if (params && params.labelSize) {
      what.labelSize = true
    }

    if (params && (params.labelColor || params.labelColor === 0x000000)) {
      what.labelColor = true
    }

    super.setParameters(params, what, rebuild)

    if (params && params.opacity !== undefined) {
      this.textBuffer.setParameters(
        {opacity: 1.0}) // Don't allow opaque labels?
    }

    if (params && params.labelVisible !== undefined) {
      this.setVisibility(this.visible)
    }

    return this
  }

  setVisibility (value, noRenderRequest) {
    super.setVisibility(value, true)
    if (this.textBuffer) {
      this.textBuffer.setVisibility(
        this.labelVisible && this.visible
      )
    }

    if (!noRenderRequest) this.viewer.requestRender()

    return this
  }

  getLabelBufferParams (params) {
    return super.getBufferParams(Object.assign({
      fontFamily: this.labelFontFamily,
      fontStyle: this.labelFontStyle,
      fontWeight: this.labelFontWeight,
      sdf: this.labelsdf,
      xOffset: this.labelXOffset,
      yOffset: this.labelYOffset,
      zOffset: this.labelZOffset,
      attachment: this.labelAttachment,
      showBorder: this.labelBorder,
      borderColor: this.labelBorderColor,
      borderWidth: this.labelBorderWidth,
      showBackground: this.labelBackground,
      backgroundColor: this.labelBackgroundColor,
      backgroundMargin: this.labelBackgroundMargin,
      backgroundOpacity: this.labelBackgroundOpacity,
      visible: this.labelVisible
    }, params, {
      opacity: 1.0 // Force labels at 100% opacity
    }))
  }
}

/**
 * MeasurementRepresentations take atom[Pair|Triple|Quad] parameters.
 *
 * Parses nested array of either integer atom indices or selection
 * expressions into a flat array of coordinates.
 *
 * NB: Unlike previous version, this peeks at first entry to determine
 * if atoms are given by int index or selection expression. It cannot
 * cope with mixtures
 *
 * @param  {Structure} sview The structure to which the atoms refer
 * @param  {Array} atoms Nested array of atom pairs|triples|quads as
 *   Integer indices or selection expressions
 * @return {Float32Array} Flattened array of position coordinates
 */
function parseNestedAtoms (sview, atoms) {
  const ap = sview.getAtomProxy()
  const sele = new Selection()

  const nSets = atoms.length
  if (nSets === 0) return new Float32Array(0)

  // Peek-ahead at first item to determine order and parse mode
  const order = atoms[ 0 ].length
  const seleMode = !(Number.isInteger(atoms[ 0 ][ 0 ]))

  const a = new Float32Array(nSets * order * 3)

  let p = 0
  atoms.forEach(function (group) {
    let _break = false
    for (var j = 0; j < order; j++) {
      if (seleMode) {
        sele.setString(group[ j ])
        const atomIndices = sview.getAtomIndices(sele)
        if (atomIndices.length) {
          ap.index = atomIndices[ 0 ]
        } else {
          _break = true
          break
        }
      } else {
        ap.index = group[ j ]
      }
      let offset = p + j * 3
      a[ offset++ ] = ap.x
      a[ offset++ ] = ap.y
      a[ offset++ ] = ap.z
    }
    if (!_break) p += 3 * order
  })

  return a.subarray(0, p)
}

/* out = v1 * cos(angle) + v2 * sin(angle) */
function calcArcPoint (out, center, v1, v2, angle) {
  const x = Math.cos(angle)
  const y = Math.sin(angle)
  out[ 0 ] = center[ 0 ] + v1[ 0 ] * x + v2[ 0 ] * y
  out[ 1 ] = center[ 1 ] + v1[ 1 ] * x + v2[ 1 ] * y
  out[ 2 ] = center[ 2 ] + v1[ 2 ] * x + v2[ 2 ] * y
}

export {
  MeasurementRepresentation as default,
  calcArcPoint,
  parseNestedAtoms
}