pub const SRID_WGS84: u32 = 4326;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
pub srid: u32,
}
impl Point {
#[must_use]
pub fn new(x: f64, y: f64) -> Self {
Self {
x,
y,
srid: SRID_WGS84,
}
}
#[must_use]
pub fn with_srid(x: f64, y: f64, srid: u32) -> Self {
Self { x, y, srid }
}
#[must_use]
pub fn to_wkt(&self) -> String {
format!("POINT({} {})", self.x, self.y)
}
}
impl Default for Point {
fn default() -> Self {
Self::new(0.0, 0.0)
}
}
impl From<Point> for crate::core::SqlValue {
fn from(p: Point) -> Self {
crate::core::SqlValue::Geometry {
x: p.x,
y: p.y,
srid: p.srid,
}
}
}
impl From<Point> for crate::core::Expr {
fn from(p: Point) -> Self {
crate::core::Expr::Literal(p.into())
}
}
impl serde::Serialize for Point {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct as _;
let mut s = serializer.serialize_struct("Point", 3)?;
s.serialize_field("x", &self.x)?;
s.serialize_field("y", &self.y)?;
s.serialize_field("srid", &self.srid)?;
s.end()
}
}
impl<'de> serde::Deserialize<'de> for Point {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct Raw {
x: f64,
y: f64,
#[serde(default = "default_srid")]
srid: u32,
}
fn default_srid() -> u32 {
SRID_WGS84
}
let r = Raw::deserialize(deserializer)?;
Ok(Self {
x: r.x,
y: r.y,
srid: r.srid,
})
}
}
#[cfg(feature = "postgres")]
const WKB_POINT: u32 = 0x0000_0001;
#[cfg(feature = "postgres")]
const EWKB_SRID_FLAG: u32 = 0x2000_0000;
#[cfg(feature = "postgres")]
fn encode_ewkb_point(p: &Point, buf: &mut Vec<u8>) {
buf.push(0x01); buf.extend_from_slice(&(WKB_POINT | EWKB_SRID_FLAG).to_le_bytes());
buf.extend_from_slice(&p.srid.to_le_bytes());
buf.extend_from_slice(&p.x.to_le_bytes());
buf.extend_from_slice(&p.y.to_le_bytes());
}
#[cfg(feature = "postgres")]
fn decode_ewkb_point(bytes: &[u8]) -> Result<Point, sqlx::error::BoxDynError> {
if bytes.len() < 5 {
return Err("EWKB geometry too short (missing byte-order + type)".into());
}
let little = match bytes[0] {
0x01 => true,
0x00 => false,
other => return Err(format!("EWKB: unknown byte-order marker {other:#x}").into()),
};
let rd_u32 = |b: &[u8]| -> u32 {
let a = [b[0], b[1], b[2], b[3]];
if little {
u32::from_le_bytes(a)
} else {
u32::from_be_bytes(a)
}
};
let rd_f64 = |b: &[u8]| -> f64 {
let a = [b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]];
if little {
f64::from_le_bytes(a)
} else {
f64::from_be_bytes(a)
}
};
let type_word = rd_u32(&bytes[1..5]);
if type_word & 0x0000_00ff != WKB_POINT {
return Err(format!(
"EWKB: expected a Point geometry, got type word {type_word:#x} (only Point is supported, issue #443)"
)
.into());
}
let has_srid = type_word & EWKB_SRID_FLAG != 0;
let mut off = 5;
let srid = if has_srid {
if bytes.len() < off + 4 {
return Err("EWKB: SRID flag set but value truncated".into());
}
let s = rd_u32(&bytes[off..off + 4]);
off += 4;
s
} else {
SRID_WGS84
};
if bytes.len() < off + 16 {
return Err("EWKB Point: coordinate payload truncated".into());
}
let x = rd_f64(&bytes[off..off + 8]);
let y = rd_f64(&bytes[off + 8..off + 16]);
Ok(Point { x, y, srid })
}
#[cfg(feature = "postgres")]
impl sqlx::Type<sqlx::Postgres> for Point {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("geometry")
}
fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
use sqlx::TypeInfo as _;
ty.name().eq_ignore_ascii_case("geometry")
}
}
#[cfg(feature = "postgres")]
impl sqlx::Encode<'_, sqlx::Postgres> for Point {
fn encode_by_ref(
&self,
buf: &mut sqlx::postgres::PgArgumentBuffer,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
let mut bytes = Vec::with_capacity(25);
encode_ewkb_point(self, &mut bytes);
buf.extend_from_slice(&bytes);
Ok(sqlx::encode::IsNull::No)
}
}
#[cfg(feature = "postgres")]
impl sqlx::Decode<'_, sqlx::Postgres> for Point {
fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
match value.format() {
sqlx::postgres::PgValueFormat::Binary => decode_ewkb_point(value.as_bytes()?),
sqlx::postgres::PgValueFormat::Text => {
let hex = value.as_str()?.trim();
let raw = decode_hex(hex)?;
decode_ewkb_point(&raw)
}
}
}
}
#[cfg(feature = "postgres")]
fn decode_hex(s: &str) -> Result<Vec<u8>, sqlx::error::BoxDynError> {
if s.len() % 2 != 0 {
return Err("EWKB hex: odd length".into());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(Into::into))
.collect()
}
#[cfg(feature = "mysql")]
impl sqlx::Type<sqlx::MySql> for Point {
fn type_info() -> sqlx::mysql::MySqlTypeInfo {
<Vec<u8> as sqlx::Type<sqlx::MySql>>::type_info()
}
}
#[cfg(feature = "mysql")]
impl sqlx::Decode<'_, sqlx::MySql> for Point {
fn decode(_value: sqlx::mysql::MySqlValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
Err(
"`Point` columns are PostgreSQL/PostGIS-only; cannot decode on MySQL (issue #443)"
.into(),
)
}
}
#[cfg(feature = "sqlite")]
impl sqlx::Type<sqlx::Sqlite> for Point {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<Vec<u8> as sqlx::Type<sqlx::Sqlite>>::type_info()
}
}
#[cfg(feature = "sqlite")]
impl sqlx::Decode<'_, sqlx::Sqlite> for Point {
fn decode(_value: sqlx::sqlite::SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
Err(
"`Point` columns are PostgreSQL/PostGIS-only; cannot decode on SQLite (issue #443)"
.into(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_defaults_to_wgs84_and_wkt() {
let p = Point::new(1.5, -2.25);
assert_eq!(p.srid, SRID_WGS84);
assert_eq!(p.to_wkt(), "POINT(1.5 -2.25)");
}
#[test]
fn into_sqlvalue_geometry() {
let sv: crate::core::SqlValue = Point::with_srid(1.0, 2.0, 3857).into();
match sv {
crate::core::SqlValue::Geometry { x, y, srid } => {
assert_eq!((x, y, srid), (1.0, 2.0, 3857));
}
_ => panic!("expected SqlValue::Geometry"),
}
}
#[test]
fn serde_round_trips_as_object() {
let p = Point::with_srid(1.5, -2.25, 4326);
let json = serde_json::to_string(&p).unwrap();
let back: Point = serde_json::from_str(&json).unwrap();
assert_eq!(back, p);
let p2: Point = serde_json::from_str(r#"{"x":1.0,"y":2.0}"#).unwrap();
assert_eq!(p2, Point::new(1.0, 2.0));
}
#[cfg(feature = "postgres")]
#[test]
fn ewkb_round_trip_and_matches_postgis() {
let p = Point::with_srid(1.5, -2.25, 4326);
let mut buf = Vec::new();
encode_ewkb_point(&p, &mut buf);
let expected = decode_hex("0101000020e6100000000000000000f83f00000000000002c0").unwrap();
assert_eq!(buf, expected, "EWKB encoding must match PostGIS");
assert_eq!(decode_ewkb_point(&buf).unwrap(), p);
}
#[cfg(feature = "postgres")]
#[test]
fn decode_rejects_non_point() {
let mut bytes = vec![0x01];
bytes.extend_from_slice(&(0x0000_0002u32 | EWKB_SRID_FLAG).to_le_bytes());
bytes.extend_from_slice(&4326u32.to_le_bytes());
assert!(decode_ewkb_point(&bytes).is_err());
}
}