geox/
point.rs

1use std::{convert::TryFrom, ops::Deref};
2
3#[cfg(feature = "sqlx")]
4use geozero::ToWkb;
5
6#[cfg(feature = "sqlx")]
7use sqlx::{
8    encode::IsNull,
9    postgres::{PgHasArrayType, PgTypeInfo, PgValueRef},
10    Postgres,
11};
12
13#[cfg(feature = "async-graphql")]
14use async_graphql::{InputValueError, InputValueResult, Number, ScalarType, Value};
15
16use crate::Geometry;
17
18#[derive(Clone, Debug)]
19#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
20#[cfg_attr(feature = "serde", serde(transparent))]
21pub struct Point(pub geo::Point<f64>);
22
23impl PartialEq for Point {
24    fn eq(&self, other: &Self) -> bool {
25        self.0 == other.0
26    }
27}
28
29impl Eq for Point {}
30
31impl Deref for Point {
32    type Target = geo::Point<f64>;
33
34    fn deref(&self) -> &Self::Target {
35        &self.0
36    }
37}
38
39#[cfg(feature = "sqlx")]
40impl sqlx::Type<Postgres> for Point {
41    fn type_info() -> PgTypeInfo {
42        PgTypeInfo::with_name("geometry")
43    }
44}
45
46#[cfg(feature = "sqlx")]
47impl PgHasArrayType for Point {
48    fn array_type_info() -> PgTypeInfo {
49        PgTypeInfo::with_name("_geometry")
50    }
51}
52
53impl Point {
54    pub fn into_inner(self) -> geo::Point<f64> {
55        self.0
56    }
57}
58
59impl TryFrom<Geometry> for Point {
60    type Error = geo_types::Error;
61
62    fn try_from(value: Geometry) -> Result<Self, Self::Error> {
63        geo::Point::try_from(value.0).map(Point)
64    }
65}
66
67#[cfg(feature = "sqlx")]
68impl<'de> sqlx::Decode<'de, Postgres> for Point {
69    fn decode(value: PgValueRef<'de>) -> Result<Self, sqlx::error::BoxDynError> {
70        let geometry = Geometry::decode(value)?;
71        let point = geo::Point::<f64>::try_from(geometry.0)?;
72        Ok(Point(point))
73    }
74}
75
76#[cfg(feature = "sqlx")]
77impl<'en> sqlx::Encode<'en, Postgres> for Point {
78    fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> IsNull {
79        let x = geo::Geometry::Point(self.0)
80            .to_ewkb(geozero::CoordDimensions::xy(), None)
81            .unwrap();
82        buf.extend(x);
83        sqlx::encode::IsNull::No
84    }
85}
86
87#[cfg(feature = "async-graphql")]
88#[async_graphql::Scalar]
89impl ScalarType for Point {
90    #[cfg(not(feature = "serde"))]
91    fn parse(_value: Value) -> InputValueResult<Self> {
92        Err(InputValueError::custom("parsing not implemented"))
93    }
94
95    #[cfg(feature = "serde")]
96    fn parse(value: Value) -> InputValueResult<Self> {
97        use geozero::{geojson::GeoJson, ToGeo};
98
99        match value {
100            Value::String(x) => {
101                let geo = GeoJson(&x)
102                    .to_geo()
103                    .map_err(|_| InputValueError::custom("failed to parse GeoJSON string"))?;
104                match geo {
105                    geo::Geometry::Point(x) => Ok(Self(x)),
106                    _ => Err(InputValueError::custom("Got invalid type for Point")),
107                }
108            }
109            _ => Err(InputValueError::custom(
110                "parsing not implemented for this type (only string)",
111            )),
112        }
113    }
114
115    fn to_value(&self) -> Value {
116        Value::List(vec![
117            Value::Number(Number::from_f64(self.x()).unwrap()),
118            Value::Number(Number::from_f64(self.y()).unwrap()),
119        ])
120    }
121}
122
123#[cfg(feature = "serde")]
124impl serde::Serialize for Point {
125    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
126    where
127        S: serde::Serializer,
128    {
129        use geozero::ToJson;
130        use serde::ser::{Error, SerializeMap};
131        use serde_json::Value;
132        use std::collections::BTreeMap;
133
134        let s = geo::Geometry::Point(self.0)
135            .to_json()
136            .map_err(Error::custom)?;
137        let s: BTreeMap<String, Value> = serde_json::from_str(&s).map_err(Error::custom)?;
138
139        let mut map = serializer.serialize_map(Some(s.len()))?;
140        for (k, v) in s {
141            map.serialize_entry(&k, &v)?;
142        }
143        map.end()
144    }
145}
146
147#[cfg(all(test, feature = "sqlx"))]
148mod sqlx_tests {
149    use super::Point;
150
151    async fn pg_roundtrip(data_to: &Point, type_name: &str) -> Point {
152        use sqlx::postgres::PgPoolOptions;
153        let conn = PgPoolOptions::new()
154            .max_connections(5)
155            .connect("postgres://postgres:password@localhost/postgres")
156            .await
157            .unwrap();
158        let mut conn = conn.begin().await.unwrap();
159
160        sqlx::query(&format!(
161            "CREATE TABLE test ( id SERIAL PRIMARY KEY, geom GEOMETRY({type_name}, 26910) )"
162        ))
163        .execute(&mut *conn)
164        .await
165        .unwrap();
166
167        sqlx::query("INSERT INTO test (geom) VALUES ($1)")
168            .bind(data_to)
169            .execute(&mut *conn)
170            .await
171            .unwrap();
172
173        let (data_from,): (Point,) = sqlx::query_as("SELECT geom FROM test")
174            .fetch_one(&mut *conn)
175            .await
176            .unwrap();
177
178        data_from
179    }
180
181    #[tokio::test]
182    async fn polygon() {
183        let data_to = Point(geo::Point::from((0., 1.)));
184        let data_from = pg_roundtrip(&data_to, "Point").await;
185        assert_eq!(data_to, data_from);
186    }
187}