Using FlatCityBuf with Rust

Table of contents

  1. Adding to your project
  2. Reading a local file
  3. Spatial queries
  4. Attribute queries
  5. HTTP streaming
  6. Zero-copy access
  7. Writing files

Rust is the reference implementation: fcb_core both reads and writes FlatCityBuf, and is what the fcb CLI and the other implementations are validated against. Its API reference is on docs.rs/fcb_core (also published at cityjson.github.io/flatcitybuf/rust), and the repository’s own guide — crate layout, features, tooling — is docs/rust.md.

Adding to your project

Initialise a new Rust project:

cargo new fcb_demo
cd fcb_demo

Add FlatCityBuf to your Cargo.toml:

[dependencies]
fcb_core = "0.7.7"

# The `http` feature is on by default. For a local-only reader without
# reqwest, turn it off:
fcb_core = { version = "0.7.7", default-features = false }

Reading a local file

use fcb_core::{deserializer::to_cj_metadata, FcbReader};
use std::fs::File;
use std::io::BufReader;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = BufReader::new(File::open("delft.fcb")?);
    let mut features = FcbReader::open(file)?.select_all()?;

    // The CityJSON metadata object: the first line of the equivalent
    // CityJSONSeq document.
    let cj = to_cj_metadata(&features.header())?;
    println!(
        "CityJSON {}, {} features",
        cj.version,
        features.header().features_count()
    );

    while let Some(feature) = features.next()? {
        let cj_feature = feature.cur_cj_feature()?;
        println!("{}", cj_feature.id);
    }

    Ok(())
}

then run it with:

cargo run

Spatial queries

select_query walks the packed R-tree and skips straight to the matching features. The bounding box is (min_x, min_y, max_x, max_y) in the file’s CRS; the two Options are limit and offset.

use fcb_core::{FcbReader, SpatialQuery};
use std::fs::File;
use std::io::BufReader;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = BufReader::new(File::open("delft.fcb")?);
    let bbox = SpatialQuery::BBox(84227.77, 445377.33, 85323.23, 446334.69);

    let mut hits = FcbReader::open(file)?.select_query(bbox, None, None)?;

    let mut count = 0;
    while let Some(feature) = hits.next()? {
        println!("in bbox: {}", feature.cur_cj_feature()?.id);
        count += 1;
    }
    println!("{count} features in the bounding box");

    Ok(())
}

SpatialQuery also has PointIntersects(x, y) and PointNearest(x, y).

Attribute queries

select_attr_query uses the static B+tree indices, so the attribute must have been indexed at write time (see fcb ser --attr-index). A query is a list of (column, operator, value) triples, AND-ed together, and the value’s KeyType must match the column’s type on disk.

use fcb_core::{AttrQuery, FcbReader, KeyType, Operator};
use std::fs::File;
use std::io::BufReader;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = BufReader::new(File::open("delft.fcb")?);

    let query: AttrQuery = vec![(
        "b3_h_dak_50p".to_string(),
        Operator::Gt,
        KeyType::Float64(20.0.into()),
    )];

    let mut hits = FcbReader::open(file)?.select_attr_query(query)?;
    while let Some(feature) = hits.next()? {
        println!("tall: {}", feature.cur_cj_feature()?.id);
    }

    Ok(())
}

Operators are Eq, Ne, Gt, Ge, Lt, Le. For string columns use KeyType::StringKey50(FixedStringKey::from_str("...")) — the index stores keys truncated to 50 bytes, so it answers with candidates that the reader verifies against the full value.

HTTP streaming

For cloud-hosted files, use the async HTTP reader. Only the bytes a query needs are fetched, so a query against a 68GB file costs a handful of range requests.

Add tokio to your Cargo.toml:

[dependencies]
fcb_core = "0.7.7"   # the `http` feature is on by default
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use fcb_core::{HttpFcbReader, SpatialQuery};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let reader =
        HttpFcbReader::open("https://flatcitybuf.open3d.city/data/3dbag_all_index.fcb")
            .await?;

    println!("{} features", reader.header().features_count());

    let bbox = SpatialQuery::BBox(120000.0, 486000.0, 120200.0, 486200.0);
    let mut iter = reader.select_query(bbox).await?;

    while let Some(feature) = iter.next().await? {
        // note: `cj_feature()` here, not `cur_cj_feature()`
        println!("{}", feature.cj_feature()?.id);
    }

    Ok(())
}

Attribute queries work the same way over HTTP with select_attr_query(&query), and both have _paged variants (select_query_paged, select_attr_query_paged) taking a limit and an offset.

Zero-copy access

Each feature can be read in two ways:

  • feature.cur_feature() returns the FlatBuffers view — zero-copy, only the fields you touch are decoded;
  • feature.cur_cj_feature() returns a CityJSONFeature, which parses the FlatBuffers into owned CityJSON structures.

If you only need a few attributes per feature, the first is considerably cheaper. See the performance tips.

Writing files

FcbWriter takes the CityJSON metadata object plus a stream of CityJSONFeatures, and assembles the header, the indices and the feature data when you call write. The CLI’s source is the fully worked example: it builds the attribute schema in a first pass, then adds every feature, then writes.