use std::fmt;
use serde::{Deserialize, Serialize};
use crate::geo::{GeoError, GeoPoint, ewkb};
#[derive(Debug, Clone, PartialEq)]
pub struct LineString {
pub(crate) points: Vec<GeoPoint>,
}
impl LineString {
pub fn new(points: &[GeoPoint]) -> Result<Self, GeoError> {
if points.len() < 2 {
return Err(GeoError::InvalidLineString {
got: points.len(),
need: 2,
});
}
Ok(Self {
points: points.to_vec(),
})
}
pub fn points(&self) -> &[GeoPoint] {
&self.points
}
pub fn to_ewkb_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(ewkb::linestring_byte_len(self));
ewkb::encode_linestring_into(self, &mut buf);
buf
}
pub fn from_ewkb_bytes(bytes: &[u8]) -> Result<Self, GeoError> {
ewkb::decode_linestring(bytes)
}
}
impl fmt::Display for LineString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("LINESTRING(")?;
for (i, p) in self.points.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{} {}", p.lon, p.lat)?;
}
f.write_str(")")
}
}
impl Serialize for LineString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.points.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for LineString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let points: Vec<GeoPoint> = Vec::deserialize(deserializer)?;
LineString::new(&points).map_err(serde::de::Error::custom)
}
}
crate::geo::impl_geography_codec!(LineString, ewkb::encode_linestring_into);
#[cfg(all(test, feature = "spatial"))]
mod tests {
use super::*;
use crate::geo::GeoError;
fn p(lat: f64, lon: f64) -> GeoPoint {
GeoPoint::new(lat, lon).unwrap()
}
#[test]
fn new_with_zero_points_errors() {
assert!(matches!(
LineString::new(&[]),
Err(GeoError::InvalidLineString { got: 0, need: 2 })
));
}
#[test]
fn new_with_one_point_errors() {
assert!(matches!(
LineString::new(&[p(0.0, 0.0)]),
Err(GeoError::InvalidLineString { got: 1, need: 2 })
));
}
#[test]
fn new_with_two_points_succeeds() {
let ls = LineString::new(&[p(0.0, 0.0), p(1.0, 1.0)]).unwrap();
assert_eq!(ls.points().len(), 2);
}
#[test]
fn new_with_many_points_succeeds() {
let pts: Vec<GeoPoint> = (0..5).map(|i| p(i as f64, i as f64)).collect();
let ls = LineString::new(&pts).unwrap();
assert_eq!(ls.points().len(), 5);
}
#[test]
fn ewkb_round_trip() {
let ls = LineString::new(&[p(37.7749, -122.4194), p(40.7128, -74.0060)]).unwrap();
let bytes = ls.to_ewkb_bytes();
let decoded = LineString::from_ewkb_bytes(&bytes).unwrap();
assert_eq!(decoded.points().len(), ls.points().len());
for (a, b) in ls.points().iter().zip(decoded.points().iter()) {
assert!((a.lat - b.lat).abs() < 1e-9);
assert!((a.lon - b.lon).abs() < 1e-9);
}
}
#[test]
fn display_matches_wkt() {
let ls = LineString::new(&[p(0.0, 0.0), p(0.0, 1.0)]).unwrap();
assert_eq!(format!("{ls}"), "LINESTRING(0 0, 1 0)");
}
#[test]
fn serde_json_round_trip() {
let original = LineString::new(&[p(37.7749, -122.4194), p(40.7128, -74.0060)]).unwrap();
let json = serde_json::to_string(&original).unwrap();
let decoded: LineString = serde_json::from_str(&json).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn serde_json_rejects_single_point() {
let json = r#"[{"lat":0.0,"lon":0.0}]"#;
assert!(serde_json::from_str::<LineString>(json).is_err());
}
#[test]
fn to_sql_writes_ewkb_bytes() {
use bytes::BytesMut;
use postgres_types::{ToSql, Type};
let ls = LineString::new(&[p(51.5074, -0.1278), p(48.8566, 2.3522)]).unwrap();
let expected = ls.to_ewkb_bytes();
let mut out = BytesMut::new();
ls.to_sql(&Type::BYTEA, &mut out).unwrap();
assert_eq!(out.as_ref(), expected.as_slice());
}
}