Skip to content

Instantly share code, notes, and snippets.

@do-me
Forked from Maxxen/app.py
Last active February 15, 2026 19:49
Show Gist options
  • Select an option

  • Save do-me/c3f3216542d1587ff30b3c9aecec8825 to your computer and use it in GitHub Desktop.

Select an option

Save do-me/c3f3216542d1587ff30b3c9aecec8825 to your computer and use it in GitHub Desktop.
DuckDB Vector Tile Serve w/ Flask + MapLibre. Loads geoparquet file with spatial sampling and uv tooling. Run with uv run app.py
# /// script
# dependencies = [
# "duckdb",
# "flask"
# ]
# ///
import duckdb
import flask
import gzip
import os
# Configuration
GEOPARQUET_PATH = "your_geoparquet_file.parquet"
DB_PATH = "tiles_cache.db"
POINTS_PER_TILE_GOAL = 10000
app = flask.Flask(__name__)
# --- AUTOMATIC CLEANUP ON START ---
if os.path.exists(DB_PATH):
print(f"Cleaning up old database: {DB_PATH}")
os.remove(DB_PATH)
TOTAL_POINTS = 0
def initialize_database():
global TOTAL_POINTS
conn = duckdb.connect(DB_PATH)
conn.execute("LOAD spatial")
print("Initializing database...")
# Hash gives uniform distribution 0-1
conn.execute(f"""
CREATE TABLE geo_data AS
SELECT DISTINCT ON (ST_X(geometry), ST_Y(geometry))
*,
hash(ST_X(geometry), ST_Y(geometry)) / 18446744073709551615.0 as priority,
ST_Transform(ST_FlipCoordinates(geometry), 'EPSG:4326', 'EPSG:3857') as geom_3857
FROM read_parquet('{GEOPARQUET_PATH}')""")
TOTAL_POINTS = conn.execute("SELECT COUNT(*) FROM geo_data").fetchone()[0]
print(f"Total points in dataset: {TOTAL_POINTS}")
print("Creating indexes...")
conn.execute("CREATE INDEX idx_spatial ON geo_data USING RTREE (geom_3857)")
conn.execute("CREATE INDEX idx_priority ON geo_data (priority)")
conn.close()
def get_db():
conn = duckdb.connect(DB_PATH, read_only=True)
conn.execute("LOAD spatial")
return conn
@app.route('/tiles/<int:z>/<int:x>/<int:y>.pbf')
def get_tile(z, x, y):
if TOTAL_POINTS == 0: return b''
threshold = (POINTS_PER_TILE_GOAL * (4 ** z)) / TOTAL_POINTS
threshold = max(0.00001, min(1.0, threshold))
conn = get_db()
try:
query = """
WITH tile_env AS (
SELECT ST_TileEnvelope($z, $x, $y) as env_geom,
ST_Extent(ST_TileEnvelope($z, $x, $y))::BOX_2D as env_box
),
sampled_data AS (
SELECT
geom_3857,
COLUMNS(* EXCLUDE (geom_3857, geometry, priority))
FROM geo_data
WHERE ST_Intersects(geom_3857, (SELECT env_geom FROM tile_env))
AND priority <= $threshold
LIMIT 2000
),
mvt_prep AS (
SELECT
ST_AsMVTGeom(geom_3857, (SELECT env_box FROM tile_env), 4096, 256, true) as geom,
* EXCLUDE (geom_3857)
FROM sampled_data
)
SELECT ST_AsMVT(mvt_prep, 'layer') FROM mvt_prep WHERE geom IS NOT NULL
"""
result = conn.execute(query, {
"z": z, "x": x, "y": y,
"threshold": threshold
}).fetchone()
tile_data = result[0] if result and result[0] else b''
if not tile_data:
return flask.Response(b'', mimetype='application/x-protobuf')
response = flask.Response(gzip.compress(tile_data), mimetype='application/x-protobuf')
response.headers['Content-Encoding'] = 'gzip'
response.headers['Access-Control-Allow-Origin'] = '*'
return response
finally:
conn.close()
@app.route("/")
def index():
return """
<!DOCTYPE html>
<html>
<head>
<title>DuckDB Density-Preserving Tiles</title>
<script src='https://unpkg.com/maplibre-gl@4.0.0/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@4.0.0/dist/maplibre-gl.css' rel='stylesheet' />
<style>
body { margin: 0; font-family: sans-serif; }
#map { position: absolute; width: 100%; height: 100%; }
#controls {
position: absolute;
top: 10px;
left: 10px;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
width: 250px;
z-index: 100;
}
.control-group { margin-bottom: 12px; }
.control-group label { display: block; font-size: 12px; font-weight: bold; margin-bottom: 5px; color: #333; }
.control-group input[type="text"] { width: 100%; padding: 5px; box-sizing: border-box; font-size: 12px; margin-bottom: 5px;}
.control-group button { width: 100%; padding: 5px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; }
.control-group button:hover { background: #2563eb; }
.row { display: flex; align-items: center; justify-content: space-between; }
</style>
</head>
<body>
<div id="controls">
<div class="control-group">
<label>Point Radius (Base)</label>
<div class="row">
<input type="range" id="radius" min="1" max="20" value="4" step="0.5" style="flex-grow:1; margin-right: 10px;">
<span id="radius-val" style="font-size:12px;">4px</span>
</div>
</div>
<div class="control-group">
<label>Point Color</label>
<div class="row">
<input type="color" id="color" value="#ef4444" style="width: 100%; height: 30px; border: none; padding: 0;">
</div>
</div>
<div class="control-group">
<label>Map Style URL</label>
<input type="text" id="style-url" value="https://tiles.openfreemap.org/styles/liberty">
<button onclick="updateStyle()">Apply Style</button>
</div>
</div>
<div id="map"></div>
<script>
// 1. Define configuration objects outside the map load event
// so they can be reused easily.
const SOURCE_ID = 'duckdb';
const LAYER_ID = 'pts';
const duckdbSourceConfig = {
type: 'vector',
tiles: [window.location.origin + '/tiles/{z}/{x}/{y}.pbf'],
minzoom: 0, maxzoom: 22
};
const duckdbLayerConfig = {
id: LAYER_ID,
type: 'circle',
source: SOURCE_ID,
'source-layer': 'layer',
paint: {
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 10, 4],
'circle-color': '#ef4444',
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
'circle-opacity': 0.8
}
};
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [15, 55], zoom: 4,
fadeDuration: 0
});
// 2. Helper function to safely add layers back
function restoreLayers() {
if (!map.getSource(SOURCE_ID)) {
map.addSource(SOURCE_ID, duckdbSourceConfig);
}
if (!map.getLayer(LAYER_ID)) {
map.addLayer(duckdbLayerConfig);
}
// Apply current UI settings to the restored layer
updateLayerPaint();
}
map.on('load', () => {
restoreLayers();
});
// --- UI Interactions ---
const radiusInput = document.getElementById('radius');
const radiusVal = document.getElementById('radius-val');
const colorInput = document.getElementById('color');
function updateLayerPaint() {
if (!map.getLayer(LAYER_ID)) return;
const r = parseFloat(radiusInput.value);
radiusVal.innerText = r + 'px';
const c = colorInput.value;
map.setPaintProperty(LAYER_ID, 'circle-color', c);
// Recalculate interpolation based on new base size
map.setPaintProperty(LAYER_ID, 'circle-radius', [
'interpolate', ['linear'], ['zoom'],
3, r * 0.375,
10, r
]);
}
radiusInput.addEventListener('input', updateLayerPaint);
colorInput.addEventListener('input', updateLayerPaint);
function updateStyle() {
const url = document.getElementById('style-url').value;
if (!url) return;
// setStyle removes all sources/layers.
// We listen for the 'style.load' event (triggered when new style is ready)
// to add our custom data back.
map.setStyle(url);
map.once('style.load', () => {
restoreLayers();
});
}
</script>
</body>
</html>
"""
if __name__ == '__main__':
initialize_database()
app.run(threaded=True, host='0.0.0.0', port=5002)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment