Skip to main content

wbvector/
lib.rs

1//! # wbvector
2//!
3//! Pure-Rust library for reading and writing common vector GIS data formats.
4//!
5//! ## Supported formats
6//!
7//! | Format      | Read | Write | Extension |
8//! |-------------|------|-------|-----------|
9//! | FlatGeobuf  | ✓    | ✓     | `.fgb`    |
10//! | GeoJSON     | ✓    | ✓     | `.geojson`|
11//! | TopoJSON    | ✓    | ✓     | `.topojson`|
12//! | GeoPackage  | ✓    | ✓     | `.gpkg`   |
13//! | GeoParquet* | ✓    | ✓     | `.parquet`|
14//! | GML         | ✓    | ✓     | `.gml`    |
15//! | GPX         | ✓    | ✓     | `.gpx`    |
16//! | KML         | ✓    | ✓     | `.kml`    |
17//! | KMZ*        | ✓    | ✓     | `.kmz`    |
18//! | MapInfo MIF | ✓    | ✓     | `.mif`    |
19//! | OSM PBF**   | ✓    | -     | `.osm.pbf`|
20//! | Shapefile   | ✓    | ✓     | `.shp`    |
21//!
22//! `*` GeoParquet and KMZ supports are optional and require `geoparquet` / `kmz` features.
23//! `**` OSM PBF support is optional and requires the `osmpbf` crate feature.
24//!
25//! ## Common API
26//!
27//! All format drivers convert to/from the same in-memory types:
28//!
29//! * [`feature::Layer`]      — named collection of features + schema
30//! * [`feature::Feature`]    — optional geometry + attribute values
31//! * [`feature::FieldValue`] — typed attribute value
32//! * [`geometry::Geometry`]  — OGC Simple Features geometry
33//!
34//! You can also use crate-level sniffed I/O:
35//!
36//! ```rust,ignore
37//! let layer = wbvector::read("roads.gpkg")?; // auto-detect format
38//! wbvector::write(&layer, "roads.fgb", wbvector::VectorFormat::FlatGeobuf)?;
39//! # Ok::<(), wbvector::error::GeoError>(())
40//! ```
41//!
42//! ## Quick start
43//!
44//! ```rust,ignore
45//! use wbvector::{flatgeobuf, geojson, geopackage, gml, gpx, kml, mapinfo, shapefile};
46//! #[cfg(feature = "geoparquet")]
47//! use wbvector::geoparquet;
48//! #[cfg(feature = "kmz")]
49//! use wbvector::kmz;
50//! #[cfg(feature = "osmpbf")]
51//! use wbvector::osmpbf;
52//! use wbvector::feature::{Layer, FieldDef, FieldType};
53//! use wbvector::geometry::{Geometry, GeometryType};
54//!
55//! // Build a layer in memory
56//! let mut layer = Layer::new("cities")
57//!     .with_geom_type(GeometryType::Point)
58//!     .with_crs_epsg(4326);
59//!
60//! layer.add_field(FieldDef::new("name",       FieldType::Text));
61//! layer.add_field(FieldDef::new("population", FieldType::Integer));
62//!
63//! layer.add_feature(
64//!     Some(Geometry::point(-0.1278, 51.5074)),
65//!     &[("name", "London".into()), ("population", 9_000_000i64.into())],
66//! )?;
67//!
68//! // Write to every format
69//! flatgeobuf::write(&layer,  "cities.fgb")?;
70//! geojson::write(&layer,     "cities.geojson")?;
71//! geopackage::write(&layer,  "cities.gpkg")?;
72//! gml::write(&layer,         "cities.gml")?;
73//! gpx::write(&layer,         "cities.gpx")?;
74//! kml::write(&layer,         "cities.kml")?;
75//! #[cfg(feature = "kmz")]
76//! kmz::write(&layer,         "cities.kmz")?;
77//! mapinfo::write(&layer,     "cities.mif")?;
78//! shapefile::write(&layer,   "cities")?;       // → cities.shp/.shx/.dbf/.prj
79//!
80//! // Read back from any format — same Layer type
81//! let from_fgb  = flatgeobuf::read("cities.fgb")?;
82//! let from_json = geojson::read("cities.geojson")?;
83//! let from_gpkg = geopackage::read("cities.gpkg")?;
84//! #[cfg(feature = "geoparquet")]
85//! geoparquet::write(&layer, "cities.parquet")?;
86//! let options = geoparquet::GeoParquetWriteOptions::for_large_files();
87//! geoparquet::write_with_options(&layer, "cities_tuned.parquet", &options)?;
88//! let fast_options = geoparquet::GeoParquetWriteOptions::for_interactive_files();
89//! geoparquet::write_with_options(&layer, "cities_fast.parquet", &fast_options)?;
90//! let from_parquet = geoparquet::read("cities.parquet")?;
91//! let from_gml  = gml::read("cities.gml")?;
92//! let from_gpx  = gpx::read("cities.gpx")?;
93//! let from_kml  = kml::read("cities.kml")?;
94//! #[cfg(feature = "kmz")]
95//! let from_kmz  = kmz::read("cities.kmz")?;
96//! let from_mif  = mapinfo::read("cities.mif")?;
97//! let from_shp  = shapefile::read("cities")?;
98//! #[cfg(feature = "osmpbf")]
99//! let from_osm  = osmpbf::read("extract.osm.pbf")?;
100//! # Ok::<(), wbvector::error::GeoError>(())
101//! ```
102//!
103//! ## Format conversion
104//!
105//! Because all drivers share the same [`feature::Layer`] type, conversion
106//! between any pair of formats is two lines:
107//!
108//! ```rust,ignore
109//! let layer = shapefile::read("roads")?;
110//! geopackage::write(&layer, "roads.gpkg")?;
111//! ```
112//!
113//! ## Dependencies
114//!
115//! Core external dependencies include `thiserror`, `flatbuffers`, and `wbprojection`.
116//! All format codecs — including the SQLite engine powering GeoPackage I/O —
117//! are implemented from scratch in pure Rust.
118
119#![deny(missing_docs)]
120
121mod crs;
122pub mod error;
123pub mod feature;
124pub mod flatgeobuf;
125pub mod geojson;
126pub mod geometry;
127pub mod geopackage;
128#[cfg(feature = "geoparquet")]
129pub mod geoparquet;
130pub mod gml;
131pub mod gpx;
132pub mod kml;
133#[cfg(feature = "kmz")]
134pub mod kmz;
135pub mod mapinfo;
136/// In-process vector memory store for passing vectors between tools without disk I/O.
137pub mod memory_store;
138#[cfg(feature = "osmpbf")]
139pub mod osmpbf;
140pub mod reproject;
141pub mod shapefile;
142pub mod topojson;
143
144// Re-export the most commonly used types at the crate root
145pub use error::{GeoError, Result};
146pub use feature::{Crs, Feature, FieldDef, FieldType, FieldValue, Layer, Schema};
147pub use geometry::{BBox, Coord, Geometry, GeometryType, Ring};
148
149/// Supported vector formats for crate-level sniffed I/O.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum VectorFormat {
152    /// FlatGeobuf binary vector format (`.fgb`).
153    FlatGeobuf,
154    /// GeoJSON text format (`.geojson`).
155    GeoJson,
156    /// TopoJSON topology-preserving JSON format (`.topojson`).
157    TopoJson,
158    /// GeoPackage SQLite container format (`.gpkg`).
159    GeoPackage,
160    #[cfg(feature = "geoparquet")]
161    /// GeoParquet columnar format (`.parquet`).
162    GeoParquet,
163    /// Geography Markup Language XML format (`.gml`).
164    Gml,
165    /// GPS Exchange Format XML format (`.gpx`).
166    Gpx,
167    /// Keyhole Markup Language XML format (`.kml`).
168    Kml,
169    #[cfg(feature = "kmz")]
170    /// Zipped KML container format (`.kmz`).
171    Kmz,
172    /// MapInfo interchange format (`.mif` + `.mid`).
173    MapInfoMif,
174    #[cfg(feature = "osmpbf")]
175    /// OpenStreetMap PBF binary format (`.osm.pbf`).
176    OsmPbf,
177    /// ESRI Shapefile dataset (`.shp` + sidecars).
178    Shapefile,
179}
180
181impl VectorFormat {
182    /// Detect format from extension and lightweight file sniffing.
183    pub fn detect<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
184        let path = path.as_ref();
185        let file_name_lc = path
186            .file_name()
187            .and_then(|s| s.to_str())
188            .unwrap_or("")
189            .to_ascii_lowercase();
190        let ext_lc = path
191            .extension()
192            .and_then(|s| s.to_str())
193            .unwrap_or("")
194            .to_ascii_lowercase();
195
196        if file_name_lc.ends_with(".osm.pbf") {
197            #[cfg(feature = "osmpbf")]
198            {
199                return Ok(Self::OsmPbf);
200            }
201            #[cfg(not(feature = "osmpbf"))]
202            {
203                return Err(GeoError::NotImplemented(
204                    "OSM PBF support requires enabling the `osmpbf` feature".into(),
205                ));
206            }
207        }
208
209        match ext_lc.as_str() {
210            "fgb" => return Ok(Self::FlatGeobuf),
211            "geojson" => return Ok(Self::GeoJson),
212            "topojson" => return Ok(Self::TopoJson),
213            "gpkg" => return Ok(Self::GeoPackage),
214            "gml" => return Ok(Self::Gml),
215            "gpx" => return Ok(Self::Gpx),
216            "kml" => return Ok(Self::Kml),
217            "mif" => return Ok(Self::MapInfoMif),
218            "shp" => return Ok(Self::Shapefile),
219            "parquet" => {
220                #[cfg(feature = "geoparquet")]
221                {
222                    return Ok(Self::GeoParquet);
223                }
224                #[cfg(not(feature = "geoparquet"))]
225                {
226                    return Err(GeoError::NotImplemented(
227                        "GeoParquet support requires enabling the `geoparquet` feature".into(),
228                    ));
229                }
230            }
231            "kmz" => {
232                #[cfg(feature = "kmz")]
233                {
234                    return Ok(Self::Kmz);
235                }
236                #[cfg(not(feature = "kmz"))]
237                {
238                    return Err(GeoError::NotImplemented(
239                        "KMZ support requires enabling the `kmz` feature".into(),
240                    ));
241                }
242            }
243            "json" => {
244                if let Some(kind) = sniff_json(path)? {
245                    return Ok(kind);
246                }
247                return Ok(Self::GeoJson);
248            }
249            "xml" => {
250                if let Some(kind) = sniff_xml(path)? {
251                    return Ok(kind);
252                }
253            }
254            _ => {}
255        }
256
257        // Shapefile convenience: accept base path without extension.
258        if path.extension().is_none() {
259            let shp = path.with_extension("shp");
260            if shp.exists() {
261                return Ok(Self::Shapefile);
262            }
263        }
264
265        if path.is_file() {
266            let sig = read_signature(path, 16)?;
267            if sig.starts_with(&flatgeobuf::MAGIC) {
268                return Ok(Self::FlatGeobuf);
269            }
270            if sig.starts_with(b"SQLite format 3\0") {
271                return Ok(Self::GeoPackage);
272            }
273            if sig.starts_with(b"PAR1") {
274                #[cfg(feature = "geoparquet")]
275                {
276                    return Ok(Self::GeoParquet);
277                }
278            }
279            if sig.starts_with(b"PK\x03\x04") {
280                #[cfg(feature = "kmz")]
281                {
282                    return Ok(Self::Kmz);
283                }
284            }
285            if let Some(kind) = sniff_xml(path)? {
286                return Ok(kind);
287            }
288        }
289
290        Err(GeoError::UnknownFormat(path.display().to_string()))
291    }
292
293    /// Read using this format driver.
294    pub fn read<P: AsRef<std::path::Path>>(&self, path: P) -> Result<Layer> {
295        match self {
296            Self::FlatGeobuf => flatgeobuf::read(path),
297            Self::GeoJson => geojson::read(path),
298            Self::TopoJson => topojson::read(path),
299            Self::GeoPackage => geopackage::read(path),
300            #[cfg(feature = "geoparquet")]
301            Self::GeoParquet => geoparquet::read(path),
302            Self::Gml => gml::read(path),
303            Self::Gpx => gpx::read(path),
304            Self::Kml => kml::read(path),
305            #[cfg(feature = "kmz")]
306            Self::Kmz => kmz::read(path),
307            Self::MapInfoMif => mapinfo::read(path),
308            #[cfg(feature = "osmpbf")]
309            Self::OsmPbf => osmpbf::read(path),
310            Self::Shapefile => shapefile::read(path),
311        }
312    }
313
314    /// Write using this format driver.
315    pub fn write<P: AsRef<std::path::Path>>(&self, layer: &Layer, path: P) -> Result<()> {
316        match self {
317            Self::FlatGeobuf => flatgeobuf::write(layer, path),
318            Self::GeoJson => geojson::write(layer, path),
319            Self::TopoJson => topojson::write(layer, path),
320            Self::GeoPackage => geopackage::write(layer, path),
321            #[cfg(feature = "geoparquet")]
322            Self::GeoParquet => geoparquet::write(layer, path),
323            Self::Gml => gml::write(layer, path),
324            Self::Gpx => gpx::write(layer, path),
325            Self::Kml => kml::write(layer, path),
326            #[cfg(feature = "kmz")]
327            Self::Kmz => kmz::write(layer, path),
328            Self::MapInfoMif => mapinfo::write(layer, path),
329            #[cfg(feature = "osmpbf")]
330            Self::OsmPbf => Err(GeoError::NotImplemented(
331                "OSM PBF writer is not implemented".into(),
332            )),
333            Self::Shapefile => shapefile::write(layer, path),
334        }
335    }
336}
337
338/// Generic vector read with automatic format sniffing.
339pub fn read<P: AsRef<std::path::Path>>(path: P) -> Result<Layer> {
340    let fmt = VectorFormat::detect(path.as_ref())?;
341    fmt.read(path)
342}
343
344/// Generic vector write when the target format is known.
345pub fn write<P: AsRef<std::path::Path>>(
346    layer: &Layer,
347    path: P,
348    format: VectorFormat,
349) -> Result<()> {
350    format.write(layer, path)
351}
352
353// ══════════════════════════════════════════════════════════════════════════════
354// Universal streaming writer
355// ══════════════════════════════════════════════════════════════════════════════
356
357// Re-export individual streaming writers at the crate root for convenience.
358pub use geopackage::GpkgStreamWriter;
359pub use geojson::GeoJsonStreamWriter;
360pub use flatgeobuf::FgbStreamWriter;
361
362/// A format-agnostic streaming vector writer.
363///
364/// `VectorStreamWriter::open` inspects the output path and selects the best
365/// streaming back-end automatically:
366///
367/// | Format         | Streaming back-end                         | Notes                              |
368/// |----------------|--------------------------------------------|------------------------------------|
369/// | GeoPackage     | [`GpkgStreamWriter`]                       | In-memory SQLite, written at finish |
370/// | GeoJSON        | [`GeoJsonStreamWriter`]                    | Direct-to-disk text; WGS 84 assumed |
371/// | FlatGeobuf     | [`FgbStreamWriter`]                        | Direct-to-disk binary; no spatial index |
372/// | Shapefile      | buffered file (existing streaming writer)  | 2 GB spec limit applies            |
373/// | Everything else / in-memory | [`Layer`] accumulation        | Written as a whole at finish       |
374///
375/// All back-ends expose the same `push_feature` / `finish` interface so callers
376/// do not need to know the output format at compile time.
377pub enum VectorStreamWriter {
378    /// Streaming GeoPackage writer: accumulates in an in-memory SQLite B-tree, written at finish.
379    GeoPackage(GpkgStreamWriter),
380    /// Streaming GeoJSON writer: writes text directly to disk feature-by-feature.
381    GeoJson(GeoJsonStreamWriter),
382    /// Streaming FlatGeobuf writer: writes binary directly to disk without a spatial index.
383    FlatGeobuf(FgbStreamWriter),
384    /// Fallback: accumulate in a [`Layer`] and write all at once in [`finish`](Self::finish).
385    Layer(Layer, String),
386}
387
388impl VectorStreamWriter {
389    /// Open a streaming writer for `path`.
390    ///
391    /// `schema` is an empty (zero-feature) [`Layer`] that supplies the table
392    /// name, CRS, geometry type, and field definitions.
393    pub fn open(path: &str, schema: &Layer) -> Result<Self> {
394        // In-memory paths always fall through to the Layer fallback.
395        if memory_store::vector_is_memory_path(path) {
396            return Ok(Self::Layer(schema_layer_empty(schema), path.to_string()));
397        }
398
399        match VectorFormat::detect(path).ok() {
400            Some(VectorFormat::GeoPackage) => {
401                Ok(Self::GeoPackage(GpkgStreamWriter::create(path, schema)?))
402            }
403            Some(VectorFormat::GeoJson) => {
404                Ok(Self::GeoJson(GeoJsonStreamWriter::create(path, schema)?))
405            }
406            Some(VectorFormat::FlatGeobuf) => {
407                Ok(Self::FlatGeobuf(FgbStreamWriter::create(path, schema)?))
408            }
409            _ => Ok(Self::Layer(schema_layer_empty(schema), path.to_string())),
410        }
411    }
412
413    /// Append one feature.  `attrs` must be aligned to the full schema supplied
414    /// to [`open`](Self::open) in schema-field order.
415    pub fn push_feature(
416        &mut self,
417        geom: Option<&Geometry>,
418        attrs: &[FieldValue],
419    ) -> Result<()> {
420        match self {
421            Self::GeoPackage(w) => w.push_feature(geom, attrs),
422            Self::GeoJson(w) => w.push_feature(geom, attrs),
423            Self::FlatGeobuf(w) => w.push_feature(geom, attrs),
424            Self::Layer(layer, _) => {
425                layer.push(Feature {
426                    fid: layer.features.len() as u64,
427                    geometry: geom.cloned(),
428                    attributes: attrs.to_vec(),
429                });
430                Ok(())
431            }
432        }
433    }
434
435    /// Finalise the output and return the written path.
436    pub fn finish(self) -> Result<String> {
437        match self {
438            Self::GeoPackage(w) => { let p = w.path.clone(); w.finish()?; Ok(p) }
439            Self::GeoJson(w) => { let p = w.path.clone(); w.finish()?; Ok(p) }
440            Self::FlatGeobuf(w) => { let p = w.path.clone(); w.finish()?; Ok(p) }
441            Self::Layer(layer, path) => {
442                if memory_store::vector_is_memory_path(&path) {
443                    let id = memory_store::put_vector(layer);
444                    Ok(memory_store::make_vector_memory_path(&id))
445                } else {
446                    let fmt = VectorFormat::detect(&path).map_err(|_| {
447                        GeoError::UnknownFormat(path.clone())
448                    })?;
449                    fmt.write(&layer, &path)?;
450                    Ok(path)
451                }
452            }
453        }
454    }
455}
456
457/// Build an empty Layer with the same schema and metadata as `schema` but no features.
458fn schema_layer_empty(schema: &Layer) -> Layer {
459    let mut layer = Layer::new(schema.name.clone());
460    layer.geom_type = schema.geom_type;
461    layer.crs = schema.crs.clone();
462    for fd in schema.schema.fields() {
463        layer.add_field(fd.clone());
464    }
465    layer
466}
467
468fn read_signature(path: &std::path::Path, n: usize) -> Result<Vec<u8>> {
469    use std::io::Read;
470    let mut f = std::fs::File::open(path)?;
471    let mut buf = vec![0u8; n];
472    let read_n = f.read(&mut buf)?;
473    buf.truncate(read_n);
474    Ok(buf)
475}
476
477fn sniff_xml(path: &std::path::Path) -> Result<Option<VectorFormat>> {
478    use std::io::Read;
479    let mut f = match std::fs::File::open(path) {
480        Ok(f) => f,
481        Err(_) => return Ok(None),
482    };
483    let mut buf = vec![0u8; 4096];
484    let n = f.read(&mut buf)?;
485    buf.truncate(n);
486    let txt = String::from_utf8_lossy(&buf).to_ascii_lowercase();
487    if txt.contains("<kml") {
488        return Ok(Some(VectorFormat::Kml));
489    }
490    if txt.contains("<gpx") {
491        return Ok(Some(VectorFormat::Gpx));
492    }
493    if txt.contains("<gml") || txt.contains("opengis.net/gml") {
494        return Ok(Some(VectorFormat::Gml));
495    }
496    Ok(None)
497}
498
499fn sniff_json(path: &std::path::Path) -> Result<Option<VectorFormat>> {
500    use std::io::Read;
501    let mut f = match std::fs::File::open(path) {
502        Ok(f) => f,
503        Err(_) => return Ok(None),
504    };
505    let mut buf = vec![0u8; 4096];
506    let n = f.read(&mut buf)?;
507    buf.truncate(n);
508    let txt = String::from_utf8_lossy(&buf).to_ascii_lowercase();
509    if txt.contains("\"type\"") && txt.contains("\"topology\"") {
510        return Ok(Some(VectorFormat::TopoJson));
511    }
512    if txt.contains("\"type\"")
513        && (txt.contains("\"featurecollection\"") || txt.contains("\"feature\""))
514    {
515        return Ok(Some(VectorFormat::GeoJson));
516    }
517    Ok(None)
518}