Skip to content

Commit d3f5bc7

Browse files
committed
feat: initial MVT ST_TileEnvelope implementation
This is similar to the PostGIS ST_TileEnvelope function, but only supports the standard (EPSG:3857) geometry, and does not support additional margins.
1 parent 0bbe0b8 commit d3f5bc7

6 files changed

Lines changed: 578 additions & 0 deletions

File tree

src/spatial/modules/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ if(SPATIAL_USE_GEOS)
66
endif()
77
add_subdirectory(osm)
88
add_subdirectory(shapefile)
9+
add_subdirectory(mvt)
910

1011
set(EXTENSION_SOURCES
1112
${EXTENSION_SOURCES}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
set(EXTENSION_SOURCES
2+
${EXTENSION_SOURCES}
3+
${CMAKE_CURRENT_SOURCE_DIR}/mvt_module.cpp
4+
PARENT_SCOPE
5+
)
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Mapbox Vector Tiles (MVT) implementation
2+
3+
#include "spatial/modules/mvt/mvt_module.hpp"
4+
5+
#include "duckdb/common/vector_operations/generic_executor.hpp"
6+
#include "spatial/geometry/geometry_serialization.hpp"
7+
#include "spatial/geometry/sgl.hpp"
8+
#include "spatial/spatial_types.hpp"
9+
#include <spatial/util/function_builder.hpp>
10+
11+
namespace duckdb {
12+
13+
namespace {
14+
// ######################################################################################################################
15+
// Util
16+
// ######################################################################################################################
17+
18+
//======================================================================================================================
19+
// LocalState
20+
//======================================================================================================================
21+
22+
class LocalState final : public FunctionLocalState {
23+
public:
24+
explicit LocalState(ClientContext &context) : arena(BufferAllocator::Get(context)), allocator(arena) {
25+
}
26+
27+
static unique_ptr<FunctionLocalState> Init(ExpressionState &state, const BoundFunctionExpression &expr,
28+
FunctionData *bind_data);
29+
static LocalState &ResetAndGet(ExpressionState &state);
30+
31+
string_t Serialize(Vector &vector, const sgl::geometry &geom);
32+
33+
GeometryAllocator &GetAllocator() {
34+
return allocator;
35+
}
36+
37+
private:
38+
ArenaAllocator arena;
39+
GeometryAllocator allocator;
40+
};
41+
42+
unique_ptr<FunctionLocalState> LocalState::Init(ExpressionState &state, const BoundFunctionExpression &expr,
43+
FunctionData *bind_data) {
44+
return make_uniq_base<FunctionLocalState, LocalState>(state.GetContext());
45+
}
46+
47+
LocalState &LocalState::ResetAndGet(ExpressionState &state) {
48+
auto &local_state = ExecuteFunctionState::GetFunctionState(state)->Cast<LocalState>();
49+
local_state.arena.Reset();
50+
return local_state;
51+
}
52+
53+
string_t LocalState::Serialize(Vector &vector, const sgl::geometry &geom) {
54+
const auto size = Serde::GetRequiredSize(geom);
55+
auto blob = StringVector::EmptyString(vector, size);
56+
Serde::Serialize(geom, blob.GetDataWriteable(), size);
57+
blob.Finalize();
58+
return blob;
59+
}
60+
61+
} // namespace
62+
63+
namespace {
64+
65+
struct ST_TileEnvelope {
66+
static constexpr double RADIUS = 6378137.0;
67+
static constexpr double PI = 3.141592653589793;
68+
static constexpr double CIRCUMFERENCE = 2 * PI * RADIUS;
69+
70+
static void ExecuteWebMercator(DataChunk &args, ExpressionState &state, Vector &result) {
71+
auto &lstate = LocalState::ResetAndGet(state);
72+
73+
TernaryExecutor::Execute<int32_t, int32_t, int32_t, string_t>(
74+
args.data[0], args.data[1], args.data[2], result, args.size(),
75+
[&](int32_t tile_zoom, int32_t tile_x, int32_t tile_y) {
76+
validate_tile_zoom_argument(tile_zoom);
77+
uint32_t zoom_extent = 1u << tile_zoom;
78+
validate_tile_index_arguments(zoom_extent, tile_x, tile_y);
79+
sgl::geometry bbox;
80+
get_tile_bbox(lstate.GetAllocator(), zoom_extent, tile_x, tile_y, bbox);
81+
return lstate.Serialize(result, bbox);
82+
});
83+
}
84+
85+
static void validate_tile_zoom_argument(int32_t tile_zoom) {
86+
if ((tile_zoom < 0) || (tile_zoom > 30)) {
87+
throw InvalidInputException("ST_TileEnvelope: tile_zoom must be in the range [0,30]");
88+
}
89+
}
90+
91+
static void validate_tile_index_arguments(uint32_t zoom_extent, int32_t tile_x, int32_t tile_y) {
92+
if ((tile_x < 0) || ((uint32_t)tile_x >= zoom_extent)) {
93+
throw InvalidInputException("ST_TileEnvelope: tile_x is out of range for specified tile_zoom");
94+
}
95+
if ((tile_y < 0) || ((uint32_t)tile_y >= zoom_extent)) {
96+
throw InvalidInputException("ST_TileEnvelope: tile_y is out of range for specified tile_zoom");
97+
}
98+
}
99+
100+
static void get_tile_bbox(GeometryAllocator &allocator, uint32_t zoom_extent,
101+
int32_t tile_x, int32_t tile_y, sgl::geometry &bbox) {
102+
double single_tile_width = CIRCUMFERENCE / zoom_extent;
103+
double single_tile_height = CIRCUMFERENCE / zoom_extent;
104+
double tile_left = get_tile_left(tile_x, single_tile_width);
105+
double tile_right = tile_left + single_tile_width;
106+
double tile_top = get_tile_top(tile_y, single_tile_height);
107+
double tile_bottom = tile_top - single_tile_height;
108+
109+
sgl::polygon::init_from_bbox(allocator, tile_left, tile_bottom, tile_right, tile_top, bbox);
110+
}
111+
112+
static double get_tile_left(uint32_t tile_x, double single_tile_width) {
113+
return -0.5 * CIRCUMFERENCE + (tile_x * single_tile_width);
114+
}
115+
116+
static double get_tile_top(uint32_t tile_y, double single_tile_height) {
117+
return 0.5 * CIRCUMFERENCE - (tile_y * single_tile_height);
118+
}
119+
120+
//------------------------------------------------------------------------------------------------------------------
121+
// Documentation
122+
//------------------------------------------------------------------------------------------------------------------
123+
static constexpr auto DESCRIPTION = R"(
124+
The `ST_TileEnvelope` scalar function generates tile envelope rectangular polygons from specified zoom level and tile indices.
125+
126+
This is used in MVT generation to select the features corresponding to the tile extent. The envelope is in the Web Mercator
127+
coordinate reference system (EPSG:3857). The tile pyramid starts at zoom level 0, corresponding to a single tile for the
128+
world. Each zoom level doubles the number of tiles in each direction, such that zoom level 1 is 2 tiles wide by 2 tiles high,
129+
zoom level 2 is 4 tiles wide by 4 tiles high, and so on. Tile indices start at `[x=0, y=0]` at the top left, and increase
130+
down and right. For example, at zoom level 2, the top right tile is `[x=3, y=0]`, the bottom left tile is `[x=0, y=3]`, and
131+
the bottom right is `[x=3, y=3]`.
132+
133+
```sql
134+
SELECT ST_TileEnvelope(2, 3, 1);
135+
```
136+
)";
137+
static constexpr auto EXAMPLE = R"(
138+
SELECT ST_TileEnvelope(2, 3, 1);
139+
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
140+
│ st_tileenvelope(2, 3, 1) │
141+
│ geometry │
142+
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
143+
│ POLYGON ((1.00188E+07 0, 1.00188E+07 1.00188E+07, 2.00375E+07 1.00188E+07, 2.00375E+07 0, 1.00188E+07 0)) │
144+
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
145+
)";
146+
147+
//------------------------------------------------------------------------------------------------------------------
148+
// Register
149+
//------------------------------------------------------------------------------------------------------------------
150+
static void Register(DatabaseInstance &db) {
151+
FunctionBuilder::RegisterScalar(db, "ST_TileEnvelope", [](ScalarFunctionBuilder &func) {
152+
func.AddVariant([](ScalarFunctionVariantBuilder &variant) {
153+
variant.AddParameter("tile_zoom", LogicalType::INTEGER);
154+
variant.AddParameter("tile_x", LogicalType::INTEGER);
155+
variant.AddParameter("tile_y", LogicalType::INTEGER);
156+
variant.SetReturnType(GeoTypes::GEOMETRY());
157+
variant.SetInit(LocalState::Init);
158+
variant.SetFunction(ExecuteWebMercator);
159+
});
160+
161+
func.SetDescription(DESCRIPTION);
162+
func.SetExample(EXAMPLE);
163+
164+
func.SetTag("ext", "spatial");
165+
func.SetTag("category", "conversion");
166+
});
167+
}
168+
};
169+
170+
} // namespace
171+
//------------------------------------------------------------------------------
172+
// Register
173+
//------------------------------------------------------------------------------
174+
void RegisterMapboxVectorTileModule(DatabaseInstance &db) {
175+
ST_TileEnvelope::Register(db);
176+
};
177+
178+
} // namespace duckdb
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
namespace duckdb {
4+
5+
class DatabaseInstance;
6+
7+
void RegisterMapboxVectorTileModule(DatabaseInstance &db);
8+
9+
} // namespace duckdb

src/spatial/spatial_extension.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#if SPATIAL_USE_GEOS
1010
#include "spatial/modules/geos/geos_module.hpp"
1111
#endif
12+
#include "spatial/modules/mvt/mvt_module.hpp"
1213
#include "operators/spatial_operator_extension.hpp"
1314
#include "spatial/modules/main/spatial_functions.hpp"
1415
#include "spatial/modules/osm/osm_module.hpp"
@@ -40,6 +41,7 @@ static void LoadInternal(DatabaseInstance &instance) {
4041
#endif
4142
RegisterOSMModule(instance);
4243
RegisterShapefileModule(instance);
44+
RegisterMapboxVectorTileModule(instance);
4345

4446
RTreeModule::RegisterIndex(instance);
4547
RTreeModule::RegisterIndexPragmas(instance);

0 commit comments

Comments
 (0)