use crate::gpb::{self, GpbHeader};
use crate::types::GeometryType;
use geo_traits::{
CoordTrait, Dimensions, GeometryTrait, LineStringTrait, LineTrait, MultiLineStringTrait,
MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait, RectTrait, TriangleTrait,
};
use geo_traits::{GeometryCollectionTrait, GeometryType as GtGeometryType};
use wkb::Endianness;
use wkb::reader::{
GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Wkb,
};
use wkb::writer::{WriteOptions, write_geometry};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum GeometryError {
#[error(transparent)]
Header(#[from] gpb::GpbError),
#[error("WKB body is unreadable (unsupported curve type or malformed geometry)")]
Body(#[from] wkb::error::WkbError),
#[error("WKB body truncated: need at least 5 bytes for the geometry type code")]
TruncatedWkb,
#[error("unknown WKB geometry type code {0}")]
UnknownWkbType(u32),
#[error("failed to encode geometry to WKB")]
EncodeWkb(#[source] wkb::error::WkbError),
}
#[derive(Debug, Clone)]
pub struct GpbGeometry<'a> {
header: GpbHeader,
body: &'a [u8],
wkb: Wkb<'a>,
}
impl<'a> GpbGeometry<'a> {
pub fn parse(blob: &'a [u8]) -> Result<Self, GeometryError> {
let (header, offset) = gpb::parse_header(blob)?;
let body = blob.get(offset..).unwrap_or_default();
let wkb = Wkb::try_new(body)?;
Ok(Self { header, body, wkb })
}
pub fn header(&self) -> &GpbHeader {
&self.header
}
pub fn wkb_body(&self) -> &'a [u8] {
self.body
}
pub fn wkb(&self) -> &Wkb<'a> {
&self.wkb
}
#[cfg(feature = "geo-types")]
pub fn to_geo(&self) -> Option<geo_types::Geometry<f64>> {
use geo_traits::to_geo::ToGeoGeometry;
self.wkb.try_to_geometry()
}
pub fn xy_envelope(&self) -> Option<[f64; 4]> {
let mut bounds = XyBounds::new();
visit_coords(&self.wkb, &mut |x, y, _| bounds.add(x, y));
bounds.finish()
}
pub fn is_empty(&self) -> bool {
self.header.empty || self.xy_envelope().is_none()
}
pub fn geometry_type(&self) -> GeometryType {
use wkb::reader::GeometryType as WkbType;
match self.wkb.geometry_type() {
WkbType::Point => GeometryType::Point,
WkbType::LineString => GeometryType::LineString,
WkbType::Polygon => GeometryType::Polygon,
WkbType::MultiPoint => GeometryType::MultiPoint,
WkbType::MultiLineString => GeometryType::MultiLineString,
WkbType::MultiPolygon => GeometryType::MultiPolygon,
WkbType::GeometryCollection => GeometryType::GeometryCollection,
_ => GeometryType::Geometry,
}
}
pub fn matches_declared(&self, declared: GeometryType) -> bool {
geometry_type_matches(self.geometry_type(), declared)
}
}
pub fn geometry_type_matches(actual: GeometryType, declared: GeometryType) -> bool {
use GeometryType::*;
if declared == Geometry || declared == actual {
return true;
}
if declared == GeometryCollection {
return matches!(
actual,
GeometryCollection
| MultiPoint
| MultiLineString
| MultiPolygon
| MultiCurve
| MultiSurface
);
}
false
}
pub fn wkb_geometry_type(wkb_body: &[u8]) -> Result<GeometryType, GeometryError> {
let &[order, c0, c1, c2, c3, ..] = wkb_body else {
return Err(GeometryError::TruncatedWkb);
};
let little_endian = match order {
0 => false,
1 => true,
_ => return Err(GeometryError::TruncatedWkb),
};
let bytes = [c0, c1, c2, c3];
let code = if little_endian {
u32::from_le_bytes(bytes)
} else {
u32::from_be_bytes(bytes)
};
let base = if code & 0xE000_0000 != 0 {
code & 0x0000_00FF
} else {
code % 1000
};
let ty = match base {
0 => GeometryType::Geometry,
1 => GeometryType::Point,
2 => GeometryType::LineString,
3 => GeometryType::Polygon,
4 => GeometryType::MultiPoint,
5 => GeometryType::MultiLineString,
6 => GeometryType::MultiPolygon,
7 => GeometryType::GeometryCollection,
8 => GeometryType::CircularString,
9 => GeometryType::CompoundCurve,
10 => GeometryType::CurvePolygon,
11 => GeometryType::MultiCurve,
12 => GeometryType::MultiSurface,
13 => GeometryType::Curve,
14 => GeometryType::Surface,
_ => return Err(GeometryError::UnknownWkbType(code)),
};
Ok(ty)
}
pub fn write_envelope<G: GeometryTrait<T = f64>>(geom: &G) -> (gpb::Envelope, bool) {
let mut bounds = XyzBounds::new();
visit_coords(geom, &mut |x, y, z| bounds.add(x, y, z));
let Some([min_x, max_x, min_y, max_y]) = bounds.xy_bounds() else {
return (gpb::Envelope::None, true);
};
match bounds.z_bounds() {
Some((min_z, max_z)) => (
gpb::Envelope::Xyz([min_x, max_x, min_y, max_y, min_z, max_z]),
false,
),
None => (gpb::Envelope::Xy([min_x, max_x, min_y, max_y]), false),
}
}
pub fn encode_gpb_from_wkb(wkb_body: &[u8], srs_id: i32) -> Result<EncodedGpb, GeometryError> {
let geometry = Wkb::try_new(wkb_body)?;
let (envelope, empty) = write_envelope(&geometry);
let xy_envelope = envelope
.xy_bounds()
.map(|(min_x, max_x, min_y, max_y)| [min_x, max_x, min_y, max_y]);
let body = geometry.buf();
let mut blob = gpb::encode_header(srs_id, &envelope, empty, false);
blob.reserve(body.len());
blob.extend_from_slice(body);
Ok(EncodedGpb {
blob,
xy_envelope,
dimensions: geometry.dim(),
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct EncodedGpb {
pub blob: Vec<u8>,
pub xy_envelope: Option<[f64; 4]>,
pub dimensions: Dimensions,
}
pub fn encode_gpb<G: GeometryTrait<T = f64>>(
geom: &G,
srs_id: i32,
) -> Result<(Vec<u8>, Option<[f64; 4]>), GeometryError> {
let (envelope, empty) = write_envelope(geom);
let xy = envelope
.xy_bounds()
.map(|(min_x, max_x, min_y, max_y)| [min_x, max_x, min_y, max_y]);
let mut blob = gpb::encode_header(srs_id, &envelope, empty, false);
let options = WriteOptions {
endianness: Endianness::LittleEndian,
};
write_geometry(&mut blob, geom, &options).map_err(GeometryError::EncodeWkb)?;
Ok((blob, xy))
}
#[derive(Debug, Clone, Copy)]
struct XyBounds {
min_x: f64,
max_x: f64,
min_y: f64,
max_y: f64,
seen: bool,
}
impl XyBounds {
fn new() -> Self {
Self {
min_x: f64::INFINITY,
max_x: f64::NEG_INFINITY,
min_y: f64::INFINITY,
max_y: f64::NEG_INFINITY,
seen: false,
}
}
fn add(&mut self, x: f64, y: f64) {
if !x.is_finite() || !y.is_finite() {
return;
}
self.min_x = self.min_x.min(x);
self.max_x = self.max_x.max(x);
self.min_y = self.min_y.min(y);
self.max_y = self.max_y.max(y);
self.seen = true;
}
fn finish(self) -> Option<[f64; 4]> {
self.seen
.then_some([self.min_x, self.max_x, self.min_y, self.max_y])
}
}
#[derive(Debug, Clone, Copy)]
struct XyzBounds {
xy: XyBounds,
min_z: f64,
max_z: f64,
seen_z: bool,
}
impl XyzBounds {
fn new() -> Self {
Self {
xy: XyBounds::new(),
min_z: f64::INFINITY,
max_z: f64::NEG_INFINITY,
seen_z: false,
}
}
fn add(&mut self, x: f64, y: f64, z: Option<f64>) {
self.xy.add(x, y);
if let Some(z) = z
&& z.is_finite()
{
self.min_z = self.min_z.min(z);
self.max_z = self.max_z.max(z);
self.seen_z = true;
}
}
fn xy_bounds(&self) -> Option<[f64; 4]> {
self.xy.finish()
}
fn z_bounds(&self) -> Option<(f64, f64)> {
self.seen_z.then_some((self.min_z, self.max_z))
}
}
fn coord_xyz(coord: &impl CoordTrait<T = f64>) -> (f64, f64, Option<f64>) {
let z = match coord.dim() {
Dimensions::Xyz | Dimensions::Xyzm => coord.nth(2),
_ => None,
};
(coord.x(), coord.y(), z)
}
fn visit_point(point: &impl PointTrait<T = f64>, visit: &mut impl FnMut(f64, f64, Option<f64>)) {
if let Some(coord) = point.coord() {
let (x, y, z) = coord_xyz(&coord);
visit(x, y, z);
}
}
fn visit_line_string(
line_string: &impl LineStringTrait<T = f64>,
visit: &mut impl FnMut(f64, f64, Option<f64>),
) {
for coord in line_string.coords() {
let (x, y, z) = coord_xyz(&coord);
visit(x, y, z);
}
}
fn visit_polygon(
polygon: &impl PolygonTrait<T = f64>,
visit: &mut impl FnMut(f64, f64, Option<f64>),
) {
if let Some(exterior) = polygon.exterior() {
visit_line_string(&exterior, visit);
}
for interior in polygon.interiors() {
visit_line_string(&interior, visit);
}
}
fn visit_coords<G: GeometryTrait<T = f64>>(
geom: &G,
visit: &mut impl FnMut(f64, f64, Option<f64>),
) {
match geom.as_type() {
GtGeometryType::Point(point) => visit_point(point, visit),
GtGeometryType::LineString(line_string) => visit_line_string(line_string, visit),
GtGeometryType::Polygon(polygon) => visit_polygon(polygon, visit),
GtGeometryType::MultiPoint(multi_point) => {
for point in multi_point.points() {
visit_point(&point, visit);
}
}
GtGeometryType::MultiLineString(multi_line_string) => {
for line_string in multi_line_string.line_strings() {
visit_line_string(&line_string, visit);
}
}
GtGeometryType::MultiPolygon(multi_polygon) => {
for polygon in multi_polygon.polygons() {
visit_polygon(&polygon, visit);
}
}
GtGeometryType::GeometryCollection(collection) => {
for member in collection.geometries() {
visit_coords(&member, visit);
}
}
GtGeometryType::Rect(rect) => {
let (min, max) = (rect.min(), rect.max());
let (min_x, min_y, min_z) = coord_xyz(&min);
let (max_x, max_y, max_z) = coord_xyz(&max);
visit(min_x, min_y, min_z);
visit(max_x, max_y, max_z);
}
GtGeometryType::Triangle(triangle) => {
for coord in triangle.coords() {
let (x, y, z) = coord_xyz(&coord);
visit(x, y, z);
}
}
GtGeometryType::Line(line) => {
for coord in line.coords() {
let (x, y, z) = coord_xyz(&coord);
visit(x, y, z);
}
}
}
}
impl<'a> GeometryTrait for GpbGeometry<'a> {
type T = f64;
type PointType<'b>
= Point<'a>
where
Self: 'b;
type LineStringType<'b>
= LineString<'a>
where
Self: 'b;
type PolygonType<'b>
= Polygon<'a>
where
Self: 'b;
type MultiPointType<'b>
= MultiPoint<'a>
where
Self: 'b;
type MultiLineStringType<'b>
= MultiLineString<'a>
where
Self: 'b;
type MultiPolygonType<'b>
= MultiPolygon<'a>
where
Self: 'b;
type GeometryCollectionType<'b>
= GeometryCollection<'a>
where
Self: 'b;
type RectType<'b>
= geo_traits::UnimplementedRect<f64>
where
Self: 'b;
type TriangleType<'b>
= geo_traits::UnimplementedTriangle<f64>
where
Self: 'b;
type LineType<'b>
= geo_traits::UnimplementedLine<f64>
where
Self: 'b;
fn dim(&self) -> Dimensions {
self.wkb.dim()
}
fn as_type(
&self,
) -> geo_traits::GeometryType<
'_,
Self::PointType<'_>,
Self::LineStringType<'_>,
Self::PolygonType<'_>,
Self::MultiPointType<'_>,
Self::MultiLineStringType<'_>,
Self::MultiPolygonType<'_>,
Self::GeometryCollectionType<'_>,
Self::RectType<'_>,
Self::TriangleType<'_>,
Self::LineType<'_>,
> {
self.wkb.as_type()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gpb::{Envelope, encode_header};
use geo_traits::GeometryType as GtType;
fn gpb(body: &[u8]) -> Vec<u8> {
let mut blob = encode_header(4326, &Envelope::None, false, false);
blob.extend_from_slice(body);
blob
}
fn wkb_point(x: f64, y: f64) -> Vec<u8> {
let mut b = vec![1u8];
b.extend_from_slice(&1u32.to_le_bytes());
b.extend_from_slice(&x.to_le_bytes());
b.extend_from_slice(&y.to_le_bytes());
b
}
#[test]
fn parses_point_and_exposes_header_and_body() {
let body = wkb_point(3.0, 4.0);
let blob = gpb(&body);
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.header().srs_id, 4326);
assert_eq!(g.wkb_body(), body.as_slice());
}
#[test]
#[expect(
clippy::float_cmp,
reason = "asserting the exact bit-level round-trip of the coordinate through WKB; the values are written and read as literals, so exact equality is the property under test"
)]
fn delegates_geometry_trait_to_wkb() {
let blob = gpb(&wkb_point(3.0, 4.0));
let g = GpbGeometry::parse(&blob).unwrap();
match g.as_type() {
GtType::Point(p) => {
use geo_traits::{CoordTrait, PointTrait};
let c = p.coord().unwrap();
assert_eq!(c.x(), 3.0);
assert_eq!(c.y(), 4.0);
}
_ => panic!("expected a point"),
}
}
#[test]
fn arbitrary_bytes_error_never_panic() {
GpbGeometry::parse(b"").unwrap_err();
GpbGeometry::parse(b"GP").unwrap_err();
GpbGeometry::parse(&gpb(&[])).unwrap_err();
GpbGeometry::parse(&gpb(&[1])).unwrap_err();
GpbGeometry::parse(&gpb(&wkb_point(1.0, 2.0)[..10])).unwrap_err();
}
#[test]
#[cfg(feature = "geo-types")]
fn to_geo_yields_geo_types() {
let blob = gpb(&wkb_point(3.0, 4.0));
let g = GpbGeometry::parse(&blob).unwrap();
let geo = g.to_geo().unwrap();
assert_eq!(
geo,
geo_types::Geometry::Point(geo_types::Point::new(3.0, 4.0))
);
}
#[test]
fn point_envelope_from_traversal() {
let blob = gpb(&wkb_point(3.0, -4.0));
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.xy_envelope(), Some([3.0, 3.0, -4.0, -4.0]));
assert!(!g.is_empty());
}
#[test]
fn empty_point_is_empty_no_envelope() {
let blob = gpb(&wkb_point(f64::NAN, f64::NAN));
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.xy_envelope(), None);
assert!(g.is_empty());
}
#[test]
fn header_empty_flag_reports_empty() {
let mut blob = encode_header(4326, &Envelope::None, true, false);
blob.extend_from_slice(&wkb_point(1.0, 2.0));
let g = GpbGeometry::parse(&blob).unwrap();
assert!(g.is_empty());
}
#[cfg(feature = "geo-types")]
fn wkb_body(geom: &geo_types::Geometry<f64>, little_endian: bool) -> Vec<u8> {
use wkb::Endianness;
use wkb::writer::{WriteOptions, write_geometry};
let options = WriteOptions {
endianness: if little_endian {
Endianness::LittleEndian
} else {
Endianness::BigEndian
},
};
let mut buf = Vec::new();
write_geometry(&mut buf, geom, &options).unwrap();
buf
}
#[test]
#[cfg(feature = "geo-types")]
fn traversal_bounds_equal_header_envelope() {
use geo_types::{
Geometry, LineString, MultiLineString, MultiPolygon, Point, Polygon, coord,
};
let line: Geometry<f64> =
LineString::from(vec![(0.0, 0.0), (10.0, -3.0), (4.0, 8.0)]).into();
let polygon: Geometry<f64> = Polygon::new(
LineString::from(vec![
(0.0, 0.0),
(6.0, 0.0),
(6.0, 5.0),
(0.0, 5.0),
(0.0, 0.0),
]),
vec![LineString::from(vec![
(1.0, 1.0),
(2.0, 1.0),
(2.0, 2.0),
(1.0, 1.0),
])],
)
.into();
let multipolygon: Geometry<f64> = MultiPolygon::new(vec![
Polygon::new(
LineString::from(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]),
vec![],
),
Polygon::new(
LineString::from(vec![(-5.0, -5.0), (-4.0, -5.0), (-4.0, -4.0), (-5.0, -5.0)]),
vec![],
),
])
.into();
let multiline: Geometry<f64> = MultiLineString::new(vec![
LineString::from(vec![(0.0, 0.0), (3.0, 3.0)]),
LineString::from(vec![(-2.0, 7.0), (9.0, -1.0)]),
])
.into();
let point: Geometry<f64> = Point::from(coord! { x: 2.5, y: -6.0 }).into();
let cases: [(Geometry<f64>, [f64; 4]); 5] = [
(point, [2.5, 2.5, -6.0, -6.0]),
(line, [0.0, 10.0, -3.0, 8.0]),
(polygon, [0.0, 6.0, 0.0, 5.0]),
(multipolygon, [-5.0, 1.0, -5.0, 1.0]),
(multiline, [-2.0, 9.0, -1.0, 7.0]),
];
for (geom, bounds) in cases {
for little_endian in [true, false] {
let mut blob = encode_header(4326, &Envelope::Xy(bounds), false, false);
blob.extend_from_slice(&wkb_body(&geom, little_endian));
let g = GpbGeometry::parse(&blob).unwrap();
let (bx0, bx1, by0, by1) = g.header().envelope.xy_bounds().unwrap();
assert_eq!(
g.xy_envelope(),
Some([bx0, bx1, by0, by1]),
"traversal must match header envelope (little_endian={little_endian})"
);
assert!(!g.is_empty());
}
}
}
#[test]
#[cfg(feature = "geo-types")]
fn geometry_collection_traversal() {
use geo_types::{Geometry, GeometryCollection, LineString, Point};
let members: Vec<Geometry<f64>> = vec![
Geometry::Point(Point::new(1.0, 1.0)),
Geometry::LineString(LineString::from(vec![(-3.0, 0.0), (4.0, 9.0)])),
];
let gc = Geometry::GeometryCollection(GeometryCollection::new_from(members));
let mut blob = encode_header(4326, &Envelope::None, false, false);
blob.extend_from_slice(&wkb_body(&gc, true));
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.xy_envelope(), Some([-3.0, 4.0, 0.0, 9.0]));
}
#[test]
fn big_endian_linestring_traversal() {
let mut body = vec![0u8]; body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&2u32.to_be_bytes()); for (x, y) in [(1.0f64, 2.0f64), (5.0, -1.0)] {
body.extend_from_slice(&x.to_be_bytes());
body.extend_from_slice(&y.to_be_bytes());
}
let blob = gpb(&body);
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.xy_envelope(), Some([1.0, 5.0, -1.0, 2.0]));
}
#[test]
fn declared_type_matching_rules() {
use GeometryType::*;
assert!(geometry_type_matches(Point, Point));
assert!(geometry_type_matches(LineString, LineString));
assert!(geometry_type_matches(Point, Geometry));
assert!(geometry_type_matches(CircularString, Geometry));
assert!(geometry_type_matches(MultiPoint, GeometryCollection));
assert!(geometry_type_matches(MultiSurface, GeometryCollection));
assert!(geometry_type_matches(
GeometryCollection,
GeometryCollection
));
assert!(!geometry_type_matches(Point, GeometryCollection));
assert!(!geometry_type_matches(LineString, MultiLineString));
assert!(!geometry_type_matches(Point, LineString));
}
#[test]
fn reads_wkb_type_code_including_curves() {
assert_eq!(
wkb_geometry_type(&wkb_point(0.0, 0.0)).unwrap(),
GeometryType::Point
);
let mut point_zm = vec![1u8];
point_zm.extend_from_slice(&3001u32.to_le_bytes());
assert_eq!(wkb_geometry_type(&point_zm).unwrap(), GeometryType::Point);
let mut circular = vec![1u8];
circular.extend_from_slice(&8u32.to_le_bytes());
assert_eq!(
wkb_geometry_type(&circular).unwrap(),
GeometryType::CircularString
);
assert!(matches!(
wkb_geometry_type(b"\x01\x00"),
Err(GeometryError::TruncatedWkb)
));
let mut unknown = vec![1u8];
unknown.extend_from_slice(&99u32.to_le_bytes());
assert!(matches!(
wkb_geometry_type(&unknown),
Err(GeometryError::UnknownWkbType(99))
));
}
#[test]
fn matches_declared_on_parsed_geometry() {
let blob = gpb(&wkb_point(1.0, 2.0));
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.geometry_type(), GeometryType::Point);
assert!(g.matches_declared(GeometryType::Point));
assert!(g.matches_declared(GeometryType::Geometry));
assert!(!g.matches_declared(GeometryType::LineString));
}
fn wkb_point_z(x: f64, y: f64, z: f64) -> Vec<u8> {
let mut b = vec![1u8];
b.extend_from_slice(&1001u32.to_le_bytes());
b.extend_from_slice(&x.to_le_bytes());
b.extend_from_slice(&y.to_le_bytes());
b.extend_from_slice(&z.to_le_bytes());
b
}
#[test]
#[cfg(feature = "geo-types")]
fn encode_gpb_xy_point_roundtrips() {
let point = geo_types::Point::new(3.0, 4.0);
let (blob, xy) = encode_gpb(&point, 4326).unwrap();
assert_eq!(xy, Some([3.0, 3.0, 4.0, 4.0]));
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.header().srs_id, 4326);
assert_eq!(g.header().envelope, Envelope::Xy([3.0, 3.0, 4.0, 4.0]));
assert!(!g.header().empty);
assert_eq!(g.geometry_type(), GeometryType::Point);
}
#[test]
#[cfg(feature = "geo-types")]
fn encode_gpb_linestring_envelope() {
let ls = geo_types::LineString::from(vec![(0.0, 0.0), (10.0, -3.0), (4.0, 8.0)]);
let (blob, xy) = encode_gpb(&ls, 4326).unwrap();
assert_eq!(xy, Some([0.0, 10.0, -3.0, 8.0]));
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(g.header().envelope, Envelope::Xy([0.0, 10.0, -3.0, 8.0]));
assert_eq!(g.geometry_type(), GeometryType::LineString);
}
#[test]
fn encode_gpb_z_point_writes_xyz_envelope() {
let src_blob = gpb(&wkb_point_z(1.0, 2.0, 9.0));
let src = GpbGeometry::parse(&src_blob).unwrap();
assert_eq!(src.dim(), Dimensions::Xyz);
let (blob, xy) = encode_gpb(&src, 4326).unwrap();
assert_eq!(xy, Some([1.0, 1.0, 2.0, 2.0]));
let g = GpbGeometry::parse(&blob).unwrap();
assert_eq!(
g.header().envelope,
Envelope::Xyz([1.0, 1.0, 2.0, 2.0, 9.0, 9.0])
);
assert_eq!(g.dim(), Dimensions::Xyz);
assert_eq!(g.geometry_type(), GeometryType::Point);
}
#[test]
fn write_envelope_empty_point_is_empty() {
let blob = gpb(&wkb_point(f64::NAN, f64::NAN));
let g = GpbGeometry::parse(&blob).unwrap();
let (envelope, empty) = write_envelope(&g);
assert_eq!(envelope, Envelope::None);
assert!(empty);
let (reblob, xy) = encode_gpb(&g, 4326).unwrap();
assert_eq!(xy, None);
let re = GpbGeometry::parse(&reblob).unwrap();
assert_eq!(re.header().envelope, Envelope::None);
assert!(re.header().empty);
}
}
#[cfg(test)]
mod encode_from_wkb_tests {
use super::*;
use geo_types::{Geometry, LineString, Point};
#[test]
fn agrees_with_encode_gpb() {
let cases: Vec<Geometry<f64>> = vec![
Geometry::Point(Point::new(1.5, -2.5)),
Geometry::LineString(LineString::from(vec![(0.0, 0.0), (10.0, 5.0), (-3.0, 7.5)])),
];
for geometry in cases {
let (expected, expected_xy) = encode_gpb(&geometry, 4326).unwrap();
let (_, offset) = gpb::parse_header(&expected).unwrap();
let actual = encode_gpb_from_wkb(&expected[offset..], 4326).unwrap();
assert_eq!(actual.blob, expected, "blob differs");
assert_eq!(actual.xy_envelope, expected_xy, "envelope differs");
}
}
#[test]
fn trailing_bytes_are_not_copied() {
let (blob, _) = encode_gpb(&Point::new(3.0, 4.0), 4326).unwrap();
let (_, offset) = gpb::parse_header(&blob).unwrap();
let mut body = blob[offset..].to_vec();
let clean = encode_gpb_from_wkb(&body, 4326).unwrap().blob;
body.extend_from_slice(b"trailing rubbish");
let padded = encode_gpb_from_wkb(&body, 4326).unwrap().blob;
assert_eq!(clean, padded, "trailing bytes reached the blob");
}
#[test]
fn a_body_that_is_not_wkb_is_rejected() {
encode_gpb_from_wkb(&[], 4326).unwrap_err();
encode_gpb_from_wkb(b"not wkb at all", 4326).unwrap_err();
encode_gpb_from_wkb(&[1, 0xff, 0xff, 0, 0], 4326).unwrap_err();
}
}