Parsing a GeoPackage and rendering tiles are CPU-intensive operations. Running them on the main browser thread blocks UI interactions. Moving this work to a Web Worker keeps your page responsive.
How it works
- The main thread creates a
Worker and sends messages to it.
- The worker loads the GeoPackage browser bundle via
importScripts, opens the .gpkg file, and handles requests.
- Results (tile
ImageData, GeoJSON feature collections) are posted back to the main thread via postMessage.
ImageData objects can be transferred rather than copied, which makes passing tile pixel data between the worker and main thread very efficient.
Setup
You need two files: worker.js (runs inside the worker) and index.js (runs on the main thread).
worker.js
index.js
index.html
// Import the GeoPackage browser bundle inside the worker
self.importScripts('/path/to/geopackage.min.js');
const {
GeoPackageManager,
Canvas,
TileUtils,
GeoPackageTileRetriever,
FeatureTiles,
BoundingBox,
setSqljsWasmLocateFile,
} = GeoPackage;
// Specify the location of the sql-wasm.wasm file
setSqljsWasmLocateFile(file => 'public/' + file);
// Message listener
onmessage = function (e) {
// Open a GeoPackage connection from a Uint8Array
if (e.data.type === 'load') {
GeoPackageManager.open(e.data.file).then(gp => {
self.gp = gp;
});
// Close the connection
} else if (e.data.type === 'close') {
self.gp.close();
// Render a feature table tile
} else if (e.data.type === 'get-feature-tile') {
const table = e.data.table;
const x = parseInt(e.data.x);
const y = parseInt(e.data.y);
const z = parseInt(e.data.z);
const width = parseInt(e.data.width);
const height = parseInt(e.data.height);
const featureDao = self.gp.getFeatureDao(table);
const ft = new FeatureTiles(self.gp, featureDao, width, height);
ft.drawTile(x, y, z).then(tile => {
postMessage({
type: 'tile',
tileImageData: tile.getImageData(),
});
});
// Retrieve an XYZ tile from a tile table
} else if (e.data.type === 'get-tile') {
const table = e.data.table;
const x = parseInt(e.data.x);
const y = parseInt(e.data.y);
const z = parseInt(e.data.z);
const width = parseInt(e.data.width);
const height = parseInt(e.data.height);
self.gp.xyzTile(table, x, y, z, width, height).then(gpTile => {
gpTile.getGeoPackageImage().then(gpImage => {
postMessage({
type: 'tile',
tileImageData: gpImage.getImageData(),
});
});
});
// Return all features from a table as GeoJSON
} else if (e.data.type === 'get-geojson') {
const table = e.data.table;
const features = [];
const featureResultSet = self.gp.queryForGeoJSONFeatures(table);
for (const feature of featureResultSet) {
features.push(feature);
}
featureResultSet.close();
postMessage({
type: 'geojson',
geojson: {
type: 'FeatureCollection',
features,
},
});
}
};
// canvas for drawing — must be defined in the HTML page
var canvas = document.getElementById('canvas');
// Create the worker
var geopackageWorker;
if (window.Worker) {
geopackageWorker = new Worker('worker.js');
// Handle responses from the worker
geopackageWorker.onmessage = function (e) {
if (e.data.type === 'tile') {
// Draw the ImageData directly into the canvas
const ctx = canvas.getContext('2d');
ctx.putImageData(e.data.tileImageData, 0, 0);
} else if (e.data.type === 'geojson') {
console.log(e.data.geojson);
}
};
}
/**
* Open a GeoPackage connection inside the worker.
* Attach to an <input type="file"> onchange event.
* @param {FileList} files
*/
function openConnection(files) {
var f = files[0];
var r = new FileReader();
r.onload = function () {
var array = new Uint8Array(r.result);
geopackageWorker.postMessage({ type: 'load', file: array });
};
r.readAsArrayBuffer(f);
}
/** Close the GeoPackage connection inside the worker. */
function closeConnection() {
geopackageWorker.postMessage({ type: 'close' });
}
/**
* Request a feature-table tile from the worker.
* @param {string} table
* @param {number} x
* @param {number} y
* @param {number} z
*/
function drawFeatureTileInCanvas(table, x, y, z) {
geopackageWorker.postMessage({
type: 'get-feature-tile',
table,
x,
y,
z,
width: canvas.width,
height: canvas.height,
});
}
/**
* Request a raster tile from the worker.
* @param {string} table
* @param {number} x
* @param {number} y
* @param {number} z
*/
function drawTileInCanvas(table, x, y, z) {
geopackageWorker.postMessage({
type: 'get-tile',
table,
x,
y,
z,
width: canvas.width,
height: canvas.height,
});
}
/**
* Request all features from a table in GeoJSON format.
* @param {string} table
*/
function getFeatureTableAsGeoJSON(table) {
geopackageWorker.postMessage({ type: 'get-geojson', table });
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GeoPackage Web Worker Demo</title>
<script src="/path/to/geopackage.min.js"></script>
</head>
<body>
<input id="fileInput" type="file"
onchange="openConnection(this.files)" />
<canvas id="canvas" width="256px" height="256px"></canvas>
<br />
<input id="table" type="text" placeholder="Table name" />
<input id="column" type="number" placeholder="X" />
<input id="row" type="number" placeholder="Y" />
<input id="zoom" type="number" placeholder="Zoom" />
<br />
<button onclick="drawFeatureTileInCanvas(
document.getElementById('table').value,
document.getElementById('column').value,
document.getElementById('row').value,
document.getElementById('zoom').value
)">Get Feature Tile</button>
<button onclick="drawTileInCanvas(
document.getElementById('table').value,
document.getElementById('column').value,
document.getElementById('row').value,
document.getElementById('zoom').value
)">Draw Tile</button>
<button onclick="getFeatureTableAsGeoJSON(
document.getElementById('table').value
)">Get GeoJSON</button>
<button onclick="closeConnection()">Close</button>
<script src="./index.js"></script>
</body>
</html>
Message protocol reference
type | Direction | Payload | Description |
|---|
load | main → worker | file: Uint8Array | Open a GeoPackage from byte array |
close | main → worker | — | Close the GeoPackage connection |
get-tile | main → worker | table, x, y, z, width, height | Request an XYZ raster tile |
get-feature-tile | main → worker | table, x, y, z, width, height | Render a feature table tile |
get-geojson | main → worker | table | Return all features as GeoJSON |
tile | worker → main | tileImageData: ImageData | Rendered tile pixel data |
geojson | worker → main | geojson: FeatureCollection | GeoJSON feature collection |
The worker example above does not queue messages — if a second load message arrives before the first resolves, the self.gp reference will be overwritten. Add a loading flag or message queue for production use.