ol-mbtiles
Version:
MBTiles format reader for Openlayers
233 lines (200 loc) • 7.53 kB
text/typescript
import { createSQLiteHTTPPool, SQLiteHTTPPool, VFSHTTP } from 'sqlite-wasm-http';
import { Options as ImageTileOptions } from 'ol/source/TileImage.js';
import { Options as OLVectorTileOptions } from 'ol/source/VectorTile.js';
import { get as getProjection, transformExtent } from 'ol/proj.js';
import { getWidth } from 'ol/extent.js';
import TileGrid from 'ol/tilegrid/TileGrid.js';
import { FeatureLike } from 'ol/Feature.js';
import { debug } from './debug.js';
// Check if the type is any
// https://stackoverflow.com/questions/55541275/typescript-check-for-the-any-type
type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
// Detect if OLVectorTiles is a generic type
// (works because with @ts-ignore invalid types resolve to any)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
type VectorTileOptions = IfAny<OLVectorTileOptions<FeatureLike>, OLVectorTileOptions, OLVectorTileOptions<FeatureLike>>;
/**
* Options for creating a MBTilesRasterSource
*/
export interface MBTilesRasterOptions extends ImageTileOptions {
/**
* Number of parallel workers to use for retrieving tiles, @default 4
*/
sqlWorkers?: number;
/**
* List of layer names to selectively include, @default everything
*/
layers?: string[];
tileUrlFunction?: never;
tileLoadFunction?: never;
/**
* Alternative method of specifying minZoom, mutually exclusive with tileGrid, requires explicit projection.
* This refers to the available zoom levels of the tiled data and it is different from the OpenLayers minZoom
* parameter that applies to the layer.
*/
minZoom?: number;
/**
* Alternative method of specifying maxZoom, mutually exclusive with tileGrid, requires explicit projection
* This refers to the available zoom levels of the tiled data and it is different from the OpenLayers maxZoom
* parameter that applies to the layer.
*/
maxZoom?: number;
/**
* Optional tile grid, refer to the Openlayers manual
*/
tileGrid?: TileGrid;
/**
* Optional already open SQLiteHTTP pool (mutually exclusive with url)
*/
pool?: Promise<SQLiteHTTPPool>;
/**
* Optional MIME type for loaded tiles (see https://github.com/mmomtchev/ol-mbtiles/issues/68)
*/
mime?: string;
}
/**
* Options for creating a MBTilesVectorSource
*/
export interface MBTilesVectorOptions extends VectorTileOptions {
/**
* Number of parallel workers to use for retrieving tiles, @default 4
*/
sqlWorkers?: number;
/**
* List of layer names to selectively include, @default everything
*/
layers?: string[];
/**
* Minimum available zoom level.
* This refers to the available zoom levels of the tiled data and it is different from the OpenLayers minZoom
* parameter that applies to the layer.
*/
minZoom?: number;
/**
* Maximum available zoom level.
* This refers to the available zoom levels of the tiled data and it is different from the OpenLayers maxZoom
* parameter that applies to the layer.
*/
maxZoom?: number;
/**
* Optional already open SQLiteHTTP pool (mutually exclusive with url)
*/
pool?: Promise<SQLiteHTTPPool>;
tileUrlFunction?: never;
tileLoadFunction?: never;
format?: never;
}
/**
* Shared options for all MBTiles
*/
export interface SQLOptions {
/**
* URL of the remote MBTiles source
*/
url: string;
/**
* Number of parallel workers to use for retrieving tiles, @default 4
*/
sqlWorkers?: number;
/**
* Maximum expected page size in bytes for SQLite3 files, @default 4096
*/
maxSqlPageSize?: number;
/**
* Memory to use for SQLite cache in KB, @default 4096
*/
sqlCacheSize?: number;
/**
* Use a specific backend type, @default 'shared'
*/
backendType?: VFSHTTP.Options['backendType'];
}
export type MBTilesOptions = MBTilesVectorOptions | MBTilesRasterOptions;
const formats: Record<string, { type: 'raster' | 'vector', mime?: string; }> = {
'jpg': { type: 'raster', mime: 'image/jpeg' },
'png': { type: 'raster', mime: 'image/png' },
'webp': { type: 'raster', mime: 'image/webp' },
'pbf': { type: 'vector' },
'mvt': { type: 'vector' },
};
export function httpPoolOptions(options?: SQLOptions) {
return {
workers: options?.sqlWorkers ?? 4,
httpOptions: {
backendType: options?.backendType,
maxPageSize: options?.maxSqlPageSize ?? 4096,
cacheSize: options?.sqlCacheSize ?? 4096
} as VFSHTTP.Options,
};
}
/**
* Automatically import MBTiles metadata and return an options object
* compatible with the source constructors.
*
* @param {(MBTilesRasterOptions | MBTilesVectorOptions) & SQLOptions} opt Any MBTiles{Raster|Vector}Source options to be overridden
* @param {string} opt.url URL of the remote tileset
* @returns {(MBTilesRasterOptions | MBTilesVectorOptions)}
*/
export function importMBTiles<T extends MBTilesOptions>(opt: SQLOptions & T): Promise<T & SQLOptions> {
const pool: Promise<SQLiteHTTPPool> = createSQLiteHTTPPool(httpPoolOptions(opt));
return pool
.then((pool) => pool.open(opt.url).then(() => pool))
.then((p) => p.exec('SELECT name,value FROM metadata'))
.then((r) => {
if (r && r.length) {
// Transform an array of form [ ['name', 'value' ], ... ] to object
const data = r.reduce((a, x) => {
a[x.row[0] as string] = x.row[1] as string;
return a;
}, {} as Record<string, string>);
debug('Loaded metadata', data);
return data;
}
throw new Error('Could not load metadata');
})
.then((md) => {
const opts = { ...opt } as T & SQLOptions;
const format = (md['format'] as string)?.toLowerCase?.();
if (!formats[format])
console.warn('Unknown tile format', format);
// Sometimes, I wonder if Mapbox doesn't hold a patent or some
// other kind of investment related to everyone using 3857
opts.projection = opt.projection ?? 'EPSG:3857';
opts.attributions = (md.attribution ?? md.description) as string;
opts.maxZoom = opt.maxZoom ?? +md['maxzoom'];
opts.minZoom = opt.minZoom ?? +md['minzoom'];
const projExtent = getProjection(opts.projection)?.getExtent?.();
const bounds = md['bounds'] as string;
const extent = bounds ?
transformExtent(bounds.split(',').map((r) => +r), 'EPSG:4326', opts.projection) :
projExtent;
if (formats[format].type === 'raster') {
if (opts.maxZoom === undefined || opts.minZoom === undefined || projExtent === undefined)
throw new Error('Cannot determine tilegrid, need minZoom, maxZoom');
const baseResolution = getWidth(projExtent) / 256;
const resolutions = [baseResolution];
for (let z = 1; z <= opts.maxZoom; z++)
resolutions.push(resolutions[resolutions.length - 1] / 2);
const mime = formats[format].mime ?? format;
(opts as MBTilesRasterOptions).mime = mime;
opts.tileGrid = new TileGrid({
origin: [projExtent[0], projExtent[2]],
extent,
minZoom: opts.minZoom,
resolutions
});
} else {
const vectorOpts = opts as MBTilesVectorOptions;
// Alas VectorTileSource in OpenLayers does not support
// constraining the extent while keeping the origin
vectorOpts.extent = projExtent;
}
opts.pool = pool;
opts.url = opt.url;
return opts;
})
.catch((e) =>
pool.then((p) => p.close()).then(() => Promise.reject(e))
);
}