FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
packed_rtree.cpp
Go to the documentation of this file.
2#include <fcb/reader.hpp>
3
4#include <algorithm>
5#include <cstring>
6#include <deque>
7#include <limits>
8#include <utility>
9
10#include "detail/checked.hpp"
11
12namespace fcb {
13
14namespace {
15
16// Byte-by-byte shift/mask assembly, not a raw memcpy of the in-memory
17// value: node items are packed with no padding and land at arbitrary
18// alignments inside a fetched block (the original reason a memcpy load was
19// used here), but a flat memcpy is ALSO only correct on a little-endian
20// host -- it silently reads/writes host-native byte order instead of the
21// wire format's little-endian one. Found during the M5 codex review (the
22// write side, added this milestone, copied the pre-existing read side's
23// bug); fixed on both sides at once, matching the explicit-shift convention
24// `key.cpp`'s `put_le`/`get_le` already use for exactly this reason.
25std::uint64_t read_u64_le(const std::uint8_t* p) {
26 std::uint64_t v = 0;
27 for (int i = 0; i < 8; ++i)
28 v |= static_cast<std::uint64_t>(p[i]) << (8 * i);
29 return v;
30}
31
32double read_f64_le(const std::uint8_t* p) {
33 const std::uint64_t bits = read_u64_le(p);
34 double d;
35 std::memcpy(&d, &bits, sizeof(d)); // reinterpret bits -> double: host-endian-agnostic
36 return d;
37}
38
39void write_u64_le(std::uint8_t* p, std::uint64_t v) {
40 for (int i = 0; i < 8; ++i)
41 p[i] = static_cast<std::uint8_t>((v >> (8 * i)) & 0xFF);
42}
43
44void write_f64_le(std::uint8_t* p, double v) {
45 std::uint64_t bits;
46 std::memcpy(&bits, &v, sizeof(bits)); // reinterpret double -> bits: host-endian-agnostic
47 write_u64_le(p, bits);
48}
49
50} // namespace
51
53 if (b.size() < kSize) {
54 throw Error(ErrorCode::NoIndex, "short rtree node item");
55 }
56 NodeItem n{};
57 n.min_x = read_f64_le(b.data() + 0);
58 n.min_y = read_f64_le(b.data() + 8);
59 n.max_x = read_f64_le(b.data() + 16);
60 n.max_y = read_f64_le(b.data() + 24);
61 n.offset = read_u64_le(b.data() + 32);
62 return n;
63}
64
66 NodeItem n{};
67 n.min_x = std::numeric_limits<double>::infinity();
68 n.min_y = std::numeric_limits<double>::infinity();
69 n.max_x = -std::numeric_limits<double>::infinity();
70 n.max_y = -std::numeric_limits<double>::infinity();
71 n.offset = offset;
72 return n;
73}
74
75void NodeItem::expand(const NodeItem& r) {
76 if (r.min_x < min_x)
77 min_x = r.min_x;
78 if (r.min_y < min_y)
79 min_y = r.min_y;
80 if (r.max_x > max_x)
81 max_x = r.max_x;
82 if (r.max_y > max_y)
83 max_y = r.max_y;
84}
85
86void NodeItem::encode(std::uint8_t* out) const {
87 write_f64_le(out + 0, min_x);
88 write_f64_le(out + 8, min_y);
89 write_f64_le(out + 16, max_x);
90 write_f64_le(out + 24, max_y);
91 write_u64_le(out + 32, offset);
92}
93
94bool NodeItem::intersects(const BBox& q) const {
95 // Strict comparisons, matching packed_rtree/mod.rs:122-134.
96 if (q.max_x < min_x)
97 return false;
98 if (q.max_y < min_y)
99 return false;
100 if (q.min_x > max_x)
101 return false;
102 if (q.min_y > max_y)
103 return false;
104 return true;
105}
106
107std::uint64_t rtree_num_nodes(std::uint64_t num_items, std::uint16_t node_size) {
108 if (node_size < 2) {
109 throw Error(ErrorCode::IllegalHeaderSize, "invalid index_node_size");
110 }
111 if (num_items == 0)
112 return 0;
113
114 std::uint64_t n = num_items;
115 std::uint64_t num_nodes = n;
116 for (;;) {
118 num_nodes = detail::checked_add(num_nodes, n, "rtree num_nodes");
119 if (n == 1)
120 break;
121 }
122 return num_nodes;
123}
124
125std::vector<LevelBound> rtree_level_bounds(std::uint64_t num_items, std::uint16_t node_size) {
126 if (node_size < 2) {
127 throw Error(ErrorCode::IllegalHeaderSize, "invalid index_node_size");
128 }
129 if (num_items == 0) {
130 throw Error(ErrorCode::NoIndex, "empty rtree");
131 }
132
133 std::vector<std::uint64_t> level_num_nodes;
134 std::uint64_t n = num_items;
135 std::uint64_t num_nodes = n;
136 level_num_nodes.push_back(n);
137 for (;;) {
139 num_nodes = detail::checked_add(num_nodes, n, "rtree num_nodes");
140 level_num_nodes.push_back(n);
141 if (n == 1)
142 break;
143 }
144
145 // Walk backwards accumulating offsets, as the Rust version does.
146 std::vector<std::uint64_t> level_offsets;
147 std::uint64_t acc = num_nodes;
148 for (std::uint64_t size : level_num_nodes) {
149 acc -= size;
150 level_offsets.push_back(acc);
151 }
152
153 std::vector<LevelBound> bounds;
154 bounds.reserve(level_num_nodes.size());
155 for (std::size_t i = 0; i < level_num_nodes.size(); ++i) {
156 bounds.push_back(LevelBound{level_offsets[i], level_offsets[i] + level_num_nodes[i]});
157 }
158 return bounds;
159}
160
161std::vector<SearchResultItem> rtree_search_bbox(RangeReader& reader, std::uint64_t index_begin,
162 std::uint64_t num_items, std::uint16_t node_size,
163 const BBox& query) {
164 std::vector<SearchResultItem> results;
165 if (num_items == 0)
166 return results;
167
168 const auto level_bounds = rtree_level_bounds(num_items, node_size);
169 const std::uint64_t num_nodes = rtree_num_nodes(num_items, node_size);
170 const std::uint64_t leaf_nodes_offset = level_bounds.front().start;
171
172 // Breadth-first, so node reads run roughly in file order.
173 std::deque<std::pair<std::uint64_t, std::size_t>> queue;
174 queue.emplace_back(0, level_bounds.size() - 1);
175
176 while (!queue.empty()) {
177 const auto [node_index, level] = queue.front();
178 queue.pop_front();
179
180 if (level >= level_bounds.size()) {
181 throw Error(ErrorCode::NoIndex, "rtree level out of range");
182 }
183 // Child indices come from the file and are hostile. Prove the node
184 // lies within the level we believe we are on BEFORE using it, and
185 // derive leaf-ness from the trusted level rather than from the
186 // index itself.
187 if (node_index < level_bounds[level].start || node_index >= level_bounds[level].end) {
188 throw Error(ErrorCode::NoIndex, "rtree node index outside its level");
189 }
190 const bool is_leaf = (level == 0);
191 const std::uint64_t end = std::min<std::uint64_t>(
192 detail::checked_add(node_index, node_size, "rtree node end"), level_bounds[level].end);
193 if (end <= node_index)
194 continue;
195
196 const std::uint64_t length = end - node_index;
197 const std::uint64_t byte_offset = detail::checked_add(
198 index_begin, detail::checked_mul(node_index, NodeItem::kSize, "rtree node offset"),
199 "rtree node base");
200 const std::uint64_t byte_len =
201 detail::checked_mul(length, NodeItem::kSize, "rtree node span");
202
203 auto block = reader.read(byte_offset, byte_len);
204 if (block.size() < byte_len) {
205 throw Error(ErrorCode::NoIndex, "truncated rtree node block");
206 }
207
208 for (std::uint64_t pos = node_index; pos < end; ++pos) {
209 const std::uint64_t slot = pos - node_index;
210 NodeItem item = NodeItem::decode(bytes_view(block).subspan(
211 static_cast<std::size_t>(slot * NodeItem::kSize), NodeItem::kSize));
212 if (!item.intersects(query))
213 continue;
214
215 if (is_leaf) {
216 results.push_back(SearchResultItem{item.offset, pos - leaf_nodes_offset});
217 } else {
218 const std::size_t child_level = level - 1;
219 if (item.offset < level_bounds[child_level].start ||
220 item.offset >= level_bounds[child_level].end) {
221 throw Error(ErrorCode::NoIndex, "rtree child index outside the child level");
222 }
223 queue.emplace_back(item.offset, child_level);
224 }
225 }
226 }
227
228 // Read forward through the features section.
229 std::sort(
230 results.begin(), results.end(),
231 [](const SearchResultItem& a, const SearchResultItem& b) { return a.offset < b.offset; });
232 return results;
233}
234
235} // namespace fcb
Every failure the library reports is one of these.
Definition error.hpp:30
Synchronous byte-range source.
virtual std::vector< std::uint8_t > read(std::uint64_t offset, std::uint64_t length)=0
Read length bytes at offset, subject to the contract above.
Minimal C++17 stand-in for std::span: a non-owning view over contiguous memory.
Definition span.hpp:13
std::size_t size() const noexcept
Definition span.hpp:26
T * data() const noexcept
Definition span.hpp:25
std::uint64_t checked_mul(std::uint64_t a, std::uint64_t b, const char *what="mul")
Definition checked.hpp:24
std::uint64_t checked_add(std::uint64_t a, std::uint64_t b, const char *what="add")
Definition checked.hpp:16
std::uint64_t ceil_div(std::uint64_t a, std::uint64_t b)
ceil(a / b) without the (a + b - 1) overflow hazard.
Definition checked.hpp:34
std::uint64_t rtree_num_nodes(std::uint64_t num_items, std::uint16_t node_size)
Total node count in the tree, per the Rust level-bounds loop (packed_rtree/mod.rs:342-375).
span< const std::uint8_t > bytes_view
The workhorse alias: a read-only view over bytes.
Definition span.hpp:43
std::vector< SearchResultItem > rtree_search_bbox(RangeReader &reader, std::uint64_t index_begin, std::uint64_t num_items, std::uint16_t node_size, const BBox &query)
Breadth-first bbox search over the packed R-tree, reading nodes through the supplied reader.
std::vector< LevelBound > rtree_level_bounds(std::uint64_t num_items, std::uint16_t node_size)
Mirrors generate_level_bounds (packed_rtree/mod.rs:342-375).
std::uint64_t offset
Definition stree.cpp:57
std::uint64_t node_size
Definition stree.cpp:167
std::uint64_t index_begin
Definition stree.cpp:163
RangeReader & reader
Definition stree.cpp:162
A 2D query rectangle.
Half-open [start, end) node index range for one tree level, in the flat node array shared by every le...
One R-tree node entry: 4 doubles then a u64, all little-endian, 40 bytes with no padding (packed_rtre...
static NodeItem decode(bytes_view b)
static NodeItem empty(std::uint64_t offset)
The "empty" node used as the fold/aggregation identity: any real bbox's expand widens it.
void expand(const NodeItem &r)
Widens this node's bbox to also cover r, leaving offset untouched.
static constexpr std::size_t kSize
std::uint64_t offset
bool intersects(const BBox &q) const
Mirrors NodeItem::intersects (packed_rtree/mod.rs:122-134), which uses strict < and >: touching edges...
void encode(std::uint8_t *out) const
Writes this node's 40 bytes (4 LE f64 then a LE u64), matching decode's layout exactly.
One hit from an index traversal.
Definition reader.hpp:18