graphql-engine/server/src-lib/Hasura/SQL/GeoJSON.hs
Vladimir Ciobanu c08e6d108b server: allow GeoJSON to be passed for Geometry/Geography operators
GitOrigin-RevId: 51ca927b55d3d717da07447f67fd4d3c068a8357
2021-03-26 17:00:18 +00:00

194 lines
5.8 KiB
Haskell

module Hasura.SQL.GeoJSON
( Position(..)
, Point(..)
, MultiPoint(..)
, LineString(..)
, LinearRing(..)
, MultiLineString(..)
, Polygon(..)
, MultiPolygon(..)
, Geometry(..)
, GeometryCollection(..)
, GeometryWithCRS(..)
) where
import qualified Data.Aeson as J
import qualified Data.Aeson.Casing as J
import qualified Data.Aeson.TH as J
import qualified Data.Aeson.Types as J
import qualified Data.Vector as V
import Control.Monad
import Hasura.Prelude
data Position
= Position !Double !Double !(Maybe Double)
deriving (Show, Eq)
withParsedArray
:: (J.FromJSON a)
=> String -> (V.Vector a -> J.Parser b) -> J.Value -> J.Parser b
withParsedArray s fn =
J.withArray s (mapM J.parseJSON >=> fn)
instance J.FromJSON Position where
parseJSON = withParsedArray "Position" $ \arr ->
if V.length arr < 2
then fail "A Position needs at least 2 elements"
-- here we are ignoring anything past 3 elements
else return $ Position
(arr `V.unsafeIndex` 0)
(arr `V.unsafeIndex` 1)
(arr V.!? 2)
instance J.ToJSON Position where
toJSON (Position a b c)
= J.toJSON $ a:b:maybeToList c
newtype Point
= Point { unPoint :: Position }
deriving (Show, Eq, J.ToJSON, J.FromJSON)
newtype MultiPoint
= MultiPoint { unMultiPoint :: [Position] }
deriving (Show, Eq, J.ToJSON, J.FromJSON)
data LineString
= LineString
{ _lsFirst :: !Position
, _lsSecond :: !Position
, _lsRest :: ![Position]
} deriving (Show, Eq)
instance J.ToJSON LineString where
toJSON (LineString a b rest)
= J.toJSON $ a:b:rest
instance J.FromJSON LineString where
parseJSON = withParsedArray "LineString" $ \arr ->
if V.length arr < 2
then fail "A LineString needs at least 2 Positions"
-- here we are ignoring anything past 3 elements
else
let fstPos = arr `V.unsafeIndex` 0
sndPos = arr `V.unsafeIndex` 1
rest = V.toList $ V.drop 2 arr
in return $ LineString fstPos sndPos rest
newtype MultiLineString
= MultiLineString { unMultiLineString :: [LineString] }
deriving (Show, Eq, J.ToJSON, J.FromJSON)
newtype GeometryCollection
= GeometryCollection { unGeometryCollection :: [GeometryWithCRS] }
deriving (Show, Eq, J.ToJSON, J.FromJSON)
data LinearRing
= LinearRing
{ _pFirst :: !Position
, _pSecond :: !Position
, _pThird :: !Position
, _pRest :: ![Position]
} deriving (Show, Eq)
instance J.FromJSON LinearRing where
parseJSON = withParsedArray "LinearRing" $ \arr ->
if V.length arr < 4
then fail "A LinearRing needs at least 4 Positions"
-- here we are ignoring anything past 3 elements
else do
let fstPos = arr `V.unsafeIndex` 0
sndPos = arr `V.unsafeIndex` 1
thrPos = arr `V.unsafeIndex` 2
rest = V.drop 3 arr
let lastPos = V.last rest
unless (fstPos == lastPos) $
fail "the first and last locations have to be equal for a LinearRing"
return $ LinearRing fstPos sndPos thrPos $ V.toList $ V.init rest
instance J.ToJSON LinearRing where
toJSON (LinearRing a b c rest)
= J.toJSON $ (V.fromList [a, b, c] <> V.fromList rest) `V.snoc` a
newtype Polygon
= Polygon { unPolygon :: [LinearRing] }
deriving (Show, Eq, J.ToJSON, J.FromJSON)
newtype MultiPolygon
= MultiPolygon { unMultiPolygon :: [Polygon] }
deriving (Show, Eq, J.ToJSON, J.FromJSON)
data Geometry
= GPoint !Point
| GMultiPoint !MultiPoint
| GLineString !LineString
| GMultiLineString !MultiLineString
| GPolygon !Polygon
| GMultiPolygon !MultiPolygon
| GGeometryCollection !GeometryCollection
deriving (Show, Eq)
data GeometryWithCRS
= GeometryWithCRS
{ _gwcGeom :: !Geometry
, _gwcCrs :: !(Maybe CRS)
} deriving (Show, Eq)
encToCoords :: (J.ToJSON a) => Text -> a -> Maybe CRS -> J.Value
encToCoords ty a Nothing =
J.object [ "type" J..= ty, "coordinates" J..= a]
encToCoords ty a (Just crs) =
J.object [ "type" J..= ty, "coordinates" J..= a, "crs" J..= crs]
instance J.ToJSON GeometryWithCRS where
toJSON (GeometryWithCRS geom crsM) = case geom of
GPoint o -> encToCoords "Point" o crsM
GMultiPoint o -> encToCoords "MultiPoint" o crsM
GLineString o -> encToCoords "LineString" o crsM
GMultiLineString o -> encToCoords "MultiLineString" o crsM
GPolygon o -> encToCoords "Polygon" o crsM
GMultiPolygon o -> encToCoords "MultiPolygon" o crsM
GGeometryCollection o ->
J.object [ "type" J..= ("GeometryCollection"::Text)
, "geometries" J..= o
]
instance J.FromJSON GeometryWithCRS where
parseJSON = J.withObject "Geometry" $ \o -> do
ty <- o J..: "type"
geom <- case ty of
"Point" -> GPoint <$> o J..: "coordinates"
"MultiPoint" -> GMultiPoint <$> o J..: "coordinates"
"LineString" -> GLineString <$> o J..: "coordinates"
"MultiLineString" -> GMultiLineString <$> o J..: "coordinates"
"Polygon" -> GPolygon <$> o J..: "coordinates"
"MultiPolygon" -> GMultiPolygon <$> o J..: "coordinates"
"GeometryCollection" -> GGeometryCollection <$> o J..: "geometries"
_ -> fail $ "unexpected geometry type: " <> ty
crsM <- o J..:? "crs"
return $ GeometryWithCRS geom crsM
data CRSNameProps
= CRSNameProps
{ _cnpName :: !Text
} deriving (Show, Eq)
data CRSLinkProps
= CRSLinkProps
{ _clpHref :: !Text
, _clpType :: !(Maybe Text)
} deriving (Show, Eq)
data CRS
= CRSName !CRSNameProps
| CRSLink !CRSLinkProps
deriving (Show, Eq)
$(J.deriveJSON (J.aesonPrefix J.camelCase) ''CRSNameProps)
$(J.deriveJSON (J.aesonPrefix J.camelCase) ''CRSLinkProps)
$(J.deriveJSON
J.defaultOptions { J.constructorTagModifier = J.camelCase . drop 3
, J.sumEncoding = J.TaggedObject "type" "properties"
}
''CRS)