FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
cityjson.cpp
Go to the documentation of this file.
1#include <fcb/cityjson.hpp>
2
3#ifdef FCB_WITH_JSON
4
5# include <fcb/attribute.hpp>
6# include <fcb/generated/feature_generated.h>
7# include <fcb/generated/header_generated.h>
8# include <fcb/geometry.hpp>
9
10# include <array>
11# include <charconv>
12# include <cstring>
13# include <string>
14
17
18namespace fcb {
19
20namespace {
21
22// ---------------------------------------------------------------------------
23// UNKNOWN-TAG POLICY
24//
25// Three enumerators reach this file with no CityJSON name of their own:
26// CityObjectType::ExtensionObject, SemanticSurfaceType::ExtraSemanticSurface,
27// and any GeometryType a newer encoder might add. One policy per tag, and each
28// matches the Rust reader exactly (deserializer.rs::to_cj_co_type,
29// geom_decoder.rs::to_cj_surface_type, geom_decoder.rs::GeometryType::to_cj).
30//
31// A City Object type and a semantic surface type each have a CityJSON
32// extension escape hatch (spec sections 8 and 3.3): any name starting with '+'
33// is legal. So an unnameable tag is spelled "+UnknownCityObject" /
34// "+GenericSurface" -- a placeholder, but a SCHEMA-VALID one. These names
35// deliberately do NOT appear in the tables below: "ExtensionObject" and
36// "ExtraSemanticSurface" are FlatBuffers enumerator names, not CityJSON type
37// names, they carry no '+', and emitting either produces a document an
38// official validator rejects. That is as much a defect as rejecting a valid
39// one.
40//
41// A geometry type has no such escape hatch -- CityJSON section 3 enumerates
42// exactly eight `type` values and admits no others -- so there is no valid
43// string to fall back to and geometry_type_name() throws instead. See
44// geometry.cpp.
45//
46// The appearance enums (wrapMode, textureType) throw as well, on both sides.
47// ---------------------------------------------------------------------------
48
52const char* const kCityObjectTypeNames[] = {
53 "Bridge",
54 "BridgePart",
55 "BridgeInstallation",
56 "BridgeConstructiveElement",
57 "BridgeRoom",
58 "BridgeFurniture",
59 "Building",
60 "BuildingPart",
61 "BuildingInstallation",
62 "BuildingConstructiveElement",
63 "BuildingFurniture",
64 "BuildingStorey",
65 "BuildingRoom",
66 "BuildingUnit",
67 "CityFurniture",
68 "CityObjectGroup",
69 "GenericCityObject",
70 "LandUse",
71 "OtherConstruction",
72 "PlantCover",
73 "SolitaryVegetationObject",
74 "TINRelief",
75 "Road",
76 "Railway",
77 "Waterway",
78 "TransportSquare",
79 "Tunnel",
80 "TunnelPart",
81 "TunnelInstallation",
82 "TunnelConstructiveElement",
83 "TunnelHollowSpace",
84 "TunnelFurniture",
85 "WaterBody",
86};
87
89constexpr const char* kUnknownCityObjectName = "+UnknownCityObject";
90
91UIntView as_uint_view(const flatbuffers::Vector<std::uint32_t>* v) {
92 if (v == nullptr)
93 return {};
94 return UIntView(v->data(), v->size());
95}
96
97const char* const kSemanticSurfaceTypeNames[] = {
98 "RoofSurface", "GroundSurface", "WallSurface", "ClosureSurface",
99 "OuterCeilingSurface", "OuterFloorSurface", "Window", "Door",
100 "InteriorWallSurface", "CeilingSurface", "FloorSurface", "WaterSurface",
101 "WaterGroundSurface", "WaterClosureSurface", "TrafficArea", "AuxiliaryTrafficArea",
102 "TransportationMarking", "TransportationHole",
103};
104
106constexpr const char* kGenericSurfaceName = "+GenericSurface";
107
108GeometryKind kind_of(const ::Geometry* g) {
109 return static_cast<GeometryKind>(static_cast<std::uint8_t>(g->type()));
110}
111
118nlohmann::json color_to_json(const flatbuffers::Vector<double>* c) {
119 if (c == nullptr || c->size() != 3)
120 return nullptr;
121 auto out = nlohmann::json::array();
122 for (double v : *c)
123 out.push_back(v);
124 return out;
125}
126
130nlohmann::json border_color_to_json(const flatbuffers::Vector<double>* c) {
131 if (c == nullptr || (c->size() != 3 && c->size() != 4))
132 return nullptr;
133 auto out = nlohmann::json::array();
134 for (double v : *c)
135 out.push_back(v);
136 return out;
137}
138
147[[noreturn]] void unknown_enum_tag(const char* member, int tag) {
149 std::string("unknown ") + member + " tag " + std::to_string(tag));
150}
151
155const char* texture_format_name(::TextureFormat f) {
156 switch (f) {
157 case ::TextureFormat::PNG:
158 return "PNG";
159 case ::TextureFormat::JPG:
160 return "JPG";
161 }
162 unknown_enum_tag("type", static_cast<int>(f));
163}
164
165const char* wrap_mode_name(::WrapMode w) {
166 switch (w) {
167 case ::WrapMode::None:
168 return "none";
169 case ::WrapMode::Wrap:
170 return "wrap";
171 case ::WrapMode::Mirror:
172 return "mirror";
173 case ::WrapMode::Clamp:
174 return "clamp";
175 case ::WrapMode::Border:
176 return "border";
177 }
178 unknown_enum_tag("wrapMode", static_cast<int>(w));
179}
180
181const char* texture_type_name(::TextureType t) {
182 switch (t) {
183 case ::TextureType::Unknown:
184 return "unknown";
185 case ::TextureType::Specific:
186 return "specific";
187 case ::TextureType::Typical:
188 return "typical";
189 }
190 unknown_enum_tag("textureType", static_cast<int>(t));
191}
192
196nlohmann::json appearance_to_json(const ::Appearance* a) {
197 nlohmann::json out = nlohmann::json::object();
198
199 if (a->materials() != nullptr) {
200 auto materials = nlohmann::json::array();
201 for (const auto* m : *a->materials()) {
202 if (m == nullptr)
203 continue;
204 nlohmann::json j = nlohmann::json::object();
205 j["name"] = (m->name() != nullptr) ? m->name()->str() : "";
206 // Every other field is optional and omitted when unset, which
207 // is what serde's skip_serializing_if does on the Rust side.
208 if (const auto v = m->ambient_intensity())
209 j["ambientIntensity"] = *v;
210 if (auto c = color_to_json(m->diffuse_color()); !c.is_null())
211 j["diffuseColor"] = c;
212 if (auto c = color_to_json(m->emissive_color()); !c.is_null())
213 j["emissiveColor"] = c;
214 if (auto c = color_to_json(m->specular_color()); !c.is_null())
215 j["specularColor"] = c;
216 if (const auto v = m->shininess())
217 j["shininess"] = *v;
218 if (const auto v = m->transparency())
219 j["transparency"] = *v;
220 if (const auto v = m->is_smooth())
221 j["isSmooth"] = *v;
222 materials.push_back(std::move(j));
223 }
224 out["materials"] = std::move(materials);
225 }
226
227 if (a->textures() != nullptr) {
228 auto textures = nlohmann::json::array();
229 for (const auto* t : *a->textures()) {
230 if (t == nullptr)
231 continue;
232 nlohmann::json j = nlohmann::json::object();
233 j["type"] = texture_format_name(t->type());
234 // `image` is mandatory in header.fbs but optional in CityJSON, so
235 // the writer stores "" both for an absent `image` and for a
236 // schema-valid `"image": ""`. The two are indistinguishable on the
237 // wire and one of them must be lost; decoding "" back to ABSENT is
238 // a deliberate choice mirrored from deserializer.rs, not something
239 // derivable from the schema. Emitting `"image": ""` here instead
240 // is what this reader used to do, and it diverged.
241 if (t->image() != nullptr && t->image()->size() > 0) {
242 j["image"] = t->image()->str();
243 }
244 if (const auto w = t->wrap_mode())
245 j["wrapMode"] = wrap_mode_name(*w);
246 if (const auto tt = t->texture_type())
247 j["textureType"] = texture_type_name(*tt);
248 if (auto c = border_color_to_json(t->border_color()); !c.is_null()) {
249 j["borderColor"] = c;
250 }
251 textures.push_back(std::move(j));
252 }
253 out["textures"] = std::move(textures);
254 }
255
256 // UV pairs. Vec2 is a struct of two doubles; read via memcpy for the
257 // same alignment reason as Transform and DoubleVertex.
258 if (a->vertices_texture() != nullptr) {
259 auto uvs = nlohmann::json::array();
260 for (const auto* v : *a->vertices_texture()) {
261 double u;
262 double w;
263 std::memcpy(&u, reinterpret_cast<const std::uint8_t*>(v), sizeof(double));
264 std::memcpy(&w, reinterpret_cast<const std::uint8_t*>(v) + sizeof(double),
265 sizeof(double));
266 uvs.push_back(nlohmann::json::array({u, w}));
267 }
268 out["vertices-texture"] = std::move(uvs);
269 }
270
271 if (a->default_theme_texture() != nullptr) {
272 out["default-theme-texture"] = a->default_theme_texture()->str();
273 }
274 if (a->default_theme_material() != nullptr) {
275 out["default-theme-material"] = a->default_theme_material()->str();
276 }
277
278 return out;
279}
280
290nlohmann::json
291materials_to_json(const flatbuffers::Vector<flatbuffers::Offset<::MaterialMapping>>* mappings,
292 GeometryKind type) {
293 nlohmann::json out = nlohmann::json::object();
294 for (const auto* m : *mappings) {
295 if (m == nullptr)
296 continue;
297 const std::string theme = (m->theme() != nullptr) ? m->theme()->str() : "theme";
298 // A `value` colours the whole object and has no depth at all.
299 if (const auto v = m->value()) {
300 out[theme] = {{"value", *v}};
301 continue;
302 }
303 if (m->vertices() == nullptr) {
304 out[theme] = {{"values", nullptr}};
305 continue;
306 }
307 out[theme] = {{"values", decode_material_values(type, as_uint_view(m->solids()),
308 as_uint_view(m->shells()),
309 as_uint_view(m->vertices()))}};
310 }
311 return out;
312}
313
321nlohmann::json
322textures_to_json(const flatbuffers::Vector<flatbuffers::Offset<::TextureMapping>>* mappings,
323 GeometryKind type) {
324 nlohmann::json out = nlohmann::json::object();
325 for (const auto* m : *mappings) {
326 if (m == nullptr)
327 continue;
328 const std::string theme = (m->theme() != nullptr) ? m->theme()->str() : "theme";
329 if (m->vertices() == nullptr) {
330 out[theme] = nlohmann::json::object();
331 continue;
332 }
333 out[theme] = {{"values", decode_texture_values(
334 type, as_uint_view(m->solids()), as_uint_view(m->shells()),
335 as_uint_view(m->surfaces()), as_uint_view(m->strings()),
336 as_uint_view(m->vertices()))}};
337 }
338 return out;
339}
340
341nlohmann::json geometry_instance_to_json(const ::GeometryInstance* gi) {
342 nlohmann::json out = nlohmann::json::object();
343 out["type"] = "GeometryInstance";
344 out["template"] = gi->template_();
345
346 // The boundaries array holds exactly one vertex index: CityGML's
347 // "referencePoint" for the instance.
348 auto b = nlohmann::json::array();
349 if (gi->boundaries() != nullptr) {
350 for (std::uint32_t v : *gi->boundaries())
351 b.push_back(v);
352 }
353 out["boundaries"] = std::move(b);
354
355 // 16 doubles in row-major order. Read via memcpy: like Transform in the
356 // header, this struct can sit at a misaligned internal offset.
357 if (const auto* m = gi->transformation()) {
358 auto mat = nlohmann::json::array();
359 for (std::size_t i = 0; i < 16; ++i) {
360 double d;
361 std::memcpy(&d, reinterpret_cast<const std::uint8_t*>(m) + i * sizeof(double),
362 sizeof(double));
363 mat.push_back(d);
364 }
365 out["transformationMatrix"] = std::move(mat);
366 }
367 return out;
368}
369
370nlohmann::json geometry_to_json(const ::Geometry* g,
371 const std::vector<ColumnInfo>& semantic_columns) {
372 nlohmann::json out = nlohmann::json::object();
373 out["type"] = geometry_type_name(static_cast<std::uint8_t>(g->type()));
374 if (g->lod() != nullptr)
375 out["lod"] = g->lod()->str();
376
377 out["boundaries"] = decode_boundaries(
378 kind_of(g), as_uint_view(g->solids()), as_uint_view(g->shells()),
379 as_uint_view(g->surfaces()), as_uint_view(g->strings()), as_uint_view(g->boundaries()));
380
381 // Semantics: a surface list plus a values array indexing into it.
382 // ABSENT-VS-EMPTY, fourth site. The reference keys the `semantics` member
383 // off the PRESENCE of `semantics_objects` alone (deserializer.rs:699), so
384 // a present-but-EMPTY surface vector is `"surfaces": []` -- a semantics
385 // member with no surfaces -- and not a missing member. The writer emits
386 // exactly that shape (serializer.rs:946 calls create_vector on a possibly
387 // empty Vec), so a `size() > 0` guard here dropped the whole member.
388 if (g->semantics_objects() != nullptr) {
389 auto surfaces = nlohmann::json::array();
390 for (const auto* so : *g->semantics_objects()) {
391 if (so == nullptr)
392 continue;
393 nlohmann::json s = nlohmann::json::object();
394 s["type"] = (so->extension_type() != nullptr)
395 ? so->extension_type()->str()
396 : semantic_surface_type_name(static_cast<std::uint8_t>(so->type()));
397 // `parent:uint = null` in the schema -- absent and zero are both
398 // real states, so this must check the Optional rather than
399 // testing for non-zero. Links a Door/Window surface back to its
400 // WallSurface (geom_decoder.rs:217).
401 if (const auto p = so->parent())
402 s["parent"] = *p;
403 // Semantic surfaces carry their own attributes, decoded against
404 // Header.semantic_columns -- a schema separate from the feature
405 // attribute columns. Merged inline, as the reference does.
406 if (so->attributes() != nullptr && so->attributes()->size() > 0) {
407 auto attrs = attributes_to_json(
408 bytes_view(so->attributes()->data(), so->attributes()->size()),
409 semantic_columns);
410 for (auto& [k, v] : attrs.items())
411 s[k] = v;
412 }
413 if (so->children() != nullptr && so->children()->size() > 0) {
414 auto kids = nlohmann::json::array();
415 for (std::uint32_t c : *so->children())
416 kids.push_back(c);
417 s["children"] = std::move(kids);
418 }
419 surfaces.push_back(std::move(s));
420 }
421
422 nlohmann::json sem = nlohmann::json::object();
423 sem["surfaces"] = std::move(surfaces);
424 // ABSENT-VS-EMPTY, third of three. The SURFACES decide whether there
425 // is a `semantics` member at all; the values vector being absent is
426 // `"values": null` -- a member with a null value, not a missing
427 // member and not an empty array (deserializer.rs:700).
428 if (g->semantics() == nullptr) {
429 sem["values"] = nullptr;
430 } else {
431 sem["values"] =
432 decode_semantics_values(kind_of(g), as_uint_view(g->solids()),
433 as_uint_view(g->shells()), as_uint_view(g->semantics()));
434 }
435 out["semantics"] = std::move(sem);
436 }
437
438 // Appearance: per-geometry mappings only -- the indices into the palette,
439 // not the palette itself. The palette these index into is emitted by
440 // `to_cityjson_metadata` (header-level) and by `to_cityjson_feature`
441 // (feature-level). It used to be dropped on the header path, on the since
442 // disproved grounds that the Rust reader did not emit it either; that was
443 // upstream finding #31, and it left these mappings pointing at nothing.
444 //
445 // An EMPTY mapping vector omits the key entirely: the reference returns
446 // None for an empty slice (geom_decoder.rs:343, :472) and serde drops the
447 // field. Every mapping in a NON-empty vector now yields a theme -- an
448 // absent `vertices` is a null or absent `values`, not a theme to skip.
449 if (g->material() != nullptr && g->material()->size() > 0) {
450 out["material"] = materials_to_json(g->material(), kind_of(g));
451 }
452 if (g->texture() != nullptr && g->texture()->size() > 0) {
453 out["texture"] = textures_to_json(g->texture(), kind_of(g));
454 }
455
456 return out;
457}
458
467nlohmann::json point_of_contact_address_to_json(const FileInfo& info) {
468 nlohmann::json addr = nlohmann::json::object();
469 const auto insert = [&addr](const char* key, const std::string& value) {
470 if (!value.empty())
471 addr[key] = value;
472 };
473 insert("thoroughfareNumber", info.poc_address_thoroughfare_number);
474 insert("thoroughfareName", info.poc_address_thoroughfare_name);
475 insert("locality", info.poc_address_locality);
476 insert("postcode", info.poc_address_postcode);
477 insert("country", info.poc_address_country);
478 if (addr.empty())
479 return nullptr;
480 return addr;
481}
482
509nlohmann::json point_of_contact_to_json(const FileInfo& info) {
510 if (!info.poc_contact_name.has_value())
511 return nullptr;
512 if (!info.poc_email.has_value()) {
513 throw Error(ErrorCode::MissingRequiredField, "email_address");
514 }
515
516 nlohmann::json poc = nlohmann::json::object();
517 poc["contactName"] = *info.poc_contact_name;
518 if (info.poc_contact_type.has_value())
519 poc["contactType"] = *info.poc_contact_type;
520 if (info.poc_role.has_value())
521 poc["role"] = *info.poc_role;
522 if (info.poc_phone.has_value())
523 poc["phone"] = *info.poc_phone;
524 poc["emailAddress"] = *info.poc_email;
525 if (info.poc_website.has_value())
526 poc["website"] = *info.poc_website;
527 if (auto addr = point_of_contact_address_to_json(info); !addr.is_null()) {
528 poc["address"] = std::move(addr);
529 }
530 return poc;
531}
532
541nlohmann::json extensions_to_json(const ::Header* hdr) {
542 if (hdr == nullptr || hdr->extensions() == nullptr)
543 return nullptr;
544
545 nlohmann::json out = nlohmann::json::object();
546 for (const auto* e : *hdr->extensions()) {
547 if (e == nullptr || e->name() == nullptr)
548 continue;
549 nlohmann::json entry = nlohmann::json::object();
550 entry["url"] = (e->url() != nullptr) ? e->url()->str() : "";
551 entry["version"] = (e->version() != nullptr) ? e->version()->str() : "";
552 out[e->name()->str()] = std::move(entry);
553 }
554 return out.empty() ? nlohmann::json(nullptr) : out;
555}
556
557} // namespace
558
559std::string city_object_type_name(std::uint8_t type) {
560 constexpr std::size_t kCount = sizeof(kCityObjectTypeNames) / sizeof(kCityObjectTypeNames[0]);
561 // UNKNOWN-TAG POLICY. ExtensionObject (the enumerator just past the table)
562 // and any tag a newer encoder may add come back as "+UnknownCityObject".
563 // This used to throw, which meant a file whose ExtensionObject had lost
564 // its `extension_type` string was unreadable here while the Rust reader
565 // read it happily. It is only ever reached when `extension_type` is
566 // absent -- when it is present, its verbatim string wins.
567 if (type >= kCount)
568 return kUnknownCityObjectName;
569 return kCityObjectTypeNames[type];
570}
571
572std::string semantic_surface_type_name(std::uint8_t type) {
573 constexpr std::size_t kCount =
574 sizeof(kSemanticSurfaceTypeNames) / sizeof(kSemanticSurfaceTypeNames[0]);
575 // UNKNOWN-TAG POLICY, as for city_object_type_name above.
576 // ExtraSemanticSurface, the enumerator just past the table, and anything a
577 // newer encoder adds come back as "+GenericSurface" -- the same string the
578 // Rust reader emits (geom_decoder.rs::to_cj_surface_type). Only reached
579 // when the surface's `extension_type` string is absent.
580 if (type >= kCount)
581 return kGenericSurfaceName;
582 return kSemanticSurfaceTypeNames[type];
583}
584
585nlohmann::json to_cityjson_metadata(const HeaderView& header) {
586 const auto& info = header.info();
587 nlohmann::json cj = nlohmann::json::object();
588
589 cj["type"] = "CityJSON";
590 cj["version"] = info.cityjson_version;
591
592 // `transform` is unconditional on the Rust side: `to_cj_metadata` starts
593 // from `CityJSON::new()`, whose `Transform::new()` defaults to
594 // scale [1,1,1] / translate [0,0,0] (cjseq2 lib.rs:1057-1064), and only
595 // overwrites it when `header.transform()` is `Some` (deserializer.rs:
596 // 24-31). Rust never omits the key, so this must not gate on
597 // `has_transform` either -- same "unconditional, not conditional" class
598 // as `metadata`/`geographicalExtent`/`extensions` above.
599 cj["transform"] = {
600 {"scale", info.has_transform ? info.scale : std::array<double, 3>{1.0, 1.0, 1.0}},
601 {"translate", info.has_transform ? info.translate : std::array<double, 3>{0.0, 0.0, 0.0}}};
602
603 // `metadata` and `metadata.geographicalExtent` are UNCONDITIONAL on the
604 // Rust side (deserializer.rs:81-90): `cj.metadata` is always
605 // `Some(CjMetadata { geographical_extent: Some(...), ... })`, defaulting
606 // the extent to six zeros via `.unwrap_or_default()` when the header
607 // carries none, rather than omitting the field. Confirmed empirically
608 // on noise_extension.fcb, whose header has no GeographicalExtent at all
609 // yet whose Rust-reader output still carries
610 // `"metadata":{"geographicalExtent":[0,0,0,0,0,0]}` -- a gap the
611 // previously-narrowed metadata comparison in test_conformance.cpp
612 // masked. Every other field stays conditional, matching the `Option`s
613 // in CjMetadata.
614 nlohmann::json meta = nlohmann::json::object();
615 meta["geographicalExtent"] =
616 info.has_extent ? info.geographical_extent : std::array<double, 6>{};
617 if (!info.crs.empty()) {
618 meta["referenceSystem"] = "https://www.opengis.net/def/crs/" +
619 info.crs.substr(0, info.crs.find(':')) + "/0/" +
620 info.crs.substr(info.crs.find(':') + 1);
621 }
622 if (info.identifier.has_value())
623 meta["identifier"] = *info.identifier;
624 if (auto poc = point_of_contact_to_json(info); !poc.is_null()) {
625 meta["pointOfContact"] = std::move(poc);
626 }
627 if (info.reference_date.has_value())
628 meta["referenceDate"] = *info.reference_date;
629 if (info.title.has_value())
630 meta["title"] = *info.title;
631 cj["metadata"] = std::move(meta);
632
633 // A CityJSONSeq header line carries no features of its own.
634 cj["CityObjects"] = nlohmann::json::object();
635 cj["vertices"] = nlohmann::json::array();
636
637 // Geometry templates: shapes shared by every GeometryInstance in the
638 // file, with their own vertex list. Emitted only when BOTH arrays are
639 // present -- a template without vertices indexes nothing
640 // (deserializer.rs:92).
641 const ::Header* hdr = detail::HeaderAccess::get(header);
642 if (auto ext = extensions_to_json(hdr); !ext.is_null()) {
643 cj["extensions"] = std::move(ext);
644 }
645
646 // The header's own appearance palette. The geometry templates below index
647 // straight into it -- a template belongs to no feature, so its
648 // `material`/`texture` mapping can refer to nothing else. Emitting the
649 // templates while dropping this left those mappings dangling: upstream
650 // finding #31, fixed in `to_cj_metadata` (deserializer.rs).
651 if (hdr != nullptr && hdr->appearance() != nullptr) {
652 cj["appearance"] = appearance_to_json(hdr->appearance());
653 }
654
655 if (hdr != nullptr && hdr->templates() != nullptr && hdr->templates_vertices() != nullptr) {
656 auto templates = nlohmann::json::array();
657 for (const auto* t : *hdr->templates()) {
658 if (t != nullptr)
659 templates.push_back(geometry_to_json(t, info.semantic_columns));
660 }
661
662 // Template vertices are absolute doubles, NOT quantised: the header
663 // transform does not apply to them. Read via memcpy for the same
664 // reason as Transform -- the struct can sit at a misaligned offset.
665 auto verts = nlohmann::json::array();
666 for (const auto* v : *hdr->templates_vertices()) {
667 std::array<double, 3> xyz{};
668 for (std::size_t i = 0; i < 3; ++i) {
669 std::memcpy(&xyz[i], reinterpret_cast<const std::uint8_t*>(v) + i * sizeof(double),
670 sizeof(double));
671 }
672 verts.push_back(nlohmann::json::array({xyz[0], xyz[1], xyz[2]}));
673 }
674
675 cj["geometry-templates"] = {{"templates", std::move(templates)},
676 {"vertices-templates", std::move(verts)}};
677 }
678
679 return cj;
680}
681
682nlohmann::json to_cityjson_feature(const Feature& feature, const HeaderView& header) {
683 const ::CityFeature* cf = detail::FeatureAccess::get(feature);
684 if (cf == nullptr) {
685 throw Error(ErrorCode::MissingRequiredField, "empty feature");
686 }
687
688 nlohmann::json out = nlohmann::json::object();
689 out["type"] = "CityJSONFeature";
690 out["id"] = feature.id();
691
692 nlohmann::json objects = nlohmann::json::object();
693 const std::size_t n = feature.city_object_count();
694 for (std::size_t i = 0; i < n; ++i) {
695 const auto* obj = cf->objects()->Get(static_cast<flatbuffers::uoffset_t>(i));
696 if (obj == nullptr)
697 continue;
698
699 nlohmann::json co = nlohmann::json::object();
700 co["type"] = (obj->extension_type() != nullptr)
701 ? obj->extension_type()->str()
702 : city_object_type_name(static_cast<std::uint8_t>(obj->type()));
703
704 // Per-object schema when declared, header schema otherwise.
705 // Emitted iff the object DECLARES an attributes vector -- a
706 // present-but-empty one becomes `{}`, an absent one is omitted
707 // entirely. The reference distinguishes these and consumers compare
708 // against it.
709 if (feature.object_has_attributes(i)) {
710 auto own = feature.object_columns(i);
711 auto blob = feature.object_attributes(i);
712 const auto& schema = feature.object_has_columns(i) ? own : header.info().columns;
713 co["attributes"] =
714 blob.empty() ? nlohmann::json::object() : attributes_to_json(blob, schema);
715 }
716
717 std::array<double, 6> extent{};
718 if (feature.object_extent(i, extent)) {
719 co["geographicalExtent"] = extent;
720 }
721
722 auto geoms = nlohmann::json::array();
723 if (obj->geometry() != nullptr) {
724 for (const auto* g : *obj->geometry()) {
725 if (g != nullptr) {
726 geoms.push_back(geometry_to_json(g, header.info().semantic_columns));
727 }
728 }
729 }
730 // Geometry templates: the shape lives once in the header and each
731 // instance references it by index plus a 4x4 placement matrix and a
732 // single reference-point vertex.
733 if (obj->geometry_instances() != nullptr) {
734 for (const auto* gi : *obj->geometry_instances()) {
735 if (gi != nullptr)
736 geoms.push_back(geometry_instance_to_json(gi));
737 }
738 }
739 if (!geoms.empty())
740 co["geometry"] = std::move(geoms);
741
742 if (obj->children() != nullptr && obj->children()->size() > 0) {
743 auto kids = nlohmann::json::array();
744 for (const auto* c : *obj->children()) {
745 if (c != nullptr)
746 kids.push_back(c->str());
747 }
748 co["children"] = std::move(kids);
749 }
750 if (obj->parents() != nullptr && obj->parents()->size() > 0) {
751 auto ps = nlohmann::json::array();
752 for (const auto* p : *obj->parents()) {
753 if (p != nullptr)
754 ps.push_back(p->str());
755 }
756 co["parents"] = std::move(ps);
757 }
758
759 objects[feature.object_id(i)] = std::move(co);
760 }
761 out["CityObjects"] = std::move(objects);
762
763 // Vertices are quantised integers; the header transform maps them back
764 // to world coordinates, so they stay integral here.
765 auto verts = nlohmann::json::array();
766 if (cf->vertices() != nullptr) {
767 for (const auto* v : *cf->vertices()) {
768 verts.push_back(nlohmann::json::array({v->x(), v->y(), v->z()}));
769 }
770 }
771 out["vertices"] = std::move(verts);
772
773 // The materials, textures and UV vertices this feature's geometry
774 // mappings index into. Without it the mappings reference nothing a
775 // consumer can resolve (deserializer.rs:503).
776 if (cf->appearance() != nullptr) {
777 out["appearance"] = appearance_to_json(cf->appearance());
778 }
779
780 return out;
781}
782
783} // namespace fcb
784
785#endif // FCB_WITH_JSON
Every failure the library reports is one of these.
Definition error.hpp:30
One decoded feature that OWNS the bytes it points into.
Definition feature.hpp:33
bool object_extent(std::size_t i, std::array< double, 6 > &out) const
CityObject i's own bounding box, if it declares one.
Definition reader.cpp:64
bool object_has_attributes(std::size_t i) const
Whether CityObject i declares an attributes vector at all.
Definition reader.cpp:59
bytes_view object_attributes(std::size_t i) const
Raw attribute blob of CityObject i, or empty if it has none.
Definition reader.cpp:51
std::string object_id(std::size_t i) const
CityObject i's id.
Definition reader.cpp:104
bool object_has_columns(std::size_t i) const
CityObject i's own column schema, if it declares one.
Definition reader.cpp:80
std::string id() const
The feature's CityJSON id. Empty when empty().
Definition reader.cpp:34
std::vector< ColumnInfo > object_columns(std::size_t i) const
Definition reader.cpp:85
std::size_t city_object_count() const
How many CityObjects this feature carries. Zero when empty().
Definition reader.cpp:111
A parsed header that OWNS its backing bytes.
Definition header.hpp:108
const FileInfo & info() const
Definition header.hpp:112
UIntView surfaces
Definition geometry.cpp:124
nlohmann::json decode_boundaries(GeometryKind type, UIntView solids, UIntView shells, UIntView surfaces, UIntView strings, UIntView indices)
Rebuild CityJSON's nested boundaries from the five flattened arrays, at the depth type implies.
Definition geometry.cpp:164
nlohmann::json decode_texture_values(GeometryKind type, UIntView solids, UIntView shells, UIntView surfaces, UIntView strings, UIntView vertices)
Rebuild the values array of one texture theme from a TextureMapping.
Definition geometry.cpp:331
span< const std::uint8_t > bytes_view
The workhorse alias: a read-only view over bytes.
Definition span.hpp:43
nlohmann::json attributes_to_json(bytes_view blob, const std::vector< ColumnInfo > &schema)
Same decode, rendered as a JSON object for CityJSON emission.
span< const std::uint32_t > UIntView
Definition geometry.hpp:16
nlohmann::json to_cityjson_feature(const Feature &feature, const HeaderView &header)
One feature as a CityJSONFeature object.
Definition cityjson.cpp:682
nlohmann::json decode_material_values(GeometryKind type, UIntView solids, UIntView shells, UIntView vertices)
Rebuild the values array of one material theme from a MaterialMapping.
Definition geometry.cpp:268
std::string semantic_surface_type_name(std::uint8_t type)
CityJSON name for a SemanticSurfaceType enumerator, e.g.
Definition cityjson.cpp:572
nlohmann::json decode_semantics_values(GeometryKind type, UIntView solids, UIntView shells, UIntView values)
Rebuild the nested semantics values array from the flat run of semantic indices.
Definition geometry.cpp:224
GeometryKind
The FlatBuffers GeometryType enumerators, mirrored here so a caller can name a geometry type without ...
Definition geometry.hpp:22
nlohmann::json to_cityjson_metadata(const HeaderView &header)
The CityJSON metadata envelope: type, version, transform, extent, CRS.
Definition cityjson.cpp:585
std::string geometry_type_name(std::uint8_t type)
CityJSON name for a GeometryType enumerator, e.g.
Definition geometry.cpp:376
std::string city_object_type_name(std::uint8_t type)
CityJSON name for a CityObjectType enumerator, e.g.
Definition cityjson.cpp:559
KeyValue key
Definition stree.cpp:56
std::vector< ColumnInfo > columns
Definition header.hpp:47
std::vector< ColumnInfo > semantic_columns
Schema for SemanticObject.attributes, which is separate from the feature attribute schema (Header....
Definition header.hpp:50
static const ::CityFeature * get(const Feature &f)
Definition reader.cpp:32
static const ::Header * get(const HeaderView &h)
Definition header.cpp:30