{-# 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