Skip to content

Instantly share code, notes, and snippets.

@MaxGabriel
Created May 9, 2020 15:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MaxGabriel/221ebc5e5afc078aedf7f607fc5c6735 to your computer and use it in GitHub Desktop.
Save MaxGabriel/221ebc5e5afc078aedf7f607fc5c6735 to your computer and use it in GitHub Desktop.
Degrees minutes seconds parser in haskell, for parsing e.g. 37 deg 8' 21.26" N, 80 deg 34' 41.84" W
latToDecimal :: DMSLat -> Double
latToDecimal (DMSLat dms direction) =
let decimal = dmsToDecimal
in case direction of
North -> decimal
South -> decimal * -1
lonToDecimal :: DMSLat -> Double
lonToDecimal (DMSLon dms direction) =
let decimal = dmsToDecimal
in case direction of
East -> decimal
West -> decimal * -1
-- | Converts degrees/minutes/seconds to decimal decimal format
dmsToDecimal :: DMS -> Double
dmsToDecimal (DMS d m s) =
d
+ (m / 60) -- 60 minutes in an hour (degree)
+ (s / 3600) -- 3600 seconds in an hour (degree)
type Parser = Parsec Void Text
-- TODO: instead of parsing DMS, have exiftool output decimal degrees.
gpsPositionParser :: Parser (DMSLat, DMSLon)
gpsPositionParser = do
lat <- parseLatitudeDMS
_ <- string ", "
lon <- parseLongitudeDMS
pure (lat, lon)
data DMSLat = DMSLat DMS LatitudeDirection
parseLatitudeDMS :: Parser DMSLat
parseLatitudeDMS = do
dms@(DMS d _ _) <- parseDegreesMinuteSeconds
unless (d >= 0 && d <= 90) (fail $ "Invalid latitude degrees: " <> show d)
direction <- parseLatitudeDirection
pure $ DMSLat dms direction
data DMSLon = DMSLon DMS LongitudeDirection
parseLongitudeDMS :: Parser DMSLon
parseLongitudeDMS = do
dms@(DMS d _ _) <- parseDegreesMinuteSeconds
unless (d >= 0 && d <= 180) (fail $ "Invalid longitude degrees: " <> show d)
direction <- parseLongitudeDirection
pure $ DMSLon dms direction
data DMS = DMS Int Int Double
-- | Parses the DMS portion of a GPS position.
--
-- Validates degrees/seconds are within valid ranges, but the caller must validate degrees, whose range depends on latitude vs longitude
parseDegreesMinuteSeconds :: Parser (Int, Int, Double) -- Not sure what data type to use for this.
parseDegreesMinuteSeconds = do
degrees <- intParser
_ <- MPC.string " deg "
minutes <- intParser
unless (minutes >= 0 && minutes <= 59) (fail $ "Invalid minutes: " <> show minutes)
_ <- MPC.string "' "
seconds <- doubleParser
unless (seconds >= 0 && seconds < 60) (fail $ "Invalid seconds: " <> show seconds)
_ <- MPC.string "\" " -- Note: Parsing the trailing space after minutes
pure $ DMS degrees minutes seconds
data LatitudeDirection = North | South
deriving (Show)
data LongitudeDirection = East | West
deriving (Show)
parseLatitudeDirection :: Parser Latitude
parseLatitudeDirection = do
c <- MPC.upperChar
case c of
'N' -> pure North
'S' -> pure South
x -> fail $ "Invalid latitude:" <> [x]
parseLongitudeDirection :: Parser Longitude
parseLongitudeDirection = do
c <- MPC.upperChar
case c of
'E' -> pure East
'W' -> pure West
x -> fail $ "Invalid longitude:" <> [x]
intParser :: Parser Int
intParser = do
digitString <- concat <$> PC.some MPC.digitChar
case readMay digitString of
Nothing -> fail $ "Couldn't create an integer from a series of digits. This is likely a bug in the parser. Digits: " <> digitString
Just i -> pure i
doubleParser :: Parser Double
doubleParser = do
digits <- concat <$> PC.some MPC.digitChar
-- In all 2904 data samples, across a range of phone manufacturers, it appeared all of them were to precisely 2 digits of precision
-- Even edge cases like all zeroes.
-- So, I'm requiring the period and trailing characters until that assumption is violated.
-- Could arguably even parse this as Centi (Fixed E2), but didn't seem like that was helpful.
period <- MPC.char '.'
digits <- concat <$> PC.some MPC.digitChar
let doubleString = digits <> [period] <> digits
case readMay doubleString of
Nothing -> fail $ "Couldn't create a double from a string. This is likely a bug in the parser. String: " <> digitString
Just d -> pure d
-- "37 deg 8' 21.26\" N, 80 deg 34' 41.84\" W"
-- "42 deg 1' 42.77\" N, 88 deg 8' 46.68\" W"
-- "37 deg 34' 35.32\" N, 122 deg 19' 1.51\" W"
-- "59 deg 14' 53.35\" N, 18 deg 5' 34.01\" E"
-- "19 deg 26' 4.49\" N, 99 deg 11' 25.77\" W"
-- "42 deg 19' 53.00\" N, 71 deg 25' 49.00\" W"
-- "39 deg 13' 7.73\" N, 77 deg 15' 10.66\" W"
-- "34 deg 26' 34.97\" S, 58 deg 38' 11.68\" W"
-- "37 deg 16' 14.20\" N, 122 deg 2' 17.74\" W"
-- "35 deg 5' 59.75\" N, 80 deg 40' 36.66\" W"
-- "41 deg 52' 46.05\" N, 87 deg 38' 10.40\" W"
-- "37 deg 45' 29.09\" N, 122 deg 26' 9.34\" W"
-- "33 deg 56' 46.65\" N, 118 deg 24' 25.80\" W"
-- "37 deg 56' 39.79\" S, 57 deg 35' 9.84\" W"
-- "35 deg 46' 24.74\" N, 78 deg 47' 30.68\" W"
-- "37 deg 21' 4.58\" N, 120 deg 36' 47.45\" W"
-- "37 deg 37' 28.09\" N, 122 deg 3' 16.52\" W"
-- "41 deg 30' 35.49\" N, 71 deg 35' 56.34\" W"
-- "43 deg 18' 24.47\" N, 73 deg 38' 47.43\" W"
-- "38 deg 55' 18.13\" N, 77 deg 2' 24.94\" W"
-- "34 deg 40' 34.39\" N, 92 deg 16' 4.11\" W"
-- "37 deg 46' 36.32\" N, 122 deg 25' 2.46\" W"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment