{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeOperators #-} -- | -- Module : Web.Google.Maps.Geocoding -- Description : Bindings to the Google Maps Geocoding API -- Copyright : (c) Mike Pilgrem 2017 -- Maintainer : public@pilgrem.com -- Stability : experimental -- -- This package has no connection with Google Inc. or its affiliates. -- -- The -- provides a direct way to access geocoding and reverse geocoding services via -- an HTTP request. This library provides bindings in Haskell to that API. -- -- NB: The use of the Google Maps Geocoding API services is subject to the -- , -- which terms restrict the use of content (eg no use without a Google map). -- -- The code below is an example console application to test privately the use of -- the library with the Google Maps Geocoding API. -- -- > {-# LANGUAGE OverloadedStrings #-} -- > -- > module Main (main) where -- > -- > import Data.Maybe (fromJust) -- > import Data.Text (Text) -- > import Data.Text.IO as T (getLine, putStr) -- > import Graphics.Gloss (Display (..), display, white) -- > import Graphics.Gloss.Juicy (fromDynamicImage) -- > import Network.HTTP.Client (Manager, newManager) -- > import Network.HTTP.Client.TLS (tlsManagerSettings) -- > import Web.Google.Maps.Geocoding (Address (..), geocode, GeocodingResponse (..), -- > Geometry (..), Key (..), LatLng (..), Result (..), Status (..)) -- > import Web.Google.Static.Maps (Center (..), Location (..), Size (..), -- > staticmap, Zoom (..)) -- > import System.IO (hFlush, stdout) -- > -- > main :: IO () -- > main = do -- > putStrLn "A test of the Google Maps Geocoding API.\nNB: The use of the \ -- > \API services is subject to the Google Maps APIs Terms of Serivce at \ -- > \https://developers.google.com/maps/terms.\n" -- > txt <- input "Enter full address: " -- > mgr <- newManager tlsManagerSettings -- > let apiKey = Key "" -- > result <- geocode mgr apiKey (Just $ Address txt) Nothing Nothing -- > Nothing Nothing -- > case result of -- > Right response -> do -- > let s = status response -- > case s of -- > OK -> do -- > let latlng = location $ geometry $ head $ results -- > response -- > center = Center $ Coord latlng -- > print center -- > displayMap mgr apiKey center -- > _ -> putStrLn $ "Error! Status: " ++ show s -- > _ -> putStrLn $ "Error! Result:\n" ++ show result -- > -- > input :: Text -> IO Text -- > input msg = T.putStr msg >> hFlush stdout >> T.getLine -- > -- > displayMap :: Manager -> Key -> Center -> IO () -- > displayMap mgr apiKey center = do -- > let zoom = Just $ Zoom 17 -- > w = 400 -- > h = 400 -- > size = Size w h -- > result <- staticmap mgr apiKey Nothing (Just center) zoom size Nothing -- > Nothing [] Nothing [] [] Nothing -- > case result of -- > Right response -> do -- > let picture = fromJust $ fromDynamicImage response -- > title = "Test Google Maps Geocoding API" -- > window = InWindow title (w, h) (10, 10) -- > display window white picture -- > Left err -> putStrLn $ "Error while displaying map: " ++ show err module Web.Google.Maps.Geocoding ( -- * Functions geocode , backGeocode -- * API , GoogleMapsGeocodingAPI , api -- * Types , Key (..) , Address (..) , FilterComponent (..) , Viewport (..) , Language (..) , Region (..) , GeocodingResponse (..) , Status (..) , Result (..) , AddressType (..) , AddressComponent (..) , PostcodeLocality (..) , Geometry (..) , LatLng (..) , PlaceId (..) , Location (..) , LocationType (..) ) where import Data.Aeson hiding (Result) import Data.Aeson.Types (Options (..)) import Data.List (intersperse) import Data.Proxy import Data.Text (Text) import qualified Data.Text as T (concat, unpack) import GHC.Generics (Generic) import Network.HTTP.Client (Manager) import Servant.API import Servant.Client import Web.Google.Maps.Common (Address (..), googleMapsApis, Key (..), Language (..), LatLng (..), Location (..), Region (..)) -- | Fliter component: a component that can be used to filter the results -- returned in a geocoding response. data FilterComponent = Route Text | Locality Text | AdministrativeArea Text | PostalCode Text | Country Region deriving (Eq, Show) instance ToHttpApiData FilterComponent where toUrlPiece filterComponent | Route route <- filterComponent = T.concat ["route:", route] | Locality locality <- filterComponent = T.concat ["locality:", locality] | AdministrativeArea adminArea <- filterComponent = T.concat ["administrative_area:", adminArea] | PostalCode postalCode <- filterComponent = T.concat ["postal_code:", postalCode] | Country country <- filterComponent = T.concat ["country:", toUrlPiece country] instance ToHttpApiData [FilterComponent] where toUrlPiece [] = "" toUrlPiece cs = T.concat $ intersperse "|" $ map toUrlPiece cs -- | Geocoding Reponse data GeocodingResponse = GeocodingResponse { status :: Status , error_message :: Maybe Text , results :: [Result] } deriving (Eq, Show, Generic) instance FromJSON GeocodingResponse -- | Contains the status of the request and may contain debugging information to -- help you track down why geocoding is not working. data Status = OK -- ^ Indicates that no errors occurred; the address was -- successfully parsed and at least one geocode was -- returned. | ZeroResults -- ^ Indicates that the geocode was successful but -- returned no results. This may occur if the geocoder was -- passed a non-existent address. | OverQueryLimit | RequestDenied | InvalidRequest -- ^ Generally indicates that the query (address, -- components or latlng) is missing. | UnknownError deriving (Eq, Show) instance FromJSON Status where parseJSON = withText "Status" $ \t -> case t of "OK" -> return OK "ZERO_RESULTS" -> return ZeroResults "OVER_QUERY_LIMIT" -> return OverQueryLimit "REQUEST_DENIED" -> return RequestDenied "INVALID_REQUEST" -> return InvalidRequest "UNKNOWN_ERROR" -> return UnknownError _ -> fail $ "Unrecognised status type, namely: " ++ T.unpack t -- | A result of the geocoder. data Result = Result { types :: [AddressType] , formatted_address :: Text , address_components :: [AddressComponent] , postcode_localities :: Maybe [PostcodeLocality] , geometry :: Geometry , partial_match :: Maybe Bool , place_id :: PlaceId } deriving (Eq, Show, Generic) instance FromJSON Result -- | Address (and address component) type: The list of types provided by Google -- (as at 4 March 2017) is incomplete. newtype AddressType = AddressType Text deriving (Eq, Show, Generic, ToHttpApiData) instance FromJSON AddressType -- | Address component data AddressComponent = AddressComponent { address_component_types :: [AddressType] , long_name :: Text , short_name :: Text } deriving (Eq, Show, Generic) instance FromJSON AddressComponent where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = \l -> case l of "address_component_types" -> "types" _ -> l } -- | Postcode locality: a locality contained in a postal code. newtype PostcodeLocality = PostcodeLocality Text deriving (Eq, Show, Generic) instance FromJSON PostcodeLocality -- | Geometry data Geometry = Geometry { location :: LatLng , location_type :: LocationType , viewport :: Viewport , bounds :: Maybe Viewport } deriving (Eq, Show, Generic) instance FromJSON Geometry -- | Location type data LocationType = Rooftop | RangeInterpolated | GeometricCenter | Approximate deriving (Eq, Show) instance ToHttpApiData LocationType where toUrlPiece locationType = case locationType of Rooftop -> "ROOFTOP" RangeInterpolated -> "RANGE_INTERPOLATED" GeometricCenter -> "GEOMETRIC_CENTER" Approximate -> "APPROXIMATE" instance FromJSON LocationType where parseJSON = withText "LocationType" $ \t -> case t of "ROOFTOP" -> return Rooftop "RANGE_INTERPOLATED" -> return RangeInterpolated "GEOMETRIC_CENTER" -> return GeometricCenter "APPROXIMATE" -> return Approximate _ -> fail $ "Unrecognised location type, namely: " ++ T.unpack t -- | Viewport data Viewport = Viewport { southwest :: LatLng , northeast :: LatLng } deriving (Eq, Show, Generic) instance ToHttpApiData Viewport where toUrlPiece (Viewport sw ne) = T.concat [toUrlPiece sw, "|", toUrlPiece ne] instance FromJSON Viewport -- | Place id newtype PlaceId = PlaceId Text deriving (Eq, Show, Generic, ToHttpApiData) instance FromJSON PlaceId -- | Google Translate API type GoogleMapsGeocodingAPI = "geocode" :> "json" :> QueryParam "key" Key :> QueryParam "address" Address :> QueryParam "components" [FilterComponent] :> QueryParam "bounds" Viewport :> QueryParam "language" Language :> QueryParam "region" Region :> Get '[JSON] GeocodingResponse :<|> "geocode" :> "json" :> QueryParam "key" Key :> QueryParam "latlng" LatLng :> QueryParam "place_id" PlaceId :> QueryParam "result_type" AddressType :> QueryParam "location_type" LocationType :> QueryParam "language" Language :> Get '[JSON] GeocodingResponse -- | API type api :: Proxy GoogleMapsGeocodingAPI api = Proxy geocode' :: Maybe Key -> Maybe Address -> Maybe [FilterComponent] -> Maybe Viewport -> Maybe Language -> Maybe Region -> ClientM GeocodingResponse backGeocode' :: Maybe Key -> Maybe LatLng -> Maybe PlaceId -> Maybe AddressType -> Maybe LocationType -> Maybe Language -> ClientM GeocodingResponse geocode' :<|> backGeocode' = client api -- | Geocode. NB: The use of the Google Maps Geocoding API services is subject -- to the . geocode :: Manager -> Key -> Maybe Address -> Maybe [FilterComponent] -> Maybe Viewport -> Maybe Language -> Maybe Region -> IO (Either ServantError GeocodingResponse) geocode mgr key addressOpt filterComponentsOpt viewportOpt languageOpt regionOpt = runClientM (geocode' (Just key) addressOpt filterComponentsOpt viewportOpt languageOpt regionOpt) -- CookieJar supported from servant-client-0.13 #if MIN_VERSION_servant_client(0,13,0) (ClientEnv mgr googleMapsApis Nothing) #else (ClientEnv mgr googleMapsApis) #endif -- | Reverse (back) geocode. NB: The use of the Google Maps Geocoding API -- services is subject to the -- . backGeocode :: Manager -> Key -> Maybe LatLng -> Maybe PlaceId -> Maybe AddressType -> Maybe LocationType -> Maybe Language -> IO (Either ServantError GeocodingResponse) backGeocode mgr key latLngOpt placeIdOpt addressTypeOpt locationTypeOpt languageOpt = runClientM (backGeocode' (Just key) latLngOpt placeIdOpt addressTypeOpt locationTypeOpt languageOpt) -- CookieJar supported from servant-client-0.13 #if MIN_VERSION_servant_client(0,13,0) (ClientEnv mgr googleMapsApis Nothing) #else (ClientEnv mgr googleMapsApis) #endif