geozero-core 0.6.2

Zero-Copy reading and writing of geospatial data.
Documentation
use crate::wkb_common::{WKBByteOrder, WKBGeometryType};
use geozero::error::Result;
use geozero::{CoordDimensions, FeatureProcessor, GeomProcessor, PropertyProcessor};
use scroll::IOwrite;
use std::io::Write;

/// WKB writer.
pub struct WkbWriter<'a, W: Write> {
    pub dims: CoordDimensions,
    pub srid: Option<i32>,
    /// Geometry envelope (GPKG)
    pub envelope: Vec<f64>,
    /// Envelope dimensions (GPKG)
    pub envelope_dims: CoordDimensions,
    /// ExtendedGeoPackageBinary
    pub extended_gpkg: bool,
    /// Empty geometry flag (GPKG)
    pub empty: bool,
    endian: scroll::Endian,
    dialect: WkbDialect,
    first_header: bool,
    geom_state: GeomState,
    out: &'a mut W,
}

/// WKB dialect.
#[derive(PartialEq, Debug)]
pub enum WkbDialect {
    Wkb,
    Ewkb,
    Geopackage,
}

#[derive(PartialEq, Debug)]
enum GeomState {
    Normal,
    RingGeom,
    MultiPointGeom,
}

impl<'a, W: Write> WkbWriter<'a, W> {
    pub fn new(out: &'a mut W, dialect: WkbDialect) -> WkbWriter<'a, W> {
        WkbWriter {
            dims: CoordDimensions::default(),
            srid: None,
            envelope: Vec::new(),
            envelope_dims: CoordDimensions::default(),
            extended_gpkg: false,
            empty: false,
            endian: scroll::LE,
            dialect,
            first_header: true,
            geom_state: GeomState::Normal,
            out,
        }
    }

    /// Write header in selected format
    fn write_header(&mut self, wkb_type: WKBGeometryType) -> Result<()> {
        match self.dialect {
            WkbDialect::Wkb => self.write_wkb_header(wkb_type)?,
            WkbDialect::Ewkb => self.write_ewkb_header(wkb_type)?,
            WkbDialect::Geopackage => {
                if self.first_header {
                    self.write_gpkg_header()?;
                    self.first_header = false;
                }
                self.write_wkb_header(wkb_type)?;
            }
        }
        Ok(())
    }
    /// OGC WKB header
    fn write_wkb_header(&mut self, wkb_type: WKBGeometryType) -> Result<()> {
        let byte_order = if self.endian == scroll::BE {
            WKBByteOrder::XDR
        } else {
            WKBByteOrder::NDR
        };
        self.out.iowrite(byte_order as u8)?;
        let mut type_id = wkb_type as u32;
        if self.dims.z {
            type_id += 1000;
        }
        if self.dims.m {
            type_id += 2000;
        }
        self.out.iowrite_with(type_id, self.endian)?;
        Ok(())
    }

    /// EWKB header according to https://git.osgeo.org/gitea/postgis/postgis/src/branch/master/doc/ZMSgeoms.txt
    fn write_ewkb_header(&mut self, wkb_type: WKBGeometryType) -> Result<()> {
        let byte_order = if self.endian == scroll::BE {
            WKBByteOrder::XDR
        } else {
            WKBByteOrder::NDR
        };
        self.out.iowrite(byte_order as u8)?;

        let mut type_id = wkb_type as u32;
        if self.dims.z {
            type_id |= 0x80000000;
        }
        if self.dims.m {
            type_id |= 0x40000000;
        }
        if self.srid.is_some() && self.first_header {
            type_id |= 0x20000000;
        }
        self.out.iowrite_with(type_id, self.endian)?;

        if self.first_header {
            // write SRID in main header only
            if let Some(srid) = self.srid {
                self.out.iowrite_with(srid, self.endian)?;
            }
            self.first_header = false;
        }

        Ok(())
    }

    /// GPKG geometry header according to http://www.geopackage.org/spec/#gpb_format
    fn write_gpkg_header(&mut self) -> Result<()> {
        let magic = b"GP";
        self.out.write(magic)?;
        let version: u8 = 0;
        self.out.iowrite(version)?;

        let mut flags: u8 = 0;
        if self.extended_gpkg {
            flags |= 0b0010_0000;
        }
        if self.empty {
            flags |= 0b0001_0000;
        }
        let env_info: u8 = if self.envelope.len() == 0 {
            0 // no envelope
        } else {
            match (self.envelope_dims.z, self.envelope_dims.m) {
                (false, false) => 1, // [minx, maxx, miny, maxy]
                (true, false) => 2,  // [minx, maxx, miny, maxy, minz, maxz]
                (false, true) => 3,  // [minx, maxx, miny, maxy, minm, maxm]
                (true, true) => 4,   // [minx, maxx, miny, maxy, minz, maxz, minm, maxm]
            }
        };
        flags |= env_info << 1;
        if self.endian == scroll::LE {
            flags |= 0b0000_0001;
        }
        // println!("flags: {:#010b}", flags);
        self.out.iowrite(flags)?;

        // srs_id
        // 0: undefined geographic coordinate reference systems
        // -1: undefined Cartesian coordinate reference systems
        self.out.iowrite_with(self.srid.unwrap_or(0), self.endian)?;

        for val in &self.envelope {
            self.out.iowrite_with(*val, self.endian)?;
        }

        Ok(())
    }
}

impl<W: Write> GeomProcessor for WkbWriter<'_, W> {
    fn dimensions(&self) -> CoordDimensions {
        self.dims
    }
    fn xy(&mut self, x: f64, y: f64, _idx: usize) -> Result<()> {
        if self.geom_state == GeomState::MultiPointGeom {
            self.write_header(WKBGeometryType::Point)?;
        }
        self.out.iowrite_with(x, self.endian)?;
        self.out.iowrite_with(y, self.endian)?;
        Ok(())
    }
    fn coordinate(
        &mut self,
        x: f64,
        y: f64,
        z: Option<f64>,
        m: Option<f64>,
        _t: Option<f64>,
        _tm: Option<u64>,
        _idx: usize,
    ) -> Result<()> {
        if self.geom_state == GeomState::MultiPointGeom {
            self.write_header(WKBGeometryType::Point)?;
        }
        self.out.iowrite_with(x, self.endian)?;
        self.out.iowrite_with(y, self.endian)?;
        if let Some(z) = z {
            self.out.iowrite_with(z, self.endian)?;
        }
        if let Some(m) = m {
            self.out.iowrite_with(m, self.endian)?;
        }
        Ok(())
    }
    fn point_begin(&mut self, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::Point)
    }
    fn multipoint_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::MultiPoint)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        self.geom_state = GeomState::MultiPointGeom;
        Ok(())
    }
    fn multipoint_end(&mut self, _idx: usize) -> Result<()> {
        self.geom_state = GeomState::Normal;
        Ok(())
    }
    fn linestring_begin(&mut self, _tagged: bool, size: usize, _idx: usize) -> Result<()> {
        if self.geom_state != GeomState::RingGeom {
            self.write_header(WKBGeometryType::LineString)?;
        }
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn multilinestring_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::MultiLineString)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn polygon_begin(&mut self, _tagged: bool, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::Polygon)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        self.geom_state = GeomState::RingGeom;
        Ok(())
    }
    fn polygon_end(&mut self, _tagged: bool, _idx: usize) -> Result<()> {
        self.geom_state = GeomState::Normal;
        Ok(())
    }
    fn multipolygon_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::MultiPolygon)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn geometrycollection_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::GeometryCollection)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }

    fn circularstring_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::CircularString)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn compoundcurve_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::CompoundCurve)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn curvepolygon_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::CurvePolygon)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn multicurve_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::MultiCurve)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn multisurface_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::MultiSurface)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }

    fn triangle_begin(&mut self, _tagged: bool, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::Triangle)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        self.geom_state = GeomState::RingGeom;
        Ok(())
    }
    fn triangle_end(&mut self, _tagged: bool, _idx: usize) -> Result<()> {
        self.geom_state = GeomState::Normal;
        Ok(())
    }
    fn polyhedralsurface_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::PolyhedralSurface)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
    fn tin_begin(&mut self, size: usize, _idx: usize) -> Result<()> {
        self.write_header(WKBGeometryType::Tin)?;
        self.out.iowrite_with(size as u32, self.endian)?;
        Ok(())
    }
}

impl<W: Write> PropertyProcessor for WkbWriter<'_, W> {}

impl<W: Write> FeatureProcessor for WkbWriter<'_, W> {}

#[cfg(test)]
mod test {
    use super::*;
    use crate::wkb::{process_ewkb_geom, process_gpkg_geom};

    fn ewkb_roundtrip(ewkbstr: &str, with_z: bool, srid: Option<i32>) -> bool {
        let wkb_in = hex::decode(ewkbstr).unwrap();
        let mut wkb_out: Vec<u8> = Vec::new();
        let mut writer = WkbWriter::new(&mut wkb_out, WkbDialect::Ewkb);
        writer.dims.z = with_z;
        writer.srid = srid;
        assert!(process_ewkb_geom(&mut wkb_in.as_slice(), &mut writer).is_ok());
        let ok = wkb_out == wkb_in;
        if !ok {
            dbg!(hex::encode(&wkb_out));
        }
        ok
    }

    #[test]
    fn ewkb_geometries() {
        // SELECT 'POINT(10 -20)'::geometry
        assert!(ewkb_roundtrip(
            "0101000000000000000000244000000000000034C0",
            false,
            None
        ));

        // SELECT 'SRID=4326;MULTIPOINT (10 -20 100, 0 -0.5 101)'::geometry
        assert!(ewkb_roundtrip("01040000A0E6100000020000000101000080000000000000244000000000000034C0000000000000594001010000800000000000000000000000000000E0BF0000000000405940", true, Some(4326)));

        // SELECT 'SRID=4326;LINESTRING (10 -20 100, 0 -0.5 101)'::geometry
        assert!(ewkb_roundtrip("01020000A0E610000002000000000000000000244000000000000034C000000000000059400000000000000000000000000000E0BF0000000000405940", true, Some(4326)));

        // SELECT 'SRID=4326;MULTILINESTRING ((10 -20, 0 -0.5), (0 0, 2 0))'::geometry
        assert!(ewkb_roundtrip("0105000020E610000002000000010200000002000000000000000000244000000000000034C00000000000000000000000000000E0BF0102000000020000000000000000000000000000000000000000000000000000400000000000000000", false, Some(4326)));

        // SELECT 'SRID=4326;POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))'::geometry
        assert!(ewkb_roundtrip("0103000020E610000001000000050000000000000000000000000000000000000000000000000000400000000000000000000000000000004000000000000000400000000000000000000000000000004000000000000000000000000000000000", false, Some(4326)));

        // SELECT 'SRID=4326;MULTIPOLYGON (((0 0, 2 0, 2 2, 0 2, 0 0)), ((10 10, -2 10, -2 -2, 10 -2, 10 10)))'::geometry
        assert!(ewkb_roundtrip("0106000020E610000002000000010300000001000000050000000000000000000000000000000000000000000000000000400000000000000000000000000000004000000000000000400000000000000000000000000000004000000000000000000000000000000000010300000001000000050000000000000000002440000000000000244000000000000000C0000000000000244000000000000000C000000000000000C0000000000000244000000000000000C000000000000024400000000000002440", false, Some(4326)));

        // SELECT 'GeometryCollection(POINT (10 10),POINT (30 30),LINESTRING (15 15, 20 20))'::geometry
        assert!(ewkb_roundtrip("01070000000300000001010000000000000000002440000000000000244001010000000000000000003E400000000000003E400102000000020000000000000000002E400000000000002E4000000000000034400000000000003440", false, None));
    }

    #[test]
    fn ewkb_curves() {
        // SELECT 'CIRCULARSTRING(0 0,1 1,2 0)'::geometry
        assert!(ewkb_roundtrip("01080000000300000000000000000000000000000000000000000000000000F03F000000000000F03F00000000000000400000000000000000", false, None));

        // SELECT 'COMPOUNDCURVE (CIRCULARSTRING (0 0,1 1,2 0),(2 0,3 0))'::geometry
        assert!(ewkb_roundtrip("01090000000200000001080000000300000000000000000000000000000000000000000000000000F03F000000000000F03F000000000000004000000000000000000102000000020000000000000000000040000000000000000000000000000008400000000000000000", false, None));

        // SELECT 'CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING(0 0,1 1,2 0),(2 0,3 0,3 -1,0 -1,0 0)))'::geometry
        assert!(ewkb_roundtrip("010A0000000100000001090000000200000001080000000300000000000000000000000000000000000000000000000000F03F000000000000F03F0000000000000040000000000000000001020000000500000000000000000000400000000000000000000000000000084000000000000000000000000000000840000000000000F0BF0000000000000000000000000000F0BF00000000000000000000000000000000", false, None));

        // SELECT 'MULTICURVE((0 0, 5 5),CIRCULARSTRING(4 0, 4 4, 8 4))'::geometry
        assert!(ewkb_roundtrip("010B000000020000000102000000020000000000000000000000000000000000000000000000000014400000000000001440010800000003000000000000000000104000000000000000000000000000001040000000000000104000000000000020400000000000001040", false, None));

        // SELECT 'MULTISURFACE (CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0,1 1,2 0),(2 0,3 0,3 -1,0 -1,0 0))))'::geometry
        assert!(ewkb_roundtrip("010C00000001000000010A0000000100000001090000000200000001080000000300000000000000000000000000000000000000000000000000F03F000000000000F03F0000000000000040000000000000000001020000000500000000000000000000400000000000000000000000000000084000000000000000000000000000000840000000000000F0BF0000000000000000000000000000F0BF00000000000000000000000000000000", false, None));
    }

    #[test]
    fn ewkb_surfaces() {
        // SELECT 'POLYHEDRALSURFACE(((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((1 1 0,1 1 1,1 0 1,1 0 0,1 1 0)),((0 1 0,0 1 1,1 1 1,1 1 0,0 1 0)),((0 0 1,1 0 1,1 1 1,0 1 1,0 0 1)))'::geometry
        assert!(ewkb_roundtrip("010F000080060000000103000080010000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F03F0000000000000000000000000000F03F000000000000F03F0000000000000000000000000000F03F0000000000000000000000000000000000000000000000000000000000000000010300008001000000050000000000000000000000000000000000000000000000000000000000000000000000000000000000F03F0000000000000000000000000000F03F000000000000F03F0000000000000000000000000000F03F0000000000000000000000000000000000000000000000000000000000000000000000000000000001030000800100000005000000000000000000000000000000000000000000000000000000000000000000F03F00000000000000000000000000000000000000000000F03F0000000000000000000000000000F03F00000000000000000000000000000000000000000000F03F00000000000000000000000000000000000000000000000001030000800100000005000000000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F00000000000000000000000000000000000000000000F03F000000000000F03F0000000000000000010300008001000000050000000000000000000000000000000000F03F00000000000000000000000000000000000000000000F03F000000000000F03F000000000000F03F000000000000F03F000000000000F03F000000000000F03F000000000000F03F00000000000000000000000000000000000000000000F03F00000000000000000103000080010000000500000000000000000000000000000000000000000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F00000000000000000000000000000000000000000000F03F", true, None));

        // SELECT 'TIN(((0 0 0,0 0 1,0 1 0,0 0 0)),((0 0 0,0 1 0,1 1 0,0 0 0)))'::geometry
        assert!(ewkb_roundtrip("0110000080020000000111000080010000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F03F0000000000000000000000000000F03F0000000000000000000000000000000000000000000000000000000000000000011100008001000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000F03F0000000000000000000000000000F03F000000000000F03F0000000000000000000000000000000000000000000000000000000000000000", true, None));

        // SELECT 'TRIANGLE((0 0,0 9,9 0,0 0))'::geometry
        assert!(ewkb_roundtrip("0111000000010000000400000000000000000000000000000000000000000000000000000000000000000022400000000000002240000000000000000000000000000000000000000000000000", false, None));
    }

    fn gpkg_roundtrip(
        ewkbstr: &str,
        dims: CoordDimensions,
        srid: Option<i32>,
        envelope: Vec<f64>,
    ) -> bool {
        let wkb_in = hex::decode(ewkbstr).unwrap();
        let mut wkb_out: Vec<u8> = Vec::new();
        let mut writer = WkbWriter::new(&mut wkb_out, WkbDialect::Geopackage);
        writer.dims = dims;
        writer.srid = srid;
        writer.envelope = envelope;
        assert!(process_gpkg_geom(&mut wkb_in.as_slice(), &mut writer).is_ok());
        let ok = wkb_out == wkb_in;
        if !ok {
            dbg!(hex::encode(&wkb_out));
        }
        ok
    }

    #[test]
    fn gpkg_geometries() {
        // pt2d
        assert!(gpkg_roundtrip("47500003E61000009A9999999999F13F9A9999999999F13F9A9999999999F13F9A9999999999F13F01010000009A9999999999F13F9A9999999999F13F",
            CoordDimensions::default(), Some(4326), vec![1.1, 1.1, 1.1, 1.1]));

        // mln3dzm
        assert!(gpkg_roundtrip("47500003E6100000000000000000244000000000000034400000000000002440000000000000344001BD0B00000100000001BA0B0000020000000000000000003440000000000000244000000000000008400000000000001440000000000000244000000000000034400000000000001C400000000000000040",
            CoordDimensions {z: true, m: true, t: false, tm: false}, Some(4326), vec![10.0, 20.0, 10.0, 20.0]));

        // gc2d
        assert!(gpkg_roundtrip("47500003e6100000000000000000f03f0000000000003640000000000000084000000000000036400107000000020000000101000000000000000000f03f00000000000008400103000000010000000400000000000000000035400000000000003540000000000000364000000000000035400000000000003540000000000000364000000000000035400000000000003540",
            CoordDimensions::default(), Some(4326), vec![1.0, 22.0, 3.0, 22.0]));
    }
}