google-static-maps-0.6.0.1: src/Web/Google/Maps/Static.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
-- |
-- Module : Web.Google.Maps.Static
-- Description : Bindings to the Google Maps Static API (formerly Static Maps
-- API)
-- Copyright : (c) Mike Pilgrem 2017, 2018
-- Maintainer : public@pilgrem.com
-- Stability : experimental
--
-- This module has no connection with Google Inc. or its affiliates.
--
-- The <https://developers.google.com/maps/documentation/maps-static/intro Google Maps Static API>
-- returns a map as an image via an HTTP request. This library provides bindings
-- in Haskell to that API (version 2).
--
-- NB: The use of the Google Maps Static API services is subject to the
-- <https://cloud.google.com/maps-platform/terms/ Google Maps Platform Terms of Service>,
-- which terms restrict the use of content. End Users' use of Google Maps is
-- subject to the then-current Google Maps/Google Earth Additional Terms of
-- Service at <https://maps.google.com/help/terms_maps.html> and Google Privacy
-- Policy at <https://www.google.com/policies/privacy/>.
--
-- The following are not yet implemented: non-PNG image formats; and encoded
-- polyline paths.
--
-- The code below is an example console application to test the use of the
-- library with the Google Maps Static API.
--
-- > {-# LANGUAGE OverloadedStrings #-}
-- >
-- > module Main (main) where
-- >
-- > import Data.Maybe (fromJust)
-- > import Graphics.Gloss (Display (..), display, white) -- package gloss
-- > import Graphics.Gloss.Juicy (fromDynamicImage) -- package gloss-juicy
-- > import Network.HTTP.Client (newManager)
-- > import Network.HTTP.Client.TLS (tlsManagerSettings)
-- > import Web.Google.Maps.Static (Center (..), Key (..), Location (..), Size (..),
-- > staticmap, StaticmapResponse (..), Zoom (..))
-- >
-- > main :: IO ()
-- > main = do
-- > putStrLn $ "A test of the Google Maps Static API.\nNB: The use of " ++
-- > "the API services is subject to the Google Maps Platform Terms of " ++
-- > "Serivce at https://cloud.google.com/maps-platform/terms/.\n"
-- > mgr <- newManager tlsManagerSettings
-- > let apiKey = Key "<REPLACE_THIS_WITH_YOUR_ACTUAL_GOOGLE_API_KEY>"
-- > -- If using a digital signature ...
-- > secret = Just $ Secret
-- > "<REPLACE_THIS_WITH_YOUR_ACTUAL_GOOGLE_URL_SIGNING_SECRET>"
-- > center = Just $ Center (Location 42.165950 (-71.362015))
-- > zoom = Just $ Zoom 17
-- > w = 400
-- > h = 400
-- > size = Size w h
-- > result <- staticmap mgr apiKey secret center zoom size Nothing Nothing
-- > [] Nothing Nothing Nothing [] [] Nothing
-- > case result of
-- > Right response -> do
-- > let picture = fromJust $ fromDynamicImage response
-- > title = "Test Google Maps Static API"
-- > window = InWindow title (w, h) (10, 10)
-- > display window white picture
-- > Left err -> putStrLn $ "Error! Result:\n" ++ show err
module Web.Google.Maps.Static
( -- * Functions
staticmap
-- * API
, GoogleMapsStaticAPI
, api
-- * Types
, Key (..)
, Secret (..)
, Signature (..)
, Center (..)
, Location (..)
, LatLng (..)
, Address (..)
, Zoom (..)
, Size (..)
, Scale (..)
, Format (..)
, MapStyle (..)
, Feature (..)
, Element (..)
, MapStyleOp (..)
, Visibility (..)
, MapType (..)
, Language (..)
, Region (..)
, Markers (..)
, MarkerStyle (..)
, MarkerSize (..)
, MarkerColor (..)
, MarkerLabel (..)
, StdColor (..)
, URI (..)
, URIAuth (..)
, Anchor (..)
, StdAnchor (..)
, Path (..)
, PathStyle (..)
, PathWeight (..)
, PathColor (..)
, PathGeodesic (..)
, Visible (..)
, StaticmapResponse
) where
import Codec.Picture.Types (DynamicImage (..))
import Crypto.Hash.Algorithms (SHA1)
import Crypto.MAC.HMAC (HMAC, hmac)
import Data.ByteArray (convert)
import Data.ByteString.Base64.URL (decode, encode)
import Data.ByteString.UTF8 as UTF8 (fromString)
import Data.List (intersperse)
import Data.Maybe (catMaybes)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import qualified Data.Text as T (append, concat, pack)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Word (Word8)
import Network.HTTP.Client (Manager)
import Network.URI (URI (..), URIAuth (..), uriToString)
import Servant.API ((:>), Get, QueryParam, QueryParams, safeLink,
ToHttpApiData (..))
import Servant.Client (BaseUrl (..), client, ClientEnv (ClientEnv), ClientM,
runClientM, ClientError)
import Servant.JuicyPixels (PNG)
import Servant.Links (LinkArrayElementStyle (..), linkURI')
import Text.Bytedump (hexString)
import Web.Google.Maps.Common (Address (..), googleMapsApis, Key (..),
Language (..), LatLng (..), Location (..), Region (..))
-- | Secret for digital signature
newtype Secret = Secret Text
deriving (Eq, Show)
-- | Signature
newtype Signature = Signature Text
deriving (Eq, Show, ToHttpApiData)
-- | Center of the map: not required if the map includes markers or paths.
newtype Center = Center Location
deriving (Eq, Show, ToHttpApiData)
-- | Zoom level: the lowest level, in which the whole world can be seen, is 0.
-- Each succeeding level doubles the precision. Not required if the map includes
-- markers or paths.
newtype Zoom = Zoom Int
deriving (Eq, Show, ToHttpApiData)
-- | Size in pixels: there are maximum allowable values.
data Size = Size
{ width :: Int
, height :: Int
} deriving (Eq, Show)
instance ToHttpApiData Size where
toUrlPiece (Size width' height') =
T.pack (show width' ++ "x" ++ show height')
-- | Scale
data Scale
= Single -- ^ The default value.
| Double
| Quadruple
deriving (Eq, Show)
instance ToHttpApiData Scale where
toUrlPiece scale = case scale of
Single -> "1"
Double -> "2"
Quadruple -> "4"
-- | Image format
data Format
= Png8 -- ^ The default value.
| Png32
deriving (Eq, Show)
instance ToHttpApiData Format where
toUrlPiece format = case format of
Png8 -> "png8"
Png32 -> "png32"
-- | MapStyle
data MapStyle = MapStyle (Maybe Feature) (Maybe Element) [MapStyleOp]
deriving (Eq, Show)
instance ToHttpApiData MapStyle where
toUrlPiece (MapStyle featureOpt elementOpt ops) =
T.concat $ intersperse "|" $ catMaybes [featureUrl, elementUrl] ++
[opsUrl]
where
featureUrl = T.append "feature:" . toUrlPiece <$> featureOpt
elementUrl = T.append "element:" . toUrlPiece <$> elementOpt
opsUrl = toUrlPiece ops
-- | Map feature
data Feature
= AllFeatures
| Administrative
| AdministrativeCountry
| AdministrativeLandParcel
| AdministrativeLocality
| AdministrativeNeighborhood
| AdministrativeProvince
| Landscape
| LandscapeManMade
| LandscapeNatural
| LandscapeNaturalLandcover
| LandscapeNaturalTerrain
| Poi
| PoiAttraction
| PoiBusiness
| PoiGovernment
| PoiMedical
| PoiPark
| PoiPlaceOfWorship
| PoiSchool
| PoiSportsComplex
| Road
| RoadArterial
| RoadHighway
| RoadHighwayControlledAccess
| RoadLocal
| Transit
| TransitLine
| TransitStation
| TransitStationAirport
| TransitStationBus
| TransitStationRail
| Water
deriving (Eq, Show)
instance ToHttpApiData Feature where
toUrlPiece feature = case feature of
AllFeatures -> "all"
Administrative -> "administrative"
AdministrativeCountry -> "administrative.country"
AdministrativeLandParcel -> "administrative.land_parcel"
AdministrativeLocality -> "administrative.locality"
AdministrativeNeighborhood -> "administrative.neighborhood"
AdministrativeProvince -> "administrative.province"
Landscape -> "landscape"
LandscapeManMade -> "landscape.man_made"
LandscapeNatural -> "landscape.natural"
LandscapeNaturalLandcover -> "landscape.landcover"
LandscapeNaturalTerrain -> "landscape.terrain"
Poi -> "poi"
PoiAttraction -> "poi.attraction"
PoiBusiness -> "poi.business"
PoiGovernment -> "poi.government"
PoiMedical -> "poi.medical"
PoiPark -> "poi.park"
PoiPlaceOfWorship -> "poi.place_of_worship"
PoiSchool -> "poi.school"
PoiSportsComplex -> "poi.sports_complex"
Road -> "road"
RoadArterial -> "road.arterial"
RoadHighway -> "road.highway"
RoadHighwayControlledAccess -> "road.controlled_access"
RoadLocal -> "road.local"
Transit -> "transit"
TransitLine -> "transit.line"
TransitStation -> "transit.station"
TransitStationAirport -> "transit.station.airport"
TransitStationBus -> "transit.station.bus"
TransitStationRail -> "transit.station.rail"
Water -> "water"
-- | Feature element
data Element
= AllElements
| AllGeometry
| GeometryFill
| GeometryStroke
| AllLabels
| LabelsIcon
| LabelsText
| LabelsTextFill
| LabelsTextStroke
deriving (Eq, Show)
instance ToHttpApiData Element where
toUrlPiece element = case element of
AllElements -> "all"
AllGeometry -> "geometry"
GeometryFill -> "geometry.fill"
GeometryStroke -> "geometry.stroke"
AllLabels -> "labels"
LabelsIcon -> "labels.icon"
LabelsText -> "labels.text"
LabelsTextFill -> "labels.text.fill"
LabelsTextStroke -> "labels.text.stroke"
-- | Map style operation
data MapStyleOp
= StyleHue Word8 Word8 Word8
| StyleLightness Double
| StyleSaturation Double
| StyleGamma Double
| StyleInvertLightness Bool
| StyleVisibility Visibility
| StyleColor Word8 Word8 Word8
| StyleWeight Int
deriving (Eq, Show)
instance ToHttpApiData MapStyleOp where
toUrlPiece mapStyleOp
| StyleHue r g b <- mapStyleOp
= T.pack $ "hue:0x" ++ hexString r ++ hexString g ++ hexString b
| StyleLightness l <- mapStyleOp
= T.concat ["lightness:", toUrlPiece l]
| StyleSaturation s <- mapStyleOp
= T.concat ["saturation:", toUrlPiece s]
| StyleGamma g <- mapStyleOp
= T.concat ["gamma:", toUrlPiece g]
| StyleInvertLightness i <- mapStyleOp
= T.concat ["invert_lightness:", toUrlPiece i]
| StyleVisibility e <- mapStyleOp
= T.concat ["visibility:", toUrlPiece e]
| StyleColor r g b <- mapStyleOp
= T.pack $ "color:0x" ++ hexString r ++ hexString g ++ hexString b
| StyleWeight w <- mapStyleOp
= T.concat ["weight:", toUrlPiece w]
instance ToHttpApiData [MapStyleOp] where
toUrlPiece ops = T.concat $ intersperse "|" $ map toUrlPiece ops
-- | Visibility
data Visibility
= On
| Off
| Simplified -- ^ Removes some, not all, style features
deriving (Eq, Show)
instance ToHttpApiData Visibility where
toUrlPiece visibility = case visibility of
On -> "on"
Off -> "off"
Simplified -> "simplified"
-- | Map type
data MapType
= RoadMap -- ^ The default value.
| Satellite
| Hybrid
| Terrain
deriving (Eq, Show)
instance ToHttpApiData MapType where
toUrlPiece mapType = case mapType of
RoadMap -> "roadmap"
Satellite -> "satellite"
Hybrid -> "hybrid"
Terrain -> "terrain"
-- | Markers
data Markers = Markers (Maybe MarkerStyle) [Location]
deriving (Eq, Show)
instance ToHttpApiData Markers where
toUrlPiece (Markers markerStyleOpt ls)
| Nothing <- markerStyleOpt
= toUrlPiece ls
| Just (StdMarkerStyle Nothing Nothing Nothing) <- markerStyleOpt
= toUrlPiece ls
| Just markerStyle <- markerStyleOpt
= case ls of
[] -> toUrlPiece markerStyle
_ -> T.concat [toUrlPiece markerStyle, "|", toUrlPiece ls]
-- | Marker style
data MarkerStyle
= StdMarkerStyle
{ markerSize :: Maybe MarkerSize
, markerColor :: Maybe MarkerColor
, markerLabel :: Maybe MarkerLabel
}
| CustomIcon
{ icon :: URI
, anchor :: Maybe Anchor
}
deriving (Eq, Show)
instance ToHttpApiData MarkerStyle where
toUrlPiece markerStyle
| StdMarkerStyle ms mc ml <- markerStyle
= let size' = T.append "size:" . toUrlPiece <$> ms
color' = T.append "color:" . toUrlPiece <$> mc
label' = T.append "label:" . toUrlPiece <$> ml
opts = catMaybes [size', color', label']
in T.concat $ intersperse "|" opts
| CustomIcon url ma <- markerStyle
= let icon' = T.concat ["icon:", toUrlPiece $ uriToString id url ""]
in case ma of
Nothing -> icon'
Just a -> T.concat [icon', "|", "anchor:", toUrlPiece a]
-- | Marker size
data MarkerSize
= Tiny
| Mid
| Small
deriving (Eq, Show)
instance ToHttpApiData MarkerSize where
toUrlPiece markerSize' = case markerSize' of
Tiny -> "tiny"
Mid -> "mid"
Small -> "small"
-- | Marker colour
data MarkerColor
= MarkerColor Word8 Word8 Word8
| StdMarkerColor StdColor
deriving (Eq, Show)
instance ToHttpApiData MarkerColor where
toUrlPiece (MarkerColor r g b) = T.pack $ "0x" ++ hexString r ++ hexString g
++ hexString b
toUrlPiece (StdMarkerColor stdColor) = toUrlPiece stdColor
-- | Standard colours
data StdColor
= Black
| Brown
| Green
| Purple
| Yellow
| Blue
| Gray
| Orange
| Red
| White
deriving (Eq, Show)
instance ToHttpApiData StdColor where
toUrlPiece stdColor = case stdColor of
Black -> "black"
Brown -> "brown"
Green -> "green"
Purple -> "purple"
Yellow -> "yellow"
Blue -> "blue"
Gray -> "gray"
Orange -> "orange"
Red -> "red"
White -> "white"
-- | Marker label character
newtype MarkerLabel = MarkerLabel Char
deriving (Eq, Show, ToHttpApiData)
-- | Anchor
data Anchor
= AnchorPoint Int Int
| StdAnchor StdAnchor
deriving (Eq, Show)
instance ToHttpApiData Anchor where
toUrlPiece anchor'
| AnchorPoint x y <- anchor'
= T.pack (show x ++ "," ++ show y)
| StdAnchor stdAnchor <- anchor'
= toUrlPiece stdAnchor
-- | Standard anchor points
data StdAnchor
= AnchorTop
| AnchorBottom
| AnchorLeft
| AnchorRight
| AnchorCenter
| AnchorTopLeft
| AnchorTopRight
| AnchorBottomLeft
| AnchorBottomRight
deriving (Eq, Show)
instance ToHttpApiData StdAnchor where
toUrlPiece stdAnchor = case stdAnchor of
AnchorTop -> "top"
AnchorBottom -> "bottom"
AnchorLeft -> "left"
AnchorRight -> "right"
AnchorCenter -> "center"
AnchorTopLeft -> "topleft"
AnchorTopRight -> "topright"
AnchorBottomLeft -> "bottomleft"
AnchorBottomRight -> "bottomright"
-- | Path
data Path = Path (Maybe PathStyle) [Location]
deriving (Eq, Show)
instance ToHttpApiData Path where
toUrlPiece (Path pathStyleOpt ls)
| Nothing <- pathStyleOpt
= toUrlPiece ls
| Just (PathStyle Nothing Nothing Nothing Nothing) <- pathStyleOpt
= toUrlPiece ls
| Just pathStyle <- pathStyleOpt
= case ls of
[] -> toUrlPiece pathStyle
_ -> T.concat [toUrlPiece pathStyle, "|", toUrlPiece ls]
-- | Path style: a geodesic path follows the curvature of the Earth.
data PathStyle = PathStyle
{ pathWeight :: Maybe PathWeight -- ^ The default value is 5.
, pathColor :: Maybe PathColor
, pathFillColor :: Maybe PathColor
, pathGeodesic :: Maybe PathGeodesic -- ^ The default value is false.
} deriving (Eq, Show)
instance ToHttpApiData PathStyle where
toUrlPiece (PathStyle mw mc mfc mg) =
T.concat $ intersperse "|" opts
where
weightUrl = T.append "weight:" . toUrlPiece <$> mw
colorUrl = T.append "color:" . toUrlPiece <$> mc
fillColorUrl = T.append "fillcolor:" . toUrlPiece <$> mfc
geodesicUrl = T.append "geodesic:" . toUrlPiece <$> mg
opts = catMaybes [weightUrl, colorUrl, fillColorUrl,
geodesicUrl]
-- | Path weight: in pixels.
newtype PathWeight = PathWeight Int
deriving (Eq, Show, ToHttpApiData)
-- | Path colour
data PathColor
= PathColor Word8 Word8 Word8
| PathColorAlpha Word8 Word8 Word8 Word8
| StdPathColor StdColor
deriving (Eq, Show)
instance ToHttpApiData PathColor where
toUrlPiece (PathColor r g b) = T.pack $ "0x" ++ hexString r ++ hexString g
++ hexString b
toUrlPiece (PathColorAlpha r g b a) = T.pack $ "0x" ++ hexString r ++
hexString g ++ hexString b ++ hexString a
toUrlPiece (StdPathColor stdColor) = toUrlPiece stdColor
-- | Path is geodesic
newtype PathGeodesic = PathGeodesic Bool
deriving (Eq, Show, ToHttpApiData)
-- | Visible locations
newtype Visible = Visible [Location]
deriving (Eq, Show, ToHttpApiData)
-- | Google Maps Static API
type GoogleMapsStaticAPI
= "staticmap"
:> QueryParam "key" Key
:> QueryParam "center" Center
:> QueryParam "zoom" Zoom
:> QueryParam "size" Size
:> QueryParam "scale" Scale
:> QueryParam "format" Format
:> QueryParams "style" MapStyle
:> QueryParam "maptype" MapType
:> QueryParam "language" Language
:> QueryParam "region" Region
:> QueryParams "markers" Markers
:> QueryParams "path" Path
:> QueryParam "visible" Visible
:> QueryParam "signature" Signature
:> Get '[PNG] StaticmapResponse
-- | StaticmapResponse
type StaticmapResponse = DynamicImage
-- | API type
api :: Proxy GoogleMapsStaticAPI
api = Proxy
staticmap'
:: Maybe Key
-> Maybe Center
-> Maybe Zoom
-> Maybe Size
-> Maybe Scale
-> Maybe Format
-> [MapStyle]
-> Maybe MapType
-> Maybe Language
-> Maybe Region
-> [Markers]
-> [Path]
-> Maybe Visible
-> Maybe Signature
-> ClientM StaticmapResponse
staticmap' = client api
-- | Retrieve a static map. NB: The use of the Google Maps Static API services
-- is subject to the
-- <https://cloud.google.com/maps-platform/terms/ Google Maps Platform Terms of Service>.
-- End Users' use of Google Maps is subject to the then-current Google
-- Maps/Google Earth Additional Terms of Service at
-- <https://maps.google.com/help/terms_maps.html> and Google Privacy Policy at
-- <https://www.google.com/policies/privacy/>.
staticmap
:: Manager
-> Key
-> Maybe Secret
-> Maybe Center
-> Maybe Zoom
-> Size
-> Maybe Scale
-> Maybe Format
-> [MapStyle]
-> Maybe MapType
-> Maybe Language
-> Maybe Region
-> [Markers]
-> [Path]
-> Maybe Visible
-> IO (Either ClientError StaticmapResponse)
staticmap
mgr
key
secretOpt
centerOpt
zoomOpt
size
scaleOpt
formatOpt
mapStyles
mapTypeOpt
languageOpt
regionOpt
markerss
paths
visibleOpt
= case secretOpt of
Nothing -> runClientM (eval staticmap' Nothing)
-- CookieJar supported from servant-client-0.13
#if MIN_VERSION_servant_client(0,13,0)
(ClientEnv mgr googleMapsApis Nothing)
#else
(ClientEnv mgr googleMapsApis)
#endif
Just secret -> do
let url = linkURI $ eval (safeLink api api) Nothing
signatureOpt = sign secret googleMapsApis url
runClientM (eval staticmap' signatureOpt)
-- CookieJar supported from servant-client-0.13
#if MIN_VERSION_servant_client(0,13,0)
(ClientEnv mgr googleMapsApis Nothing)
#else
(ClientEnv mgr googleMapsApis)
#endif
where
linkURI = linkURI' LinkArrayElementPlain
eval f = f (Just key) centerOpt zoomOpt (Just size) scaleOpt formatOpt
mapStyles mapTypeOpt languageOpt regionOpt markerss paths
visibleOpt
sign :: Secret -> BaseUrl -> URI -> Maybe Signature
sign (Secret secret) baseUrl url = do
secret' <- either (const Nothing) Just (decode $ encodeUtf8 secret)
let url' = UTF8.fromString $ baseUrlPath baseUrl ++ "/" ++ uriToString id url ""
signature = hmac secret' url' :: HMAC SHA1
signature' = decodeUtf8 $ encode $ convert signature
return $ Signature signature'