Skip to main content

fcb_core/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2// The crate docs below link to `HttpFcbReader` and `http_reader`, which only
3// exist with the (default) `http` feature. Documenting without it is a valid
4// configuration, so silence the links there rather than dropping them.
5#![cfg_attr(
6    not(all(feature = "http", not(target_arch = "wasm32"))),
7    allow(rustdoc::broken_intra_doc_links)
8)]
9//! **FlatCityBuf** (`.fcb`) — a cloud-optimized binary encoding of
10//! [CityJSON]. It carries the standard's semantics in [FlatBuffers], laid out
11//! so that a client can read only the bytes it actually needs.
12//!
13//! A file is five contiguous sections:
14//!
15//! ```text
16//! | magic bytes | header | packed Hilbert | static B+tree | features |
17//! | 8 bytes     |        | R-tree (opt.)  | indices (opt.)|          |
18//! ```
19//!
20//! - the **header** is one FlatBuffers table: transform (scale/translate for
21//!   the quantized integer vertices), CRS, geographical extent, appearance,
22//!   geometry templates, and the attribute column schema;
23//! - the **packed Hilbert R-tree** answers bbox and point queries without a
24//!   scan. Features are stored in Hilbert order, so a hit list is a set of
25//!   sorted, coalescible byte ranges;
26//! - the **static B+tree** indices answer attribute queries (`==`, `!=`, `<`,
27//!   `<=`, `>`, `>=`) over the columns chosen at write time;
28//! - each **feature** is a size-prefixed `CityFeature` table — one per line of
29//!   the source CityJSONSeq.
30//!
31//! Because the layout is seek-friendly, the same reader works over a local
32//! file (`Read + Seek`), a non-seekable stream (`Read`), or a remote URL via
33//! HTTP range requests ([`HttpFcbReader`], `http` feature, enabled by default).
34//!
35//! `fcb_core` is the reference implementation of the format and the only one
36//! that *writes* it; the C++, Python and TypeScript readers in the same
37//! repository are validated against its output.
38//!
39//! # Reading a file
40//!
41//! [`FcbReader::open`] parses and verifies the header; `select_*` then returns
42//! a fallible streaming iterator over the features. Only one feature is held
43//! in memory at a time.
44//!
45//! ```no_run
46//! use fcb_core::{deserializer::to_cj_metadata, FcbReader};
47//! use std::fs::File;
48//! use std::io::BufReader;
49//!
50//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
51//! let file = BufReader::new(File::open("delft.fcb")?);
52//! let mut features = FcbReader::open(file)?.select_all()?;
53//!
54//! // The CityJSON metadata object (version, transform, CRS, extent) is the
55//! // header; it is the first line of the equivalent CityJSONSeq document.
56//! let cj = to_cj_metadata(&features.header())?;
57//! println!("CityJSON {}, {} features", cj.version, features.header().features_count());
58//!
59//! while let Some(feature) = features.next()? {
60//!     let cj_feature = feature.cur_cj_feature()?;
61//!     println!("{}: {} city object(s)", cj_feature.id, cj_feature.city_objects.len());
62//! }
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! ## Spatial and attribute queries
68//!
69//! [`FcbReader::select_query`] uses the R-tree; [`FcbReader::select_attr_query`]
70//! uses the B+tree indices. Both skip straight to the matching features.
71//!
72//! ```no_run
73//! use fcb_core::{AttrQuery, FcbReader, KeyType, Operator, SpatialQuery};
74//! use std::fs::File;
75//! use std::io::BufReader;
76//!
77//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
78//! // Everything inside a bounding box (min_x, min_y, max_x, max_y).
79//! let file = BufReader::new(File::open("delft.fcb")?);
80//! let bbox = SpatialQuery::BBox(84_000.0, 446_000.0, 85_000.0, 447_000.0);
81//! let mut hits = FcbReader::open(file)?.select_query(bbox, None, None)?;
82//! while let Some(feature) = hits.next()? {
83//!     println!("{}", feature.cur_cj_feature()?.id);
84//! }
85//!
86//! // Everything whose indexed `b3_h_dak_50p` attribute exceeds 2.0.
87//! let file = BufReader::new(File::open("delft.fcb")?);
88//! let query: AttrQuery = vec![(
89//!     "b3_h_dak_50p".to_string(),
90//!     Operator::Gt,
91//!     KeyType::Float64(2.0.into()),
92//! )];
93//! let mut hits = FcbReader::open(file)?.select_attr_query(query)?;
94//! while let Some(feature) = hits.next()? {
95//!     println!("{}", feature.cur_cj_feature()?.id);
96//! }
97//! # Ok(())
98//! # }
99//! ```
100//!
101//! # Writing a file
102//!
103//! [`FcbWriter`] takes the CityJSON metadata object plus a stream of
104//! `CityJSONFeature`s and assembles header, indices and feature data on
105//! [`FcbWriter::write`].
106//!
107//! ```no_run
108//! use fcb_core::{
109//!     attribute::{AttributeSchema, AttributeSchemaMethods},
110//!     header_writer::HeaderWriterOptions,
111//!     read_cityjson_from_reader, CJType, CJTypeKind, CityJSONSeq, FcbWriter,
112//! };
113//! use std::fs::File;
114//! use std::io::{BufReader, BufWriter};
115//!
116//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
117//! let input = BufReader::new(File::open("delft.city.jsonl")?);
118//! let CJType::Seq(CityJSONSeq { cj, features }) =
119//!     read_cityjson_from_reader(input, CJTypeKind::Seq)?
120//! else {
121//!     unreachable!("CJTypeKind::Seq always yields CJType::Seq")
122//! };
123//!
124//! // Collect the attribute columns. Iterate the city objects in a
125//! // deterministic order: `add_attributes` hands each new name the next free
126//! // column index, so a `HashMap`'s random order would number the columns
127//! // differently on every run.
128//! let mut schema = AttributeSchema::new();
129//! for feature in &features {
130//!     let mut ids: Vec<&String> = feature.city_objects.keys().collect();
131//!     ids.sort_unstable();
132//!     for co in ids.into_iter().filter_map(|id| feature.city_objects.get(id)) {
133//!         if let Some(attributes) = &co.attributes {
134//!             schema.add_attributes(attributes);
135//!         }
136//!     }
137//! }
138//!
139//! let options = HeaderWriterOptions {
140//!     write_index: true,
141//!     feature_count: features.len() as u64,
142//!     index_node_size: 16,
143//!     // Build a static B+tree over these columns. `None` = default
144//!     // branching factor.
145//!     attribute_indices: Some(vec![("b3_h_dak_50p".to_string(), None)]),
146//!     geographical_extent: None,
147//! };
148//!
149//! let mut fcb = FcbWriter::new(cj, Some(options), Some(schema), None)?;
150//! for feature in &features {
151//!     fcb.add_feature(feature)?;
152//! }
153//! fcb.write(BufWriter::new(File::create("delft.fcb")?))?;
154//! # Ok(())
155//! # }
156//! ```
157//!
158//! # Reading over HTTP
159//!
160//! [`HttpFcbReader`] fetches the header, then the index, then only the byte
161//! ranges holding the matching features — typically a handful of range
162//! requests for a query against a multi-gigabyte file.
163//!
164//! ```no_run
165//! # #[cfg(all(feature = "http", not(target_arch = "wasm32")))]
166//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
167//! use fcb_core::{HttpFcbReader, SpatialQuery};
168//!
169//! let reader = HttpFcbReader::open("https://example.com/delft.fcb").await?;
170//! let bbox = SpatialQuery::BBox(84_000.0, 446_000.0, 85_000.0, 447_000.0);
171//! let mut features = reader.select_query(bbox).await?;
172//!
173//! while features.next().await?.is_some() {
174//!     println!("{}", features.cur_cj_feature()?.id);
175//! }
176//! # Ok(())
177//! # }
178//! ```
179//!
180//! # Feature flags
181//!
182//! | Flag | Default | Effect |
183//! |---|---|---|
184//! | `http` | **yes** | [`HttpFcbReader`] and the range-request query paths, via `reqwest` and `http-range-client`. Disable it (`default-features = false`) for a dependency-light, purely local reader. |
185//!
186//! [`http_reader`] is additionally gated on `not(target_arch = "wasm32")`
187//! because it reaches for `reqwest`'s native client. The rest of the crate,
188//! including the index search paths, still compiles for `wasm32`.
189//!
190//! # Attribution
191//!
192//! **Portions of this software are derived from FlatGeobuf**
193//! - Source: <https://github.com/flatgeobuf/flatgeobuf>
194//! - License: BSD 2-Clause License
195//! - Copyright (c) 2018-2024, Björn Harrtell and contributors
196//!
197//! Specifically, the following components contain code derived from FlatGeobuf:
198//! - Spatial indexing algorithms (packed R-tree implementation)
199//! - HTTP range request handling (for Rust native part)
200//! - Binary format design patterns
201//!
202//! We extend our gratitude to the FlatGeobuf team for their excellent work on efficient
203//! geospatial binary formats, which provided the foundation for FlatCityBuf's spatial
204//! indexing and serialization architecture.
205//!
206//! # License
207//!
208//! This project is licensed under the MIT License.
209//! FlatGeobuf portions remain under their original BSD 2-Clause License.
210//!
211//! [CityJSON]: https://www.cityjson.org/
212//! [FlatBuffers]: https://flatbuffers.dev/
213
214mod cj_utils;
215mod cjerror;
216mod const_vars;
217pub mod error;
218pub mod fb;
219#[allow(dead_code, unused_imports, clippy::all, warnings)]
220#[cfg(all(feature = "http", not(target_arch = "wasm32")))]
221#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
222pub mod http_reader;
223pub mod obj;
224
225pub mod packed_rtree;
226mod reader;
227pub mod static_btree;
228mod writer;
229
230pub use cj_utils::*;
231pub use const_vars::*;
232pub use error::Error;
233pub use fb::*;
234pub use packed_rtree::{NodeItem, PackedRTree, Query as SpatialQuery, SearchResultItem};
235pub use reader::*;
236pub use static_btree::{
237    Entry, FixedStringKey, Float, Key, KeyType, MemoryIndex, MemoryMultiIndex, MultiIndex,
238    Operator, Query, QueryCondition, StreamIndex, StreamMultiIndex,
239};
240pub use writer::*;
241
242#[cfg(all(feature = "http", not(target_arch = "wasm32")))]
243#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
244pub use http_reader::*;
245
246/// Returns `true` if `bytes` starts with a FlatCityBuf magic-byte sequence
247/// this build can read.
248///
249/// The 8-byte magic is `fcb` + a major version byte + `fcb` + a patch byte
250/// (see [`MAGIC_BYTES`]). Only the two `fcb` triplets and the major version
251/// are checked: byte 3 must be no greater than [`VERSION`], and byte 7 is
252/// ignored.
253///
254/// # Panics
255///
256/// Panics if `bytes` is shorter than [`MAGIC_BYTES_SIZE`].
257pub fn check_magic_bytes(bytes: &[u8]) -> bool {
258    bytes[0..3] == MAGIC_BYTES[0..3] && bytes[4..7] == MAGIC_BYTES[4..7] && bytes[3] <= VERSION
259}