#####################  axismap.rb -- brent@mbari.org  ######################
# $Source: /home/cvs/ESP/gen2/software/esp/lib/axismap.rb,v $
#    Copyright (C) 2003 MBARI
#    MBARI Proprietary Information. All rights reserved.
# $Id: axismap.rb,v 1.10 2005/05/07 05:26:51 brent Exp $
#
#  Generic class for mapping axes logical and raw positions
#
############################################################################

class AxisKernel  #methods common to _all_ axes

  unless defined?(@@defined) and defined?(@@axis) and defined?(@@present)
    @@defined = []    #all defined axes
    @@present = []    #all axes currently available
    @@axis = {}       #hash of axes names to axes
  end
  
  class Error < StandardError
    def initialize text, axis=nil
      @axisName=(axis.name if axis)
      super text
    end
    attr_reader :axisName
    def axis
      AxisKernel.byName @axisName
    end
  end

  class Missing < Error; end
  
  class << self  #class methods
    include Enumerable
    
    def each list=@@present
    #run block on each axis of this type
    #the list argument defaults to the set of all axes presently accessible
      list.each {|a|
        yield a if a.kind_of? self
      }  
      nil
    end

    def present
      result=[]
      @@present.each {|a| result << a if a.kind_of? self}
      result
    end
    alias_method :all, :present
    
    def missing
      result=[]
      (@@defined - @@present).each {|a| result << a if a.kind_of? self}
      result
    end
    def defined
      result=[]
      @@defined.each {|a| result << a if a.kind_of? self}
      result
    end
    
    def defined? axis
      @@defined.include? axis
    end
    def present? axis
      @@present.include? axis
    end
    def missing? axis
      missing.include? axis
    end
    
    def byName name
      @@axis[name]
    end
    
    def add axis
    #add axes to the list of those currently available
      define axis  #ensure the axis is added to list of all defined
      @@present<<axis unless @@present.include? axis
    end
    
    def removeAll
    #remove all axes from the list of those currently available
      @@present.clear
    end
    
    def remove axis
    #remove axis from the list of those currently available
      @@present.delete axis
    end
    
    def undefine axis
    #remove an axis from the list
    #return nil if it was already removed
      remove axis
      @@axis.delete axis.name
      @@defined.delete axis
    end

    def define axis
    #add axis to list of all defined.
    #returns nil if axis is a duplicate
      unless @@defined.include? axis  #don't add same axis twice
        name = axis.name
        raise Error.new "Attempt to define duplicate #{name}", self if
          @@axis.has_key? name
        @@axis[name]=axis
        @@defined<<axis
      end
    end

    def asIRBtext
    #display the state of all axes
      collect{|a|
        begin
          a.asIRBtext
        rescue => err
          err.message
        end
      }.join("\n")
    end
  end
    
end


class AxisMap 

  class Error < StandardError; end
  
  def initialize rawMap
  #make a new AxisMap object
  #rawMap associates raw positions with a label and optional aliases
  #if ids is an array, its first element is the label, others are aliases
  #of that raw position.
    @directory={}
    @legend={}
    rawMap.each {|rawPos, id|
      ids = if id.is_a? Array
              (entry=id.collect {|id| id.intern}).uniq!
              @legend[rawPos]=entry
            else
              [@legend[rawPos]=id]
            end
      ids.each_with_index {|id, i|
        if @directory.has_key? id
          raise Error, "More than One Position #{i==0?"Name":"Alias"} '#{id}'"
        end
        @directory[id] = rawPos
      }
    }
  end
  attr_reader :legend, :directory

  def positions
    @legend.size
  end

  def labels
  #return mapping of just the position labels to their raw positions
    result={}
    @legend.each {|rawPos, position|
      position = position.first if position.is_a? Array
      result[position]=rawPos
    }
    result
  end
  
  def aliases
  #return hash mapping aliases => to their labels
    result={}
    @legend.each {|rawPos, ids|
      if ids.is_a?(Array) and (sz=ids.size) > 1
        primaryName = ids.first
        for aliasIndex in 1...sz
          result[ids[aliasIndex]] = primaryName
        end
      end
    }
    result
  end
  
  def alias hash={}
  #add or change aliases for existing positions
  #each hash maps a new alias to an existing one
  #The new id must not denote an existing label
  #see relabel below
    primary=labels
    hash.each {|newIds, oldId|
      rawPos = fetch oldId
      if newIds.is_a? Array
        newIds=(newIds.collect{|id| id.intern}).uniq!
      else
        newIds=[newIds]
      end
      newIds.each {|id|
        if primary.has_key? id
          raise Error, 
           "Primary Position Label '#{id}' cannot also be Defined as an Alias"
        end    
        unaliasQuick id
        oldPos=@directory[id]
        @legend[oldPos].delete id if oldPos
        @directory[id] = rawPos
        if (existingId = @legend[rawPos]).is_a? Array
          existingId<<id    #simply add an alias
        else
          @legend[rawPos]=[existingId, id]  #create 1st alias
        end
      }
    }
    self
  end
  
  def unalias id
  #remove alias
  #returns the raw position being aliased or nil if no such alias existed   
    if labels.has_key? (id = id.intern)
      raise Error, "Primary Position Label '#{id}' cannot be removed"
    end
    unaliasQuick id
  end
    
  def relabel newLabel, oldId
  #replace label for position oldId with newLabel
  #the old label becomes an alias (that may be unaliased)
  #returns the raw position that was relabeled
    rawPos = fetch oldId
    unalias newLabel
    newLabel=newLabel.intern
    oldId=oldId.intern
    if (legEntry = @legend[rawPos]).is_a? Array
      legEntry.unshift newLabel
    else
      @legend[rawPos]=[newLabel, legEntry]
    end
    @directory[newLabel]=rawPos
  end
  
  def fetch(positionId, &punt)
  #get raw position corresponding to positionId
    punt=proc {|b| posErr b} unless punt
    @directory.fetch(positionId.intern, &punt)
  end
  
  def rawId(rawPos, &punt)
  #return Label corresponding to rawPos
    punt=proc {|b| rawErr b} unless punt
    if (ids = @legend.fetch(rawPos, &punt)).is_a? Array
      ids.first
    else
      ids
    end    
  end
  
  protected
  def posErr id
    raise Error, "Unknown Position '#{id}'"
  end
  
  def rawErr raw
    raise Error, "Unknown Raw position '#{raw}'"
  end
  
  def unaliasQuick aliasId
  #remove alias
  #returns the raw position being aliased or nil if no such alias existed
    if oldPos=@directory.delete(aliasId)
      (shorter=@legend[oldPos]).delete(aliasId)
      @legend[oldPos]=shorter.first if shorter.size <= 1
    end
    oldPos
  end
    
end  #AxisMap class
