FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
geom_encoder.cpp
Go to the documentation of this file.
2
3#ifdef FCB_WITH_JSON
4
5# include <fcb/error.hpp>
6
7# include <algorithm>
8# include <cstdint>
9# include <limits>
10
11namespace fcb {
12
13namespace {
14
15constexpr std::uint32_t kNull = std::numeric_limits<std::uint32_t>::max();
16
17void push_ring(const nlohmann::ordered_json& ring, GMBoundaries& b) {
18 b.strings.push_back(static_cast<std::uint32_t>(ring.size()));
19 for (const auto& idx : ring)
20 b.indices.push_back(idx.get<std::uint32_t>());
21}
22
23void push_surface(const nlohmann::ordered_json& surface, GMBoundaries& b) {
24 for (const auto& ring : surface)
25 push_ring(ring, b);
26 b.surfaces.push_back(static_cast<std::uint32_t>(surface.size()));
27}
28
29void push_shell(const nlohmann::ordered_json& shell, GMBoundaries& b) {
30 for (const auto& surface : shell)
31 push_surface(surface, b);
32 b.shells.push_back(static_cast<std::uint32_t>(shell.size()));
33}
34
35void push_solid(const nlohmann::ordered_json& solid, GMBoundaries& b) {
36 for (const auto& shell : solid)
37 push_shell(shell, b);
38 b.solids.push_back(static_cast<std::uint32_t>(solid.size()));
39}
40
41std::uint32_t semantics_index(const nlohmann::ordered_json& v) {
42 return v.is_null() ? kNull : v.get<std::uint32_t>();
43}
44
48void push_semantics_shell(const nlohmann::ordered_json* shell, const GMBoundaries& boundaries,
49 std::size_t& shell_cursor, std::vector<std::uint32_t>& flattened) {
50 const std::uint32_t surface_count =
51 shell_cursor < boundaries.shells.size() ? boundaries.shells[shell_cursor] : 0;
52 ++shell_cursor;
53 if (shell == nullptr) {
54 flattened.insert(flattened.end(), surface_count, kNull);
55 } else {
56 for (const auto& v : *shell)
57 flattened.push_back(semantics_index(v));
58 }
59}
60
61std::uint32_t material_index(const nlohmann::ordered_json& v) {
62 return v.is_null() ? kNull : v.get<std::uint32_t>();
63}
64
68void push_material_shell(const nlohmann::ordered_json* shell, MaterialMapping& mv) {
69 if (shell == nullptr) {
70 mv.shells.push_back(kNull);
71 } else {
72 mv.shells.push_back(static_cast<std::uint32_t>(shell->size()));
73 for (const auto& v : *shell)
74 mv.vertices.push_back(material_index(v));
75 }
76}
77
78void push_textured_ring(const nlohmann::ordered_json& ring, TextureMapping& m) {
79 m.strings.push_back(static_cast<std::uint32_t>(ring.size()));
80 for (const auto& v : ring)
81 m.vertices.push_back(material_index(v)); // null-aware, same as material
82}
83
84void push_textured_surface(const nlohmann::ordered_json& surface, TextureMapping& m) {
85 for (const auto& ring : surface)
86 push_textured_ring(ring, m);
87 m.surfaces.push_back(static_cast<std::uint32_t>(surface.size()));
88}
89
90void push_textured_shell(const nlohmann::ordered_json& shell, TextureMapping& m) {
91 for (const auto& surface : shell)
92 push_textured_surface(surface, m);
93 m.shells.push_back(static_cast<std::uint32_t>(shell.size()));
94}
95
96// ---------------------------------------------------------------------------
97// Shape sniffing for semantics.values / material.values / texture.values.
98//
99// Rust's SemanticsValues/MaterialValues/TextureValues are `#[serde(untagged)]`
100// enums: deserialization tries each variant in DECLARATION order (shallowest
101// first) and uses the first one the JSON structurally fits. That means the
102// depth Rust actually uses is a property of the VALUES ARRAY ITSELF, not of
103// the enclosing geometry's type -- the two agree for every cardinality-
104// consistent file (which is every real file any encoder produces), but a
105// schema-valid, cardinality-INCONSISTENT file (e.g. a `Solid` whose
106// `semantics.values` is a flat one-element array instead of properly nested)
107// would be read at a different depth by shape alone than by geometry type.
108// Sniffing here, rather than keying on GeometryKind, is what keeps this port
109// byte-compatible with Rust on those files too.
110//
111// Known, deliberately unhandled gap (found in the M2 codex review's
112// follow-up pass): a leaf here is accepted as long as it is not an array,
113// but Rust's leaf type is `Option<usize>`, so a NEGATIVE integer (e.g.
114// `semantics.values: [-1]`) fails every variant in Rust -- the whole
115// CityJSON document fails to parse before ever reaching this code -- while
116// this sniffs it as rank 1 and silently encodes it as NULL. No real
117// encoder emits a negative semantic/material/UV index, so this is not
118// fixed: doing so would mean threading non-negativity checks through every
119// leaf of every rank, for a case with no realistic input.
120// ---------------------------------------------------------------------------
121
126bool fits_nullable_rank(const nlohmann::ordered_json& arr, int rank) {
127 if (!arr.is_array())
128 return false;
129 for (const auto& el : arr) {
130 if (el.is_null())
131 continue;
132 if (rank == 1) {
133 if (el.is_array())
134 return false;
135 } else if (!fits_nullable_rank(el, rank - 1)) {
136 return false;
137 }
138 }
139 return true;
140}
141
145int sniff_nullable_rank(const nlohmann::ordered_json& arr) {
146 for (int rank = 1; rank <= 3; ++rank)
147 if (fits_nullable_rank(arr, rank))
148 return rank;
149 throw Error(ErrorCode::InvalidAttributeValue, "values array nests deeper than a Solid permits");
150}
151
156bool is_texture_ring(const nlohmann::ordered_json& v) {
157 if (!v.is_array())
158 return false;
159 for (const auto& el : v)
160 if (!el.is_null() && el.is_array())
161 return false;
162 return true;
163}
164
168bool fits_texture_depth(const nlohmann::ordered_json& arr, int depth) {
169 if (depth == 0)
170 return is_texture_ring(arr);
171 if (!arr.is_array())
172 return false;
173 for (const auto& el : arr)
174 if (!fits_texture_depth(el, depth - 1))
175 return false;
176 return true;
177}
178
186int sniff_texture_depth(const nlohmann::ordered_json& arr) {
187 for (int rank = 1; rank <= 3; ++rank)
188 if (fits_texture_depth(arr, rank + 1))
189 return rank;
191 "texture values array nests deeper than a Solid permits");
192}
193
194} // namespace
195
196GeometryKind geometry_kind_from_name(const std::string& name) {
197 if (name == "MultiPoint")
199 if (name == "MultiLineString")
201 if (name == "MultiSurface")
203 if (name == "CompositeSurface")
205 if (name == "Solid")
206 return GeometryKind::Solid;
207 if (name == "MultiSolid")
209 if (name == "CompositeSolid")
211 if (name == "GeometryInstance")
213 throw Error(ErrorCode::InvalidAttributeValue, "unknown CityJSON geometry type '" + name + "'");
214}
215
216GMBoundaries encode_boundaries(GeometryKind kind, const nlohmann::ordered_json& boundaries) {
217 GMBoundaries b;
218 switch (kind) {
220 push_ring(boundaries, b);
221 break;
223 for (const auto& ring : boundaries)
224 push_ring(ring, b);
225 b.surfaces.push_back(static_cast<std::uint32_t>(boundaries.size()));
226 break;
229 for (const auto& surface : boundaries)
230 push_surface(surface, b);
231 b.shells.push_back(static_cast<std::uint32_t>(boundaries.size()));
232 break;
234 push_solid(boundaries, b);
235 break;
238 for (const auto& solid : boundaries)
239 push_solid(solid, b);
240 break;
242 // Encoded separately by `to_geometry_instance` (M3).
243 break;
244 }
245 return b;
246}
247
248GMSemantics encode_semantics(const nlohmann::ordered_json& semantics,
249 const GMBoundaries& boundaries) {
250 GMSemantics result;
251 result.surfaces = semantics.value("surfaces", nlohmann::ordered_json::array());
252
253 auto values_it = semantics.find("values");
254 if (values_it == semantics.end() || values_it->is_null())
255 return result; // values stays std::nullopt
256
257 std::vector<std::uint32_t> flattened;
258 switch (sniff_nullable_rank(*values_it)) {
259 case 1:
260 for (const auto& v : *values_it)
261 flattened.push_back(semantics_index(v));
262 break;
263
264 case 2: {
265 std::size_t shell_cursor = 0;
266 for (const auto& shell : *values_it)
267 push_semantics_shell(shell.is_null() ? nullptr : &shell, boundaries, shell_cursor,
268 flattened);
269 break;
270 }
271
272 case 3: {
273 std::size_t shell_cursor = 0;
274 std::size_t solid_i = 0;
275 for (const auto& solid : *values_it) {
276 const std::uint32_t shell_count =
277 solid_i < boundaries.solids.size() ? boundaries.solids[solid_i] : 0;
278 if (solid.is_null()) {
279 for (std::uint32_t k = 0; k < shell_count; ++k)
280 push_semantics_shell(nullptr, boundaries, shell_cursor, flattened);
281 } else {
282 for (const auto& shell : solid)
283 push_semantics_shell(shell.is_null() ? nullptr : &shell, boundaries,
284 shell_cursor, flattened);
285 }
286 ++solid_i;
287 }
288 break;
289 }
290 }
291 result.values = std::move(flattened);
292 return result;
293}
294
295std::vector<MaterialMapping> encode_material(const nlohmann::ordered_json& material) {
296 std::vector<MaterialMapping> out;
297 if (!material.is_object())
298 return out;
299
300 // Unlike attribute schemas (document order, see writer/attribute.hpp),
301 // `material` corresponds to Rust's `HashMap<String, CjMaterialReference>`
302 // -- genuinely unordered -- so Rust sorts theme names explicitly
303 // (`themes.sort_unstable()`) rather than relying on iteration order.
304 // `material` here is `ordered_json` like everything else in this writer,
305 // but that only preserves WHATEVER order the caller's JSON happened to
306 // have; themes must be sorted explicitly to match Rust regardless.
307 std::vector<std::string> themes;
308 for (const auto& [theme, unused] : material.items())
309 themes.push_back(theme);
310 std::sort(themes.begin(), themes.end());
311
312 for (const auto& theme : themes) {
313 const auto& m = material.at(theme);
314 auto value_it = m.find("value");
315 if (value_it != m.end() && !value_it->is_null()) {
316 MaterialMapping mapping;
318 mapping.theme = theme;
319 mapping.value = value_it->get<std::uint32_t>();
320 out.push_back(std::move(mapping));
321 continue;
322 }
323
324 auto values_it = m.find("values");
325 if (values_it == m.end())
326 continue; // neither `value` nor `values`: nothing to store
327 if (values_it->is_null()) {
328 MaterialMapping mapping;
330 mapping.theme = theme;
331 out.push_back(std::move(mapping));
332 continue;
333 }
334
335 MaterialMapping mapping;
337 mapping.theme = theme;
338 switch (sniff_nullable_rank(*values_it)) {
339 case 1:
340 // One index per surface.
341 for (const auto& v : *values_it)
342 mapping.vertices.push_back(material_index(v));
343 break;
344
345 case 2:
346 // A single implicit solid: one index per surface, per shell.
347 mapping.solids.push_back(static_cast<std::uint32_t>(values_it->size()));
348 for (const auto& shell : *values_it)
349 push_material_shell(shell.is_null() ? nullptr : &shell, mapping);
350 break;
351
352 case 3:
353 for (const auto& solid : *values_it) {
354 if (solid.is_null()) {
355 mapping.solids.push_back(kNull);
356 } else {
357 mapping.solids.push_back(static_cast<std::uint32_t>(solid.size()));
358 for (const auto& shell : solid)
359 push_material_shell(shell.is_null() ? nullptr : &shell, mapping);
360 }
361 }
362 break;
363 }
364 out.push_back(std::move(mapping));
365 }
366 return out;
367}
368
369std::vector<TextureMapping> encode_texture(const nlohmann::ordered_json& texture) {
370 std::vector<TextureMapping> out;
371 if (!texture.is_object())
372 return out;
373
374 // Same reasoning as encode_material: Rust's texture map is a genuine
375 // HashMap, sorted explicitly, so theme names are sorted here too rather
376 // than trusting iteration order.
377 std::vector<std::string> themes;
378 for (const auto& [theme, unused] : texture.items())
379 themes.push_back(theme);
380 std::sort(themes.begin(), themes.end());
381
382 for (const auto& theme : themes) {
383 const auto& t = texture.at(theme);
384 TextureMapping mapping;
385 mapping.theme = theme;
386
387 auto values_it = t.find("values");
388 if (values_it != t.end() && !values_it->is_null()) {
389 mapping.has_values = true;
390 switch (sniff_texture_depth(*values_it)) {
391 case 1:
392 for (const auto& surface : *values_it)
393 push_textured_surface(surface, mapping);
394 mapping.shells.push_back(static_cast<std::uint32_t>(values_it->size()));
395 break;
396 case 2:
397 for (const auto& shell : *values_it)
398 push_textured_shell(shell, mapping);
399 mapping.solids.push_back(static_cast<std::uint32_t>(values_it->size()));
400 break;
401 case 3:
402 for (const auto& solid : *values_it) {
403 for (const auto& shell : solid)
404 push_textured_shell(shell, mapping);
405 mapping.solids.push_back(static_cast<std::uint32_t>(solid.size()));
406 }
407 break;
408 }
409 }
410 out.push_back(std::move(mapping));
411 }
412 return out;
413}
414
415EncodedGeometry encode(const nlohmann::ordered_json& geometry) {
416 EncodedGeometry result;
417 const GeometryKind kind = geometry_kind_from_name(geometry.at("type").get<std::string>());
418
419 auto boundaries_it = geometry.find("boundaries");
421 kind, boundaries_it != geometry.end() ? *boundaries_it : nlohmann::ordered_json::array());
422
423 auto semantics_it = geometry.find("semantics");
424 if (semantics_it != geometry.end() && semantics_it->is_object())
425 result.semantics = encode_semantics(*semantics_it, result.boundaries);
426
427 auto material_it = geometry.find("material");
428 if (material_it != geometry.end() && material_it->is_object())
429 result.materials = encode_material(*material_it);
430
431 auto texture_it = geometry.find("texture");
432 if (texture_it != geometry.end() && texture_it->is_object())
433 result.textures = encode_texture(*texture_it);
434
435 return result;
436}
437
438} // namespace fcb
439
440#endif // FCB_WITH_JSON
Every failure the library reports is one of these.
Definition error.hpp:30
UIntView vertices
Definition geometry.cpp:126
std::size_t shell
Definition geometry.cpp:67
std::size_t surface
Definition geometry.cpp:68
std::size_t ring
Definition geometry.cpp:69
GeometryKind geometry_kind_from_name(const std::string &name)
Maps a CityJSON geometry type string to GeometryKind.
GMBoundaries encode_boundaries(GeometryKind kind, const nlohmann::ordered_json &boundaries)
Flattens boundaries (nested CityJSON boundary arrays, straight off the JSON as parsed – no intermedia...
GMSemantics encode_semantics(const nlohmann::ordered_json &semantics, const GMBoundaries &boundaries)
Flattens semantics (the CityJSON semantics object: surfaces plus values), using boundaries's shell/so...
std::vector< MaterialMapping > encode_material(const nlohmann::ordered_json &material)
Flattens material (the CityJSON geometry.material object: theme name -> {"value": N} / {"values": [....
EncodedGeometry encode(const nlohmann::ordered_json &geometry)
Flattens one CityJSON geometry object – its boundaries and whatever semantics, material and texture i...
GeometryKind
The FlatBuffers GeometryType enumerators, mirrored here so a caller can name a geometry type without ...
Definition geometry.hpp:22
std::vector< TextureMapping > encode_texture(const nlohmann::ordered_json &texture)
Flattens texture (the CityJSON geometry.texture object).
KeyKind kind
Definition stree.cpp:166
Everything one CityJSON geometry object flattens to.
std::optional< std::vector< TextureMapping > > textures
std::optional< GMSemantics > semantics
std::optional< std::vector< MaterialMapping > > materials
Flattened geometry boundaries: a flat vertex-index list plus one count array per level of the dimensi...
std::vector< std::uint32_t > shells
std::vector< std::uint32_t > solids
std::vector< std::uint32_t > surfaces
Flattened semantics: surfaces is the CityJSON semantics.surfaces array, passed through verbatim (Flat...
nlohmann::ordered_json surfaces
std::optional< std::vector< std::uint32_t > > values
One theme's material mapping.
std::vector< std::uint32_t > vertices
std::vector< std::uint32_t > solids
One theme's texture mapping.
std::vector< std::uint32_t > solids
std::vector< std::uint32_t > shells