How GeoLibre's Custom MapLibre Protocol Handles Local MBTiles Files
GeoLibre registers a custom geolibre-mbtiles protocol with MapLibre-GL that intercepts tile requests and forwards them to a Tauri Rust backend, enabling direct rendering of local SQLite MBTiles databases without requiring a web server.
The GeoLibre desktop application leverages a custom MapLibre protocol to render local MBTiles files as standard map layers. By registering a protocol handler with MapLibre-GL's addProtocol API, GeoLibre transforms local SQLite-based MBTiles databases into virtual tile servers that work seamlessly with the library's existing raster and vector source types.
Understanding the geolibre-mbtiles Protocol Scheme
The protocol uses a custom URL scheme geolibre-mbtiles:// that embeds the absolute filesystem path and tile coordinates. When MapLibre-GL encounters this scheme in a source URL, it triggers the registered handler instead of making an HTTP request.
Generating Tile URLs
The helper function mbtilesTileUrl() in apps/geolibre-desktop/src/lib/mbtiles.ts constructs these specialized URLs:
// apps/geolibre-desktop/src/lib/mbtiles.ts
export function mbtilesTileUrl(path: string): string {
return `${MBTILES_PROTOCOL}://tile/{z}/{x}/{y}?path=${encodeURIComponent(path)}`;
}
This generates URLs like geolibre-mbtiles://tile/{z}/{x}/{y}?path=%2FUsers%2F...%2Fdata.mbtiles, encoding the filesystem path as a query parameter to support any OS path structure.
Registering the Protocol with MapLibre-GL
During application startup, registerMbtilesProtocol() registers the handler using MapLibre-GL's addProtocol method. This is implemented in apps/geolibre-desktop/src/lib/mbtiles.ts:
// apps/geolibre-desktop/src/lib/mbtiles.ts
export function registerMbtilesProtocol(): void {
if (protocolRegistered) return;
addProtocol(MBTILES_PROTOCOL, async (request) => {
const params = parseMbtilesTileRequest(request);
const bytes = await invoke<number[] | Uint8Array>("read_mbtiles_tile", params);
const array = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
return {
data: array.buffer.slice(array.byteOffset, array.byteOffset + array.byteLength),
};
});
protocolRegistered = true;
}
The handler extracts tile coordinates via parseMbtilesTileRequest() and invokes the Tauri command read_mbtiles_tile to fetch raw tile bytes from the SQLite database.
Parsing Tile Requests
The parseMbtilesTileRequest() function validates incoming request URLs and extracts the zoom level (z), tile column (x), tile row (y), and file path:
// apps/geolibre-desktop/src/lib/mbtiles.ts
function parseMbtilesTileRequest(request: RequestParameters) {
const url = new URL(request.url);
const parts = url.pathname.split("/").filter(Boolean);
if (parts.length !== 3) throw new Error("Invalid MBTiles tile URL.");
const path = url.searchParams.get("path");
if (!path) throw new Error("Invalid MBTiles tile path.");
return {
path,
z: parseTileCoordinate(parts[0], "z"),
x: parseTileCoordinate(parts[1], "x"),
y: parseTileCoordinate(parts[2], "y"),
};
}
This ensures only well-formed requests reach the Rust backend, throwing descriptive errors for malformed URLs or missing path parameters.
Backend Tile Retrieval in Rust
The actual tile data retrieval happens in the Tauri backend via the read_mbtiles_tile command defined in apps/geolibre-desktop/src-tauri/src/lib.rs:
// apps/geolibre-desktop/src-tauri/src/lib.rs
#[tauri::command]
fn read_mbtiles_tile(path: String, z: u32, x: u32, y: u32) -> Result<Vec<u8>, String> {
// Open the MBTiles SQLite file at `path`,
// execute `SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?`,
// and return the blob.
}
This command opens the specified MBTiles SQLite database, queries the tiles table using the provided coordinates, and returns the raw tile blob as a Vec<u8>. The JavaScript handler then converts these bytes into an ArrayBuffer suitable for MapLibre-GL's consumption.
Accessing MBTiles Metadata
Beyond tiles, GeoLibre can query MBTiles metadata (name, format, bounds, min/max zoom) via the readMbtilesMetadata function:
// apps/geolibre-desktop/src/lib/mbtiles.ts
export async function readMbtilesMetadata(path: string): Promise<MbtilesMetadata> {
if (!isTauri()) throw new Error("MBTiles files require GeoLibre Desktop.");
return invoke<MbtilesMetadata>("read_mbtiles_metadata", { path });
}
This wrapper ensures the feature is only available in the desktop environment by checking isTauri() before invoking the backend command.
Implementation Example
To use local MBTiles files in your GeoLibre desktop application:
- Generate the tile URL for your local file:
import { mbtilesTileUrl, registerMbtilesProtocol } from './lib/mbtiles';
const mbtilesPath = "/Users/alice/data/country.mbtiles";
const tileUrl = mbtilesTileUrl(mbtilesPath);
// Result: "geolibre-mbtiles://tile/{z}/{x}/{y}?path=%2FUsers%2Falice%2Fdata%2Fcountry.mbtiles"
- Register the protocol during app initialization:
registerMbtilesProtocol();
- Add as a MapLibre source:
const source = {
type: "raster", // or "vector" for vector tiles
tiles: [tileUrl],
tileSize: 256,
};
map.addSource("local-mbtiles", source);
Summary
- GeoLibre implements a custom MapLibre protocol named
geolibre-mbtilesto handle local MBTiles files in the desktop environment. - The protocol handler is registered via MapLibre-GL's
addProtocol()inapps/geolibre-desktop/src/lib/mbtiles.ts. - Tile URLs embed the absolute filesystem path and coordinates in the format
geolibre-mbtiles://tile/{z}/{x}/{y}?path=.... - The Tauri backend command
read_mbtiles_tileinapps/geolibre-desktop/src-tauri/src/lib.rsqueries the SQLite database and returns raw tile bytes. - This architecture allows MapLibre-GL to treat local MBTiles databases as native tile sources without requiring a local HTTP server.
Frequently Asked Questions
Why does the custom protocol only work in GeoLibre Desktop?
The geolibre-mbtiles protocol relies on Tauri's IPC invoke mechanism to call Rust commands that read the SQLite database. Since browser-based environments cannot access the local filesystem directly, the protocol checks isTauri() and throws an error if accessed outside the desktop application.
How does the protocol handle different tile formats (raster vs. vector)?
The protocol is format-agnostic. The Rust backend returns raw bytes from the tile_data column regardless of content type. MapLibre-GL interprets these bytes based on the source type property you specify ("raster" or "vector") when adding the source to the map.
What happens if the MBTiles file path is invalid or the tile doesn't exist?
The parseMbtilesTileRequest function validates the URL structure and throws descriptive errors for malformed requests. If the file path is valid but the tile doesn't exist in the database, the Rust command returns an empty result or error, which the protocol handler translates into a failed request that MapLibre-GL handles gracefully by not rendering that specific tile.
Can I use this protocol with multiple MBTiles files simultaneously?
Yes. You can register multiple sources using different mbtilesTileUrl() calls with distinct file paths. Each source ID in MapLibre-GL can point to a different geolibre-mbtiles:// URL with its own path parameter, allowing you to overlay multiple local tile layers on the same map.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →