Sunday, June 29, 2008

Geo-conversion finished, for now.

Well, I got my little geo-conversion program finished. Nothing too fancy, but it works. I think I'd like to see if I can use a arbitrary-precision library to do the calculations instead. Right now, everything is double, but I'm not sure how much it matters. Hell, I don't even know how many digits past the decimal point anybody really cares about.

Anyway, here's the code:

import Text.ParserCombinators.Parsec
import Data.List

-- This data type stores the three pieces of info we need from each line.
data ConvLine = ConvLine {conversion::String, value::String} deriving Show

-- This function parses each line of input.
parseLines = do
lines <- many inputLines
return lines

-- Separate each line of the input into two parts: the conversion to be
-- performed and the value to convert.
inputLines = do
conversion <- many1 (letter <|> char ':')
value <- anyChar `manyTill` newline
return (ConvLine conversion value)

-- This does the conversion. Using the whole conversion key as the different
-- cases.
geoConvert :: [ConvLine] -> [String]
geoConvert l = map doConvert l
where doConvert s = case (conversion s) of
"DMS:DD" -> dmsToDD (value s)
"DD:DMS" -> ddToDMS (value s)
_ -> error $ "Undefined conversion: " ++ show(s)

-- Convert DMS to Decimal Degrees.
dmsToDD :: String -> String
dmsToDD s = case (parse parseDMSToDD "" s) of
Left err -> error $ "Input:\n" ++ show s ++
"\nError:\n" ++ show err
Right result -> show result

-- Convert Decimal Degrees to DMS.
ddToDMS :: String -> String
ddToDMS s = case (parse parseDDToDMS "" s) of
Left err -> error $ "Input:\n" ++ show s ++
"\nError:\n" ++ show err
Right result -> result

-- This one is simple. First, break apart the different pieces of the value
-- into their numeric components. Next, calculate and return the Decimal
-- Degrees value.
-- Key point here: read takes a string and converts it to a number if
-- possible.
parseDMSToDD :: Parser Double
parseDMSToDD = do
degrees <- many1 digit
char 'D'
minutes <- many1 digit
char 'M'
seconds <- many1 digit
char 'S'
return ((read degrees) + ((read minutes) / 60) + ((read seconds) / 3600))

-- This parses out the number from the value and passes it to the calcDMS
-- function.
parseDDToDMS :: Parser String
parseDDToDMS = do
decimalDeg <- many1 (digit <|> char '.')
return (calcDMS (read decimalDeg))

-- This function creates the DMS string.
calcDMS :: Double -> String
calcDMS dd = (show (getDegrees dd)) ++ "D" ++
(show (getMinutes dd)) ++ "M" ++
(show (getSeconds dd)) ++ "S"

-- This function gets the degrees part.
getDegrees :: Double -> Integer
getDegrees n = (truncate n)

-- This function gets the minutes part.
-- Key point here: need to use fromInteger to convert from an Integer to a
-- Double.
getMinutes :: Double -> Integer
getMinutes n = (truncate ((n - (fromInteger $ getDegrees n)) * 60))

-- This gets the full value for the minutes.
getFullMinutes :: Double -> Double
getFullMinutes n = ((n - (fromInteger $ getDegrees n)) * 60)

-- This function gets the seconds part.
getSeconds :: Double -> Double
getSeconds n = (((getFullMinutes n) - (fromInteger $ getMinutes n)) * 60)

main = do
-- Read from stdin.
input <- getContents

-- Get the result of the parsing.
-- Left == error
-- Right == success
let convLines = case (parse parseLines "stdin" input) of
Left err -> error $ "Input:\n" ++ show input ++
"\nError:\n" ++ show err
Right result -> result

-- Do the conversion.
let outLines = geoConvert convLines
print outLines

My only complaint is how I had to do the conversion from Decimal Degrees to DMS. The various parts of the process are reusable, however I really wish I could have gotten it to work in only one function. When I tried it, the "return ..." statement at the end kept throwing a compilation error. Oh well, I'm probably just missing something. I'm sure I'll figure it out later.


O.K., I really shouldn't post when I'm not thinking clearly as I still have at least one thing to do: convert to/from radians. I was also planning to convert between lats/longs and UTM/MGRS grid coordinate systems, however those are much more complicated to do, so I will most likely be putting them off for a while. I may have to deal with them at work, but until then I don't want to spend a lot of time on them since the real purpose of this exercise is to learn Haskell and not how to do the conversions.

Another note, as to how this program works, it reads from STDIN and outputs to STDOUT. I'm expecting the data to be stored in a text file or it can be streamed it. The format is very simple:

operation value

Where operation is "DMS:DD" or "DD:DMS" and the value is the value you want to convert. Currently, only the 1D2M3S format for Degress Minutes Seconds is supported for simplicity.

As I'm still under the weather right now but unable to sleep, I bid you all farewell and good night.



Post a Comment

Links to this post:

Create a Link

<< Home