Created
May 9, 2020 15:41
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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