packages feed

google-static-maps-0.7.0.3: 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, 2024
-- 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 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
                  )
#if MIN_VERSION_servant_client(0,17,0)
import           Servant.Client ( defaultMakeClientRequest )
#endif
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)
-- Middleware supported from servant-client-0.20.2
#if MIN_VERSION_servant_client(0,20,2)
                            (ClientEnv mgr
                                       googleMapsApis
                                       Nothing
                                       defaultMakeClientRequest
                                       id)
-- makeClientRequest supported from servant-client-0.17
#elif MIN_VERSION_servant_client(0,17,0)
                            (ClientEnv mgr
                                       googleMapsApis
                                       Nothing
                                       defaultMakeClientRequest)
-- CookieJar supported from servant-client-0.13
#elif 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)
-- Middleware supported from servant-client-0.20.2
#if MIN_VERSION_servant_client(0,20,2)
                   (ClientEnv mgr
                              googleMapsApis
                              Nothing
                              defaultMakeClientRequest
                              id)
-- makeClientRequest supported from servant-client-0.17
#elif MIN_VERSION_servant_client(0,17,0)
                   (ClientEnv mgr
                              googleMapsApis
                              Nothing
                              defaultMakeClientRequest)
-- CookieJar supported from servant-client-0.13
#elif 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'