Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions _posts/2026-08-10-flatcitybuf-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
layout: post
title: "FlatCityBuf now speaks C++, Python, TypeScript—and runs in your browser 🏙️"
categories: news
date: 2026-08-10
---

![]({{ site.baseurl }}/flatcitybuf/flatcitybuf_logo.png){:width="50%"}

[FlatCityBuf]({{ site.baseurl }}/flatcitybuf/), the cloud-optimised CityJSON format, has had a busy couple of months. The headline: it is no longer a Rust format with bindings around it. There are now **four independent implementations**—Rust, C++, Python and TypeScript—each reading the same files and checked against each other on a shared conformance corpus.

## A browser viewer, with no server and no WebAssembly

**[flatcitybuf-prototype.hideba.me](https://flatcitybuf-prototype.hideba.me)** opens the complete [3DBAG](https://3dbag.nl)—one ~68GB file, 10.7 million buildings—straight from cloud storage and lets you fly around it. Pan and zoom, and it queries the visible area over HTTP range requests, decoding only the buildings it needs; draw a bounding box or filter on an attribute to narrow it down, switch level of detail, colour by an attribute, click a building for its attributes, and export what you see as CityJSON, CityJSONSeq or OBJ.

Nothing is preprocessed for the viewer: it is the same `.fcb` file the CLI writes and the other implementations read, and there is no server component at all. See [the web viewer page]({{ site.baseurl }}/flatcitybuf/viewer/).

## Native C++, Python and TypeScript

Each binding was rewritten as a native implementation of the format, rather than a wrapper around the Rust core:

- **[C++]({{ site.baseurl }}/flatcitybuf/cpp/)** — a from-scratch C++17 library that both reads *and* writes FlatCityBuf. No Rust toolchain, no FFI bridge, no async runtime, and no TLS dependency unless you opt into the HTTP adapter. All IO goes through one synchronous `RangeReader` interface you can implement yourself, so it drops into engines and desktop applications that own their own threading.
- **[Python]({{ site.baseurl }}/flatcitybuf/python/) — pure Python.** One universal wheel, no compiled extension, no per-platform builds: `pip install flatcitybuf` and you are done. `numpy` is optional and makes bulk decoding ~2.4× faster.
- **[TypeScript]({{ site.baseurl }}/flatcitybuf/typescript/) — pure TypeScript.** The WebAssembly binding is retired; `@cityjson/flatcitybuf` now ships no `.wasm` and runs the same code in the browser and in Node.js, with one runtime dependency. It also fixes several bugs the WASM binding had, from attribute queries on non-double columns to a range client that accepted a full-body `200` as if it were the requested range.

Both the Python and TypeScript packages are new APIs rather than drop-in replacements; each page has a migration table.

## A CLI that also inspects

The `fcb` CLI moved to positional arguments (`fcb ser input.city.jsonl output.fcb`—no more `-i`/`-o`), and gained a way to look inside a file before reading it:

- `fcb info city.fcb` prints size, version, feature count, extent, transform and, most usefully, which attributes carry an index and are therefore queryable.
- `fcb inspect city.fcb` opens an interactive terminal UI with metadata, the full column schema and a map of the dataset's extent. It takes a **URL** as well as a path, and reads only the header—so inspecting that 68GB file over the network is instant.

Details on [the CLI page]({{ site.baseurl }}/flatcitybuf/conversion/).

## And the docs

Every FlatCityBuf page on this site has been rewritten around all this: [datasets]({{ site.baseurl }}/flatcitybuf/datasets/) (including how to serve your own `.fcb` files—the CORS header everyone trips over), the [CLI]({{ site.baseurl }}/flatcitybuf/conversion/), the four language pages, and the [performance tips and FAQ]({{ site.baseurl }}/flatcitybuf/faq/). Every example on those pages was run against the same 1115-building Delft file, and all four implementations return the same answers.

FlatCityBuf was developed by [Hidemichi Baba](https://3d.bk.tudelft.nl/hideba) for his MSc thesis in Geomatics at TU Delft; the paper is in the [ISPRS archives](https://doi.org/10.5194/isprs-archives-XLVIII-4-W15-2025-17-2025). The code lives at [github.com/cityjson/flatcitybuf](https://github.com/cityjson/flatcitybuf).
226 changes: 226 additions & 0 deletions flatcitybuf/cpp/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
---
layout: default
title: C++
parent: FlatCityBuf
nav_order: 5
has_children: false
permalink: /flatcitybuf/cpp/
---

# Using FlatCityBuf with C++

## Table of contents
{: .no_toc .text-delta }

1. TOC
{:toc}

---

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`](https://github.com/cityjson/flatcitybuf/tree/main/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 |

{% raw %}

```bash
# macOS
brew install flatbuffers nlohmann-json doctest

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

{% endraw %}

## Building and installing

{% raw %}

```bash
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
```

{% endraw %}

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:

{% raw %}

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

{% endraw %}

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

## Reading a local file

{% raw %}

```cpp
#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";
}
}
```

{% endraw %}

`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

{% raw %}

```cpp
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";
}
```

{% endraw %}

## 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.

{% raw %}

```cpp
#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
}
```

{% endraw %}

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`:

{% raw %}

```cpp
#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});
```

{% endraw %}

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:

{% raw %}

```cpp
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.
};
```

{% endraw %}

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.

{% raw %}

```cpp
#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`
```

{% endraw %}

`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`](https://github.com/cityjson/flatcitybuf/tree/main/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](https://github.com/cityjson/flatcitybuf/tree/main/conformance) — 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.
65 changes: 59 additions & 6 deletions flatcitybuf/datasets/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,67 @@ permalink: /flatcitybuf/datasets/

# FlatCityBuf example datasets

We offer a few example FlatCityBuf files for testing:
## Table of contents
{: .no_toc .text-delta }

1. TOC
{:toc}

---

## Ready-made files

<!-- TODO: add actual URLs to example files -->
We offer a few example FlatCityBuf files for testing:

- [Delft (6MB)](https://github.com/cityjson/flatcitybuf/blob/bf270c345715e1020d314b7235ede8fffc6e0d85/examples/data/delft.fcb)
- [Delft (7MB)](https://github.com/cityjson/flatcitybuf/blob/main/examples/data/delft.fcb) — 1115 buildings from the 3DBAG, all 44 attributes indexed. The file every example on this site uses.
- [3DBAG small (3.4GB)](https://storage.googleapis.com/flatcitybuf/3dbag_subset_all_index.fcb)
- [3DBAG all (70GB)](https://storage.googleapis.com/flatcitybuf/3dbag_all_index.fcb): Complete 3DBAG dataset with spatial indexing and all attributes indexed
- [3DBAG all (~68GB)](https://storage.googleapis.com/flatcitybuf/3dbag_all_index.fcb) — the complete 3DBAG, 10 771 547 features, with spatial indexing and all attributes indexed

The two large files are hosted on Google Cloud Storage with range requests and CORS enabled, so you can query them directly from a script or from the browser — including from the [web viewer]({{ '/flatcitybuf/viewer/' | prepend: site.baseurl }}), which opens the 68GB one by default.

You can always convert any CityJSONSeq file to a FlatCityBuf (and vice-versa), see [the CLI page]({{ '/flatcitybuf/conversion/' | prepend: site.baseurl }}).

## Looking inside a file

Before writing any code, `fcb info` (a one-shot summary) and `fcb inspect` (an interactive terminal UI) tell you what a file contains — how many features, in which CRS, and, crucially, **which attributes are indexed** and therefore queryable. `fcb inspect` accepts a URL and reads only the header, so it is instant even on the 68GB file:

{% raw %}

```bash
fcb info delft.fcb
fcb inspect https://storage.googleapis.com/flatcitybuf/3dbag_all_index.fcb
```

{% endraw %}

See [inspecting a file]({{ '/flatcitybuf/conversion/#inspecting-a-file' | prepend: site.baseurl }}).

## Serving your own `.fcb` files

FlatCityBuf needs no special server: static hosting is enough, as long as the server

1. **supports HTTP range requests** (`Accept-Ranges: bytes`, and honours `Range` with a `206` response), and
2. **exposes the `Content-Range` header to the browser** when the file is on another origin.

The second one is the usual stumbling block. Browsers hide response headers on cross-origin requests unless the server explicitly exposes them, and a reader learns the file size from `Content-Range` — so it refuses to guess and reports something like *"sent a 206 response without an accessible Content-Range header"*. The fix is a CORS header:

{% raw %}

```
Access-Control-Expose-Headers: Content-Range, Accept-Ranges
```

{% endraw %}

For a Google Cloud Storage bucket:

{% raw %}

```bash
echo '[{"maxAgeSeconds":3600,"method":["GET","HEAD","OPTIONS"],"origin":["*"],"responseHeader":["Content-Type","Content-Range","Accept-Ranges"]}]' > cors.json
gsutil cors set cors.json gs://your-bucket
```

These files are hosted on Google Cloud Storage and can be accessed directly via HTTP.
{% endraw %}

You can always convert any CityJSONSeq file to a FlatCityBuf (and vice-versa), see our [conversion page]({{ '/flatcitybuf/conversion/' | prepend: site.baseurl }}).
After changing CORS, hard-reload the page: the browser may have cached the earlier failed response.
Loading
Loading