SpeciesFileGroup/taxonworks

View on GitHub
app/models/sled_image.rb

Summary

Maintainability
C
1 day
Test Coverage
# A SledImage contains the metadata to parse one image into many depictions.
#
# * if `metadata` is provided without a `collection_object` stub then Specimen is assumed
#
# A section is verbatim from sled
#
# @!attribute metadata
#   @return [JSON]
#     position coordinates
#
# @!attribute object_layout
#   @return [JSON]
#      ROI metadata per metadata type, expandable to all sled sections
#
class SledImage < ApplicationRecord
  include Housekeeping
  include Shared::Tags
  include Shared::Notes
  include Shared::IsData

  # Stubs coming from UI
  attr_accessor :collection_object_params # note, tag, deterimination

  # Stubs coming from UI
  attr_accessor :depiction_params # for is_metadata_depiction

  # @param step_identifier [String]
  #  defaults to 'column', used only to compute object identifiers
  attr_accessor :step_identifier_on #row, column

  # If assigned to 'nuke', and .destroy, then will
  # additionall destroy all related collection objects
  attr_accessor :nuke

  # Internal processing
  attr_accessor :_first_identifier, :_row_total, :_column_total

  # A nil value occurs when `!section.metadata.nil?`
  attr_accessor :_identifier_matrix

  belongs_to :image
  has_many :depictions, through: :image
  has_many :collection_objects, through: :depictions, source: :depiction_object, source_type: 'CollectionObject'

  before_destroy :destroy_related

  validates_presence_of :image
  validates_uniqueness_of :image_id

  after_save :set_cached, unless: Proc.new {|n| n.metadata&.empty? || errors.any? } # must be before process
  after_save :process, unless: Proc.new { |n| n.metadata&.empty? }

  # zero beers
  def total(direction = 'row')
    i = nil
    metadata.each do |o|
      i = o[direction] if i.nil? || (!i.nil? && o[direction] > i)
    end
    i
  end

  def metadata=(value)
    if value.kind_of? Array
      write_attribute(:metadata, value)
    elsif value.kind_of? String
      write_attribute(:metadata, JSON.parse(value))
    elsif value.nil?
      write_attribute(:metadata, [])
    else
      raise
    end
  end

  def summary
    m = []
    metadata.each do |s|
      c = s['column'].to_i
      r = s['row'].to_i
      m[r] ||= []
      d = depiction_for(s)
      m[r][c] = {
        depiction_id: d&.id,
        collection_object_id: d&.depiction_object_id,
        identifier: d&.depiction_object&.identifiers&.first&.cached
      }
    end
    m
  end

  def depiction_for(section)
    x,y = section['column'], section['row']
    depictions.where(sled_image_x_position: x, sled_image_y_position: y).first
  end

  def step_identifier_on
    @step_identifier_on ||= 'column'
  end

  private

  def destroy_related
    if nuke == 'nuke'
      collection_objects.reload.each do |d|
        d.destroy
      end
    end

    depictions.each do |d|
      d.destroy
    end
    true
  end

  def new_collection_object
    CollectionObject.new(collection_object_params)
  end

  def _first_identifier
    @_first_identifier ||= new_collection_object.identifiers&.first&.identifier&.to_i
  end

  def _row_total
    @_row_total ||= total('row')
  end

  def _column_total
    @_column_total ||= total('column')
  end

  # @return [Array of Arrays]
  def _identifier_matrix
    @_identifier_matrix ||= get_identifier_matrix
  end

  def identity_matrix
    i = []
    metadata.each do |s|
      r = s['row'].to_i
      c = s['column'].to_i
      i[r] ||= []
      i[r][c] = s['metadata'].blank? ? 0 : 1
    end
    i
  end

  def increment_matrix
    i = []
    j = identity_matrix
    k = 0
    case step_identifier_on
    when 'row'
      (0.._row_total).each do |r|
        (0.._column_total).each do |c|
          k += j[r][c]
          i[r] ||= []
          i[r][c] = (j[r][c] == 1 ? 0 : k)
        end
      end

    when 'column'
      (0.._column_total).each do |c|
        (0.._row_total).each do |r|
          k += j[r][c]
          i[r] ||= []
          i[r][c] = (j[r][c] == 1 ? 0 : k)
        end
      end
    end
    i
  end

  def get_identifier_matrix
    i = increment_matrix

    m = []

    metadata.each do |s|
      r = s['row'].to_i
      c = s['column'].to_i
      m[r] ||= []
      m[r][c] = nil if !s['metadata'].blank?
      next if !s['metadata'].blank? || _first_identifier.nil?

      inc = r + c - i[r][c] + _first_identifier
      v = nil
      case step_identifier_on
      when 'row'
        v = (r * _column_total) + inc
      when 'column'
        v = (c * _row_total) + inc
      end

      m[r][c] = v
    end
    @_identifier_matrix = m
  end

  def identifier_for(section)
    _identifier_matrix[section['row'].to_i][section['column'].to_i]
  end

  def process
    depictions.any? ?  syncronize : create_objects
    true
  end

  def create_objects
    return true unless !collection_object_params.nil?
    begin
      metadata.each do |i|
        next unless i['metadata'].blank?
        p = collection_object_params.merge(
          depictions_attributes: [
            {
              sled_image_id: id,
              image_id: image_id,
              svg_clip: svg_clip(i),
              svg_view_box: svg_view_box(i),
              sled_image_x_position: i['column'],
              sled_image_y_position: i['row'],
              is_metadata_depiction: is_metadata_depiction?
            }
          ]
        )

        j = identifier_for(i)

        # Check to see if object exists
        if j && k = Identifier::Local::CatalogNumber.find_by(p[:identifiers_attributes].first.merge(identifier: j, identifier_object_type: 'CollectionObject'))
          # Remove the identifier attributes, identifier exists
          p.delete :identifiers_attributes
          p.delete :tags_attributes
          p.delete :notes_attributes
          p.delete :taxon_determinations_attributes
          p.delete :data_attributes_attributes
          p.delete :total

          k.identifier_object.update!(p)
        else
          c = CollectionObject.new(p)

          if c.identifiers.first
            c.identifiers.first.identifier = identifier_for(i)
          end
          c.save!
        end

      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, e.message)
      raise
    end
    true
  end

  def syncronize
    if metadata_was == []
      process if !metadata&.empty?
    else
      # At this point we only allow coordinate updates
      metadata.each do |i|
        next if !i['metadata'].nil?
        if d = depiction_for(i)
          d.update_columns(
            svg_clip: svg_clip(i),
            svg_view_box: svg_view_box(i),
          )
        end
      end
    end
    true
  end

  def set_cached
    update_columns(
      cached_total_collection_objects: total_metadata_objects,
      cached_total_rows: _row_total,
      cached_total_columns: _column_total
    )
  end

  def total_metadata_objects
    metadata.count
  end

  # x1, y1, x2, y2
  def coordinates(section)
    [
      section['upperCorner']['x'].to_f, section['upperCorner']['y'].to_f,
      section['lowerCorner']['x'].to_f, section['lowerCorner']['y'].to_f]
  end


  def svg_view_box(section)
    view_box_values(section).join(' ')
  end

  # @return top left x, top left y, height, width
  def view_box_values(section)
    x1, y1, x2, y2 = coordinates(section)
    [x1, y1, x2 - x1, y2 - y1]
  end

  def svg_clip(section)
    x, y, h, w = view_box_values(section)
    "<rect x=\"#{x}\" y=\"#{y}\" width=\"#{h}\" height=\"#{w}\" />"
  end

  private

  def is_metadata_depiction?
    if depiction_params && !depiction_params.empty?
      a = depiction_params[:is_metadata_depiction]
      ((a == 'true') || (a == true)) ? true : false
    else
      false
    end
  end

end