FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
attribute.cpp
Go to the documentation of this file.
2
3#ifdef FCB_WITH_JSON
4
5# include <algorithm>
6# include <cstring>
7# include <limits>
8# include <optional>
9# include <type_traits>
10
11namespace fcb {
12
13namespace {
14
18std::int64_t days_from_civil(int y, unsigned m, unsigned d) {
19 y -= (m <= 2) ? 1 : 0;
20 const std::int64_t era = (y >= 0 ? y : y - 399) / 400;
21 const unsigned yoe = static_cast<unsigned>(y - era * 400);
22 const unsigned doy = (153 * (m + (m > 2 ? static_cast<unsigned>(-3) : 9)) + 2) / 5 + d - 1;
23 const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
24 return era * 146097 + static_cast<std::int64_t>(doe) - 719468;
25}
26
27struct Rfc3339Result {
28 bool ok = false;
29 std::int64_t seconds = 0;
30 std::uint32_t nanos = 0;
31};
32
40Rfc3339Result parse_rfc3339(const std::string& s) {
41 Rfc3339Result r;
42 if (s.size() < 20)
43 return r; // shortest valid form: "YYYY-MM-DDTHH:MM:SSZ"
44
45 auto is_digit = [](char c) { return c >= '0' && c <= '9'; };
46 auto two = [&](std::size_t at) -> int { return (s[at] - '0') * 10 + (s[at + 1] - '0'); };
47
48 for (std::size_t i : {0u, 1u, 2u, 3u, 5u, 6u, 8u, 9u})
49 if (!is_digit(s[i]))
50 return r;
51 if (s[4] != '-' || s[7] != '-')
52 return r;
53 if (s[10] != 'T' && s[10] != 't' && s[10] != ' ')
54 return r;
55 for (std::size_t i : {11u, 12u, 14u, 15u, 17u, 18u})
56 if (!is_digit(s[i]))
57 return r;
58 if (s[13] != ':' || s[16] != ':')
59 return r;
60
61 const int year = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 + (s[3] - '0');
62 const int month = two(5);
63 const int day = two(8);
64 const int hour = two(11);
65 const int minute = two(14);
66 const int second = two(17);
67
68 if (month < 1 || month > 12)
69 return r;
70 static const int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
71 const bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
72 const int max_day = (month == 2 && leap) ? 29 : days_in_month[month - 1];
73 if (day < 1 || day > max_day)
74 return r;
75 if (hour > 23 || minute > 59 || second > 60) // 60 == leap second
76 return r;
77
78 std::size_t pos = 19;
79 std::uint32_t nanos = 0;
80 if (pos < s.size() && s[pos] == '.') {
81 const std::size_t start = ++pos;
82 while (pos < s.size() && is_digit(s[pos]))
83 ++pos;
84 if (pos == start)
85 return r; // '.' with no digits after it
86 std::string frac = s.substr(start, pos - start);
87 if (frac.size() > 9)
88 frac.resize(9);
89 else
90 frac.resize(9, '0');
91 nanos = static_cast<std::uint32_t>(std::stoul(frac));
92 }
93 if (pos >= s.size())
94 return r; // no timezone marker at all -- not RFC3339
95
96 std::int64_t offset_seconds = 0;
97 if (s[pos] == 'Z' || s[pos] == 'z') {
98 ++pos;
99 } else if (s[pos] == '+' || s[pos] == '-') {
100 const bool neg = s[pos] == '-';
101 ++pos;
102 if (pos + 5 > s.size() || !is_digit(s[pos]) || !is_digit(s[pos + 1]) || s[pos + 2] != ':' ||
103 !is_digit(s[pos + 3]) || !is_digit(s[pos + 4]))
104 return r;
105 const int oh = two(pos);
106 const int om = two(pos + 3);
107 if (oh > 23 || om > 59)
108 return r;
109 offset_seconds = (oh * 3600 + om * 60) * (neg ? -1 : 1);
110 pos += 5;
111 } else {
112 return r;
113 }
114 if (pos != s.size())
115 return r; // trailing garbage after the offset
116
117 const std::int64_t days =
118 days_from_civil(year, static_cast<unsigned>(month), static_cast<unsigned>(day));
119 // A leap second (:60) is chrono's `NaiveTime` convention: it occupies the
120 // SAME epoch second as :59, flagged by adding 1_000_000_000 to the
121 // nanosecond field, rather than rolling over into the next minute. Rolling
122 // over (as a naive `+ second` would) both produces the wrong epoch second
123 // and can wrap a leap second at day's end onto the NEXT day's midnight.
124 const int second_for_epoch = (second == 60) ? 59 : second;
125 if (second == 60)
126 nanos += 1'000'000'000u;
127 r.ok = true;
128 r.seconds = days * 86400 + hour * 3600 + minute * 60 + second_for_epoch - offset_seconds;
129 r.nanos = nanos;
130 return r;
131}
132
133bool looks_like_rfc3339(const std::string& s) { return parse_rfc3339(s).ok; }
134
135std::optional<::ColumnType> guess_type(const nlohmann::ordered_json& value) {
136 if (value.is_boolean())
137 return ::ColumnType::Bool;
138 if (value.is_number()) {
139 if (value.is_number_float())
140 return ::ColumnType::Double;
141 if (value.is_number_unsigned())
142 return ::ColumnType::ULong;
143 if (value.is_number_integer()) {
144 // serde_json::Number splits PosInt/NegInt by the VALUE's sign,
145 // regardless of the Rust literal's own signed/unsigned type. A
146 // C++ value built via `nlohmann::ordered_json(5)` (as opposed to parsed
147 // from JSON text with no leading '-') lands in nlohmann's signed
148 // `number_integer_t` bucket even though 5 is non-negative, so
149 // this must also decide by value, not by which nlohmann bucket
150 // it landed in, to stay oracle-compatible for callers that
151 // construct JSON directly rather than parsing it.
152 const std::int64_t i = value.get<std::int64_t>();
153 return i < 0 ? ::ColumnType::Long : ::ColumnType::ULong;
154 }
155 return ::ColumnType::ULong;
156 }
157 if (value.is_string())
158 return looks_like_rfc3339(value.get_ref<const std::string&>()) ? ::ColumnType::DateTime
159 : ::ColumnType::String;
160 if (value.is_array() || value.is_object())
161 return ::ColumnType::Json;
162 return std::nullopt; // null, or anything else
163}
164
165template <typename T> void put_le(std::vector<std::uint8_t>& out, std::size_t at, T v) {
166 using U = typename std::make_unsigned<T>::type;
167 const U u = static_cast<U>(v);
168 for (std::size_t i = 0; i < sizeof(T); ++i)
169 out[at + i] = static_cast<std::uint8_t>(u >> (8 * i));
170}
171
172void put_f32(std::vector<std::uint8_t>& out, std::size_t at, float f) {
173 std::uint32_t bits;
174 std::memcpy(&bits, &f, sizeof(bits));
175 put_le<std::uint32_t>(out, at, bits);
176}
177
178void put_f64(std::vector<std::uint8_t>& out, std::size_t at, double d) {
179 std::uint64_t bits;
180 std::memcpy(&bits, &d, sizeof(bits));
181 put_le<std::uint64_t>(out, at, bits);
182}
183
184// The following mirror serde_json::Value::as_i64/as_u64/as_f64/as_bool/as_str:
185// a type/range mismatch yields the fallback rather than throwing, because the
186// Rust writer being ported never throws on this path either -- a value whose
187// JSON shape drifted from the schema's remembered column type is written as
188// the type's zero value, not rejected.
189
190std::int64_t as_i64_or0(const nlohmann::ordered_json& v) {
191 if (v.is_number_unsigned()) {
192 const std::uint64_t u = v.get<std::uint64_t>();
193 return u <= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())
194 ? static_cast<std::int64_t>(u)
195 : 0;
196 }
197 if (v.is_number_integer())
198 return v.get<std::int64_t>();
199 return 0;
200}
201
202std::uint64_t as_u64_or0(const nlohmann::ordered_json& v) {
203 if (v.is_number_unsigned())
204 return v.get<std::uint64_t>();
205 if (v.is_number_integer()) {
206 const std::int64_t i = v.get<std::int64_t>();
207 return i >= 0 ? static_cast<std::uint64_t>(i) : 0;
208 }
209 return 0;
210}
211
212double as_f64_or0(const nlohmann::ordered_json& v) { return v.is_number() ? v.get<double>() : 0.0; }
213
214bool as_bool_or_false(const nlohmann::ordered_json& v) { return v.is_boolean() && v.get<bool>(); }
215
216std::string as_str_or_empty(const nlohmann::ordered_json& v) {
217 return v.is_string() ? v.get<std::string>() : std::string();
218}
219
220} // namespace
221
222void add_attributes(AttributeSchema& schema, const nlohmann::ordered_json& attrs) {
223 if (!attrs.is_object()) {
224 // Rust's `BTreeMap::insert` here ALWAYS overwrites -- even a "json"
225 // column that already exists gets reassigned a new index equal to
226 // the map's current size, which can orphan whatever previously held
227 // that index. That is a real quirk of the oracle, not a bug to
228 // paper over: `insert_or_assign` (not the no-op-on-existing-key
229 // `emplace`) is what reproduces it exactly.
230 schema.insert_or_assign(
231 "json", std::make_pair(static_cast<std::uint16_t>(schema.size()), ::ColumnType::Json));
232 return;
233 }
234 for (const auto& [key, val] : attrs.items()) {
235 if (schema.find(key) != schema.end() || val.is_null())
236 continue;
237 if (auto coltype = guess_type(val)) {
238 schema.emplace(key,
239 std::make_pair(static_cast<std::uint16_t>(schema.size()), *coltype));
240 }
241 }
242}
243
244std::size_t attr_size(::ColumnType coltype, const nlohmann::ordered_json& colval) {
245 switch (coltype) {
246 case ::ColumnType::Byte:
247 return sizeof(std::int8_t);
248 case ::ColumnType::UByte:
249 return sizeof(std::uint8_t);
250 case ::ColumnType::Bool:
251 return sizeof(std::uint8_t);
252 case ::ColumnType::Short:
253 return sizeof(std::int16_t);
254 case ::ColumnType::UShort:
255 return sizeof(std::uint16_t);
256 case ::ColumnType::Int:
257 return sizeof(std::int32_t);
258 case ::ColumnType::UInt:
259 return sizeof(std::uint32_t);
260 case ::ColumnType::Long:
261 return sizeof(std::int64_t);
262 case ::ColumnType::ULong:
263 return sizeof(std::uint64_t);
264 case ::ColumnType::Float:
265 return sizeof(float);
266 case ::ColumnType::Double:
267 return sizeof(double);
268 case ::ColumnType::String:
269 case ::ColumnType::DateTime:
270 return sizeof(std::uint32_t) + as_str_or_empty(colval).size();
271 case ::ColumnType::Json:
272 return sizeof(std::uint32_t) + colval.dump().size();
273 case ::ColumnType::Binary:
274 return sizeof(std::uint32_t) + as_str_or_empty(colval).size();
275 }
276 throw Error(ErrorCode::UnsupportedColumnType, "attr_size: unknown column type");
277}
278
279std::vector<std::uint8_t> encode_attributes_with_schema(const nlohmann::ordered_json& attr,
280 const AttributeSchema& schema) {
281 std::vector<std::uint8_t> out;
282 if (!attr.is_object() || attr.empty())
283 return out;
284
285 std::vector<std::pair<std::string, std::pair<std::uint16_t, ::ColumnType>>> sorted(
286 schema.begin(), schema.end());
287 std::sort(sorted.begin(), sorted.end(),
288 [](const auto& a, const auto& b) { return a.second.first < b.second.first; });
289
290 for (const auto& [name, idx_type] : sorted) {
291 const auto [index, coltype] = idx_type;
292 auto it = attr.find(name);
293 if (it == attr.end() || it->is_null())
294 continue;
295 const nlohmann::ordered_json& val = *it;
296
297 const std::size_t offset = out.size();
298 const std::size_t size = attr_size(coltype, val);
299 out.resize(offset + sizeof(std::uint16_t) + size, 0);
300 put_le<std::uint16_t>(out, offset, index);
301 const std::size_t value_offset = offset + sizeof(std::uint16_t);
302
303 switch (coltype) {
304 case ::ColumnType::Bool:
305 out[value_offset] = as_bool_or_false(val) ? 1 : 0;
306 break;
307 case ::ColumnType::Int:
308 put_le<std::int32_t>(out, value_offset, static_cast<std::int32_t>(as_i64_or0(val)));
309 break;
310 case ::ColumnType::UInt:
311 put_le<std::uint32_t>(out, value_offset,
312 static_cast<std::uint32_t>(as_u64_or0(val)));
313 break;
314 case ::ColumnType::Byte:
315 out[value_offset] = static_cast<std::uint8_t>(as_i64_or0(val));
316 break;
317 case ::ColumnType::UByte:
318 out[value_offset] = static_cast<std::uint8_t>(as_u64_or0(val));
319 break;
320 case ::ColumnType::Short:
321 put_le<std::int16_t>(out, value_offset, static_cast<std::int16_t>(as_i64_or0(val)));
322 break;
323 case ::ColumnType::UShort:
324 put_le<std::uint16_t>(out, value_offset,
325 static_cast<std::uint16_t>(as_u64_or0(val)));
326 break;
327 case ::ColumnType::Long:
328 put_le<std::int64_t>(out, value_offset, as_i64_or0(val));
329 break;
330 case ::ColumnType::ULong:
331 put_le<std::uint64_t>(out, value_offset, as_u64_or0(val));
332 break;
333 case ::ColumnType::Float:
334 put_f32(out, value_offset, static_cast<float>(as_f64_or0(val)));
335 break;
336 case ::ColumnType::Double:
337 put_f64(out, value_offset, as_f64_or0(val));
338 break;
339 case ::ColumnType::String:
340 case ::ColumnType::DateTime: {
341 const std::string s = as_str_or_empty(val);
342 put_le<std::uint32_t>(out, value_offset, static_cast<std::uint32_t>(s.size()));
343 std::memcpy(out.data() + value_offset + sizeof(std::uint32_t), s.data(), s.size());
344 break;
345 }
346 case ::ColumnType::Json: {
347 const std::string json_str = val.dump();
348 put_le<std::uint32_t>(out, value_offset,
349 static_cast<std::uint32_t>(json_str.size()));
350 std::memcpy(out.data() + value_offset + sizeof(std::uint32_t), json_str.data(),
351 json_str.size());
352 break;
353 }
354 case ::ColumnType::Binary: {
355 const std::string s = as_str_or_empty(val);
356 put_le<std::uint32_t>(out, value_offset, static_cast<std::uint32_t>(s.size()));
357 std::memcpy(out.data() + value_offset + sizeof(std::uint32_t), s.data(), s.size());
358 break;
359 }
360 }
361 }
362 return out;
363}
364
365::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset<::Column>>>
366to_columns(::flatbuffers::FlatBufferBuilder& fbb, const AttributeSchema& schema) {
367 std::vector<std::pair<std::string, std::pair<std::uint16_t, ::ColumnType>>> sorted(
368 schema.begin(), schema.end());
369 std::sort(sorted.begin(), sorted.end(),
370 [](const auto& a, const auto& b) { return a.second.first < b.second.first; });
371
372 std::vector<::flatbuffers::Offset<::Column>> columns;
373 columns.reserve(sorted.size());
374 for (const auto& [name, idx_type] : sorted) {
375 auto name_off = fbb.CreateString(name);
376 columns.push_back(CreateColumn(fbb, idx_type.first, name_off, idx_type.second));
377 }
378 return fbb.CreateVector(columns);
379}
380
381std::vector<AttributeIndexEntry>
382attribute_to_index_entries(const nlohmann::ordered_json& attr, const AttributeSchema& schema,
383 const std::vector<std::string>& indexing_attr) {
384 std::vector<AttributeIndexEntry> out;
385 if (!attr.is_object() || attr.empty())
386 return out;
387
388 for (const auto& name : indexing_attr) {
389 auto val_it = attr.find(name);
390 if (val_it == attr.end())
391 continue;
392 auto schema_it = schema.find(name);
393 if (schema_it == schema.end())
394 continue;
395 const auto [index, coltype] = schema_it->second;
396 const nlohmann::ordered_json& val = *val_it;
397
398 switch (coltype) {
399 case ::ColumnType::Bool:
400 out.push_back({index, KeyValue::from_bool(as_bool_or_false(val))});
401 break;
402 case ::ColumnType::Int:
403 out.push_back(
404 {index, KeyValue::from_i32(static_cast<std::int32_t>(as_i64_or0(val)))});
405 break;
406 case ::ColumnType::UInt:
407 out.push_back(
408 {index, KeyValue::from_u32(static_cast<std::uint32_t>(as_u64_or0(val)))});
409 break;
410 case ::ColumnType::Long:
411 out.push_back({index, KeyValue::from_i64(as_i64_or0(val))});
412 break;
413 case ::ColumnType::ULong:
414 out.push_back({index, KeyValue::from_u64(as_u64_or0(val))});
415 break;
416 case ::ColumnType::Float:
417 out.push_back({index, KeyValue::from_f32(static_cast<float>(as_f64_or0(val)))});
418 break;
419 case ::ColumnType::Double:
420 out.push_back({index, KeyValue::from_f64(as_f64_or0(val))});
421 break;
422 case ::ColumnType::String:
423 out.push_back(
424 {index, KeyValue::from_string(KeyKind::String50, as_str_or_empty(val))});
425 break;
426 case ::ColumnType::DateTime: {
427 const Rfc3339Result parsed = parse_rfc3339(as_str_or_empty(val));
428 out.push_back({index, KeyValue::from_datetime(parsed.ok ? parsed.seconds : 0,
429 parsed.ok ? parsed.nanos : 0)});
430 break;
431 }
432 case ::ColumnType::Byte:
433 case ::ColumnType::UByte:
434 case ::ColumnType::Short:
435 case ::ColumnType::UShort:
436 case ::ColumnType::Json:
437 case ::ColumnType::Binary:
438 // Not supported for indexing at extraction time -- matches
439 // writer/attribute.rs's `attribute_to_index_entries`.
440 break;
441 }
442 }
443 return out;
444}
445
446std::vector<AttributeIndexEntry>
447cityfeature_to_index_entries(const nlohmann::ordered_json& city_feature,
448 const AttributeSchema& schema,
449 const std::vector<std::string>& indexing_attr) {
450 std::vector<AttributeIndexEntry> out;
451 auto co_it = city_feature.find("CityObjects");
452 if (co_it == city_feature.end() || !co_it->is_object())
453 return out;
454
455 std::vector<std::string> object_ids;
456 object_ids.reserve(co_it->size());
457 for (const auto& [id, unused] : co_it->items())
458 object_ids.push_back(id);
459 std::sort(object_ids.begin(), object_ids.end());
460
461 for (const auto& id : object_ids) {
462 const nlohmann::ordered_json& co = co_it->at(id);
463 auto attr_it = co.find("attributes");
464 if (attr_it == co.end() || attr_it->is_null())
465 continue;
466 auto entries = attribute_to_index_entries(*attr_it, schema, indexing_attr);
467 out.insert(out.end(), entries.begin(), entries.end());
468 }
469 return out;
470}
471
472} // namespace fcb
473
474#endif // FCB_WITH_JSON
Every failure the library reports is one of these.
Definition error.hpp:30
static KeyValue from_datetime(std::int64_t seconds, std::uint32_t nanos)
Definition key.cpp:129
static KeyValue from_i64(std::int64_t v)
Definition key.cpp:104
static KeyValue from_u32(std::uint32_t v)
Definition key.cpp:103
static KeyValue from_f32(float v)
Definition key.cpp:108
static KeyValue from_i32(std::int32_t v)
Definition key.cpp:102
static KeyValue from_f64(double v)
Definition key.cpp:115
static KeyValue from_string(KeyKind kind, const std::string &v)
Definition key.cpp:137
static KeyValue from_u64(std::uint64_t v)
Definition key.cpp:105
static KeyValue from_bool(bool v)
Definition key.cpp:122
std::size_t string
Definition geometry.cpp:129
std::size_t index
Definition geometry.cpp:70
std::vector< std::uint8_t > encode_attributes_with_schema(const nlohmann::ordered_json &attr, const AttributeSchema &schema)
Encodes attr (a CityJSON attributes object) against schema: repeated [u16 LE column index][value] rec...
std::size_t attr_size(::ColumnType coltype, const nlohmann::ordered_json &colval)
Byte width one value of coltype occupies in the attribute blob, EXCLUDING the 2-byte column-index pre...
::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.
void add_attributes(AttributeSchema &schema, const nlohmann::ordered_json &attrs)
Adds every member of a JSON object to schema, assigning each new, non-null name the next free column ...
std::vector< AttributeIndexEntry > cityfeature_to_index_entries(const nlohmann::ordered_json &city_feature, const AttributeSchema &schema, const std::vector< std::string > &indexing_attr)
Same, over every object in one CityJSONFeature's CityObjects, visited in ascending object-id order (n...
std::map< std::string, std::pair< std::uint16_t, ::ColumnType > > AttributeSchema
Attribute schema: name -> (column index, column type).
Definition attribute.hpp:42
std::vector< AttributeIndexEntry > attribute_to_index_entries(const nlohmann::ordered_json &attr, const AttributeSchema &schema, const std::vector< std::string > &indexing_attr)
Extracts index entries for indexing_attr from one CityJSON attributes object.
std::uint64_t offset
Definition stree.cpp:57
KeyValue key
Definition stree.cpp:56
bool ok
Definition attribute.cpp:28
std::uint32_t nanos
Definition attribute.cpp:30
std::int64_t seconds
Definition attribute.cpp:29