Skip to content
Open
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ from `src/ts`. See the [TypeScript guide](docs/ts.md).

### 🛠️ CLI Usage

#### Convert CityJSON/CityJSONSeq to FlatCityBuf
#### Convert CityJSON/CityJSONSeq/CityGML to FlatCityBuf

Replace `cargo run -p fcb_cli --` with `fcb` in the following commands if you want to use the installed binary directly.

Expand All @@ -197,6 +197,10 @@ fcb ser data.city.jsonl data.fcb --attr-index attribute_name,attribute_name2
# Back to CityJSONSeq
fcb deser data.fcb output.city.jsonl

# CityGML 2.0 / 3.0 in and out; the file extension picks the format
fcb ser 53394519_bldg_6697_op.gml bldg.fcb
fcb deser bldg.fcb bldg.gml --citygml-version 3.0

# Show information about the file (static text report)
fcb inspect data.fcb --static

Expand Down
2 changes: 2 additions & 0 deletions conformance/extension_members.expected.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"CityJSON","version":"2.0","transform":{"scale":[1e-9,1e-9,0.001],"translate":[139.7,35.6,0.0]},"CityObjects":{},"vertices":[],"metadata":{"geographicalExtent":[0.0,0.0,0.0,0.0,0.0,0.0],"referenceSystem":"https://www.opengis.net/def/crs/EPSG/0/6697"},"extensions":{"uro":{"url":"https://www.geospatial.jp/iur/schemas/uro/3.2/urbanObject.xsd","version":"3.2","xmlns":"https://www.geospatial.jp/iur/uro/3.2","xmlAttributes":{"uro:buildingID":{"codeSpace":"../../codelists/Common_buildingID.xml"}},"extraCityObjects":{"+UrbanFunction":{}}},"plain":{"url":"https://example.org/plain.ext.json","version":"1.0"}}}
{"type":"CityJSONFeature","id":"f1","CityObjects":{"f1":{"type":"+UrbanFunction","attributes":{"+buildingID":"13101-bldg-1"},"geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,1,2]]]}]}},"vertices":[[0,0,0],[1000,0,0],[0,1000,0]]}
Binary file added conformance/extension_members.fcb
Binary file not shown.
4 changes: 2 additions & 2 deletions conformance/geom_temp.expected.jsonl

Large diffs are not rendered by default.

Binary file modified conformance/geom_temp.fcb
Binary file not shown.
2 changes: 2 additions & 0 deletions conformance/inputs/extension_members.city.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"CityJSON","version":"2.0","transform":{"scale":[1e-9,1e-9,0.001],"translate":[139.7,35.6,0.0]},"CityObjects":{},"vertices":[],"metadata":{"referenceSystem":"https://www.opengis.net/def/crs/EPSG/0/6697"},"extensions":{"uro":{"url":"https://www.geospatial.jp/iur/schemas/uro/3.2/urbanObject.xsd","version":"3.2","xmlns":"https://www.geospatial.jp/iur/uro/3.2","xmlAttributes":{"uro:buildingID":{"codeSpace":"../../codelists/Common_buildingID.xml"}},"extraCityObjects":{"+UrbanFunction":{}}},"plain":{"url":"https://example.org/plain.ext.json","version":"1.0"}}}
{"type":"CityJSONFeature","id":"f1","CityObjects":{"f1":{"type":"+UrbanFunction","attributes":{"+buildingID":"13101-bldg-1"},"geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,1,2]]]}]}},"vertices":[[0,0,0],[1000,0,0],[0,1000,0]]}
3 changes: 3 additions & 0 deletions conformance/inputs/object_address.city.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"type":"CityJSON","version":"2.0","transform":{"scale":[0.001,0.001,0.001],"translate":[85000.0,446000.0,0.0]},"CityObjects":{},"vertices":[],"metadata":{"referenceSystem":"https://www.opengis.net/def/crs/EPSG/0/7415"}}
{"type":"CityJSONFeature","id":"b1","CityObjects":{"b1":{"type":"Building","attributes":{"yearOfConstruction":1932},"address":[{"CountryName":"Netherlands","LocalityName":"Delft","ThoroughfareName":"Julianalaan","ThoroughfareNumber":134,"PostalCode":"2628 BL","location":{"type":"MultiPoint","lod":"1","boundaries":[3]}},{"LocalityName":"Delft","ThoroughfareNumber":"134a","isPrimary":false,"unit":null,"lines":["Julianalaan 134a","Delft"]}],"geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,1,2]]]}],"children":["b1-p1"]},"b1-p1":{"type":"BuildingPart","parents":["b1"],"geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,2,1]]]}]}},"vertices":[[0,0,0],[1000,0,0],[0,1000,0],[500,500,0]]}
{"type":"CityJSONFeature","id":"b2","CityObjects":{"b2":{"type":"Building","address":[{"location":{"type":"MultiPoint","lod":"0","boundaries":[0]}}],"geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,1,2]]]}]}},"vertices":[[20000,0,0],[21000,0,0],[20000,1000,0]]}
3 changes: 3 additions & 0 deletions conformance/object_address.expected.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"type":"CityJSON","version":"2.0","transform":{"scale":[0.001,0.001,0.001],"translate":[85000.0,446000.0,0.0]},"CityObjects":{},"vertices":[],"metadata":{"geographicalExtent":[0.0,0.0,0.0,0.0,0.0,0.0],"referenceSystem":"https://www.opengis.net/def/crs/EPSG/0/7415"}}
{"type":"CityJSONFeature","id":"b2","CityObjects":{"b2":{"type":"Building","address":[{"location":{"type":"MultiPoint","lod":"0","boundaries":[0]}}],"geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,1,2]]]}]}},"vertices":[[20000,0,0],[21000,0,0],[20000,1000,0]]}
{"type":"CityJSONFeature","id":"b1","CityObjects":{"b1":{"type":"Building","attributes":{"yearOfConstruction":1932},"address":[{"location":{"type":"MultiPoint","lod":"1","boundaries":[3]},"CountryName":"Netherlands","LocalityName":"Delft","ThoroughfareName":"Julianalaan","ThoroughfareNumber":134,"PostalCode":"2628 BL"},{"LocalityName":"Delft","ThoroughfareNumber":"134a","isPrimary":false,"lines":["Julianalaan 134a","Delft"]}],"geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,1,2]]]}],"children":["b1-p1"]},"b1-p1":{"type":"BuildingPart","geometry":[{"type":"MultiSurface","lod":"1","boundaries":[[[0,2,1]]]}],"parents":["b1"]}},"vertices":[[0,0,0],[1000,0,0],[0,1000,0],[500,500,0]]}
Binary file added conformance/object_address.fcb
Binary file not shown.
13 changes: 12 additions & 1 deletion docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ the `feature.fbs` schema (see `src/fbs/feature.fbs`) defines the structure of ci

- **cityfeature**: the root object containing city objects and shared vertices
- **cityobject**: individual 3d features with type, geometry, and attributes
- **address**: one entry of a city object's CityJSON `address` array (see below)
- **geometry**: complex structure for 3d geometries with boundaries and semantics
- **semanticobject**: semantic classification of geometry parts

Expand All @@ -52,7 +53,7 @@ the `geometry.fbs` schema (see `src/fbs/geometry.fbs`) defines geometry structur

the `extension.fbs` schema (see `src/fbs/extension.fbs`) defines support for cityjson extensions:

- **extension**: contains extension metadata and schema definitions
- **extension**: contains extension metadata and schema definitions. `members` holds every member of the CityJSON `extensions` entry other than `url` and `version` (a CityGML ADE's `xmlns`, say) as one stringified JSON object, so the entry reads back whole
- supports custom cityobject types and semantic surfaces
- enables extensibility while maintaining core format efficiency

Expand Down Expand Up @@ -338,6 +339,16 @@ attributes must be decoded against the schema of the `CityObject` that owns them
- objects within one feature differ: the `Building` parent carries no attributes while its `BuildingPart` child carries them all. code must walk all objects rather than assuming object 0.
- getting this wrong does **not** fail loudly. attribute records are not self-delimiting — each value's width comes from its column's type — so a wrong schema desynchronises the remainder of the blob and yields plausible-looking garbage instead of an error. it surfaced as a nonsense column index (28777, which is ASCII `"ip"` from the middle of a string value) during the native C++ port.

### city object addresses

`CityObject.address` holds the CityJSON `address` array, one `Address` table per entry, in order. an address's members are free-form, so each `Address` carries its own schema:

- `Address.columns` is the schema its `attributes` blob is encoded with. it is always set when `attributes` is; `Header.columns` is never used for an address.
- `Address.attributes` holds every member but `location`, encoded exactly as object attributes are (see "null values": a `null` member is not written).
- `Address.location` is the `location` member, a `MultiPoint` `Geometry` whose boundaries index the feature's `vertices`, like any other geometry of the feature.

an absent `address` field means the object has no addresses. files written before the field existed read as having none.

### attribute access

to access an attribute:
Expand Down
6 changes: 3 additions & 3 deletions docs/ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ surface than the Rust and C++ implementations.** Concretely:
|---|---|
| Read | Yes — header, features, geometry, appearance, attributes, spatial and attribute queries |
| Write | **No.** Only Rust (`src/rust/fcb_core`) and C++ (`src/cpp`) produce `.fcb` files |
| Conformance | **14 / 14** corpus cases pass, compared whole-line against the Rust reader's own output ([`src/ts/test/conformance.test.ts:29-70`](../src/ts/test/conformance.test.ts)) |
| Node suite | 16 files, 244 tests passing, 1 skipped (the opt-in live-3DBAG test) |
| Conformance | **16 / 16** corpus cases pass, compared whole-line against the Rust reader's own output ([`src/ts/test/conformance.test.ts:29-72`](../src/ts/test/conformance.test.ts)) |
| Node suite | 17 files, 264 tests passing, 1 skipped (the opt-in live-3DBAG test) |
| Browser suite | 3 tests in real headless Chromium ([`src/ts/test/browser/`](../src/ts/test/browser/)) |
| npm | Published as `@cityjson/flatcitybuf`, currently `0.3.0` |

Expand Down Expand Up @@ -216,7 +216,7 @@ startup budget — do not run this concurrently with a heavy build.
inputs, the `.fcb` binaries, and `.expected.jsonl` files holding **the Rust
reader's own output** for each case. The `.fcb` files are tracked, so a clean
checkout runs the suite with no Rust toolchain. `test/conformance.test.ts`
walks all 14 cases that have a matching `.expected.jsonl`, emits metadata plus
walks all 16 cases that have a matching `.expected.jsonl`, emits metadata plus
every feature through `toCityJSONMetadata`/`toCityJSONFeature`, and compares
**whole lines** — comparing selected keys is exactly what once hid a missing
per-feature `appearance` object through an entire port.
Expand Down
102 changes: 102 additions & 0 deletions docs/upstream-findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -1781,3 +1781,105 @@ above is size, `inspect`, and a `deser` round-trip compared line by line.
**What a consumer saw:** piping the writer's output (`ser … - | …`) on real
heterogeneous data produced a file that no reader can open, with a clean exit
status. No justfile recipe uses the `-` path, which is why it went unnoticed.

## 37. The `geom_temp` fixture's geometry templates pointed past their materials palette — FIXED (fixture repaired)

**Where:** `src/rust/fcb_core/tests/data/geom_temp.city.jsonl`, line 1, and
the `conformance/geom_temp.{fcb,expected.jsonl}` pair built from it. The defect
is upstream, in cjseq's `cat`, which wrote this header.

The header holds two materials, but template 0 used material `1` and
template 1 used material `2`. cjseq 0.5's `validate` rejects it:

```
$ cjseq validate geom_temp.city.jsonl
header: in geometry template 1: Material index 2 is out of range (there are 2)
```

cjseq's `cat_metadata` (up to 0.4.x) renumbered the templates' material,
texture and UV indices into a clone, sliced the palette to match, and then
wrote the **original** templates beside the sliced palette. Materials were the
only part that went wrong here: the templates' textures (`0`, `1`) and UVs
(`0..=1014`) were already numbered in first-use order, so their renumbering
was the identity. Every reader in this repo round-tripped the dangling indices
faithfully, which is why no test noticed.

**Fix:** the renumbering `cat` computed and dropped is applied by hand —
template materials `1 → 0` and `2 → 1`, nothing else in the file touched — and
the fixture now validates. `conformance/geom_temp.fcb` and
`geom_temp.expected.jsonl` were regenerated with `scripts/gen_conformance.sh`;
every other corpus file is unchanged. No test asserts on the material index
values, so the Rust, C++, Python and TypeScript suites pass unchanged.

`scripts/gen_conformance.sh` passes `-A` to every input under
`conformance/inputs/`, but `btree_multilevel`, `header_metadata_full` and
`rtree_multilevel` were built with other flags (`--attr-branching-factor 4`;
no `-A`; no `-A`). Re-running the script rewrites those three `.fcb`s, and
`test_writer_oracle.cpp` then compares against the wrong bytes. Restore them
from git after a run.

## 38. City Object `address` was dropped on write, with no error — FIXED (branch `citygml-io`)

**Where:** `src/fbs/feature.fbs` `CityObject` had no field for it, and
`fcb_core`'s `to_city_object` never read `co.address`. Every port inherited
the gap.

A CityJSON 2.0 City Object may carry `address`, an array of objects whose
`location` is a MultiPoint into the feature's vertices. `fcb ser` then `fcb
deser` returned the object without it. CityGML is where it bites: the CityGML
crates fill `address` from `bldg:address` (xAL), so every PLATEAU building
address was lost.

**Fix:** `CityObject.address:[Address]`, where `Address` holds every member
but `location` as an attribute blob with its own `columns`, and `location` as a
`Geometry`. Written and read by Rust and C++; read by Python and TypeScript.
Corpus fixture: `object_address`. Null members are not written, as everywhere
else in the format.

## 39. Extension declarations kept only `url` and `version` — FIXED (branch `citygml-io`)

**Where:** `src/fbs/extension.fbs` `Extension` held `name`, `url`, `version`
and the schema strings only.

A CityJSON `extensions` entry may hold further members. The CityGML crates put
an ADE's `xmlns` and its constant XML attributes there, and the CityGML writer
refuses an ADE prefix whose `xmlns` it cannot find. So a PLATEAU file with the
`uro` ADE could be read into `.fcb` but not written back out as CityGML.

**Fix:** `Extension.members`, the entry's other members as one JSON object
string, absent when there are none. Written and read by Rust and C++; read by
Python and TypeScript. A `members` that is not a JSON object is an
`InvalidAttributeValue` error. Corpus fixture: `extension_members`; the C++
writer's output for it is byte-identical to Rust's.

## 40. A vertex outside the 32-bit range panicked the Rust writer and was truncated by the C++ writer — FIXED (branch `citygml-io`)

**Where:** `fcb_core/src/writer/serializer.rs` converted each quantised
coordinate with `i32::try_from(c).unwrap()`; `src/cpp/src/writer/feature_serializer.cpp`
used `nlohmann::json::get<std::int32_t>()`, which casts.

A `Vertex` is three `i32`. A CityJSON file whose transform is too fine for its
extent quantises past `i32::MAX`; the Rust writer then panicked, and the C++
writer wrapped the coordinate silently into a different position.

**Fix:** both writers return an error (`Error::VertexOutOfRange` in Rust,
`ErrorCode::VertexOutOfRange` in C++). Nothing that fits is affected; the
corpus is byte-identical.

## 41. Every reader invents a zero `geographicalExtent` for a header that has none — NOT FIXED (worked around in `fcb deser` to CityGML)

**Where:** `fcb_core/src/reader/deserializer.rs` `to_cj_metadata` reads
`header.geographical_extent()` with `.unwrap_or_default()`, so an absent
extent becomes `[0, 0, 0, 0, 0, 0]`. The C++, Python and TypeScript readers
copy this on purpose to match the oracle (`src/cpp/src/cityjson.cpp`,
`src/py/flatcitybuf/cityjson.py`, `src/ts/src/cityjson/index.ts`), and 7 of
the 20 `conformance/*.expected.jsonl` files carry it.

It invents data: a CityGML document written from such a header declares the
envelope `0 0 0` to `0 0 0`, and reading that document again quantises its
coordinates from translate 0, past the 32-bit vertex range.

**Workaround:** `fcb_cli::citygml::write` drops the extent when the file's
header has none. **Fix, if wanted:** read an absent extent as absent in all
four readers and regenerate the seven expected files; a reader change across
the corpus, so left for a decision.
2 changes: 1 addition & 1 deletion src/cpp/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#---------------------------------------------------------------------------
DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = "FlatCityBuf C++ reader"
PROJECT_NUMBER = 0.8.0
PROJECT_NUMBER = 0.9.0
PROJECT_BRIEF = "Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format"
PROJECT_LOGO =
PROJECT_ICON =
Expand Down
1 change: 1 addition & 0 deletions src/cpp/include/fcb/error.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum class ErrorCode {
IoError,
HttpError,
JsonError,
VertexOutOfRange,
};

/// Every failure the library reports is one of these. Derives from
Expand Down
22 changes: 18 additions & 4 deletions src/cpp/include/fcb/generated/extension_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ struct Extension FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_EXTRA_ATTRIBUTES = 14,
VT_EXTRA_CITY_OBJECTS = 16,
VT_EXTRA_ROOT_PROPERTIES = 18,
VT_EXTRA_SEMANTIC_SURFACES = 20
VT_EXTRA_SEMANTIC_SURFACES = 20,
VT_MEMBERS = 22
};
const ::flatbuffers::String *name() const {
return GetPointer<const ::flatbuffers::String *>(VT_NAME);
Expand Down Expand Up @@ -56,6 +57,9 @@ struct Extension FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
const ::flatbuffers::String *extra_semantic_surfaces() const {
return GetPointer<const ::flatbuffers::String *>(VT_EXTRA_SEMANTIC_SURFACES);
}
const ::flatbuffers::String *members() const {
return GetPointer<const ::flatbuffers::String *>(VT_MEMBERS);
}
bool Verify(::flatbuffers::Verifier &verifier) const {
return VerifyTableStart(verifier) &&
VerifyOffset(verifier, VT_NAME) &&
Expand All @@ -76,6 +80,8 @@ struct Extension FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
verifier.VerifyString(extra_root_properties()) &&
VerifyOffset(verifier, VT_EXTRA_SEMANTIC_SURFACES) &&
verifier.VerifyString(extra_semantic_surfaces()) &&
VerifyOffset(verifier, VT_MEMBERS) &&
verifier.VerifyString(members()) &&
verifier.EndTable();
}
};
Expand Down Expand Up @@ -111,6 +117,9 @@ struct ExtensionBuilder {
void add_extra_semantic_surfaces(::flatbuffers::Offset<::flatbuffers::String> extra_semantic_surfaces) {
fbb_.AddOffset(Extension::VT_EXTRA_SEMANTIC_SURFACES, extra_semantic_surfaces);
}
void add_members(::flatbuffers::Offset<::flatbuffers::String> members) {
fbb_.AddOffset(Extension::VT_MEMBERS, members);
}
explicit ExtensionBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
Expand All @@ -132,8 +141,10 @@ inline ::flatbuffers::Offset<Extension> CreateExtension(
::flatbuffers::Offset<::flatbuffers::String> extra_attributes = 0,
::flatbuffers::Offset<::flatbuffers::String> extra_city_objects = 0,
::flatbuffers::Offset<::flatbuffers::String> extra_root_properties = 0,
::flatbuffers::Offset<::flatbuffers::String> extra_semantic_surfaces = 0) {
::flatbuffers::Offset<::flatbuffers::String> extra_semantic_surfaces = 0,
::flatbuffers::Offset<::flatbuffers::String> members = 0) {
ExtensionBuilder builder_(_fbb);
builder_.add_members(members);
builder_.add_extra_semantic_surfaces(extra_semantic_surfaces);
builder_.add_extra_root_properties(extra_root_properties);
builder_.add_extra_city_objects(extra_city_objects);
Expand All @@ -156,7 +167,8 @@ inline ::flatbuffers::Offset<Extension> CreateExtensionDirect(
const char *extra_attributes = nullptr,
const char *extra_city_objects = nullptr,
const char *extra_root_properties = nullptr,
const char *extra_semantic_surfaces = nullptr) {
const char *extra_semantic_surfaces = nullptr,
const char *members = nullptr) {
auto name__ = name ? _fbb.CreateString(name) : 0;
auto description__ = description ? _fbb.CreateString(description) : 0;
auto url__ = url ? _fbb.CreateString(url) : 0;
Expand All @@ -166,6 +178,7 @@ inline ::flatbuffers::Offset<Extension> CreateExtensionDirect(
auto extra_city_objects__ = extra_city_objects ? _fbb.CreateString(extra_city_objects) : 0;
auto extra_root_properties__ = extra_root_properties ? _fbb.CreateString(extra_root_properties) : 0;
auto extra_semantic_surfaces__ = extra_semantic_surfaces ? _fbb.CreateString(extra_semantic_surfaces) : 0;
auto members__ = members ? _fbb.CreateString(members) : 0;
return CreateExtension(
_fbb,
name__,
Expand All @@ -176,7 +189,8 @@ inline ::flatbuffers::Offset<Extension> CreateExtensionDirect(
extra_attributes__,
extra_city_objects__,
extra_root_properties__,
extra_semantic_surfaces__);
extra_semantic_surfaces__,
members__);
}

#endif // FLATBUFFERS_GENERATED_EXTENSION_H_
Loading
Loading