Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy-zoomies

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);
    }
}