wireform-parquet-0.1.0.0: src/Parquet/Arrow/Geospatial.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Translate geospatial semantics without changing the WKB storage bytes.
module Parquet.Arrow.Geospatial (toArrowMetadata, fromArrowField) where
import Arrow.Types qualified as A
import Data.Aeson ((.=))
import Data.Aeson qualified as J
import Data.Aeson.Key (Key)
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy qualified as BL
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Vector qualified as V
import Parquet.Types qualified as P
data CRSMetadata = CRSMetadata
{ crsValue :: !J.Value
, crsHint :: !(Maybe Text)
}
encodeJSON :: J.Value -> Text
encodeJSON = TE.decodeUtf8 . BL.toStrict . J.encode
objectJSON :: Text -> Either String J.Object
objectJSON value = case J.eitherDecodeStrict' (TE.encodeUtf8 value) of
Right (J.Object object) -> Right object
_ -> Left "Parquet.Arrow: CRS definition must be a JSON object"
resolveCRS :: P.FileMetadata -> Maybe Text -> Either String CRSMetadata
resolveCRS _ Nothing = Right (CRSMetadata (J.String "OGC:CRS84") (Just "authority_code"))
resolveCRS footer (Just raw)
| Just name <- T.stripPrefix "projjson:" raw = do
if T.null name then Left "Parquet.Arrow: empty PROJJSON metadata reference" else Right ()
let entries = V.filter ((== TE.encodeUtf8 name) . P.kvKey) (maybe V.empty id (P.fmKeyValueMetadata footer))
value <- case V.toList entries of
[P.KeyValue _ (Just bytes)] -> either (const (Left "Parquet.Arrow: PROJJSON metadata is not UTF-8")) Right (TE.decodeUtf8' bytes)
_ -> Left "Parquet.Arrow: missing, absent-valued or ambiguous PROJJSON metadata reference"
object <- objectJSON value
Right (CRSMetadata (J.Object object) (Just "projjson"))
| Just identifier <- T.stripPrefix "srid:" raw =
if T.null identifier then Left "Parquet.Arrow: empty SRID" else Right (CRSMetadata (J.String identifier) (Just "srid"))
| T.null raw = Left "Parquet.Arrow: empty CRS does not identify a coordinate reference system"
| otherwise = case J.eitherDecodeStrict' (TE.encodeUtf8 raw) of
Right (J.Object object) -> Right (CRSMetadata (J.Object object) (Just "projjson"))
_ -> Right (CRSMetadata (J.String raw) Nothing)
edgeName :: P.EdgeInterpolationAlgorithm -> Maybe Text
edgeName P.Spherical = Just "spherical"
edgeName P.Vincenty = Just "vincenty"
edgeName P.Thomas = Just "thomas"
edgeName P.Andoyer = Just "andoyer"
edgeName P.Karney = Just "karney"
edgeName P.UnrecognizedInterpolation {} = Nothing
parseEdge :: Text -> Either String P.EdgeInterpolationAlgorithm
parseEdge "spherical" = Right P.Spherical
parseEdge "vincenty" = Right P.Vincenty
parseEdge "thomas" = Right P.Thomas
parseEdge "andoyer" = Right P.Andoyer
parseEdge "karney" = Right P.Karney
parseEdge _ = Left "Parquet.Arrow: unsupported GeoArrow edge interpolation"
toArrowMetadata :: P.FileMetadata -> P.SchemaElement -> Either String (V.Vector (Text, Text))
toArrowMetadata footer element
| P.seType element /= Just P.PTByteArray = Right V.empty
| otherwise = case P.seLogicalType element of
Just (P.LTGeometry parameters) -> build (P.geometryCrs parameters) Nothing
Just (P.LTGeography parameters) -> case edgeName (maybe P.Spherical id (P.geographyAlgorithm parameters)) of
-- A future algorithm cannot be labelled as planar or spherical WKB.
Nothing -> Right V.empty
Just edges -> build (P.geographyCrs parameters) (Just edges)
_ -> Right V.empty
where
build :: Maybe Text -> Maybe Text -> Either String (V.Vector (Text, Text))
build raw edges = do
crs <- resolveCRS footer raw
let fields =
["crs" .= crsValue crs]
++ maybe [] (\hint -> ["crs_type" .= hint]) (crsHint crs)
++ maybe [] (\value -> ["edges" .= value]) edges
Right (V.fromList [("ARROW:extension:name", "geoarrow.wkb"), ("ARROW:extension:metadata", encodeJSON (J.Object (KM.fromList fields)))])
reservedField :: Text -> A.Field -> Either String (Maybe Text)
reservedField name field = case V.toList (V.filter ((== name) . fst) (A.fieldMetadata field)) of
[] -> Right Nothing
[(_, value)] -> Right (Just value)
_ -> Left "Parquet.Arrow: duplicate Arrow extension metadata key"
optionalText :: Key -> J.Object -> Either String (Maybe Text)
optionalText name object = case KM.lookup name object of
Nothing -> Right Nothing
Just (J.String value) -> Right (Just value)
_ -> Left "Parquet.Arrow: GeoArrow metadata field must be a string"
fromArrowField :: A.Field -> Either String (Maybe P.LogicalType)
fromArrowField field
| not (V.any (\(key, value) -> key == "ARROW:extension:name" && "geoarrow." `T.isPrefixOf` value) (A.fieldMetadata field)) = Right Nothing
| otherwise = do
extension <- reservedField "ARROW:extension:name" field
case extension of
Just "geoarrow.wkb" -> convert
Just name | "geoarrow." `T.isPrefixOf` name -> Left "Parquet.Arrow: only WKB GeoArrow storage can be written as Parquet geospatial data"
_ -> Right Nothing
where
convert = do
if A.fieldType field `elem` [A.ABinary, A.ALargeBinary]
then Right ()
else Left "Parquet.Arrow: GeoArrow WKB requires binary storage"
encoded <- reservedField "ARROW:extension:metadata" field
object <- case encoded of
Nothing -> Right KM.empty
Just value -> case J.eitherDecodeStrict' (TE.encodeUtf8 value) of
Right (J.Object fields) -> Right fields
_ -> Left "Parquet.Arrow: GeoArrow metadata must be a JSON object"
hint <- optionalText "crs_type" object
crs <- case KM.lookup "crs" object of
Just (J.Object fields) | hint `elem` [Nothing, Just "projjson"] -> Right (encodeJSON (J.Object fields))
Just (J.String value) | not (T.null value) -> case hint of
Just "srid" -> Right ("srid:" <> value)
Nothing -> literal value
Just "authority_code" -> literal value
Just "wkt2:2019" -> literal value
_ -> Left "Parquet.Arrow: unsupported GeoArrow CRS representation"
_ -> Left "Parquet.Arrow: unknown GeoArrow CRS cannot be replaced by Parquet's OGC:CRS84 default"
edges <- optionalText "edges" object
case edges of
Nothing -> Right (Just (P.LTGeometry (P.GeometryParameters (Just crs))))
Just value -> do
algorithm <- parseEdge value
Right (Just (P.LTGeography (P.GeographyParameters (Just crs) (Just algorithm))))
literal value
| "projjson:" `T.isPrefixOf` value = Left "Parquet.Arrow: GeoArrow CRS cannot reference missing file metadata"
| otherwise = Right value