#!/usr/bin/ruby -w # # cdntrans.rb v1.1 - 2007/01 # # this script converts GPS coordinates between 3 different formats: # - Degrees, Minutes, Seconds # - Degrees, Minutes, Decimal # - Degrees, Decimal # # I wrote because when geotagging pictures, each application uses its format # for GPS data. I looked for the most widely used and wrote this conversion # tool. # # Copyright (C) 2006 by Pedro Venda < pjvenda (at) pjvenda org > # # Distributed under the terms of the GNU Public License Agreement (GPLv2) # http://www.fsf.org/licensing/licenses/gpl.html or # http://www.fsf.org/licensing/licenses/gpl.txt # # Credit # ====== # # The following websites helped me write this application: # http://jeeep.com/details/coord/ # http://life.csu.edu.au/geo/coord.html # http://www.csgnetwork.com/gpscoordconv.html # # This one has a massive amount of GPS related information: # http://www.colorado.edu/geography/gcraft/notes/gps/gps_f.html # # Usage # ===== # # this script receives its input from stdin and outputs it into stdout. # # input coordinates can be given in any of the output formats (the program # detects which format was supplied). output is the same input data written # in all supported formats. # # example: $ echo "38º 41' 29.69\" N, 9º 12' 57.27\" W" | ./cdntrans.rb # DMS: 38 41' 29.69" N, 9 12' 57.27" W # Min Dec: 38 41.494833, -9 12.954500 # Deg Dec: 38.69158056, -9.21590833 # $ # alternatively, the input can be given as a command line argument # example: $ ./cdntrans.rb "38º 41' 29.69\" N, 9º 12' 57.27\" W" # DMS: 38 41' 29.69" N, 9 12' 57.27" W # Min Dec: 38 41.494833, -9 12.954500 # Deg Dec: 38.69158056, -9.21590833 # $ # # as a funny (useful) side effect, google maps' links are correctly parsed: # $ ./cdntrans.rb "http://maps.google.com/maps?f=q&hl=en&ie=UTF8&z=18&ll=38.691581,-9.215909&spn=0.003341,0.00736&t=h&om=1" # DMS: 38 41' 29.69" N, 9 12' 57.27" W # Min Dec: 38 41.494860, -9 -12.954540 # Deg Dec: 38.69158100, -9.21590900 # $ # # finally it can be included as a class 'Coordinate' to instantiate objects # # Changelog # ========= # # v1.1 - 2007/01/28 # - added input flexibility: 38.69158100 S, -9.21590900 W is now accepted # (although it may not make any sense) # - added input flexibility: 40° 24.991 N 007° 31.861 W is now accepted # - added input flexibility: N 40° 24.991 W 007° 31.861 is now accepted # - added lower case directions (N-n, S-s, W-w, E-e) for mindec, degdec # and dms coordinates # # v1.0 - 2006/12/31 # - initial (ruby) version. basically works as I needed # class Coordinate @@VERSION="1.1" # regular expressions to trap and identify input format @@dms_re=Regexp.new('(\d+)[\W\D]*\s+(\d+)\'\s+(\d+([.,]\d+)?)\"\s*([NnSs])[,\s]\s*(\d+)[\W\D]*\s+(\d+)\'\s+(\d+([.,]\d+)?)\"\s*([WwEe])(?# )') @@mindec_re=Regexp.new('([+-]?\d+)[\W\D]*\s*(\d+(\.\d+))\s*([NnSs]?)[\s,;]+([+-]?\d+)[\W\D]*\s*(\d+(\.\d+))\s*([WwEe]?)(?# )') # alternative for mindec input: directions (N,S,W,E) are given # at the beginning of the coordinate string @@mindec_alt_re=Regexp.new('([NnSs]?)\s*([+-]?\d+)[\W\D]*\s*(\d+(\.\d+))[\s,;]+([WwEe]?)\s*([+-]?\d+)[\W\D]*\s*(\d+(\.\d+))(?# )') @@degdec_re=Regexp.new('([+-]?\d+(\.\d+)?)\s*([NnSs]?)[\s,;]+([+-]?\d+(\.\d+)?)\s*([WwEe]?)(?# )') @@degdec_alt_re=Regexp.new('([NnSs]?)\s*([+-]?\d+(\.\d+)?)[\s,;]+\s*([WwEe]?)\s*([+-]?\d+(\.\d+)?)(?# )') # object and class variables accessible by the outsude attr_reader :dms, :mindec, :degdec attr_reader :dms_re, :mindec_re, :degdec_re def initialize(input='') # initialize all data @dms={ :lat => { :deg => 0, :min => 0, :sec => 0, :ref => '' }, :lon => { :deg => 0, :min => 0, :sec => 0, :ref => '' } } @mindec={ :lat => { :deg => 0, :frac => 0 }, :lon => { :deg => 0, :frac => 0 } } @degdec={ :lat => 0, :lon => 0 } @parsed=false # identify, parse and convert input data through all supported formats parse_str(input) end # print coordinate data in all supported formats def to_s if @parsed "DMS:".ljust(10)\ +(sprintf("%2d",@dms[:lat][:deg])+" "\ +sprintf("%2d",@dms[:lat][:min])+"' "\ +sprintf("%2.2f",@dms[:lat][:sec])+"\" "\ +@dms[:lat][:ref].to_s.upcase).rjust(14)+", "\ +(sprintf("%2d",@dms[:lon][:deg])+" "\ +sprintf("%2d",@dms[:lon][:min])+"' "\ +sprintf("%2.2f",@dms[:lon][:sec])+"\" "\ +@dms[:lon][:ref].to_s.upcase).rjust(14)+"\n"\ +"Min Dec:".ljust(10)\ +(sprintf("%2d",@mindec[:lat][:deg])+" "\ +sprintf("%2.6f",@mindec[:lat][:frac])).rjust(15)+", "\ +(sprintf("%2d",@mindec[:lon][:deg])+" "\ +sprintf("%2.6f",@mindec[:lon][:frac])).rjust(15)+"\n"\ +"Deg Dec:".ljust(10)\ +sprintf("%2.8f",@degdec[:lat]).rjust(15)+", "\ +sprintf("%2.8f",@degdec[:lon]).rjust(15)+"\n" else "unable to parse input string\n\n" end end def parse_str(input_str) #puts " input string: "+input_str+"\n" if res=@@dms_re.match(input_str) #puts " input string matches dms format" # fill in @dms variable @dms[:lat][:deg]=res[1].to_i @dms[:lat][:min]=res[2].to_i @dms[:lat][:sec]=res[3].to_f @dms[:lat][:ref]=res[5].to_s @dms[:lon][:deg]=res[6].to_i @dms[:lon][:min]=res[7].to_i @dms[:lon][:sec]=res[8].to_f @dms[:lon][:ref]=res[10].to_s # fill in @mindec and @degdec variables dms_to_mindec dms_to_degdec @parsed=true elsif res=@@mindec_re.match(input_str) #puts " input string matches mindec format" # fill in @mindec variable @mindec[:lat][:deg]=res[1].to_f @mindec[:lat][:frac]=res[2].to_f if res[4].to_s.upcase=="S" @mindec[:lat][:deg]*=-1 end @mindec[:lon][:deg]=res[5].to_f @mindec[:lon][:frac]=res[6].to_f if res[8].to_s.upcase=="W" @mindec[:lon][:deg]*=-1 end # fill in @dms and @degdec variables mindec_to_dms mindec_to_degdec @parsed=true elsif res=@@mindec_alt_re.match(input_str) #puts " input string matches alternative mindec format" # fill in @mindec variable @mindec[:lat][:deg]=res[2].to_f @mindec[:lat][:frac]=res[3].to_f if res[1].to_s.upcase=="S" @mindec[:lat][:deg]*=-1 end @mindec[:lon][:deg]=res[6].to_f @mindec[:lon][:frac]=res[7].to_f if res[5].to_s.upcase=="W" @mindec[:lon][:deg]*=-1 end # fill in @dms and @degdec variables mindec_to_dms mindec_to_degdec @parsed=true elsif res=@@degdec_alt_re.match(input_str) #puts " input string matches degdec format" # fill in @degdec variable @degdec[:lat]=res[2].to_f if res[1].to_s.upcase=="S" @degdec[:lat]*=-1 end @degdec[:lon]=res[5].to_f if res[4].to_s.upcase=="W" @degdec[:lon]*=-1 end # fill in @dms and @mindec variables degdec_to_dms degdec_to_mindec @parsed=true elsif res=@@degdec_re.match(input_str) #puts " input string matches degdec format" # fill in @degdec variable @degdec[:lat]=res[1].to_f if res[3].to_s.upcase=="S" @degdec[:lat]*=-1 end @degdec[:lon]=res[4].to_f if res[6].to_s.upcase=="W" @degdec[:lon]*=-1 end # fill in @dms and @mindec variables degdec_to_dms degdec_to_mindec @parsed=true else return "input string doesn't match any known format...\n\n" end end # conversion from dms to degdec def dms_to_degdec @degdec[:lat]=@dms[:lat][:deg]\ +((@dms[:lat][:min]*1.0)/60)\ +((@dms[:lat][:sec]*1.0)/3600) if @dms[:lat][:ref].upcase=='S' @degdec[:lat]*=-1 end @degdec[:lon]=@dms[:lon][:deg]\ +((@dms[:lon][:min]*1.0)/60)\ +((@dms[:lon][:sec]*1.0)/3600) if @dms[:lon][:ref].upcase=='W' @degdec[:lon]*=-1 end end # conversion from degdec to dms def degdec_to_dms @dms[:lat][:deg]=@degdec[:lat].abs.truncate @dms[:lat][:min]=((@degdec[:lat].abs-@dms[:lat][:deg])*60).truncate @dms[:lat][:sec]=((((@degdec[:lat].abs-@dms[:lat][:deg])*60)-@dms[:lat][:min]))*60 if @degdec[:lat] < 0 @dms[:lat][:ref]="S" else @dms[:lat][:ref]="N" end @dms[:lon][:deg]=@degdec[:lon].abs.truncate @dms[:lon][:min]=((@degdec[:lon].abs-@dms[:lon][:deg])*60).truncate @dms[:lon][:sec]=((((@degdec[:lon].abs-@dms[:lon][:deg])*60)-@dms[:lon][:min]))*60 if @degdec[:lon] < 0 @dms[:lon][:ref]="W" else @dms[:lon][:ref]="E" end end # conversion from dms to mindec def dms_to_mindec @mindec[:lat][:deg]=@dms[:lat][:deg] @mindec[:lat][:frac]=@dms[:lat][:min]+((@dms[:lat][:sec]*1.0)/60) if @dms[:lat][:ref].upcase=='S' @mindec[:lat][:deg]*=-1 end @mindec[:lon][:deg]=@dms[:lon][:deg] @mindec[:lon][:frac]=@dms[:lon][:min]+((@dms[:lon][:sec]*1.0)/60) if @dms[:lon][:ref].upcase=='W' @mindec[:lon][:deg]*=-1 end end # conversion from mindec to dms def mindec_to_dms @dms[:lat][:deg]=@mindec[:lat][:deg].abs @dms[:lat][:min]=@mindec[:lat][:frac].abs.truncate @dms[:lat][:sec]=(@mindec[:lat][:frac].abs-@dms[:lat][:min])*60 if @mindec[:lat][:deg] < 0 @dms[:lat][:ref]="S" else @dms[:lat][:ref]="N" end @dms[:lon][:deg]=@mindec[:lon][:deg].abs @dms[:lon][:min]=@mindec[:lon][:frac].abs.truncate @dms[:lon][:sec]=(@mindec[:lon][:frac].abs-@dms[:lon][:min])*60 if @mindec[:lon][:deg] < 0 @dms[:lon][:ref]="W" else @dms[:lon][:ref]="E" end end # conversion from mindec to degdec def mindec_to_degdec @degdec[:lat]=@mindec[:lat][:deg].abs @degdec[:lat]+=@mindec[:lat][:frac]/60 if @mindec[:lat][:deg] < 0 @degdec[:lat]*=-1 end @degdec[:lon]=@mindec[:lon][:deg].abs @degdec[:lon]+=@mindec[:lon][:frac]/60 if @mindec[:lon][:deg] < 0 @degdec[:lon]*=-1 end end # conversion from degdec to mindec def degdec_to_mindec @mindec[:lat][:deg]=@degdec[:lat].truncate @mindec[:lat][:frac]=(@degdec[:lat]-@degdec[:lat].truncate)*60 @mindec[:lon][:deg]=@degdec[:lon].truncate @mindec[:lon][:frac]=(@degdec[:lon]-@degdec[:lon].truncate)*60 end end # see if we were called directly: if __FILE__ == $0 if ARGV.length == 0 # accept input from stdin puts c=Coordinate.new(gets) else # accept input from command line puts c=Coordinate.new(ARGV[0].to_s) end end