require 'geo_calc/core_ext'

 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  
 #  Geodesy representation conversion functions (c) Chris Veness 2002-2010
 #   - www.movable-type.co.uk/scripts/latlong.html
 #
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  

# Parses string representing degrees/minutes/seconds into numeric degrees
# 
# This is very flexible on formats, allowing signed decimal degrees, or deg-min-sec optionally
# suffixed by compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W) 
# or fixed-width format without separators (eg 0033709W). Seconds and minutes may be omitted. 
# (Note minimal validation is done).
# 
# @param   {String|Number} dmsStr: Degrees or deg/min/sec in variety of formats
# @returns {Number} Degrees as decimal number
# @throws  ArgumentError

module Geo
  extend self  
  extend ::NumericCheckExt
  include ::NumericCheckExt
  
  def parse_dms dms_str  
    # check for signed decimal degrees without NSEW, if so return it directly
    return dms_str if is_numeric?(dms_str)
  
    # strip off any sign or compass dir'n & split out separate d/m/s
    dms = dms_str.trim.gsub(/^-/,'').gsub(/[NSEW]$/i,'').split(/[^0-9.,]+/).map(&:trim).map(&:to_f)
    return nil if dms.empty?
  
    # and convert to decimal degrees...
    deg = case dms.length      
    when 3 # interpret 3-part result as d/m/s
       dms[0]/1 + dms[1]/60 + dms[2]/3600
    when 2 # interpret 2-part result as d/m
      dms[0]/1 + dms[1]/60        
    when 1 # just d (possibly decimal) or non-separated dddmmss        
      d = dms[0];
      # check for fixed-width unseparated format eg 0033709W
      d = "0#{d}" if (/[NS]/i.match(dms_str)) # - normalise N/S to 3-digit degrees
      d = "#{d.slice(0,3)/1}#{deg.slice(3,5)/60}#{deg.slice(5)/3600}" if (/[0-9]{7}/.match(deg)) 
      d
    else
      nil
    end
    return nil if !deg
    deg = (deg * -1) if (/^-|[WS]$/i.match(dms_str.trim)) # take '-', west and south as -ve
    deg.to_f
  end

  # Convert decimal degrees to deg/min/sec format
  #  - degree, prime, double-prime symbols are added, but sign is discarded, though no compass
  #    direction is added
  # 
  # 
  # @param   {Number} deg: Degrees
  # @param   {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  # @param   {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  # @returns {String} deg formatted as deg/min/secs according to specified format
  # @throws  {TypeError} deg is an object, perhaps DOM object without .value?

  def to_dms deg, format = :dms, dp = nil
    deg = begin
      deg.to_f
    rescue
      nil
    end
    return nil if !deg # give up here if we can't make a number from deg   
  
    # default values
    format ||= :dms
    dp = if dp.nil?
      case format.to_sym
      when :d 
        4
      when :dm 
        2
      else
        0 # default
      end
    end
    dp ||= 0
  
    deg = deg.abs # (unsigned result ready for appending compass dir'n)
  
    case format
    when :d
      d = deg.round(dp)       # round degrees
      ds = "0#{d}" if (d <100)    # pad with leading zeros
      ds = "0#{ds}" if (d <10) 
      dms = ds.to_s.concat("\u00B0")  # add º symbol
    when :dm            
      min = (deg*60).round(dp)   # convert degrees to minutes & round
      d = d.to_i
      d = (min / 60).floor          # get component deg/min
      m = (min % 60).round(dp)   # pad with trailing zeros
      ds = d
      ms = m
      ds = "0#{d}" if (d<100)        # pad with leading zeros
      ds = "0#{d}" if (d<10)
      ms = "0#{m}" if (m<10)               
      dms = ds.to_s.concat("\u00B0", ms, "\u2032") # add º, ' symbols
    when :dms
      sec = (deg * 3600).round   # convert degrees to seconds & round
      d = (sec / 3600).floor          # get component deg/min/sec
      m = ((sec / 60) % 60).floor
      s = (sec % 60).round(dp)     # pad with trailing zeros
      ds = d
      ms = m
      ss = s
      ds = "0#{d}" if (d < 100)          # pad with leading zeros
      ds = "0#{ds}" if (d < 10) 
      ms = "0#{m}" if (m < 10) 
      ss = "0#{s}" if (s < 10) 
      dms = ds.to_s.concat("\u00B0", ms, "\u2032", ss, "\u2033")  # add º, ', " symbols
    end
    return dms
  end


  # Convert numeric degrees to deg/min/sec latitude (suffixed with N/S)
  # 
  # @param   {Number} deg: Degrees
  # @param   {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  # @param   {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  # @returns {String} Deg/min/seconds

  def to_lat deg, format = :dms, dp = 0
    _lat = to_dms deg, format, dp
    _lat == '' ? '' : _lat[1..-1] + (deg<0 ? 'S' : 'N')  # knock off initial '0' for lat!
  end


  # Convert numeric degrees to deg/min/sec longitude (suffixed with E/W)
  # 
  # @param   {Number} deg: Degrees
  # @param   {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  # @param   {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  # @returns {String} Deg/min/seconds

  def to_lon deg, format = :dms, dp = 0
    deg = (360 - deg) * -1 if deg % 360 > 180
    lon = to_dms deg, format, dp
    lon == '' ? '' : lon + (deg<0 ? 'W' : 'E')
  end


  # Convert numeric degrees to deg/min/sec as a bearing (0º..360º)
  # 
  # @param   {Number} deg: Degrees
  # @param   {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  # @param   {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  # @returns {String} Deg/min/seconds

  def to_brng deg, format = :dms, dp = 0
    deg = (deg.to_f + 360) % 360  # normalise -ve values to 180º..360º
    brng =  to_dms deg, format, dp
    brng.gsub /360/, '0'  # just in case rounding took us up to 360º!
  end 
  
  protected

  include NumericCheckExt
end

# class String
#   include ::Geo
# end