geo-resolver-0.1.0.1: src/Network/Google/GeoResolver/Parser.hs
{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}
{-|
Module : GeoResolver.Parser
Description : Parsing helper definitions for Googles geocoding API.
Copyright : (c) 2015, Markenwerk, Jan Greve
License : MIT
Maintainer : jg@markenwerk.net
-}
module Network.Google.GeoResolver.Parser (
-- * Data type definition for parsed types
Status(..),
GoogleAnswer(..),
GoogleResult(..),
Component(..),
Geometry(..),
GoogleBoundingBox(..),
Location(..),
-- * parsing
parseAnswer,
-- * convenience functions
getLocation,
getAddress,
getProperty,
-- * Typeclasses
GoogleArgumentListShow(..)
) where
import GHC.Generics
import Data.Aeson
import Data.HashMap.Strict
import Data.Text
import Data.Maybe (fromMaybe)
import Control.Arrow
import qualified Data.ByteString.Lazy.Char8 as LBS
-- | A class to format instances the way google expects them.
class GoogleArgumentListShow a where
argListShow :: a -> String
-- | Represents the status of the operation returned by google. Constructors represent possible 'String' values
-- according to googles documentation.
data Status = OK | ZERO_RESULTS | OVER_QUERY_LIMIT | REQUEST_DENIED | INVALID_REQUEST | UNKNOWN_ERROR
deriving (Eq, Show, Generic)
instance FromJSON Status
-- | Represents the answer google returned.
data GoogleAnswer r = GoogleAnswer {
-- | The 'Status' returned by google.
status :: Status,
-- | An optional error message.
errorMessage :: Maybe String,
-- | Optional list of actual 'GoogleResult' values.
results :: Maybe [r]
} deriving (Show, Eq)
instance (FromJSON a) => FromJSON (GoogleAnswer a) where
parseJSON (Object v) = GoogleAnswer <$>
v .: "status" <*>
v .:? "error_message" <*>
v .:? "results"
parseJSON _ = mempty
instance Functor GoogleAnswer where
fmap f (GoogleAnswer s e Nothing) = (GoogleAnswer s e Nothing)
fmap f (GoogleAnswer s e (Just xs)) = (GoogleAnswer s e (Just $ fmap f xs))
instance Foldable GoogleAnswer where
foldMap _ (GoogleAnswer _ _ Nothing) = mempty
foldMap f (GoogleAnswer _ _ (Just [r])) = f r
foldMap f (GoogleAnswer _ _ (Just rs)) = foldMap f rs
-- | A single Result from the list of results from google.
data GoogleResult = GoogleResult {
-- | List of 'Component' values for this result
addressComponents :: [Component],
-- | The formatted address google returned
formattedAddress :: String,
-- | The 'Geometry' value for this result
geometry :: Geometry,
-- | The google places ID for this result
placeId :: String,
-- | Some list of strings. Goole says:
--
-- The types[] array indicates the type of the returned result.
-- This array contains a set of zero or more tags identifying the type of feature returned in the result.
-- For example, a geocode of "Chicago" returns "locality" which indicates that "Chicago" is a city,
-- and also returns "political" which indicates it is a political entity.
types :: [String],
-- | If present, hinting that this result only partially matches the requested entity.
partialMatch :: Maybe Bool
} deriving (Show, Generic)
instance FromJSON GoogleResult where
parseJSON (Object v) = GoogleResult <$>
v .: "address_components" <*>
v .: "formatted_address" <*>
v .: "geometry" <*>
v .: "place_id" <*>
v .: "types" <*>
v .:? "partial_match"
parseJSON _ = mempty
-- | A part of the address in a 'GoogleResult'
data Component = Component {
-- | A long name for the component
longName :: String,
-- | A short name for the component
shortName :: String,
-- | indicating the type of the address component.
cTypes :: [String]
} deriving (Show, Eq)
instance FromJSON Component where
parseJSON (Object v) = Component <$>
v .: "long_name" <*>
v .: "short_name" <*>
v .: "types"
parseJSON _ = mempty
-- | Holds geometry information about a 'GoogleResult'
data Geometry = Geometry {
-- | The result's location
location :: Location,
-- | The kind of location. As of 2015/10/07, the
-- following values are to be expected. For future compatibility,
-- no Enum type is introduced to map this.
--
-- * @ROOFTOP@ indicates that the returned result is a precise geocode for
-- which we have location information accurate down to street address precision.
--
-- * @RANGE_INTERPOLATED@ indicates that the returned result reflects
-- an approximation (usually on a road) interpolated between two precise points
-- (such as intersections). Interpolated results are generally returned
-- when rooftop geocodes are unavailable for a street address.
--
-- * @GEOMETRIC_CENTER@ indicates that the returned result is the geometric
-- center of a result such as a polyline (for example, a street) or
-- polygon (region).
--
-- * @APPROXIMATE@ indicates that the returned result is approximate.
locationType :: String,
-- | contains the recommended viewport for displaying the returned result.
-- Generally the viewport
-- is used to frame a result when displaying it to a user.
viewport :: GoogleBoundingBox,
-- | If viewport is not applicable, bounds contain a more sensible bounding box.
bounds :: Maybe GoogleBoundingBox
} deriving (Show, Eq)
instance FromJSON Geometry where
parseJSON (Object v) = Geometry <$>
v .: "location" <*>
v .: "location_type" <*>
v .: "viewport" <*>
v .:? "bounds"
parseJSON _ = mempty
-- | A Bounding box for a location.
data GoogleBoundingBox = GoogleBoundingBox {
-- | The north east location of the bounding box
northeast :: Location,
-- | The south west location of the bounding box
southwest :: Location
} deriving (Show, Eq, Generic)
instance FromJSON GoogleBoundingBox
instance GoogleArgumentListShow GoogleBoundingBox where
argListShow (GoogleBoundingBox ne sw) = argListShow ne ++ '|' : argListShow sw
-- | Abstraction of a geo location
data Location = Location {
-- | Latitude of the location
latitude :: Double,
-- | Longitude of the location
longitude :: Double
} deriving (Show, Eq)
instance GoogleArgumentListShow Location where
argListShow (Location lat long) = show lat ++ ',': show long
instance FromJSON Location where
parseJSON (Object o) = Location <$>
o .: "lat" <*>
o .: "lng"
parseJSON _ = mempty
-- | Takes a 'GoogleAnswer' and applies the function to the first 'GoogleResult'.
-- Returns a 'Left' with an error description if anything unexpected happens.
--
-- For example, 'getLocation' uses this with
-- @
-- ((latitude &&& longitude) . location . geometry)
-- @
getProperty :: GoogleAnswer r -- ^ The answer to process.
-> (r -> a) -- ^ The function to be applied to the first (if any) result.
-> Either String a -- ^ Error or result of the function application
getProperty a f = case status a of
OK -> fromMaybe (Left "No results.") (results a >>= (\res -> case res of
(x:_) -> (Just . Right . f) x
_ -> Just $ Left "Empty resultset"))
otherwise -> Left (show otherwise ++ show (errorMessage a))
-- | Gets the location from a 'GoogleAnswer', or returns an error.
getLocation :: GoogleAnswer GoogleResult -> Either String (Double, Double)
getLocation a = a `getProperty` ((latitude &&& longitude) . location . geometry)
-- | Gets the formatted address from a GoogleAnswer (or an error)
getAddress :: GoogleAnswer GoogleResult -> Either String String
getAddress a = a `getProperty` formattedAddress
-- | Parses a Lazy ByteString into a 'GoogleAnswer' or returns an error describing the problem.
parseAnswer :: LBS.ByteString -> Either String (GoogleAnswer GoogleResult)
parseAnswer = eitherDecode