use crate::geometry::{GeometryError, XyBounds, XyzBounds};
use crate::gpb;
use crate::types::{GeometryType, GeometryTypeSet};
use geo_traits::Dimensions;
const MAX_DEPTH: u32 = 32;
const COLLINEAR_TOLERANCE: f64 = 1e-14;
pub fn xy_envelope(wkb_body: &[u8]) -> Result<Option<[f64; 4]>, GeometryError> {
Ok(scan(wkb_body)?.xy_envelope)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BodyScan {
pub envelope: gpb::Envelope,
pub xy_envelope: Option<[f64; 4]>,
pub empty: bool,
pub dimensions: Dimensions,
pub len: usize,
pub extension_types: GeometryTypeSet,
}
pub fn scan(wkb_body: &[u8]) -> Result<BodyScan, GeometryError> {
let mut cursor = Cursor::new(wkb_body);
let mut bounds = XyzBounds::new();
let mut extension_types = GeometryTypeSet::new();
let dimensions = read_geometry(&mut cursor, &mut bounds, &mut extension_types, 0)?;
let len = cursor.offset;
let Some([min_x, max_x, min_y, max_y]) = bounds.xy_bounds() else {
return Ok(BodyScan {
envelope: gpb::Envelope::None,
xy_envelope: None,
empty: true,
dimensions,
len,
extension_types,
});
};
let envelope = match bounds.z_bounds() {
Some((min_z, max_z)) => gpb::Envelope::Xyz([min_x, max_x, min_y, max_y, min_z, max_z]),
None => gpb::Envelope::Xy([min_x, max_x, min_y, max_y]),
};
Ok(BodyScan {
envelope,
xy_envelope: Some([min_x, max_x, min_y, max_y]),
empty: false,
dimensions,
len,
extension_types,
})
}
pub fn arc_envelope(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2]) -> Option<[f64; 4]> {
let mut bounds = XyBounds::new();
for [x, y] in [p0, p1, p2] {
bounds.add(x, y);
}
for [x, y] in arc_extremes(p0, p1, p2).into_iter().flatten() {
bounds.add(x, y);
}
bounds.finish()
}
fn arc_extremes(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2]) -> [Option<[f64; 2]>; 4] {
let [x0, y0] = p0;
let [x2, y2] = p2;
let Some(([cx, cy], radius)) = arc_centre(p0, p1, p2) else {
return [None; 4];
};
let candidates = [
[cx - radius, cy],
[cx + radius, cy],
[cx, cy - radius],
[cx, cy + radius],
];
#[expect(
clippy::float_cmp,
reason = "the closed-circle convention is written as an identical point, so exact equality is the encoding being tested, not an approximation of one"
)]
let closes_the_circle = x0 == x2 && y0 == y2;
if closes_the_circle {
return candidates.map(Some);
}
let interior = chord_side(p0, p2, p1);
candidates.map(|candidate| (chord_side(p0, p2, candidate) == interior).then_some(candidate))
}
fn arc_centre(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2]) -> Option<([f64; 2], f64)> {
let [x0, y0] = p0;
let [x1, y1] = p1;
let [x2, y2] = p2;
#[expect(
clippy::float_cmp,
reason = "the closed-circle convention is written as an identical point, so exact equality is the encoding being tested, not an approximation of one"
)]
let closes_the_circle = x0 == x2 && y0 == y2;
if closes_the_circle {
let centre = [x0 + (x1 - x0) / 2.0, y0 + (y1 - y0) / 2.0];
let radius = (x1 - x0).hypot(y1 - y0) / 2.0;
let [centre_x, centre_y] = centre;
if !centre_x.is_finite() || !centre_y.is_finite() || !radius.is_finite() {
return None;
}
return Some((centre, radius));
}
let (ax, ay) = (x0 - x1, y0 - y1);
let (cx, cy) = (x2 - x1, y2 - y1);
let cross = ax * cy - ay * cx;
let scale = ax.abs().max(ay.abs()).max(cx.abs()).max(cy.abs());
if !cross.is_finite() || cross.abs() <= COLLINEAR_TOLERANCE * scale * scale {
return None;
}
let a2 = ax * ax + ay * ay;
let c2 = cx * cx + cy * cy;
let denominator = 2.0 * cross;
let ox = (cy * a2 - ay * c2) / denominator;
let oy = (ax * c2 - cx * a2) / denominator;
let radius = ox.hypot(oy);
if !ox.is_finite() || !oy.is_finite() || !radius.is_finite() {
return None;
}
Some(([x1 + ox, y1 + oy], radius))
}
fn chord_side(a: [f64; 2], b: [f64; 2], q: [f64; 2]) -> i8 {
let [ax, ay] = a;
let [bx, by] = b;
let [qx, qy] = q;
let cross = (bx - ax) * (qy - ay) - (by - ay) * (qx - ax);
if cross > 0.0 {
1
} else if cross < 0.0 {
-1
} else {
0
}
}
fn read_geometry(
cursor: &mut Cursor<'_>,
bounds: &mut XyzBounds,
extension_types: &mut GeometryTypeSet,
depth: u32,
) -> Result<Dimensions, GeometryError> {
if depth > MAX_DEPTH {
return Err(GeometryError::NestingTooDeep);
}
let little_endian = cursor.read_byte_order()?;
let code = cursor.read_u32(little_endian)?;
let (base, coord) = decode_type(code)?;
if let Some(ty) = GeometryType::from_wkb_base(base).filter(|ty| ty.is_extension()) {
extension_types.insert(ty);
}
match base {
1 => {
let ([x, y], z) = cursor.read_coord(little_endian, coord)?;
bounds.add(x, y, z);
}
2 => read_coords(cursor, little_endian, coord, bounds)?,
3 => {
let rings = cursor.read_u32(little_endian)?;
for _ in 0..rings {
read_coords(cursor, little_endian, coord, bounds)?;
}
}
8 => read_circular_string(cursor, little_endian, coord, bounds)?,
4..=7 | 9..=12 => {
let count = cursor.read_u32(little_endian)?;
for _ in 0..count {
read_geometry(cursor, bounds, extension_types, depth + 1)?;
}
}
_ => return Err(GeometryError::AbstractWkbType(code)),
}
Ok(coord.dimensions)
}
fn read_coords(
cursor: &mut Cursor<'_>,
little_endian: bool,
coord: CoordLayout,
bounds: &mut XyzBounds,
) -> Result<(), GeometryError> {
let count = cursor.read_u32(little_endian)?;
for _ in 0..count {
let ([x, y], z) = cursor.read_coord(little_endian, coord)?;
bounds.add(x, y, z);
}
Ok(())
}
fn read_circular_string(
cursor: &mut Cursor<'_>,
little_endian: bool,
coord: CoordLayout,
bounds: &mut XyzBounds,
) -> Result<(), GeometryError> {
let count = cursor.read_u32(little_endian)?;
let mut start: Option<[f64; 2]> = None;
let mut middle: Option<[f64; 2]> = None;
for _ in 0..count {
let (point, z) = cursor.read_coord(little_endian, coord)?;
let [x, y] = point;
bounds.add(x, y, z);
match (start, middle) {
(None, _) => start = Some(point),
(Some(_), None) => middle = Some(point),
(Some(from), Some(via)) => {
for [ex, ey] in arc_extremes(from, via, point).into_iter().flatten() {
bounds.add(ex, ey, None);
}
start = Some(point);
middle = None;
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
struct CoordLayout {
stride: usize,
has_z: bool,
dimensions: Dimensions,
}
fn decode_type(code: u32) -> Result<(u32, CoordLayout), GeometryError> {
if code & 0xE000_0000 != 0 {
return Err(GeometryError::UnknownWkbType(code));
}
let coord = match code / 1000 {
0 => CoordLayout {
stride: 2,
has_z: false,
dimensions: Dimensions::Xy,
},
1 => CoordLayout {
stride: 3,
has_z: true,
dimensions: Dimensions::Xyz,
},
2 => CoordLayout {
stride: 3,
has_z: false,
dimensions: Dimensions::Xym,
},
3 => CoordLayout {
stride: 4,
has_z: true,
dimensions: Dimensions::Xyzm,
},
_ => return Err(GeometryError::UnknownWkbType(code)),
};
let base = code % 1000;
if base == 0 || base > 14 {
return Err(GeometryError::UnknownWkbType(code));
}
Ok((base, coord))
}
struct Cursor<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> Cursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
fn read_byte_order(&mut self) -> Result<bool, GeometryError> {
let offset = self.offset;
match self.take(1)? {
[0] => Ok(false),
[1] => Ok(true),
_ => Err(GeometryError::InvalidByteOrder { offset }),
}
}
fn read_u32(&mut self, little_endian: bool) -> Result<u32, GeometryError> {
let &[b0, b1, b2, b3] = self.take(4)? else {
return Err(GeometryError::TruncatedAt {
offset: self.offset,
});
};
let bytes = [b0, b1, b2, b3];
Ok(if little_endian {
u32::from_le_bytes(bytes)
} else {
u32::from_be_bytes(bytes)
})
}
fn read_f64(&mut self, little_endian: bool) -> Result<f64, GeometryError> {
let &[b0, b1, b2, b3, b4, b5, b6, b7] = self.take(8)? else {
return Err(GeometryError::TruncatedAt {
offset: self.offset,
});
};
let bytes = [b0, b1, b2, b3, b4, b5, b6, b7];
Ok(if little_endian {
f64::from_le_bytes(bytes)
} else {
f64::from_be_bytes(bytes)
})
}
fn read_coord(
&mut self,
little_endian: bool,
coord: CoordLayout,
) -> Result<([f64; 2], Option<f64>), GeometryError> {
let x = self.read_f64(little_endian)?;
let y = self.read_f64(little_endian)?;
let z = if coord.stride >= 3 {
let third = self.read_f64(little_endian)?;
coord.has_z.then_some(third)
} else {
None
};
let skipped = coord.stride.saturating_sub(3).saturating_mul(8);
self.take(skipped)?;
Ok(([x, y], z))
}
fn take(&mut self, len: usize) -> Result<&'a [u8], GeometryError> {
let end = self
.offset
.checked_add(len)
.ok_or(GeometryError::TruncatedAt {
offset: self.offset,
})?;
let slice = self
.bytes
.get(self.offset..end)
.ok_or(GeometryError::TruncatedAt {
offset: self.offset,
})?;
self.offset = end;
Ok(slice)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[track_caller]
fn assert_envelope(actual: Option<[f64; 4]>, expected: [f64; 4], tolerance: f64) {
let Some(actual) = actual else {
panic!("expected an envelope, got None");
};
let [amin_x, amax_x, amin_y, amax_y] = actual;
let [emin_x, emax_x, emin_y, emax_y] = expected;
for (got, want, name) in [
(amin_x, emin_x, "min_x"),
(amax_x, emax_x, "max_x"),
(amin_y, emin_y, "min_y"),
(amax_y, emax_y, "max_y"),
] {
assert!(
(got - want).abs() <= tolerance,
"{name}: got {got}, want {want} (tolerance {tolerance})"
);
}
}
fn on_circle(centre: [f64; 2], radius: f64, t: f64) -> [f64; 2] {
let [cx, cy] = centre;
[cx + radius * t.cos(), cy + radius * t.sin()]
}
fn arc_controls(
centre: [f64; 2],
radius: f64,
start: f64,
sweep: f64,
) -> ([f64; 2], [f64; 2], [f64; 2]) {
(
on_circle(centre, radius, start),
on_circle(centre, radius, start + sweep / 2.0),
on_circle(centre, radius, start + sweep),
)
}
fn wkb(code: u32, payload: &[u8]) -> Vec<u8> {
let mut bytes = vec![1u8];
bytes.extend_from_slice(&code.to_le_bytes());
bytes.extend_from_slice(payload);
bytes
}
fn coords(points: &[[f64; 2]]) -> Vec<u8> {
let count = u32::try_from(points.len()).expect("test point count fits in u32");
let mut bytes = count.to_le_bytes().to_vec();
for [x, y] in points {
bytes.extend_from_slice(&x.to_le_bytes());
bytes.extend_from_slice(&y.to_le_bytes());
}
bytes
}
fn children(parts: &[Vec<u8>]) -> Vec<u8> {
let count = u32::try_from(parts.len()).expect("test child count fits in u32");
let mut bytes = count.to_le_bytes().to_vec();
for part in parts {
bytes.extend_from_slice(part);
}
bytes
}
#[test]
fn upper_semicircle_reaches_its_top() {
let envelope = arc_envelope([-1.0, 0.0], [0.0, 1.0], [1.0, 0.0]);
assert_envelope(envelope, [-1.0, 1.0, 0.0, 1.0], 1e-12);
}
#[test]
fn arc_bulges_past_its_control_points() {
let (p0, p1, p2) = arc_controls([0.0, 0.0], 1.0, PI / 9.0, 35.0 * PI / 18.0);
let control_box = {
let mut bounds = XyBounds::new();
for [x, y] in [p0, p1, p2] {
bounds.add(x, y);
}
bounds.finish().expect("control points are finite")
};
let envelope = arc_envelope(p0, p1, p2).expect("arc has an envelope");
let [cmin_x, cmax_x, cmin_y, cmax_y] = control_box;
let [amin_x, amax_x, amin_y, amax_y] = envelope;
assert!(
amin_x < cmin_x && amax_x > cmax_x && amin_y < cmin_y && amax_y > cmax_y,
"arc box {envelope:?} should be strictly wider than the control box {control_box:?}"
);
}
#[test]
fn shallow_arc_reaching_no_extreme_stays_tight() {
let (p0, p1, p2) = arc_controls([0.0, 0.0], 1.0, PI / 18.0, 2.0 * PI / 9.0);
let expected = [
(50.0_f64).to_radians().cos(),
(10.0_f64).to_radians().cos(),
(10.0_f64).to_radians().sin(),
(50.0_f64).to_radians().sin(),
];
assert_envelope(arc_envelope(p0, p1, p2), expected, 1e-12);
}
#[test]
fn matched_endpoints_give_the_whole_circle() {
let envelope = arc_envelope([1.0, 0.0], [-1.0, 0.0], [1.0, 0.0]);
assert_envelope(envelope, [-1.0, 1.0, -1.0, 1.0], 1e-12);
}
#[test]
fn collinear_points_bound_the_chord() {
let envelope = arc_envelope([0.0, 0.0], [1.0, 1.0], [2.0, 2.0]);
assert_envelope(envelope, [0.0, 2.0, 0.0, 2.0], 0.0);
}
#[test]
fn coincident_points_bound_themselves() {
let envelope = arc_envelope([5.0, 5.0], [5.0, 5.0], [5.0, 5.0]);
assert_envelope(envelope, [5.0, 5.0, 5.0, 5.0], 0.0);
}
#[test]
fn a_flat_arc_gives_up_less_than_the_rtree_rounds_away() {
let chord = 1000.0_f64;
let half_chord = chord / 2.0;
let mut worst_relative = 0.0_f64;
for exponent in 4..30 {
let radius = 10.0_f64.powi(exponent) * chord;
let sagitta = half_chord * half_chord
/ (radius + (radius * radius - half_chord * half_chord).sqrt());
for step in 1..100 {
let x1 = half_chord * (f64::from(step) / 50.0 - 1.0);
let y1 = sagitta - x1 * x1 / (radius + (radius * radius - x1 * x1).sqrt());
let p0 = [-half_chord, 0.0];
let p1 = [x1, y1];
let p2 = [half_chord, 0.0];
let [_, _, _, max_y] = arc_envelope(p0, p1, p2).expect("control points are finite");
worst_relative = worst_relative.max((sagitta - max_y).max(0.0) / chord);
}
}
let f32_step = f64::from(f32::EPSILON) * half_chord / chord;
assert!(
worst_relative < f32_step,
"worst shortfall {worst_relative} of the coordinate scale, \
against an f32 step of {f32_step}"
);
}
#[test]
fn nearly_collinear_arc_does_not_blow_up() {
let envelope = arc_envelope([0.0, 0.0], [1.0, 0.001], [2.0, 0.0]);
assert_envelope(envelope, [0.0, 2.0, 0.0, 0.001], 1e-9);
}
#[test]
fn non_finite_control_points_give_no_envelope() {
let envelope = arc_envelope(
[f64::NAN, f64::NAN],
[f64::NAN, f64::NAN],
[f64::NAN, f64::NAN],
);
assert!(envelope.is_none());
}
#[test]
fn envelope_contains_a_dense_sample_of_every_arc() {
let centre = [412_345.0, 5_678_901.0];
let radius = 1_234.5;
for start_step in 0..12 {
for sweep_step in 1..24 {
let start = f64::from(start_step) * PI / 6.0;
let sweep = f64::from(sweep_step) * (2.0 * PI - 0.05) / 24.0;
let (p0, p1, p2) = arc_controls(centre, radius, start, sweep);
let [min_x, max_x, min_y, max_y] =
arc_envelope(p0, p1, p2).expect("arc has an envelope");
for sample in 0..=4096 {
let t = start + sweep * f64::from(sample) / 4096.0;
let [x, y] = on_circle(centre, radius, t);
assert!(
x >= min_x - 1e-6
&& x <= max_x + 1e-6
&& y >= min_y - 1e-6
&& y <= max_y + 1e-6,
"sample ({x}, {y}) outside {:?} for start {start} sweep {sweep}",
[min_x, max_x, min_y, max_y]
);
}
}
}
}
#[test]
fn envelope_is_no_larger_than_a_dense_sample_needs() {
let centre = [-3.5, 47.25];
let radius = 2.75;
for start_step in 0..12 {
for sweep_step in 1..24 {
let start = f64::from(start_step) * PI / 6.0;
let sweep = f64::from(sweep_step) * (2.0 * PI - 0.05) / 24.0;
let (p0, p1, p2) = arc_controls(centre, radius, start, sweep);
let envelope = arc_envelope(p0, p1, p2);
let mut sampled = XyBounds::new();
for sample in 0..=4096 {
let t = start + sweep * f64::from(sample) / 4096.0;
let [x, y] = on_circle(centre, radius, t);
sampled.add(x, y);
}
let expected = sampled.finish().expect("samples are finite");
assert_envelope(envelope, expected, 1e-5);
}
}
}
#[test]
fn reads_a_circular_string() {
let body = wkb(8, &coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0]]));
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 1.0, 0.0, 1.0],
1e-12,
);
}
#[test]
fn reads_a_chain_of_arcs() {
let body = wkb(
8,
&coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0], [3.0, 2.0], [5.0, 0.0]]),
);
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 5.0, 0.0, 2.0],
1e-12,
);
}
#[test]
fn reads_a_compound_curve_of_line_and_arc() {
let line = wkb(2, &coords(&[[-3.0, -1.0], [-1.0, 0.0]]));
let arc = wkb(8, &coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0]]));
let body = wkb(9, &children(&[line, arc]));
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-3.0, 1.0, -1.0, 1.0],
1e-12,
);
}
#[test]
fn reads_a_curve_polygon_whose_rings_are_full_geometries() {
let ring = wkb(8, &coords(&[[1.0, 0.0], [-1.0, 0.0], [1.0, 0.0]]));
let body = wkb(10, &children(&[ring]));
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 1.0, -1.0, 1.0],
1e-12,
);
}
#[test]
fn reads_polygon_rings_as_bare_sequences() {
let mut payload = 1u32.to_le_bytes().to_vec();
payload.extend_from_slice(&coords(&[[0.0, 0.0], [4.0, 0.0], [4.0, 3.0], [0.0, 0.0]]));
let body = wkb(3, &payload);
assert_envelope(
xy_envelope(&body).expect("valid body"),
[0.0, 4.0, 0.0, 3.0],
0.0,
);
}
#[test]
fn reads_a_multisurface_of_curve_polygons() {
let ring = wkb(8, &coords(&[[1.0, 0.0], [-1.0, 0.0], [1.0, 0.0]]));
let curve_polygon = wkb(10, &children(&[ring]));
let body = wkb(12, &children(&[curve_polygon]));
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 1.0, -1.0, 1.0],
1e-12,
);
}
#[test]
fn reads_a_geometry_collection_holding_a_curve() {
let point = wkb(1, &{
let mut b = 9.0_f64.to_le_bytes().to_vec();
b.extend_from_slice(&9.0_f64.to_le_bytes());
b
});
let arc = wkb(8, &coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0]]));
let body = wkb(7, &children(&[point, arc]));
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 9.0, 0.0, 9.0],
1e-12,
);
}
#[test]
fn reads_a_big_endian_child_inside_a_little_endian_parent() {
let mut arc = vec![0u8];
arc.extend_from_slice(&8u32.to_be_bytes());
arc.extend_from_slice(&3u32.to_be_bytes());
for [x, y] in [[-1.0_f64, 0.0_f64], [0.0, 1.0], [1.0, 0.0]] {
arc.extend_from_slice(&x.to_be_bytes());
arc.extend_from_slice(&y.to_be_bytes());
}
let body = wkb(11, &children(&[arc]));
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 1.0, 0.0, 1.0],
1e-12,
);
}
#[test]
fn skips_z_and_m_without_letting_them_widen_the_box() {
let mut payload = 3u32.to_le_bytes().to_vec();
for ([x, y], z, m) in [
([-1.0_f64, 0.0_f64], 100.0_f64, -100.0_f64),
([0.0, 1.0], 200.0, -200.0),
([1.0, 0.0], 300.0, -300.0),
] {
payload.extend_from_slice(&x.to_le_bytes());
payload.extend_from_slice(&y.to_le_bytes());
payload.extend_from_slice(&z.to_le_bytes());
payload.extend_from_slice(&m.to_le_bytes());
}
let body = wkb(3008, &payload);
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 1.0, 0.0, 1.0],
1e-12,
);
}
#[test]
fn a_z_body_gets_an_xyz_envelope_and_an_m_body_does_not() {
let points = [
([-1.0_f64, 0.0_f64], 5.0_f64),
([0.0, 1.0], 9.0),
([1.0, 0.0], 7.0),
];
let payload = |third_written: bool| {
let mut bytes = 3u32.to_le_bytes().to_vec();
for ([x, y], third) in points {
bytes.extend_from_slice(&x.to_le_bytes());
bytes.extend_from_slice(&y.to_le_bytes());
if third_written {
bytes.extend_from_slice(&third.to_le_bytes());
}
}
bytes
};
let z = scan(&wkb(1008, &payload(true))).expect("valid body");
assert!(!z.empty);
assert_eq!(z.dimensions, Dimensions::Xyz);
assert_eq!(
z.envelope,
gpb::Envelope::Xyz([-1.0, 1.0, 0.0, 1.0, 5.0, 9.0])
);
let m = scan(&wkb(2008, &payload(true))).expect("valid body");
assert!(!m.empty);
assert_eq!(m.dimensions, Dimensions::Xym);
assert_eq!(m.envelope, gpb::Envelope::Xy([-1.0, 1.0, 0.0, 1.0]));
}
#[test]
fn an_empty_curve_body_is_reported_empty_by_the_write_path() {
let scanned = scan(&wkb(8, &coords(&[]))).expect("valid body");
assert_eq!(scanned.envelope, gpb::Envelope::None);
assert_eq!(scanned.xy_envelope, None);
assert!(scanned.empty);
}
#[test]
fn a_scan_reports_the_geometry_extent_without_trailing_bytes() {
let mut body = wkb(8, &coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0]]));
let extent = body.len();
body.extend_from_slice(b"trailing rubbish");
assert_eq!(scan(&body).expect("valid body").len, extent);
}
#[test]
fn encoding_a_curve_body_sets_the_extended_flag_and_a_true_envelope() {
use crate::geometry::encode_gpb_from_wkb;
let body = wkb(8, &coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0]]));
let encoded = encode_gpb_from_wkb(&body, 4326).expect("valid curve body");
let (header, offset) = gpb::parse_header(&encoded.blob).expect("valid header");
assert!(header.extended, "Annex F.1 requires the extended flag");
assert!(!header.empty);
assert_eq!(
header.envelope,
gpb::Envelope::Xy([-1.0, 1.0, 0.0, 1.0]),
"header envelope should bound the arc"
);
assert_eq!(encoded.xy_envelope, Some([-1.0, 1.0, 0.0, 1.0]));
assert_eq!(encoded.dimensions, Dimensions::Xy);
assert_eq!(
encoded.blob.get(offset..),
Some(body.as_slice()),
"the body should be copied through unchanged"
);
}
#[test]
fn an_empty_point_has_no_envelope() {
let mut payload = f64::NAN.to_le_bytes().to_vec();
payload.extend_from_slice(&f64::NAN.to_le_bytes());
let body = wkb(1, &payload);
assert!(xy_envelope(&body).expect("valid body").is_none());
}
#[test]
fn an_even_point_count_still_bounds_its_points() {
let body = wkb(
8,
&coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0], [7.0, 7.0]]),
);
assert_envelope(
xy_envelope(&body).expect("valid body"),
[-1.0, 7.0, 0.0, 7.0],
1e-12,
);
}
#[test]
fn a_truncated_body_is_an_error() {
let full = wkb(8, &coords(&[[-1.0, 0.0], [0.0, 1.0], [1.0, 0.0]]));
let truncated = full
.get(..full.len() - 8)
.expect("body is longer than 8 bytes");
assert!(matches!(
xy_envelope(truncated),
Err(GeometryError::TruncatedAt { .. })
));
}
#[test]
fn a_count_larger_than_the_body_is_an_error() {
let mut payload = u32::MAX.to_le_bytes().to_vec();
payload.extend_from_slice(&0.0_f64.to_le_bytes());
payload.extend_from_slice(&0.0_f64.to_le_bytes());
let body = wkb(8, &payload);
assert!(matches!(
xy_envelope(&body),
Err(GeometryError::TruncatedAt { .. })
));
}
#[test]
fn an_abstract_supertype_is_rejected() {
let body = wkb(13, &coords(&[[0.0, 0.0]]));
assert!(matches!(
xy_envelope(&body),
Err(GeometryError::AbstractWkbType(13))
));
}
#[test]
fn ewkb_is_rejected_rather_than_guessed_at() {
let body = wkb(0x8000_0002, &coords(&[[0.0, 0.0]]));
assert!(matches!(
xy_envelope(&body),
Err(GeometryError::UnknownWkbType(_))
));
}
#[test]
fn an_invalid_byte_order_marker_is_an_error() {
let mut body = vec![7u8];
body.extend_from_slice(&8u32.to_le_bytes());
assert!(matches!(
xy_envelope(&body),
Err(GeometryError::InvalidByteOrder { .. })
));
}
#[test]
fn nesting_past_the_limit_is_an_error_not_a_stack_overflow() {
let mut body = wkb(1, &{
let mut b = 0.0_f64.to_le_bytes().to_vec();
b.extend_from_slice(&0.0_f64.to_le_bytes());
b
});
for _ in 0..(MAX_DEPTH + 2) {
body = wkb(7, &children(&[body]));
}
assert!(matches!(
xy_envelope(&body),
Err(GeometryError::NestingTooDeep)
));
}
}