gps-0.2.4: Data/GPS.hs
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances #-}
-- |Author: Thomas DuBuisson
-- Copyright: Thomas DuBuisson
-- License: BSD3
--
-- A basic GPS library with calculations for distance and speed along
-- with helper functions for filtering/smoothing trails. All distances
-- are in meters and time is in seconds. Speed is thus meters/second
--
-- The current intent of this library is to
-- 1) Provide a standard type class interface for coordinates
-- 2) Include fleshed out support for relevant libraries, a task
-- too often neglected in modern Hackage packages.
--
-- Integration includes KML support via the xml package, pretty printing,
-- anb binary.
module Data.GPS
( -- * Types and Classes
Coordinate(..)
, Location(..)
, DMSCoordinate(..)
, DMS(..)
, Heading
, Speed
, Vector
, Trail
-- * Helper Functions
, smoothTrails
, longestSmoothTrail
, restLocations
, closestDistance
, normalizeDMS
-- * KML Operations (Open format used by GoogleEarth, among others)
, KML
, trailToKML
, pointsToKML
, kmlToString
) where
import Data.Ord (comparing)
import Data.List (sort, mapAccumL, maximumBy)
import Data.Binary
import Data.Binary.Put
import Data.Binary.Get
import Text.PrettyPrint.HughesPJClass
import Text.PrettyPrint
import Numeric (showFFloat)
import Data.Time
import Data.Maybe (listToMaybe)
import Text.XML.Light
-- |The KML type and operations might be moved into a Data.KML module in the future
type KML = Element
-- |Converts the KML elements to a string and prepends the proper XML header,
-- thus making it the correct format for saving as a file and opening it
-- with other programs such as GoogleEarth.
kmlToString :: KML -> String
kmlToString = (++) xml_header . ppElement
kmlTop = Element (QName "kml" Nothing Nothing) [Attr (basicName "xmlns") "http://www.opengis.net/kml/2.2"] [] Nothing
addElem :: Element -> Element -> Element
addElem top child = top { elContent = Elem child : elContent top }
basicName :: String -> QName
basicName n = QName n Nothing Nothing
basicElem :: String -> Element
basicElem n = Element (basicName n) [] [] Nothing
filledElem :: String -> String -> Element
filledElem name val = Element (basicName name) [] [Text (CData CDataText val Nothing)] Nothing
-- |converts a given set of coordinates to a trail in KML format.
-- Useful for saving as a file.
trailToKML :: Coordinate a => String -> Trail a -> KML
trailToKML name ps = addElem kmlTop doc
where
doc = foldl addElem (basicElem "Document") (nameElem : trailElem : [])
nameElem = filledElem "name" name
trailName = filledElem "name" (name ++ " trail")
trailElem = foldl addElem (basicElem "Placemark") [trailName, pointsElem]
pointsElem = addElem (basicElem "LineString") (filledElem "coordinates" points)
points :: String
points = concatMap packCoordinate ps
packCoordinate :: Coordinate a => a -> String
packCoordinate x =
let (lat, lon) = getDegreeLatLon x
in concat [lon, ",", lat, ",0 "]
-- |converts a given set of coordinates to points in KML format.
-- Useful for saving as a file.
pointsToKML :: Coordinate a => String -> [a] -> [String] -> KML
pointsToKML name ps pointNames = addElem kmlTop doc
where
doc = foldl addElem (basicElem "Document") (nameElem : placemarkElems)
nameElem = filledElem "name" name
placemarkElems = map buildPoint (zip ps pointNames)
buildPoint (p,n) =
let (t,g) = getDegreeLatLon p
in foldl addElem (basicElem "Placemark")
[ filledElem "name" n
, foldl addElem (basicElem "LookAt")
[ filledElem "longitude" g
, filledElem "latitude" t
, filledElem "altitude" "0"
, filledElem "range" "500"
]
, addElem (basicElem "Point") (filledElem "coordinates" (g ++ "," ++ t))
]
-- |Gets a pair of lat lon bytestrings (in degrees)
getDegreeLatLon :: Coordinate a => a -> (String,String)
getDegreeLatLon x =
let (lat,lon) = dmsToLatLon . toDMS $ x
f = show . toDecimal
in (f lat, f lon)
-- |radius of the earth in meters
radiusOfEarth :: Double
radiusOfEarth = 6378700
-- |A coordinate is a place on earths surface. While twoDMS and fromDMS are the only
-- required functions, coordinate systems may provide more accurate calculations
-- of heading, distance, and vectors without the intermediate translation.
class Coordinate a where
toDMS :: a -> DMSCoordinate
fromDMS :: DMSCoordinate -> a
distance :: a -> a -> Distance
distance a b =
3963000 * acos( sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(lon2 - lon1) )
where
(lat1, lon1) = dmsToLatLon (toDMS a)
(lat2, lon2) = dmsToLatLon (toDMS b)
heading :: a -> a -> Heading -- 0 = North, pi/2 = West...
heading a b = dmsHeading (toDMS a) (toDMS b)
getVector :: a -> a -> Vector
getVector a b = (distance a b, heading a b)
-- |A location is a coordinate at a specific time
class (Coordinate coord) => Location loc coord time | loc -> coord, loc -> time where
getCoordinate :: loc -> coord
getTime :: loc -> time
getUTC :: loc -> UTCTime
speed :: loc -> loc -> Speed -- meters per second
speed a b = distMeter / timeSec
where
timeSec = realToFrac $ diffUTCTime (getUTC b) (getUTC a)
distMeter = distance (getDMS a) (getDMS b)
toDecimal = (*) (180 / pi)
-- |Extract a lat/lon from a location.
getDMS :: (Location loc x y) => loc -> DMSCoordinate
getDMS = toDMS . getCoordinate
-- |provides a heading in radians from North
dmsHeading :: DMSCoordinate -> DMSCoordinate -> Heading
dmsHeading a b =
atan2 (sin (diffLon) * cos (lat2))
(cos(lat1) * sin (lat2) - sin(lat1) * cos lat2 * cos (diffLon))
where
(lat1, lon1) = dmsToLatLon a
(lat2, lon2) = dmsToLatLon b
diffLon = lon1 - lon2
-- provides a lat/lon pair in radians
dmsToLatLon :: DMSCoordinate -> (Double, Double)
dmsToLatLon (DMSCoord (DMS latD latM latS) (DMS lonD lonM lonS)) = (lat, lon)
where
lon = toRadians lonD lonM lonS
lat = toRadians latD latM latS
toRadians d m s = (d + m / 60 + s / 3600) * (pi / 180)
-- |Typically useful for printing, normalizes degrees/minutes/seconds
-- into just degrees and decimal minutes:
-- DMSCoord (DMS 45 36.938455 0) (DMS ...)
normalizeDMS :: DMSCoordinate -> DMSCoordinate
normalizeDMS (DMSCoord lat lon) =
DMSCoord lat' lon'
where
lat' = normalize lat
lon' = normalize lon
normalize (DMS d m s) =
let all = d + (m / 60) + (s / 3600)
d' = fromIntegral (floor all)
m' = (all-d') * 60
in DMS d' m' 0
-- |Distances are expressed in meters
type Distance = Double
-- |Angles are expressed in radians from North.
-- 0 == North
-- pi/2 == West
-- pi == South
-- (3/2)pi == East == - (pi / 2)
type Heading = Double
-- |Speed is hard coded as meters per second
type Speed = Double
type Vector = (Distance, Heading)
type Trail a = [a]
-- |DMSCoordinate is the typical degree minute second
-- for latitude/longitude used by most civilian GPS devices.
data DMSCoordinate = DMSCoord
{ latitude :: DMS
, longitude :: DMS
} deriving (Eq, Ord, Show, Read)
-- |DMS is the degree, minute, seconds used for both latitude and longitude
data DMS = DMS { degrees :: Double
, minutes :: Double
, seconds :: Double }
deriving (Eq, Ord, Show, Read)
instance Coordinate DMSCoordinate where
toDMS = id
fromDMS = id
instance (Coordinate c) => Location (c, UTCTime) c UTCTime where
getCoordinate = fst
getTime = snd
getUTC = snd
instance (Coordinate c) => Coordinate (c,UTCTime) where
toDMS = toDMS . getCoordinate
fromDMS d = (fromDMS d , UTCTime (toEnum 0) 0)
data TempTrail a = T (Trail a) a
fromTemp :: TempTrail a -> Trail a
fromTemp (T ps p) = reverse (p:ps)
-- |'smoothTrails speed trail' should separate points that would mandate a speed
-- in excess of the given rate into separate Trails. No point are deleted;
-- if there is only one 'correct' trail expected then the largest resulting
-- trail will likely be the one your looking for.
smoothTrails :: Location a b c => Speed -> Trail a -> [Trail a]
smoothTrails s ps =
let (trails, _) = mapAccumL go [] (linearTime ps)
in map fromTemp trails
where
go acc p =
let (i,trails) = mapAccumL (add p) 0 acc
in if i == 0
then (T [] p : trails, ())
else (trails, ())
add p i (T ts l) =
if (speed l p) <= s
then (i + 1, T (l:ts) p)
else (i, T ts l)
-- |longestSmoothTrail is ust 'smoothTrails' returning only the longest smooth trail.
longestSmoothTrail :: Location a b c => Speed -> Trail a -> Trail a
longestSmoothTrail metersPerSec trail = maximumBy (comparing length) ([] : smoothTrails metersPerSec trail)
linearTime :: Location a b c => Trail a -> Trail a
linearTime [] = []
linearTime (p:ps) = go (getUTC p) ps
where
go _ [] = []
go t (p:ps) = if getUTC p < t then go t ps else p : go (getUTC p) ps
-- |'restLocation dist timeDiff' will create a list of trails, all of which are
-- within 'distance' of each other spanning atleast the given amount of time.
--
-- For example:
-- > restLocations 50 600
--
-- Would return lists of all points that are within 50 meters of each other and
-- span at least 10 minutes (600 seconds).
--
-- Note this gives points within fifty meters of the earliest point - wandering
-- in a rest area with a 50 meter radius could result in several rest points
-- ([a,b..]) or even none if the distance between individual points exceeds 50m.
restLocations :: Location a b c => Distance -> NominalDiffTime -> Trail a -> [Trail a]
restLocations d s = filter timeSpan . span2
where
span2 :: Location a b c => [a] -> [[a]]
span2 [] = []
span2 (a:as) = let (x,y) = span ((>) d . distance (getCoordinate a) . getCoordinate) as in (a : x) : (span2 y)
timeSpan :: Location a b c => Trail a -> Bool
timeSpan [] = False
timeSpan t = s <= diffUTCTime (getUTC (last t)) (getUTC (head t))
-- |Returns the closest distance between two trails (or Nothing if a trail is empty)
-- O( (n * m) * log (n * m) )
closestDistance :: Coordinate a => Trail a -> Trail a -> Maybe Distance
closestDistance as bs = listToMaybe $ sort [distance a b | a <- as, b <- bs]
instance Pretty DMSCoordinate where
pPrint c =
let (DMSCoord (DMS d' m' _) (DMS d m _)) = normalizeDMS c
showF = showFFloat (Just 6)
showX = shows . floor
in text $ showX d' (' ' : showF m' ('\t' : showX d (' ' : showF m "")))
instance Binary DMSCoordinate where
put (DMSCoord (DMS tD tM tS) (DMS gD gM gS)) = do
put tD >> put tM >> put tS >> put gD >> put gM >> put gS
get = do
tD <- get
tM <- get
tS <- get
gD <- get
gM <- get
gS <- get
return (DMSCoord (DMS tD tM tS) (DMS gD gM gS))
putLocation :: Binary c => (c, UTCTime) -> Put
putLocation (c,t) = put c >> put t
getLocation :: Binary c => Get (c, UTCTime)
getLocation = do
c <- get
t <- get
return (c,t)
instance Binary UTCTime where
get = do
day <- get
diff <- get
return (UTCTime (toEnum day) (fromRational diff))
put (UTCTime day diff) = do
put (fromEnum day)
put (toRational diff)