osm-katana/src/shared/gpu_backend.rs
#![allow(clippy::float_arithmetic, clippy::arithmetic_side_effects)]
pub struct RawCoord {
pub lat: f32,
pub lon: f32,
}
/// A way with resolved coordinates, ready for WKB encoding.
pub struct RawWay {
/// (lon, lat) pairs in WGS-84 — original coordinate order.
pub coords: Vec<(f64, f64)>,
/// true → encode as Polygon exterior, false → encode as LineString.
pub is_area: bool,
}
pub struct Bbox {
pub min_x: f32,
pub min_y: f32,
pub max_x: f32,
pub max_y: f32,
}
pub trait GpuBackend: Send + Sync {
fn project_mercator(&self, coords: &[RawCoord]) -> Vec<(f32, f32)>;
fn compute_bbox(&self, point_sets: &[Vec<(f32, f32)>]) -> Vec<Bbox>;
fn encode_wkb(&self, ways: &[RawWay]) -> Vec<Option<Vec<u8>>>;
}
pub struct CpuBackend;
impl GpuBackend for CpuBackend {
fn project_mercator(&self, coords: &[RawCoord]) -> Vec<(f32, f32)> {
coords
.iter()
.map(|c| {
let x = (c.lon + 180.0) / 360.0;
let lat_r = c.lat.to_radians();
let y = (1.0 - (lat_r.tan() + 1.0 / lat_r.cos()).ln() / std::f32::consts::PI) / 2.0;
(x, y)
})
.collect()
}
fn compute_bbox(&self, point_sets: &[Vec<(f32, f32)>]) -> Vec<Bbox> {
point_sets
.iter()
.map(|pts| {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for &(x, y) in pts {
if x < min_x {
min_x = x;
}
if y < min_y {
min_y = y;
}
if x > max_x {
max_x = x;
}
if y > max_y {
max_y = y;
}
}
Bbox {
min_x,
min_y,
max_x,
max_y,
}
})
.collect()
}
fn encode_wkb(&self, ways: &[RawWay]) -> Vec<Option<Vec<u8>>> {
ways.iter()
.map(|way| {
if way.coords.len() < 2 {
return None;
}
Some(if way.is_area && way.coords.len() >= 4 {
wkb_polygon(&way.coords)
} else {
wkb_linestring(&way.coords)
})
})
.collect()
}
}
fn wkb_linestring(coords: &[(f64, f64)]) -> Vec<u8> {
let n = coords.len();
let mut buf = Vec::with_capacity(9 + n * 16);
buf.push(1u8);
buf.extend_from_slice(&2u32.to_le_bytes());
buf.extend_from_slice(&(n as u32).to_le_bytes());
for &(x, y) in coords {
buf.extend_from_slice(&x.to_le_bytes());
buf.extend_from_slice(&y.to_le_bytes());
}
buf
}
/// **The ring is emitted CLOSED** — the same rule `shared::geometry::push_wkb_ring`
/// states ("Every ring is emitted closed"), which this encoder predates and did not
/// follow. SEEN RED on the live Sweden cut 2026-09-02 by
/// `osm-katana verify --geometry-quality`: **99 unclosed polygon rings** in
/// `ways::geometry`, every sampled one an OSM area way whose own node list does not
/// return to its first node (NMD2018 landuse imports, a nature reserve, …). The tag
/// ladder rightly calls them areas; encoding the broken ring VERBATIM handed every
/// downstream reader an unfillable "polygon" — `is_closed_ring` refuses, the fill
/// drops, and the area strokes as an outline. Closing at encode is the one writer
/// where the repair is free and loses nothing: the intended geometry of an area way
/// IS the closed ring.
fn wkb_polygon(exterior: &[(f64, f64)]) -> Vec<u8> {
let close = usize::from(exterior.len() > 1 && exterior.first() != exterior.last());
let n = exterior.len() + close;
let mut buf = Vec::with_capacity(13 + n * 16);
buf.push(1u8);
buf.extend_from_slice(&3u32.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&(n as u32).to_le_bytes());
for &(x, y) in exterior.iter().chain((close == 1).then(|| &exterior[0])) {
buf.extend_from_slice(&x.to_le_bytes());
buf.extend_from_slice(&y.to_le_bytes());
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
fn way(coords: Vec<(f64, f64)>, is_area: bool) -> RawWay {
RawWay { coords, is_area }
}
/// **An OPEN area way encodes to a CLOSED polygon ring** — the 99-row census red
/// (`verify --geometry-quality`, live Sweden cut 2026-09-02) pinned. An already
/// closed ring is not closed twice, and a line is untouched.
#[test]
fn an_open_area_way_is_encoded_as_a_closed_ring() {
let open = vec![(15.0, 60.0), (15.1, 60.0), (15.1, 60.1), (15.0, 60.1)];
let out = CpuBackend.encode_wkb(&[way(open.clone(), true)]);
let wkb = out[0].as_ref().expect("an area way encodes");
let shape = crate::shared::geometry::decode_wkb(wkb).expect("decodes");
let crate::shared::geometry::WkbShape::Polys(ps) = shape else {
panic!("an area way must encode as a polygon, got {shape:?}");
};
let ring = &ps[0].0;
assert_eq!(ring.len(), 5, "the open 4-vertex ring gains its closing vertex");
assert_eq!(ring.first(), ring.last(), "and really is closed");
// Already closed: byte count unchanged, no doubled seam.
let mut closed = open.clone();
closed.push(open[0]);
let out = CpuBackend.encode_wkb(&[way(closed, true)]);
let shape = crate::shared::geometry::decode_wkb(out[0].as_ref().unwrap()).unwrap();
let crate::shared::geometry::WkbShape::Polys(ps) = shape else { panic!() };
assert_eq!(ps[0].0.len(), 5, "a closed ring is not closed twice");
// A line is a line, verbatim.
let out = CpuBackend.encode_wkb(&[way(open.clone(), false)]);
let shape = crate::shared::geometry::decode_wkb(out[0].as_ref().unwrap()).unwrap();
let crate::shared::geometry::WkbShape::Lines(runs) = shape else {
panic!("a non-area way stays a LineString");
};
assert_eq!(runs[0], open);
}
}