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}