Using FlatCityBuf with C++

Table of contents

  1. Dependencies
  2. Building and installing
  3. Reading a local file
  4. Spatial queries
  5. Attribute queries
  6. Reading over HTTP
  7. Bringing your own transport
  8. Writing files
  9. Examples
  10. Verification

The C++ library is a from-scratch native C++17 implementation that reads and writes FlatCityBuf. It replaces the earlier CXX-bridge bindings over the Rust core: there is no Rust toolchain to install, no generated bridge source to compile, no async runtime, and — unless you ask for the HTTP adapter — no TLS dependency.

Source and full documentation: src/cpp.

Dependencies

Dependency Required? Why
flatbuffers yes the on-disk format
nlohmann-json with FCB_WITH_JSON=ON (the default) CityJSON emission
libcurl with FCB_WITH_CURL=ON (default OFF) HTTP range requests
doctest with FCB_BUILD_TESTS=ON (the default) tests only, never installed
# macOS
brew install flatbuffers nlohmann-json doctest

# Debian / Ubuntu
sudo apt-get install libflatbuffers-dev nlohmann-json3-dev doctest-dev

Building and installing

git clone https://github.com/cityjson/flatcitybuf.git
cd flatcitybuf/src/cpp

cmake -B build -S .
cmake --build build
cmake --install build --prefix /your/prefix

Useful options: -DFCB_WITH_CURL=ON (HTTP support), -DFCB_WITH_JSON=OFF (drop CityJSON emission and the nlohmann dependency), -DFCB_BUILD_TESTS=OFF, -DFCB_BUILD_EXAMPLES=OFF.

Then, from your own CMake project:

find_package(flatcitybuf CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE flatcitybuf::flatcitybuf)

That is the whole integration — the FlatBuffers headers are generated and committed, so consumers never need flatc.

Reading a local file

#include <fcb/cityjson.hpp>
#include <fcb/reader.hpp>

#include <iostream>

int main() {
    fcb::FcbReader reader = fcb::FcbReader::open_file("delft.fcb");

    const auto& info = reader.header().info();
    std::cout << info.features_count << " features, CityJSON "
              << info.cityjson_version << ", " << info.crs << "\n";

    // One CityJSONSeq metadata line, then one CityJSONFeature per line.
    std::cout << fcb::to_cityjson_metadata(reader.header()).dump() << "\n";

    auto it = reader.select_all();
    while (it.next()) {
        std::cout << fcb::to_cityjson_feature(it.current(), reader.header()).dump() << "\n";
    }
}

to_cityjson_metadata and to_cityjson_feature return nlohmann::json, so field access is the ordinary nlohmann API: .at("k"), .value("k", default), .contains("k"), .get<double>().

Spatial queries

auto it = reader.select_bbox({84227.77, 445377.33, 85323.23, 446334.69});
while (it.next()) {
    std::cout << fcb::to_cityjson_feature(it.current(), reader.header())["id"] << "\n";
}

Attribute queries

select_attr uses the static B+trees, so the column must have been indexed at write time. The comparison value is a typed KeyValue and its type must match the column’s type on disk — a mismatch does not throw, it reinterprets bytes.

#include <fcb/stree.hpp>

fcb::AttrQuery query = {
    {"b3_h_dak_50p", fcb::Operator::Gt, fcb::KeyValue::from_f64(20.0)},
};

auto it = reader.select_attr(query);
while (it.next()) {
    // 4 of 1115 features match in the Delft example file
}

String columns are indexed on keys truncated to 50 bytes (100 for JSON/binary columns), so the index returns candidates; the default AttrQueryOptions verify each one against the fully decoded attribute. Pass {true} to skip verification — faster, and wrong for long strings.

Reading over HTTP

Build with -DFCB_WITH_CURL=ON:

#include <fcb/http/curl_range_reader.hpp>

auto transport = std::make_shared<fcb::CurlRangeReader>(
    "https://storage.googleapis.com/flatcitybuf/3dbag_all_index.fcb");
fcb::FcbReader reader = fcb::FcbReader::open(transport);

auto it = reader.select_bbox({120000, 486000, 121000, 487000});

Only the intersecting features are fetched. On the published 3DBAG file (~68GB, 10.7M features) the example program opens the file in 2 HTTP requests and answers a 1km bounding box in 37.

Bringing your own transport

fcb::RangeReader is the library’s only IO seam — implement it to read from an object store, a game-engine VFS, an mmap, memory, or a decrypting layer:

class MyReader : public fcb::RangeReader {
    std::uint64_t total_size() override { /* ... */ }
    std::vector<std::uint8_t> read(std::uint64_t offset, std::uint64_t length) override { /* ... */ }
    // Optionally override read_batch() to pipeline or multiplex.
};

The interface is deliberately synchronous: batching, not asynchrony, is the concurrency primitive, and a blocking interface is trivially wrapped by whatever threading model your application already has. Read the contract comment in include/fcb/range_reader.hpp before implementing one.

Writing files

fcb::FcbWriter writes .fcb from CityJSON-shaped JSON, with no Rust toolchain involved. Its output is validated byte-for-byte against files written by the Rust writer.

#include <fcb/writer/attribute.hpp>
#include <fcb/writer/fcb_writer.hpp>

// `cj` is the CityJSONSeq metadata line; the schemas must already describe
// every feature that will be added, so scan all features first (column
// numbering is insertion order, exactly as the Rust CLI does it).
fcb::FcbWriter writer(cj, options, attr_schema, semantic_attr_schema);

for (const auto& feature : features) {
    writer.add_feature(feature); // spooled to a temp file, not kept in memory
}

std::ofstream out("city.fcb", std::ios::binary);
writer.write(out); // streams header, indices and features straight to `out`

add_feature spools each encoded feature to a private temporary file and write(std::ostream&) streams the finished file out in fixed-size chunks, so memory stays bounded regardless of dataset size. (There is also a write() overload returning a std::vector<std::uint8_t>; it is a convenience for small files and does not have that property.)

Examples

The repository ships eight self-contained example programs, one per capability — see src/cpp/examples, which documents the exact output of each:

Program Shows
fcb_inspect_header header only: extent, CRS, transform, and which columns are queryable
fcb_read_local the whole file (or a bbox) as CityJSONSeq
fcb_to_cityjson the CityJSON representation, and how to reach into its fields
fcb_query_attributes attribute queries through the B+tree
fcb_read_features raw feature access, without CityJSON conversion
fcb_custom_reader implementing fcb::RangeReader yourself
fcb_read_http remote reads over HTTP range requests
fcb_write_cityjson writing a CityJSONSeq out as .fcb

fcb_custom_reader is the one that makes the format’s argument concrete: on the Delft file, reading everything costs 7 reads and 90.7% of the bytes, while a bounding-box query costs 4 reads and 31.7% of the bytes for 170 of 1115 features.

Verification

The reader’s output is compared against the Rust reader on the full Delft fixture (all 1115 features, compared as parsed JSON trees), plus the shared conformance corpus — single-feature files, prefix-colliding strings, duplicate keys, zero-area extents, geometry templates, appearance at every nesting depth. The test suite runs clean under ASan and UBSan.