FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
fcb_writer.cpp
Go to the documentation of this file.
1#include <fcb/error.hpp>
5
6#include <algorithm>
7#include <limits>
8#include <sstream>
9
10#if defined(_WIN32)
11# define FCB_FSEEK _fseeki64
12using fcb_off_t = long long;
13#else
14# include <sys/types.h>
15# define FCB_FSEEK fseeko
16using fcb_off_t = off_t;
17#endif
18
19namespace fcb {
20
21namespace {
22
23// "fcb" + VERSION + "fcb" + 0 (const_vars.rs:5, layout.hpp's kVersion == 1).
24constexpr std::uint8_t kMagicBytes[8] = {'f', 'c', 'b', kVersion, 'f', 'c', 'b', 0};
25
26// Feature bytes are streamed from the spool to the output in chunks this
27// size, rather than ever materializing a whole feature (let alone the
28// whole feature section) as one buffer.
29constexpr std::size_t kCopyChunkSize = 1 << 16; // 64 KiB
30
31} // namespace
32
33FcbWriter::FcbWriter(nlohmann::ordered_json cj, FcbWriterOptions options,
34 AttributeSchema attr_schema,
35 std::optional<AttributeSchema> semantic_attr_schema)
36 : cj_(std::move(cj)), options_(std::move(options)), attr_schema_(std::move(attr_schema)),
37 semantic_attr_schema_(std::move(semantic_attr_schema)) {
38 for (const auto& [name, unused] : options_.attribute_indices)
39 indexing_attr_.push_back(name);
40
41 const auto& transform = cj_.at("transform");
42 scale_x_ = transform.at("scale").at(0).get<double>();
43 scale_y_ = transform.at("scale").at(1).get<double>();
44 translate_x_ = transform.at("translate").at(0).get<double>();
45 translate_y_ = transform.at("translate").at(1).get<double>();
46
47 tmp_ = std::tmpfile();
48 if (tmp_ == nullptr) {
49 throw Error(ErrorCode::IoError, "FcbWriter: failed to create a temporary file for feature "
50 "spooling");
51 }
52}
53
55 if (tmp_ != nullptr) {
56 std::fclose(tmp_); // removes the file too: that is std::tmpfile()'s whole contract
57 }
58}
59
60void FcbWriter::add_feature(const nlohmann::ordered_json& feature) {
61 if (written_) {
62 throw Error(ErrorCode::IoError, "FcbWriter::add_feature called after write()");
63 }
64
65 flatbuffers::FlatBufferBuilder fbb;
66 auto [off, raw_bbox] =
67 to_fcb_city_feature(fbb, feature.at("id").get<std::string>(), feature, attr_schema_,
68 semantic_attr_schema_ ? &*semantic_attr_schema_ : nullptr);
69 fbb.FinishSizePrefixed(off);
70 const std::uint64_t size = fbb.GetSize();
71
72 if (size > 0) {
73 if (FCB_FSEEK(tmp_, static_cast<fcb_off_t>(tmp_write_pos_), SEEK_SET) != 0 ||
74 std::fwrite(fbb.GetBufferPointer(), 1, size, tmp_) != size) {
75 throw Error(ErrorCode::IoError, "FcbWriter: failed writing a feature to the temp file");
76 }
77 }
78
79 const std::uint64_t temp_id = feat_offsets_.size();
80 feat_offsets_.push_back(FeatureSlot{tmp_write_pos_, size});
81 tmp_write_pos_ += size;
82
83 feat_nodes_.push_back(NodeItem{raw_bbox.min_x * scale_x_ + translate_x_,
84 raw_bbox.min_y * scale_y_ + translate_y_,
85 raw_bbox.max_x * scale_x_ + translate_x_,
86 raw_bbox.max_y * scale_y_ + translate_y_, temp_id});
87
88 if (!indexing_attr_.empty()) {
89 index_entries_by_feature_.push_back(
90 cityfeature_to_index_entries(feature, attr_schema_, indexing_attr_));
91 } else {
92 index_entries_by_feature_.emplace_back();
93 }
94}
95
96void FcbWriter::write(std::ostream& out) {
97 if (written_) {
98 throw Error(ErrorCode::IoError, "FcbWriter::write called more than once");
99 }
100 written_ = true;
101 std::fflush(tmp_);
102
103 // `write_index: false` forces index_node_size to 0 in the header,
104 // exactly like `HeaderWriter::new_with_options` (header_writer.rs:
105 // 87-94) -- computed once here so every downstream decision (whether
106 // to hilbert_sort, whether to build an R-tree, what the header
107 // records) uses the SAME effective value.
108 const std::uint16_t effective_node_size = options_.write_index ? options_.index_node_size : 0;
109
110 // Rust's own `if index_node_size > 0 && !feat_nodes.is_empty()` guards
111 // `hilbert_sort` itself, not just the R-tree build (writer/mod.rs:
112 // 208-225) -- when it's false, features stay in ORIGINAL order (each
113 // `.offset` still its temp id from `add_feature`).
114 NodeItem extent = NodeItem::empty(0);
115 const bool build_rtree = effective_node_size > 0 && !feat_nodes_.empty();
116 if (build_rtree) {
117 extent = calc_extent(feat_nodes_);
118 hilbert_sort(feat_nodes_, extent);
119 }
120
121 // Bookkeeping-only pass: compute each feature's FINAL byte offset in
122 // the (sorted or original) output order, WITHOUT reading any feature
123 // bytes yet -- `feat_nodes_` itself is left untouched (`.offset` still
124 // each entry's original temp id) so the later streaming pass can still
125 // look up where in the spool file to read each one from.
126 std::vector<std::uint64_t> final_offset_by_temp_id(feat_offsets_.size());
127 {
128 std::uint64_t running = 0;
129 for (const auto& node : feat_nodes_) {
130 final_offset_by_temp_id[static_cast<std::size_t>(node.offset)] = running;
131 running += feat_offsets_[static_cast<std::size_t>(node.offset)].size;
132 }
133 }
134
135 std::vector<std::uint8_t> rtree_bytes;
136 if (build_rtree) {
137 // A throwaway copy with `.offset` remapped to final byte offsets --
138 // `feat_nodes_` itself keeps carrying temp ids for the streaming
139 // pass below.
140 std::vector<NodeItem> rtree_input = feat_nodes_;
141 for (auto& node : rtree_input)
142 node.offset = final_offset_by_temp_id[static_cast<std::size_t>(node.offset)];
143 std::vector<NodeItem> tree = build_packed_rtree(rtree_input, extent, effective_node_size);
144 rtree_bytes = encode_packed_rtree(tree);
145 }
146
147 // Per-column attribute index dispatch (writer/mod.rs:192-202,252-265),
148 // sorted by SCHEMA COLUMN INDEX (not request order). A requested name
149 // absent from the schema, or with zero indexable entries, is silently
150 // skipped -- mirrors `if let Ok(...) = build_attribute_index_for_attr(
151 // ...)` (writer/mod.rs:255-264), which discards an `Err` (from either
152 // `Error::AttributeIndexNotFound` or `Stree::init`'s empty-tree check)
153 // rather than propagating it.
154 std::vector<std::pair<std::string, std::optional<std::uint16_t>>> sorted_indices =
155 options_.attribute_indices;
156 std::stable_sort(
157 sorted_indices.begin(), sorted_indices.end(), [this](const auto& a, const auto& b) {
158 const auto ia = attr_schema_.find(a.first);
159 const auto ib = attr_schema_.find(b.first);
160 const std::uint16_t idx_a = ia != attr_schema_.end()
161 ? ia->second.first
162 : std::numeric_limits<std::uint16_t>::max();
163 const std::uint16_t idx_b = ib != attr_schema_.end()
164 ? ib->second.first
165 : std::numeric_limits<std::uint16_t>::max();
166 return idx_a < idx_b;
167 });
168
169 std::vector<std::uint8_t> attr_index_bytes;
170 std::vector<AttributeIndexInfo> attr_index_info;
171 if (!sorted_indices.empty()) {
172 std::vector<std::vector<BtreeEntry>> entries_by_column(attr_schema_.size());
173 for (std::size_t temp_id = 0; temp_id < feat_offsets_.size(); ++temp_id) {
174 const std::uint64_t feature_offset = final_offset_by_temp_id[temp_id];
175 for (const auto& e : index_entries_by_feature_[temp_id])
176 entries_by_column.at(e.index).push_back(BtreeEntry{e.value, feature_offset});
177 }
178
179 for (const auto& [name, bf_opt] : sorted_indices) {
180 const auto it = attr_schema_.find(name);
181 if (it == attr_schema_.end())
182 continue;
183 const std::uint16_t schema_index = it->second.first;
184 const auto& col_entries = entries_by_column.at(schema_index);
185 if (col_entries.empty())
186 continue;
187
188 const KeyKind kind = key_kind_for_column(static_cast<std::uint8_t>(it->second.second));
189 const std::uint16_t branching_factor = bf_opt.value_or(kDefaultBranchingFactor);
190 BuiltBtreeIndex built = build_static_btree(col_entries, kind, branching_factor);
191
192 attr_index_info.push_back(
193 AttributeIndexInfo{schema_index, static_cast<std::uint32_t>(built.bytes.size()),
194 built.branching_factor, built.num_unique_items});
195 attr_index_bytes.insert(attr_index_bytes.end(), built.bytes.begin(), built.bytes.end());
196 }
197 }
198
199 HeaderWriterOptions header_options;
200 header_options.feature_count = feat_offsets_.size();
201 header_options.index_node_size = effective_node_size;
202 header_options.geographical_extent = options_.geographical_extent;
203
204 flatbuffers::FlatBufferBuilder header_fbb;
205 auto header_off = to_fcb_header(header_fbb, cj_, header_options, attr_schema_,
206 semantic_attr_schema_ ? &*semantic_attr_schema_ : nullptr,
207 attr_index_info.empty() ? nullptr : &attr_index_info);
208 header_fbb.FinishSizePrefixed(header_off);
209
210 out.write(reinterpret_cast<const char*>(kMagicBytes), sizeof(kMagicBytes));
211 out.write(reinterpret_cast<const char*>(header_fbb.GetBufferPointer()),
212 static_cast<std::streamsize>(header_fbb.GetSize()));
213 if (!rtree_bytes.empty())
214 out.write(reinterpret_cast<const char*>(rtree_bytes.data()),
215 static_cast<std::streamsize>(rtree_bytes.size()));
216 if (!attr_index_bytes.empty())
217 out.write(reinterpret_cast<const char*>(attr_index_bytes.data()),
218 static_cast<std::streamsize>(attr_index_bytes.size()));
219 if (!out) {
220 throw Error(ErrorCode::IoError, "FcbWriter: failed writing the header/index sections");
221 }
222
223 // Stream every feature's bytes straight from the spool to `out`, in
224 // `feat_nodes_`'s CURRENT (sorted or original) order, through a fixed-
225 // size buffer -- never holding more than one chunk of feature data (let
226 // alone the whole feature section) in memory at once.
227 std::vector<char> chunk(kCopyChunkSize);
228 for (const auto& node : feat_nodes_) {
229 const FeatureSlot& slot = feat_offsets_[static_cast<std::size_t>(node.offset)];
230 if (slot.size == 0)
231 continue;
232 if (FCB_FSEEK(tmp_, static_cast<fcb_off_t>(slot.offset), SEEK_SET) != 0) {
233 throw Error(ErrorCode::IoError, "FcbWriter: failed to seek the temp file");
234 }
235 std::uint64_t remaining = slot.size;
236 while (remaining > 0) {
237 const std::size_t want =
238 static_cast<std::size_t>(std::min<std::uint64_t>(remaining, chunk.size()));
239 if (std::fread(chunk.data(), 1, want, tmp_) != want) {
241 "FcbWriter: failed reading a feature back from the temp file");
242 }
243 out.write(chunk.data(), static_cast<std::streamsize>(want));
244 if (!out) {
246 "FcbWriter: failed writing a feature to the output");
247 }
248 remaining -= want;
249 }
250 }
251}
252
253std::vector<std::uint8_t> FcbWriter::write() {
254 std::ostringstream oss(std::ios::binary);
255 write(oss);
256 const std::string& s = oss.str();
257 return std::vector<std::uint8_t>(s.begin(), s.end());
258}
259
260} // namespace fcb
Every failure the library reports is one of these.
Definition error.hpp:30
void add_feature(const nlohmann::ordered_json &city_json_feature)
Encodes and spools one CityJSONFeature line.
std::vector< std::uint8_t > write()
Convenience wrapper around write(std::ostream&) that returns the complete file as one buffer.
FcbWriter(nlohmann::ordered_json cj, FcbWriterOptions options, AttributeSchema attr_schema, std::optional< AttributeSchema > semantic_attr_schema)
cj is the CityJSONSeq's metadata line (first line: type/ version/transform/metadata/etc).
off_t fcb_off_t
#define FCB_FSEEK
void hilbert_sort(std::vector< NodeItem > &items, const NodeItem &extent)
Sorts items in place by descending Hilbert index (the item furthest along the curve first) – a STABLE...
std::vector< std::uint8_t > encode_packed_rtree(const std::vector< NodeItem > &tree)
Serializes every node in tree (as returned by build_packed_rtree) in array order, 40 bytes each.
constexpr std::uint16_t kDefaultBranchingFactor
Mirrors static_btree::DEFAULT_BRANCHING_FACTOR (static_btree/mod.rs:19), used whenever a caller reque...
BuiltBtreeIndex build_static_btree(const std::vector< BtreeEntry > &entries, KeyKind kind, std::uint16_t branching_factor)
Builds one column's complete attribute index blob from its (key, offset) entries.
KeyKind key_kind_for_column(std::uint8_t column_type)
Column type to key kind, following what the WRITER emits.
Definition key.cpp:387
std::vector< NodeItem > build_packed_rtree(const std::vector< NodeItem > &nodes, const NodeItem &extent, std::uint16_t node_size)
Builds the full flat packed-R-tree node array (leaves first in nodes's own order at the array's tail ...
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...
KeyKind
The concrete key types the B+tree index can hold.
Definition key.hpp:14
NodeItem calc_extent(const std::vector< NodeItem > &nodes)
The bbox union of every item, via repeated NodeItem::expand starting from NodeItem::empty(0).
constexpr std::uint8_t kVersion
Definition layout.hpp:14
std::map< std::string, std::pair< std::uint16_t, ::ColumnType > > AttributeSchema
Attribute schema: name -> (column index, column type).
Definition attribute.hpp:42
std::pair<::flatbuffers::Offset<::CityFeature >, NodeItem > to_fcb_city_feature(::flatbuffers::FlatBufferBuilder &fbb, const std::string &id, const nlohmann::ordered_json &city_feature, const AttributeSchema &attr_schema, const AttributeSchema *semantic_attr_schema)
Builds one CityFeature table – its CityObjects (visited in ascending id order), vertices,...
::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/...
KeyKind kind
Definition stree.cpp:166
One attribute column's B+tree index metadata, as recorded in the header (AttributeIndex,...
One (key, feature byte offset) pair to be indexed.
The finished index: the flat node array concatenated with the payload section (mirrors Stree::stream_...
std::vector< std::uint8_t > bytes
std::uint32_t num_unique_items
std::uint16_t branching_factor
Configuration for FcbWriter.
bool write_index
false forces index_node_size to 0 in the written header (no R-tree at all), regardless of index_node_...
std::uint16_t index_node_size
std::vector< std::pair< std::string, std::optional< std::uint16_t > > > attribute_indices
(attribute name, branching factor); std::nullopt branching factor means build_static_btree's own defa...
std::optional< std::array< double, 6 > > geographical_extent
Configuration for header writing.
std::optional< std::array< double, 6 > > geographical_extent
One R-tree node entry: 4 doubles then a u64, all little-endian, 40 bytes with no padding (packed_rtre...
static NodeItem empty(std::uint64_t offset)
The "empty" node used as the fold/aggregation identity: any real bbox's expand widens it.