FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
header_serializer.cpp
Go to the documentation of this file.
1#include <fcb/error.hpp>
3
4#include <charconv>
5#include <string_view>
6
7namespace fcb {
8
9namespace {
10
11std::string as_str_or_empty(const nlohmann::ordered_json& obj, const std::string& key) {
12 auto it = obj.find(key);
13 if (it == obj.end() || !it->is_string())
14 return std::string();
15 return it->get<std::string>();
16}
17
24std::string require_string_field(const nlohmann::ordered_json& obj, const std::string& key) {
25 auto it = obj.find(key);
26 if (it == obj.end() || !it->is_string())
28 "pointOfContact." + key + " is required and must be a string");
29 return it->get<std::string>();
30}
31
42std::optional<std::string> optional_string_field(const nlohmann::ordered_json& obj,
43 const std::string& key) {
44 auto it = obj.find(key);
45 if (it == obj.end() || !it->is_string())
46 return std::nullopt;
47 return it->get<std::string>();
48}
49
55std::optional<std::string> address_member(const nlohmann::ordered_json& address,
56 const std::string& key) {
57 auto it = address.find(key);
58 if (it == address.end() || it->is_null())
59 return std::nullopt;
60 if (it->is_string())
61 return it->get<std::string>();
62 return it->dump();
63}
64
65std::optional<std::string> address_either(const nlohmann::ordered_json& address,
66 const std::string& a, const std::string& b) {
67 if (auto v = address_member(address, a))
68 return v;
69 return address_member(address, b);
70}
71
72std::int32_t parse_i32_whole(std::string_view s) {
73 if (s.empty())
74 return 0;
75 // `std::from_chars` rejects a leading '+' for signed integers, but
76 // Rust's `str::parse::<i32>()` accepts one (its `FromStr` impl allows an
77 // optional leading `+` or `-`) -- ".../EPSG/0/+7415" is a legal `code`
78 // segment there. Strip it here, but only when a digit actually follows,
79 // so a malformed "+-7415" or bare "+" still falls through to failure
80 // below rather than silently parsing "-7415".
81 if (s.front() == '+') {
82 s.remove_prefix(1);
83 if (s.empty() || !(s.front() >= '0' && s.front() <= '9'))
84 return 0;
85 }
86 std::int32_t value = 0;
87 auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
88 if (ec != std::errc() || ptr != s.data() + s.size())
89 return 0;
90 return value;
91}
92
93} // namespace
94
95::Transform to_transform(const nlohmann::ordered_json& transform) {
96 const auto& scale = transform.at("scale");
97 const auto& translate = transform.at("translate");
98 return ::Transform(
99 ::Vector(scale.at(0).get<double>(), scale.at(1).get<double>(), scale.at(2).get<double>()),
100 ::Vector(translate.at(0).get<double>(), translate.at(1).get<double>(),
101 translate.at(2).get<double>()));
102}
103
104std::optional<ParsedReferenceSystem> parse_reference_system(const std::string& url) {
105 static constexpr std::string_view kPrefixes[] = {"http://www.opengis.net/def/crs/",
106 "https://www.opengis.net/def/crs/"};
107 for (const auto& prefix : kPrefixes) {
108 if (url.compare(0, prefix.size(), prefix) != 0)
109 continue;
110
111 const std::string rest = url.substr(prefix.size());
112 std::vector<std::string> segments;
113 std::size_t start = 0;
114 while (true) {
115 const std::size_t pos = rest.find('/', start);
116 const std::size_t end = pos == std::string::npos ? rest.size() : pos;
117 segments.push_back(rest.substr(start, end - start));
118 if (pos == std::string::npos)
119 break;
120 start = pos + 1;
121 }
122
124 out.authority = segments[0];
125 out.version = segments.size() > 1 ? parse_i32_whole(segments[1]) : 0;
126 out.code = segments.size() > 2 ? parse_i32_whole(segments[2]) : 0;
127 return out;
128 }
129 return std::nullopt;
130}
131
132::flatbuffers::Offset<::ReferenceSystem> to_reference_system(::flatbuffers::FlatBufferBuilder& fbb,
133 const ParsedReferenceSystem& ref_sys) {
134 auto authority = fbb.CreateString(ref_sys.authority);
135 return CreateReferenceSystem(fbb, authority, ref_sys.version, ref_sys.code, 0);
136}
137
138::flatbuffers::Offset<::Extension> to_extension(::flatbuffers::FlatBufferBuilder& fbb,
139 const std::string& name, const std::string& url,
140 const std::string& version) {
141 auto name_off = fbb.CreateString(name);
142 auto url_off = fbb.CreateString(url);
143 auto version_off = fbb.CreateString(version);
144 return CreateExtension(fbb, name_off, 0, url_off, version_off);
145}
146
147::flatbuffers::Offset<::flatbuffers::Vector<const ::DoubleVertex*>>
148to_templates_vertices(::flatbuffers::FlatBufferBuilder& fbb,
149 const nlohmann::ordered_json& vertices_templates) {
150 std::vector<::DoubleVertex> verts;
151 if (vertices_templates.is_array()) {
152 for (const auto& v : vertices_templates) {
153 if (!v.is_array())
154 continue;
155 std::vector<double> coords;
156 for (const auto& c : v)
157 if (c.is_number())
158 coords.push_back(c.get<double>());
159 if (coords.size() == 3)
160 verts.emplace_back(coords[0], coords[1], coords[2]);
161 }
162 }
163 return fbb.CreateVectorOfStructs(verts);
164}
165
166PocOffsets to_point_of_contact(::flatbuffers::FlatBufferBuilder& fbb,
167 const nlohmann::ordered_json& poc) {
168 PocOffsets out;
169 out.contact_name = fbb.CreateString(require_string_field(poc, "contactName"));
170
171 if (auto v = optional_string_field(poc, "contactType"))
172 out.contact_type = fbb.CreateString(*v);
173 if (auto v = optional_string_field(poc, "role"))
174 out.role = fbb.CreateString(*v);
175 if (auto v = optional_string_field(poc, "phone"))
176 out.phone = fbb.CreateString(*v);
177 out.email = fbb.CreateString(require_string_field(poc, "emailAddress"));
178 if (auto v = optional_string_field(poc, "website"))
179 out.website = fbb.CreateString(*v);
180
181 // `address`'s presence check is the same disclosed leniency as
182 // `optional_string_field`: Rust's `Option<Address>` would reject the
183 // whole document if `address` were present but not a JSON object (an
184 // `Address`'s `#[serde(flatten)]` map requires object shape); this
185 // writer just treats it as absent instead.
186 if (auto addr_it = poc.find("address"); addr_it != poc.end() && addr_it->is_object()) {
187 const nlohmann::ordered_json& address = *addr_it;
188 if (auto v = address_member(address, "thoroughfareNumber"))
189 out.address_thoroughfare_number = fbb.CreateString(*v);
190 if (auto v = address_member(address, "thoroughfareName"))
191 out.address_thoroughfare_name = fbb.CreateString(*v);
192 if (auto v = address_member(address, "locality"))
193 out.address_locality = fbb.CreateString(*v);
194 if (auto v = address_either(address, "postcode", "postalCode"))
195 out.address_postcode = fbb.CreateString(*v);
196 if (auto v = address_member(address, "country"))
197 out.address_country = fbb.CreateString(*v);
198 }
199 return out;
200}
201
202::flatbuffers::Offset<::Header>
203to_fcb_header(::flatbuffers::FlatBufferBuilder& fbb, const nlohmann::ordered_json& cj,
204 const HeaderWriterOptions& options, const AttributeSchema& attr_schema,
205 const AttributeSchema* semantic_attr_schema,
206 const std::vector<AttributeIndexInfo>* attribute_indices_info) {
207 auto version = fbb.CreateString(cj.at("version").get<std::string>());
208 ::Transform transform = to_transform(cj.at("transform"));
209 const std::uint64_t features_count = options.feature_count;
210
211 auto columns = to_columns(fbb, attr_schema);
212 std::optional<::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset<::Column>>>>
213 semantic_columns;
214 if (semantic_attr_schema != nullptr)
215 semantic_columns = to_columns(fbb, *semantic_attr_schema);
216
217 const std::uint16_t index_node_size = options.index_node_size;
218
219 std::optional<::flatbuffers::Offset<::flatbuffers::Vector<const ::AttributeIndex*>>>
220 attribute_index;
221 if (attribute_indices_info != nullptr) {
222 std::vector<::AttributeIndex> entries;
223 entries.reserve(attribute_indices_info->size());
224 for (const auto& info : *attribute_indices_info)
225 entries.emplace_back(info.index, info.length, info.branching_factor,
226 info.num_unique_items);
227 attribute_index = fbb.CreateVectorOfStructs(entries);
228 }
229
230 std::optional<::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset<::Extension>>>>
231 extensions;
232 if (auto it = cj.find("extensions"); it != cj.end() && it->is_object()) {
233 std::vector<::flatbuffers::Offset<::Extension>> ext_offs;
234 for (const auto& [name, ext] : it->items())
235 ext_offs.push_back(to_extension(fbb, name, as_str_or_empty(ext, "url"),
236 as_str_or_empty(ext, "version")));
237 extensions = fbb.CreateVector(ext_offs);
238 }
239
240 std::optional<::GeographicalExtent> geographical_extent;
241 if (options.geographical_extent)
242 geographical_extent = to_geographical_extent(*options.geographical_extent);
243
244 std::optional<::flatbuffers::Offset<::Appearance>> appearance;
245 if (auto it = cj.find("appearance"); it != cj.end() && it->is_object())
246 appearance = to_appearance(fbb, *it);
247
248 std::optional<::flatbuffers::Offset<::flatbuffers::Vector<const ::DoubleVertex*>>>
249 templates_vertices;
250 std::optional<::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset<::Geometry>>>>
251 templates;
252 if (auto gm_it = cj.find("geometry-templates"); gm_it != cj.end() && gm_it->is_object()) {
253 templates_vertices = to_templates_vertices(fbb, gm_it->at("vertices-templates"));
254
255 std::vector<::flatbuffers::Offset<::Geometry>> geom_offs;
256 for (const auto& g : gm_it->at("templates"))
257 geom_offs.push_back(to_geometry(fbb, g, semantic_attr_schema));
258 templates = fbb.CreateVector(geom_offs);
259 }
260
261 std::optional<::flatbuffers::Offset<::ReferenceSystem>> reference_system;
262 std::optional<::flatbuffers::Offset<::flatbuffers::String>> identifier;
263 std::optional<::flatbuffers::Offset<::flatbuffers::String>> reference_date;
264 std::optional<::flatbuffers::Offset<::flatbuffers::String>> title;
265 PocOffsets poc;
266
267 if (auto meta_it = cj.find("metadata"); meta_it != cj.end() && meta_it->is_object()) {
268 const nlohmann::ordered_json& meta = *meta_it;
269
270 if (auto rs_it = meta.find("referenceSystem"); rs_it != meta.end() && rs_it->is_string()) {
271 if (auto parsed = parse_reference_system(rs_it->get<std::string>()))
272 reference_system = to_reference_system(fbb, *parsed);
273 }
274
275 if (!geographical_extent) {
276 if (auto ge_it = meta.find("geographicalExtent");
277 ge_it != meta.end() && ge_it->is_array() && ge_it->size() == 6) {
278 std::array<double, 6> extent{};
279 for (std::size_t i = 0; i < 6; ++i)
280 extent[i] = ge_it->at(i).get<double>();
281 geographical_extent = to_geographical_extent(extent);
282 }
283 }
284
285 if (auto v = optional_string_field(meta, "identifier"))
286 identifier = fbb.CreateString(*v);
287 if (auto v = optional_string_field(meta, "referenceDate"))
288 reference_date = fbb.CreateString(*v);
289 if (auto v = optional_string_field(meta, "title"))
290 title = fbb.CreateString(*v);
291
292 if (auto poc_it = meta.find("pointOfContact"); poc_it != meta.end() && poc_it->is_object())
293 poc = to_point_of_contact(fbb, *poc_it);
294 }
295
296 // `CreateHeader` (flatc-generated, header_generated.h) is used here
297 // instead of hand-sequenced `HeaderBuilder::add_*` calls: flatc's
298 // generated `Create*` helper adds fields in a fixed, WIDTH-sorted order
299 // (widest fields first) to minimize padding, and it does so IDENTICALLY
300 // across every language backend -- which is the only reason Rust's and
301 // C++'s output can be byte-identical at all. A field's byte offset
302 // WITHIN the table is determined by the order its `add_*` was actually
303 // CALLED (each call appends at the builder's current cursor), so calling
304 // them by hand in any other order -- e.g. grouped by CityJSON-source
305 // semantics, as this function's own local-variable order reads -- lays
306 // the table out differently even though every field ends up present
307 // with the right value: a real byte-exact regression this milestone's
308 // own oracle test caught (see test_writer_oracle.cpp).
309 return CreateHeader(
310 fbb, &transform, appearance.value_or(0), columns, semantic_columns.value_or(0),
311 features_count, index_node_size, attribute_index.value_or(0),
312 geographical_extent ? &*geographical_extent : nullptr, reference_system.value_or(0),
313 identifier.value_or(0), reference_date.value_or(0), title.value_or(0),
314 templates.value_or(0), templates_vertices.value_or(0), extensions.value_or(0),
315 poc.contact_name.value_or(0), poc.contact_type.value_or(0), poc.role.value_or(0),
316 poc.phone.value_or(0), poc.email.value_or(0), poc.website.value_or(0),
317 poc.address_thoroughfare_number.value_or(0), poc.address_thoroughfare_name.value_or(0),
318 poc.address_locality.value_or(0), poc.address_postcode.value_or(0),
319 poc.address_country.value_or(0), /*attributes=*/0, version);
320}
321
322} // namespace fcb
::flatbuffers::Offset<::flatbuffers::Vector< const ::DoubleVertex * > > to_templates_vertices(::flatbuffers::FlatBufferBuilder &fbb, const nlohmann::ordered_json &vertices_templates)
Builds Header.templates_vertices from CityJSON's geometry-templates.vertices-templates (f64 precision...
::Transform to_transform(const nlohmann::ordered_json &transform)
Builds the Transform struct (scale + translate) from CityJSON's top-level transform member.
::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset<::Column > > > to_columns(::flatbuffers::FlatBufferBuilder &fbb, const AttributeSchema &schema)
Builds the Column vector for Header.columns or CityObject.columns, in ascending column-index order.
PocOffsets to_point_of_contact(::flatbuffers::FlatBufferBuilder &fbb, const nlohmann::ordered_json &poc)
::flatbuffers::Offset<::Appearance > to_appearance(::flatbuffers::FlatBufferBuilder &fbb, const nlohmann::ordered_json &appearance)
Builds the Appearance table (materials, textures, UV vertices, default themes) from a CityJSON appear...
::flatbuffers::Offset<::Geometry > to_geometry(::flatbuffers::FlatBufferBuilder &fbb, const nlohmann::ordered_json &geometry, const AttributeSchema *semantic_attr_schema)
Builds one Geometry table – boundaries, and whatever semantics, material and texture it carries – fro...
::GeographicalExtent to_geographical_extent(const std::array< double, 6 > &extent)
Builds a GeographicalExtent struct from a 6-element [minx,miny,minz,maxx,maxy,maxz] array.
std::map< std::string, std::pair< std::uint16_t, ::ColumnType > > AttributeSchema
Attribute schema: name -> (column index, column type).
Definition attribute.hpp:42
::flatbuffers::Offset<::ReferenceSystem > to_reference_system(::flatbuffers::FlatBufferBuilder &fbb, const ParsedReferenceSystem &ref_sys)
Builds the ReferenceSystem table from a parsed metadata.referenceSystem URL.
::flatbuffers::Offset<::Extension > to_extension(::flatbuffers::FlatBufferBuilder &fbb, const std::string &name, const std::string &url, const std::string &version)
Builds one extensions entry: only name/url/version are written (the schema document itself is never f...
std::optional< ParsedReferenceSystem > parse_reference_system(const std::string &url)
Parses a referenceSystem URL (https://www.opengis.net/def/crs/{authority}/{version}/{code},...
::flatbuffers::Offset<::Header > to_fcb_header(::flatbuffers::FlatBufferBuilder &fbb, const nlohmann::ordered_json &cj, const HeaderWriterOptions &options, const AttributeSchema &attr_schema, const AttributeSchema *semantic_attr_schema, const std::vector< AttributeIndexInfo > *attribute_indices_info)
Builds the whole Header table from the CityJSON metadata line (the first line of a CityJSONSeq: type/...
KeyValue key
Definition stree.cpp:56
Configuration for header writing.
std::optional< std::array< double, 6 > > geographical_extent
A metadata.referenceSystem URL, parsed into its OGC three-element form.
Builds the pointOfContact fields of a header from CityJSON metadata.pointOfContact.
std::optional<::flatbuffers::Offset<::flatbuffers::String > > contact_name
std::optional<::flatbuffers::Offset<::flatbuffers::String > > role
std::optional<::flatbuffers::Offset<::flatbuffers::String > > phone
std::optional<::flatbuffers::Offset<::flatbuffers::String > > website
std::optional<::flatbuffers::Offset<::flatbuffers::String > > address_postcode
std::optional<::flatbuffers::Offset<::flatbuffers::String > > email
std::optional<::flatbuffers::Offset<::flatbuffers::String > > address_thoroughfare_number
std::optional<::flatbuffers::Offset<::flatbuffers::String > > address_country
std::optional<::flatbuffers::Offset<::flatbuffers::String > > address_thoroughfare_name
std::optional<::flatbuffers::Offset<::flatbuffers::String > > contact_type
std::optional<::flatbuffers::Offset<::flatbuffers::String > > address_locality