| 1 | #pragma once |
| 2 | |
| 3 | #include <mapbox/geojsonvt/convert.hpp> |
| 4 | #include <mapbox/geojsonvt/tile.hpp> |
| 5 | #include <mapbox/geojsonvt/types.hpp> |
| 6 | #include <mapbox/geojsonvt/wrap.hpp> |
| 7 | |
| 8 | #include <chrono> |
| 9 | #include <cmath> |
| 10 | #include <map> |
| 11 | #include <unordered_map> |
| 12 | |
| 13 | namespace mapbox { |
| 14 | namespace geojsonvt { |
| 15 | |
| 16 | using geometry = mapbox::geometry::geometry<double>; |
| 17 | using feature = mapbox::geometry::feature<double>; |
| 18 | using feature_collection = mapbox::geometry::feature_collection<double>; |
| 19 | using geometry_collection = mapbox::geometry::geometry_collection<double>; |
| 20 | using geojson = mapbox::util::variant<geometry, feature, feature_collection>; |
| 21 | |
| 22 | struct ToFeatureCollection { |
| 23 | feature_collection operator()(const feature_collection& value) const { |
| 24 | return value; |
| 25 | } |
| 26 | feature_collection operator()(const feature& value) const { |
| 27 | return { value }; |
| 28 | } |
| 29 | feature_collection operator()(const geometry& value) const { |
| 30 | return { { value } }; |
| 31 | } |
| 32 | }; |
| 33 | |
| 34 | struct TileOptions { |
| 35 | // simplification tolerance (higher means simpler) |
| 36 | double tolerance = 3; |
| 37 | |
| 38 | // tile extent |
| 39 | uint16_t extent = 4096; |
| 40 | |
| 41 | // tile buffer on each side |
| 42 | uint16_t buffer = 64; |
| 43 | }; |
| 44 | |
| 45 | struct Options : TileOptions { |
| 46 | // max zoom to preserve detail on |
| 47 | uint8_t maxZoom = 18; |
| 48 | |
| 49 | // max zoom in the tile index |
| 50 | uint8_t indexMaxZoom = 5; |
| 51 | |
| 52 | // max number of points per tile in the tile index |
| 53 | uint32_t indexMaxPoints = 100000; |
| 54 | }; |
| 55 | |
| 56 | const Tile empty_tile{}; |
| 57 | |
| 58 | inline uint64_t toID(uint8_t z, uint32_t x, uint32_t y) { |
| 59 | return (((1ull << z) * y + x) * 32) + z; |
| 60 | } |
| 61 | |
| 62 | inline const Tile geoJSONToTile(const geojson& geojson_, |
| 63 | uint8_t z, |
| 64 | uint32_t x, |
| 65 | uint32_t y, |
| 66 | const TileOptions& options = TileOptions(), |
| 67 | bool wrap = false, |
| 68 | bool clip = false) { |
| 69 | |
| 70 | const auto features_ = geojson::visit(v: geojson_, f: ToFeatureCollection{}); |
| 71 | auto z2 = 1u << z; |
| 72 | auto tolerance = (options.tolerance / options.extent) / z2; |
| 73 | auto features = detail::convert(features: features_, tolerance); |
| 74 | if (wrap) { |
| 75 | features = detail::wrap(features, buffer: double(options.buffer) / options.extent); |
| 76 | } |
| 77 | if (clip) { |
| 78 | const double p = options.buffer / options.extent; |
| 79 | |
| 80 | const auto left = detail::clip<0>(features, k1: (x - p) / z2, k2: (x + 1 + p) / z2, minAll: -1, maxAll: 2); |
| 81 | features = detail::clip<1>(features: left, k1: (y - p) / z2, k2: (y + 1 + p) / z2, minAll: -1, maxAll: 2); |
| 82 | } |
| 83 | return detail::InternalTile({ features, z, x, y, options.extent, tolerance }).tile; |
| 84 | } |
| 85 | |
| 86 | class GeoJSONVT { |
| 87 | public: |
| 88 | const Options options; |
| 89 | |
| 90 | GeoJSONVT(const mapbox::geometry::feature_collection<double>& features_, |
| 91 | const Options& options_ = Options()) |
| 92 | : options(options_) { |
| 93 | |
| 94 | const uint32_t z2 = 1u << options.maxZoom; |
| 95 | |
| 96 | auto converted = detail::convert(features: features_, tolerance: (options.tolerance / options.extent) / z2); |
| 97 | auto features = detail::wrap(features: converted, buffer: double(options.buffer) / options.extent); |
| 98 | |
| 99 | splitTile(features, z: 0, x: 0, y: 0); |
| 100 | } |
| 101 | |
| 102 | GeoJSONVT(const geojson& geojson_, const Options& options_ = Options()) |
| 103 | : GeoJSONVT(geojson::visit(v: geojson_, f: ToFeatureCollection{}), options_) { |
| 104 | } |
| 105 | |
| 106 | std::map<uint8_t, uint32_t> stats; |
| 107 | uint32_t total = 0; |
| 108 | |
| 109 | const Tile& getTile(const uint8_t z, const uint32_t x_, const uint32_t y) { |
| 110 | |
| 111 | if (z > options.maxZoom) |
| 112 | throw std::runtime_error("Requested zoom higher than maxZoom: " + std::to_string(val: z)); |
| 113 | |
| 114 | const uint32_t z2 = 1u << z; |
| 115 | const uint32_t x = ((x_ % z2) + z2) % z2; // wrap tile x coordinate |
| 116 | const uint64_t id = toID(z, x, y); |
| 117 | |
| 118 | auto it = tiles.find(x: id); |
| 119 | if (it != tiles.end()) |
| 120 | return it->second.tile; |
| 121 | |
| 122 | it = findParent(z, x, y); |
| 123 | |
| 124 | if (it == tiles.end()) |
| 125 | throw std::runtime_error("Parent tile not found" ); |
| 126 | |
| 127 | // if we found a parent tile containing the original geometry, we can drill down from it |
| 128 | const auto& parent = it->second; |
| 129 | |
| 130 | // drill down parent tile up to the requested one |
| 131 | splitTile(features: parent.source_features, z: parent.z, x: parent.x, y: parent.y, cz: z, cx: x, cy: y); |
| 132 | |
| 133 | it = tiles.find(x: id); |
| 134 | if (it != tiles.end()) |
| 135 | return it->second.tile; |
| 136 | |
| 137 | it = findParent(z, x, y); |
| 138 | if (it == tiles.end()) |
| 139 | throw std::runtime_error("Parent tile not found" ); |
| 140 | |
| 141 | return empty_tile; |
| 142 | } |
| 143 | |
| 144 | const std::unordered_map<uint64_t, detail::InternalTile>& getInternalTiles() const { |
| 145 | return tiles; |
| 146 | } |
| 147 | |
| 148 | private: |
| 149 | std::unordered_map<uint64_t, detail::InternalTile> tiles; |
| 150 | |
| 151 | std::unordered_map<uint64_t, detail::InternalTile>::iterator |
| 152 | findParent(const uint8_t z, const uint32_t x, const uint32_t y) { |
| 153 | uint8_t z0 = z; |
| 154 | uint32_t x0 = x; |
| 155 | uint32_t y0 = y; |
| 156 | |
| 157 | const auto end = tiles.end(); |
| 158 | auto parent = end; |
| 159 | |
| 160 | while ((parent == end) && (z0 != 0)) { |
| 161 | z0--; |
| 162 | x0 = x0 / 2; |
| 163 | y0 = y0 / 2; |
| 164 | parent = tiles.find(x: toID(z: z0, x: x0, y: y0)); |
| 165 | } |
| 166 | |
| 167 | return parent; |
| 168 | } |
| 169 | |
| 170 | void splitTile(const detail::vt_features& features, |
| 171 | const uint8_t z, |
| 172 | const uint32_t x, |
| 173 | const uint32_t y, |
| 174 | const uint8_t cz = 0, |
| 175 | const uint32_t cx = 0, |
| 176 | const uint32_t cy = 0) { |
| 177 | |
| 178 | const double z2 = 1u << z; |
| 179 | const uint64_t id = toID(z, x, y); |
| 180 | |
| 181 | auto it = tiles.find(x: id); |
| 182 | |
| 183 | if (it == tiles.end()) { |
| 184 | const double tolerance = |
| 185 | (z == options.maxZoom ? 0 : options.tolerance / (z2 * options.extent)); |
| 186 | |
| 187 | it = tiles |
| 188 | .emplace(args: id, |
| 189 | args: detail::InternalTile{ features, z, x, y, options.extent, tolerance }) |
| 190 | .first; |
| 191 | stats[z] = (stats.count(x: z) ? stats[z] + 1 : 1); |
| 192 | total++; |
| 193 | // printf("tile z%i-%i-%i\n", z, x, y); |
| 194 | } |
| 195 | |
| 196 | auto& tile = it->second; |
| 197 | |
| 198 | if (features.empty()) |
| 199 | return; |
| 200 | |
| 201 | // if it's the first-pass tiling |
| 202 | if (cz == 0u) { |
| 203 | // stop tiling if we reached max zoom, or if the tile is too simple |
| 204 | if (z == options.indexMaxZoom || tile.tile.num_points <= options.indexMaxPoints) { |
| 205 | tile.source_features = features; |
| 206 | return; |
| 207 | } |
| 208 | |
| 209 | } else { // drilldown to a specific tile; |
| 210 | // stop tiling if we reached base zoom |
| 211 | if (z == options.maxZoom) |
| 212 | return; |
| 213 | |
| 214 | // stop tiling if it's our target tile zoom |
| 215 | if (z == cz) { |
| 216 | tile.source_features = features; |
| 217 | return; |
| 218 | } |
| 219 | |
| 220 | // stop tiling if it's not an ancestor of the target tile |
| 221 | const double m = 1u << (cz - z); |
| 222 | if (x != static_cast<uint32_t>(std::floor(x: cx / m)) || |
| 223 | y != static_cast<uint32_t>(std::floor(x: cy / m))) { |
| 224 | tile.source_features = features; |
| 225 | return; |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | const double p = 0.5 * options.buffer / options.extent; |
| 230 | const auto& min = tile.bbox.min; |
| 231 | const auto& max = tile.bbox.max; |
| 232 | |
| 233 | const auto left = detail::clip<0>(features, k1: (x - p) / z2, k2: (x + 0.5 + p) / z2, minAll: min.x, maxAll: max.x); |
| 234 | |
| 235 | splitTile(features: detail::clip<1>(features: left, k1: (y - p) / z2, k2: (y + 0.5 + p) / z2, minAll: min.y, maxAll: max.y), z: z + 1, |
| 236 | x: x * 2, y: y * 2, cz, cx, cy); |
| 237 | splitTile(features: detail::clip<1>(features: left, k1: (y + 0.5 - p) / z2, k2: (y + 1 + p) / z2, minAll: min.y, maxAll: max.y), z: z + 1, |
| 238 | x: x * 2, y: y * 2 + 1, cz, cx, cy); |
| 239 | |
| 240 | const auto right = |
| 241 | detail::clip<0>(features, k1: (x + 0.5 - p) / z2, k2: (x + 1 + p) / z2, minAll: min.x, maxAll: max.x); |
| 242 | |
| 243 | splitTile(features: detail::clip<1>(features: right, k1: (y - p) / z2, k2: (y + 0.5 + p) / z2, minAll: min.y, maxAll: max.y), z: z + 1, |
| 244 | x: x * 2 + 1, y: y * 2, cz, cx, cy); |
| 245 | splitTile(features: detail::clip<1>(features: right, k1: (y + 0.5 - p) / z2, k2: (y + 1 + p) / z2, minAll: min.y, maxAll: max.y), z: z + 1, |
| 246 | x: x * 2 + 1, y: y * 2 + 1, cz, cx, cy); |
| 247 | |
| 248 | // if we sliced further down, no need to keep source geometry |
| 249 | tile.source_features = {}; |
| 250 | } |
| 251 | }; |
| 252 | |
| 253 | } // namespace geojsonvt |
| 254 | } // namespace mapbox |
| 255 | |