packages feed

esqueleto-postgis-4.1.0: src/Database/Esqueleto/Postgis.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Haskell bindings for postgres postgis
--   for a good explenation see <https://postgis.net/>
--
--   Make sure to use the correct 'SpatialType'.
--   Earth spanning applications should use Geography,
--   local applications should use 'Geometry' because it's more convenient.
--   You don't have to use Geography if you're only interested in
--   topological functions such as 'st_intersects' and 'st_union',
--   these are indifferent to space distortions,
--   see <https://jappie.me/announcement-esqueleto-postgis-v4.html>
--
--   if you can't use a function for example when you're using 'Geography'.
--   there is the option to 'st_transform_geography'.
module Database.Esqueleto.Postgis
  (
    Postgis(..),
    SpatialType(..),
    getPoints,

    -- * Spatial relationships
    module Database.Esqueleto.Postgis.Spatial,

    -- * Measurement functions
    module Database.Esqueleto.Postgis.Measurement,

    -- * Geometry accessors
    module Database.Esqueleto.Postgis.Accessor,

    -- * Geometry constructors
    st_collect,
    st_makeenvelope,
    st_makeline,
    st_makepolygon_line,

    -- * Geometry editors
    module Database.Esqueleto.Postgis.Editor,

    -- * Geometry validation
    st_makevalid,
    st_isvalidreason,

    -- * SRS functions
    st_setsrid,

    -- * Geometry output
    st_astext,
    st_asgeojson,
    st_asewkt,
    st_geohash,

    -- * Geometry processing
    module Database.Esqueleto.Postgis.Processing,

    -- * Affine transformations
    st_translate,
    st_scale,
    st_rotate,
    st_rotatex,
    st_rotatez,

    -- * Bounding box
    st_expand,

    -- * Linear referencing
    st_lineinterpolatepoint,
    st_linelocatepoint,
    st_linesubstring,

    -- * Points
    point,
    point_v,
    st_point,
    st_point_xyz,
    st_point_xyzm,

    -- * Transform
    st_transform_geography,
    st_transform_geometry,
    -- ** SRID
    SRID,
    wgs84,
    mercator,
    britishNationalGrid,
    SridUnit(..),

    -- * Other
    makePolygon,
    PostgisGeometry,
    HasPgType,

    -- * Re-exports
    PointXY(..),
    PointXYZ(..),
    PointXYZM(..),
  )
where

import Database.Esqueleto.Postgis.Geometry (Postgis(..), SpatialType(..), HasPgType(..), PostgisGeometry)
import Database.Esqueleto.Postgis.Spatial
import Database.Esqueleto.Postgis.Measurement
import Database.Esqueleto.Postgis.Accessor
import Database.Esqueleto.Postgis.Editor
import Database.Esqueleto.Postgis.Processing

import Database.Esqueleto.Experimental(val)
import Data.Proxy
import Data.Bifunctor (first)
import Database.Esqueleto.Postgis.Ewkb (parseHexByteString)
import Data.Foldable (Foldable (toList), fold)
import Data.Geospatial (GeoPoint (..), GeoPositionWithoutCRS (..), GeospatialGeometry, PointXY (..), PointXYZ (..), PointXYZM (..))
import Data.Geospatial qualified as Geospatial
import Data.LineString (LineString, fromLineString, lineStringHead)
import Data.LinearRing (LinearRing, fromLinearRing, makeLinearRing, ringHead, toSeq)
import Data.List qualified as List
import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty)
import Data.List.NonEmpty qualified as Non
import Data.Semigroup qualified as S
import Data.Sequence (Seq (..), (|>))
import Data.Sequence qualified as Seq
import Data.String (IsString (..))
import Data.Text (Text, pack)
import Data.Text.Encoding(encodeUtf8)
import Data.Text.Lazy (toStrict)
import Data.Text.Lazy.Builder (toLazyText)
import Data.Text.Lazy.Builder qualified as Text
import Database.Esqueleto.Experimental (SqlExpr, Value)
import Database.Esqueleto.Internal.Internal (unsafeSqlFunction, unsafeSqlCastAs )
import Database.Persist.Sql
import Data.Base16.Types(assertBase16)
import Data.ByteString(fromStrict)

-- | unwrap postgis geometry so you can for example return it from an API
getPoints :: PostgisGeometry point -> NonEmpty point
getPoints geom = case geom of
  Point p -> p :| []
  MultiPoint pts -> pts
  Line ls -> linestringNonEmpty ls
  Multiline lss -> S.sconcat (fmap linestringNonEmpty lss)
  Polygon ring -> linearRingNonEmpty ring
  MultiPolygon rings -> S.sconcat (fmap linearRingNonEmpty rings)
  Collection geoms -> S.sconcat (fmap getPoints geoms)

linestringNonEmpty :: LineString a -> NonEmpty a
linestringNonEmpty ls = lineStringHead ls :| drop 1 (fromLineString ls)

linearRingNonEmpty :: LinearRing a -> NonEmpty a
linearRingNonEmpty ls = ringHead ls :| drop 1 (fromLinearRing ls)

tshow :: (Show a) => a -> Text
tshow = pack . show

data GeomErrors
  = MismatchingDimensionsXYZ PointXYZ
  | MismatchingDimensionsXYZM PointXYZM
  | MismatchingDimensionsXY PointXY
  | NoGeometry
  | EmptyPoint
  | NotImplemented
  | EmptyMultiline
  | EmptyMultiPoint
  | NotEnoughElements
  | EmptyMultipolygon
  | EmptyCollection
  deriving (Show)

-- | checks if the first point is the last, and if not so makes it so.
--   this is required for inserting into the database
makePolygon :: (Eq point, Show point) => point -> point -> point -> Seq point -> LinearRing point
makePolygon one two three other =
  if Just one == last'
    then makeLinearRing one two three other
    else makeLinearRing one two three (other |> one)
  where
    last' = Seq.lookup (length other) other

from2dGeoPositionWithoutCRSToPoint :: GeoPositionWithoutCRS -> Either GeomErrors PointXY
from2dGeoPositionWithoutCRSToPoint = \case
  GeoEmpty -> Left EmptyPoint
  GeoPointXY x -> Right x
  GeoPointXYZ x -> Left (MismatchingDimensionsXYZ x)
  GeoPointXYZM x -> Left (MismatchingDimensionsXYZM x)

from3dGeoPositionWithoutCRSToPoint :: GeoPositionWithoutCRS -> Either GeomErrors PointXYZ
from3dGeoPositionWithoutCRSToPoint = \case
  GeoEmpty -> Left EmptyPoint
  GeoPointXY x -> Left (MismatchingDimensionsXY x)
  GeoPointXYZ x -> Right x
  GeoPointXYZM x -> Left (MismatchingDimensionsXYZM x)

from4dGeoPositionWithoutCRSToPoint :: GeoPositionWithoutCRS -> Either GeomErrors PointXYZM
from4dGeoPositionWithoutCRSToPoint = \case
  GeoEmpty -> Left EmptyPoint
  GeoPointXY x -> Left (MismatchingDimensionsXY x)
  GeoPointXYZ x -> Left (MismatchingDimensionsXYZ x)
  GeoPointXYZM x -> Right x

renderPair :: PointXY -> Text.Builder
renderPair (PointXY {..}) = fromString (show _xyX) <> " " <> fromString (show _xyY)

renderXYZ :: PointXYZ -> Text.Builder
renderXYZ (PointXYZ {..}) = fromString (show _xyzX) <> " " <> fromString (show _xyzY) <> " " <> fromString (show _xyzZ)

renderXYZM :: PointXYZM -> Text.Builder
renderXYZM (PointXYZM {..}) = fromString (show _xyzmX) <> " " <> fromString (show _xyzmY) <> " " <> fromString (show _xyzmZ) <> " " <> fromString (show _xyzmM)

renderGeometry :: forall (spatialType :: SpatialType) . HasPgType spatialType => Postgis spatialType Text.Builder -> Text.Builder
renderGeometry geom =
  let result = renderGeometryUntyped geom
  -- wrap it in quotes and cast it to whatever type we decided it should be
  in "'" <> result <> "' :: " <> (Text.fromText $ pgType $ (Proxy @spatialType) )

-- can't add quotes and types in the recursion because it's already part of the string
renderGeometryUntyped :: Postgis spatialType Text.Builder -> Text.Builder
renderGeometryUntyped = \case
  Point point' -> "POINT(" <> point' <> ")"
  MultiPoint points -> "MULTIPOINT (" <> fold (Non.intersperse "," ((\x -> "(" <> x <> ")") <$> points)) <> ")"
  Line line -> "LINESTRING(" <> renderLines line <> ")"
  Multiline multiline -> "MULTILINESTRING(" <> fold (Non.intersperse "," ((\line -> "(" <> renderLines line <> ")") <$> multiline)) <> ")"
  Polygon polygon -> "POLYGON((" <> renderLines polygon <> "))"
  MultiPolygon multipolygon -> "MULTIPOLYGON(" <> fold (Non.intersperse "," ((\line -> "((" <> renderLines line <> "))") <$> multipolygon)) <> ")"
  Collection collection -> "GEOMETRYCOLLECTION(" <> fold (Non.intersperse "," (renderGeometryUntyped <$> collection)) <> ")"



renderLines :: (Foldable f) => f Text.Builder -> Text.Builder
renderLines line = fold (List.intersperse "," $ toList line)

from2dGeospatialGeometry :: (Eq a, Show a) => (GeoPositionWithoutCRS -> Either GeomErrors a) -> GeospatialGeometry -> Either GeomErrors (Postgis spatialType a)
from2dGeospatialGeometry interpreter = \case
  Geospatial.NoGeometry -> Left NoGeometry
  Geospatial.Point (GeoPoint point') -> (Point <$> interpreter point')
  Geospatial.MultiPoint (Geospatial.GeoMultiPoint points) -> do
    list' <- sequence $ toList (interpreter <$> points)
    case nonEmpty list' of
      Nothing -> Left EmptyMultiPoint
      Just x -> Right $ MultiPoint x
  Geospatial.Line (Geospatial.GeoLine linestring) -> Line <$> traverse interpreter linestring
  Geospatial.MultiLine (Geospatial.GeoMultiLine multiline) -> do
    seqRes <- traverse (traverse interpreter) multiline
    case Non.nonEmpty (toList seqRes) of
      Just nonEmpty' -> Right $ Multiline nonEmpty'
      Nothing -> Left EmptyMultiline
  Geospatial.Polygon (Geospatial.GeoPolygon polygon) -> Polygon <$> (toLinearRing interpreter) polygon
  Geospatial.MultiPolygon (Geospatial.GeoMultiPolygon multipolygon) -> do
    seqRings <- traverse (toLinearRing interpreter) multipolygon
    case Non.nonEmpty (toList seqRings) of
      Just nonEmpty' -> Right $ MultiPolygon nonEmpty'
      Nothing -> Left EmptyMultipolygon
  Geospatial.Collection seq' -> do
    seqs <- traverse (from2dGeospatialGeometry interpreter) seq'
    case Non.nonEmpty (toList seqs) of
      Just nonEmpty' -> Right $ Collection nonEmpty'
      Nothing -> Left EmptyCollection

toLinearRing :: (Eq a, Show a) => (GeoPositionWithoutCRS -> Either GeomErrors a) -> Seq (LinearRing GeoPositionWithoutCRS) -> Either GeomErrors (LinearRing a)
toLinearRing interpreter polygon = do
  aSeq <- traverse interpreter (foldMap toSeq polygon)
  case aSeq of
    (one :<| two :<| three :<| rem') -> Right $ makeLinearRing one two three rem'
    _other -> Left NotEnoughElements

instance HasPgType spatialType => PersistField (Postgis spatialType PointXY) where
  toPersistValue geom =
    PersistLiteral_ Unescaped $ encodeUtf8 $ toStrict $ toLazyText $ renderGeometry  $ renderPair <$> geom
  fromPersistValue (PersistLiteral_ Escaped bs) = do
    result <- first pack $ parseHexByteString $ assertBase16 $ fromStrict bs
    first tshow $ (from2dGeospatialGeometry from2dGeoPositionWithoutCRSToPoint) result
  fromPersistValue other = Left ("PersistField.Polygon: invalid persist value:" <> tshow other)

instance HasPgType spatialType => PersistField (Postgis spatialType PointXYZ) where
  toPersistValue geom =
    PersistLiteral_ Unescaped $ encodeUtf8 $ toStrict $ toLazyText $ renderGeometry $ renderXYZ <$> geom
  fromPersistValue (PersistLiteral_ Escaped bs) = do
    result <- first pack $ parseHexByteString $ assertBase16 $ fromStrict bs
    first tshow $ (from2dGeospatialGeometry from3dGeoPositionWithoutCRSToPoint) result
  fromPersistValue other = Left ("PersistField.Polygon: invalid persist value:" <> tshow other)

instance HasPgType spatialType => PersistField (Postgis spatialType PointXYZM) where
  toPersistValue geom =
    PersistLiteral_ Unescaped $ encodeUtf8 $ toStrict $ toLazyText $ renderGeometry $ renderXYZM <$> geom
  fromPersistValue (PersistLiteral_ Escaped bs) = do
    result <- first pack $ parseHexByteString $ assertBase16 $ fromStrict bs
    first tshow $ (from2dGeospatialGeometry from4dGeoPositionWithoutCRSToPoint) result
  fromPersistValue other = Left ("PersistField.Polygon: invalid persist value:" <> tshow other)

instance forall spatialType . HasPgType spatialType => PersistFieldSql (Postgis spatialType PointXY) where
  sqlType _ = SqlOther $ pgType $ Proxy @spatialType

instance HasPgType spatialType => PersistFieldSql (Postgis spatialType PointXYZ) where
  sqlType _ = SqlOther $ pgType $ Proxy @spatialType

instance HasPgType spatialType => PersistFieldSql (Postgis spatialType PointXYZM) where
  sqlType _ = SqlOther $ pgType $ Proxy @spatialType

-- | SRID
-- you can find your local like this: https://blog.rustprooflabs.com/2020/11/postgis-find-local-srid
-- geography appears to use 'wgs84'. So I hardcoded the use going from geom to geography as that.
--
-- you can use the num instance to put in whatever.
-- however, if you miss a srid please submit a PR.
newtype SRID (unit :: SridUnit) = SRID Int
  deriving newtype (Num, PersistField)

-- | default for geography type in postgis.
--   Geodetic CRS: WGS 84
--   https://epsg.io/4326
wgs84 :: SRID 'Degree
wgs84 = 4326

-- | most maps are in this, it has some large distortions further away from equator
--   https://epsg.io/3857
mercator :: SRID 'Linear
mercator = 3857

-- | if you're in england this is pretty good.
--   https://epsg.io/27700
britishNationalGrid :: SRID 'Linear
britishNationalGrid = 27700


-- | Diferent 'SRID' come in different units,
--   important for converting from geograhy to geometry.
data SridUnit = Linear -- ^ meters or feet
          | Degree -- ^ spheroids

-- | Project a geography onto a geometry.
--   allows using of functions such as 'st_union' which only work in flat space (geometry).
--   https://postgis.net/docs/ST_Transform.html
st_transform_geography :: forall a. SRID 'Linear ->
                        SqlExpr (Value (Postgis 'Geography a)) -> -- ^ g1 (library handles the conversion)
                        SqlExpr (Value (Postgis 'Geometry a))
st_transform_geography srid geography =
  unsafeSqlFunction "ST_Transform" (casted, val srid)
  where
    casted :: SqlExpr (Value (Postgis 'Geometry a))
    casted = unsafe_cast_pg_type geography

-- | project a geometry as a geography, assumes 'wgs84'.
--   https://postgis.net/docs/ST_Transform.html
st_transform_geometry :: SqlExpr (Value (Postgis 'Geometry a)) -> -- ^ g1 (library handles the conversion)
                        SqlExpr (Value (Postgis 'Geography a))
st_transform_geometry input = unsafe_cast_pg_type transformed
  where
    transformed :: SqlExpr (Value (Postgis 'Geometry a))
    transformed =
      unsafeSqlFunction "ST_Transform" (input, val wgs84)

-- postgis doesn't appear to care about casting between spaces,
-- the user probably wants to use st_transform instead.
unsafe_cast_pg_type :: forall two one a . HasPgType two => SqlExpr (Value (Postgis one a)) -> SqlExpr (Value (Postgis two a))
unsafe_cast_pg_type = unsafeSqlCastAs castAs
  where
    castAs = pgType $ Proxy @two

-- ---------------------------------------------------------------------------
-- Geometry Constructors
-- ---------------------------------------------------------------------------

-- | Aggregate function that collects geometries into a geometry collection.
--   https://postgis.net/docs/ST_Collect.html
st_collect ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value (Postgis 'Geometry a))
st_collect a = unsafeSqlFunction "ST_Collect" a

-- | Creates a rectangular polygon from minimum and maximum coordinates.
--   https://postgis.net/docs/ST_MakeEnvelope.html
st_makeenvelope ::
  SqlExpr (Value Double) -> -- ^ xmin
  SqlExpr (Value Double) -> -- ^ ymin
  SqlExpr (Value Double) -> -- ^ xmax
  SqlExpr (Value Double) -> -- ^ ymax
  SqlExpr (Value Int) ->    -- ^ srid
  SqlExpr (Value (Postgis 'Geometry a))
st_makeenvelope xmin' ymin' xmax' ymax' srid' = unsafeSqlFunction "ST_MakeEnvelope" (xmin', ymin', xmax', ymax', srid')

-- | Creates a linestring from two point geometries.
--   https://postgis.net/docs/ST_MakeLine.html
st_makeline ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value (Postgis 'Geometry a))
st_makeline a b = unsafeSqlFunction "ST_MakeLine" (a, b)

-- | Creates a polygon from a closed linestring shell.
--   Named st_makepolygon_line to avoid collision with 'makePolygon'.
--   https://postgis.net/docs/ST_MakePolygon.html
st_makepolygon_line ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value (Postgis 'Geometry a))
st_makepolygon_line a = unsafeSqlFunction "ST_MakePolygon" a

-- ---------------------------------------------------------------------------
-- Geometry Validation
-- ---------------------------------------------------------------------------

-- | Attempts to make an invalid geometry valid without losing vertices.
--   https://postgis.net/docs/ST_MakeValid.html
st_makevalid ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value (Postgis 'Geometry a))
st_makevalid a = unsafeSqlFunction "ST_MakeValid" a

-- | Returns text describing why a geometry is invalid, or "Valid Geometry".
--   https://postgis.net/docs/ST_IsValidReason.html
st_isvalidreason ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Text)
st_isvalidreason a = unsafeSqlFunction "ST_IsValidReason" a

-- ---------------------------------------------------------------------------
-- SRS Functions
-- ---------------------------------------------------------------------------

-- | Sets the SRID on a geometry to a particular integer value.
--   https://postgis.net/docs/ST_SetSRID.html
st_setsrid ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Int) ->
  SqlExpr (Value (Postgis 'Geometry a))
st_setsrid a srid' = unsafeSqlFunction "ST_SetSRID" (a, srid')

-- ---------------------------------------------------------------------------
-- Geometry Output
-- ---------------------------------------------------------------------------

-- | Returns the Well-Known Text (WKT) representation of the geometry.
--   https://postgis.net/docs/ST_AsText.html
st_astext ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Text)
st_astext a = unsafeSqlFunction "ST_AsText" a

-- | Returns the GeoJSON representation of the geometry.
--   https://postgis.net/docs/ST_AsGeoJSON.html
st_asgeojson ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Text)
st_asgeojson a = unsafeSqlFunction "ST_AsGeoJSON" a

-- | Returns the Extended Well-Known Text (EWKT) representation of the geometry.
--   https://postgis.net/docs/ST_AsEWKT.html
st_asewkt ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Text)
st_asewkt a = unsafeSqlFunction "ST_AsEWKT" a

-- | Returns a GeoHash representation of the geometry.
--   https://postgis.net/docs/ST_GeoHash.html
st_geohash ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Text)
st_geohash a = unsafeSqlFunction "ST_GeoHash" a

-- ---------------------------------------------------------------------------
-- Affine Transformations
-- ---------------------------------------------------------------------------

-- | Translates a geometry by given X and Y offsets.
--   https://postgis.net/docs/ST_Translate.html
st_translate ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) -> -- ^ deltaX
  SqlExpr (Value Double) -> -- ^ deltaY
  SqlExpr (Value (Postgis 'Geometry a))
st_translate a dx dy = unsafeSqlFunction "ST_Translate" (a, dx, dy)

-- | Scales a geometry by given X and Y factors.
--   https://postgis.net/docs/ST_Scale.html
st_scale ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) -> -- ^ scaleX
  SqlExpr (Value Double) -> -- ^ scaleY
  SqlExpr (Value (Postgis 'Geometry a))
st_scale a sx sy = unsafeSqlFunction "ST_Scale" (a, sx, sy)

-- | Rotates a geometry around the origin by an angle in radians.
--   https://postgis.net/docs/ST_Rotate.html
st_rotate ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) -> -- ^ angle in radians
  SqlExpr (Value (Postgis 'Geometry a))
st_rotate a angle = unsafeSqlFunction "ST_Rotate" (a, angle)

-- | Rotates a geometry around the X axis by an angle in radians.
--   https://postgis.net/docs/ST_RotateX.html
st_rotatex ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) -> -- ^ angle in radians
  SqlExpr (Value (Postgis 'Geometry a))
st_rotatex a angle = unsafeSqlFunction "ST_RotateX" (a, angle)

-- | Rotates a geometry around the Z axis by an angle in radians.
--   https://postgis.net/docs/ST_RotateZ.html
st_rotatez ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) -> -- ^ angle in radians
  SqlExpr (Value (Postgis 'Geometry a))
st_rotatez a angle = unsafeSqlFunction "ST_RotateZ" (a, angle)

-- ---------------------------------------------------------------------------
-- Bounding Box
-- ---------------------------------------------------------------------------

-- | Returns a bounding box expanded in all directions by a given distance.
--   https://postgis.net/docs/ST_Expand.html
st_expand ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) ->
  SqlExpr (Value (Postgis 'Geometry a))
st_expand a d = unsafeSqlFunction "ST_Expand" (a, d)

-- ---------------------------------------------------------------------------
-- Linear Referencing
-- ---------------------------------------------------------------------------

-- | Returns a point interpolated along a line at a fractional distance (0.0 to 1.0).
--   https://postgis.net/docs/ST_LineInterpolatePoint.html
st_lineinterpolatepoint ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) -> -- ^ fraction (0.0 to 1.0)
  SqlExpr (Value (Postgis 'Geometry a))
st_lineinterpolatepoint a f = unsafeSqlFunction "ST_LineInterpolatePoint" (a, f)

-- | Returns a float between 0 and 1 representing the location of the closest point on a line to a given point.
--   https://postgis.net/docs/ST_LineLocatePoint.html
st_linelocatepoint ::
  SqlExpr (Value (Postgis 'Geometry a)) -> -- ^ line
  SqlExpr (Value (Postgis 'Geometry a)) -> -- ^ point
  SqlExpr (Value Double)
st_linelocatepoint a b = unsafeSqlFunction "ST_LineLocatePoint" (a, b)

-- | Returns the portion of a line between two fractional locations (0.0 to 1.0).
--   https://postgis.net/docs/ST_LineSubstring.html
st_linesubstring ::
  SqlExpr (Value (Postgis 'Geometry a)) ->
  SqlExpr (Value Double) -> -- ^ start fraction
  SqlExpr (Value Double) -> -- ^ end fraction
  SqlExpr (Value (Postgis 'Geometry a))
st_linesubstring a s e = unsafeSqlFunction "ST_LineSubstring" (a, s, e)

point ::
  Double -> -- ^ x or longitude
  Double -> -- ^ y or latitude
  (Postgis spatialType PointXY)
point x y = Point (PointXY {_xyX = x, _xyY = y})

point_v :: HasPgType spatialType =>
  Double -> -- ^ x or longitude
  Double -> -- ^ y or latitude
  SqlExpr (Value (Postgis spatialType PointXY))
point_v = fmap val . point

st_point ::  forall spatialType . HasPgType spatialType =>
  SqlExpr (Value Double) -> -- ^ x or longitude
  SqlExpr (Value Double) -> -- ^ y or latitude
  SqlExpr (Value (Postgis spatialType PointXY))
st_point a b = unsafeSqlCastAs castAs $ unsafeSqlFunction "ST_POINT" (a, b)
  where
    castAs = pgType $ Proxy @spatialType


st_point_xyz ::  forall spatialType . HasPgType spatialType =>
  SqlExpr (Value Double) -> -- ^ x or longitude
  SqlExpr (Value Double) -> -- ^ y or latitude
  SqlExpr (Value Double) -> -- ^ z elevation/altitude
  SqlExpr (Value (Postgis spatialType PointXYZ))
st_point_xyz a b c = unsafeSqlCastAs castAs $ unsafeSqlFunction "ST_POINT" (a, b, c)
  where
    castAs = pgType $ Proxy @spatialType

st_point_xyzm :: forall spatialType . HasPgType spatialType =>
  SqlExpr (Value Double) -> -- ^ x or longitude
  SqlExpr (Value Double) -> -- ^ y or latitude
  SqlExpr (Value Double) -> -- ^ z elevation/altitude
  SqlExpr (Value Double) -> -- ^ m measure, user defined dimension
  SqlExpr (Value (Postgis spatialType PointXYZM))
st_point_xyzm a b c m = unsafeSqlCastAs castAs $ unsafeSqlFunction "ST_POINT" (a, b, c, m)
  where
    castAs = pgType $ Proxy @spatialType