ol-mbtiles
Version:
MBTiles format reader for Openlayers
321 lines (314 loc) • 14.4 kB
JavaScript
import { createSQLiteHTTPPool } from 'sqlite-wasm-http';
import { get, transformExtent } from 'ol/proj.js';
import { getWidth } from 'ol/extent.js';
import TileGrid from 'ol/tilegrid/TileGrid.js';
import VectorTileSource from 'ol/source/VectorTile.js';
import Protobuf from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';
import pako from 'pako';
import FeatureFormat from 'ol/format/Feature.js';
import Projection from 'ol/proj/Projection.js';
import RenderFeature from 'ol/render/Feature.js';
import ImageTileSource from 'ol/source/TileImage.js';
import TileState from 'ol/TileState.js';
var _a;
const debugEnabled = (typeof OL_MBTILES_DEBUG !== 'undefined' && OL_MBTILES_DEBUG) ||
(typeof process !== 'undefined' && typeof ((_a = process === null || process === undefined ? undefined : process.env) === null || _a === undefined ? undefined : _a.OL_MBTILES_DEBUG) !== 'undefined' && process.env.OL_MBTILES_DEBUG);
const debug = debugEnabled ? console.debug.bind(console) : () => undefined;
const formats = {
'jpg': { type: 'raster', mime: 'image/jpeg' },
'png': { type: 'raster', mime: 'image/png' },
'webp': { type: 'raster', mime: 'image/webp' },
'pbf': { type: 'vector' },
'mvt': { type: 'vector' },
};
function httpPoolOptions(options) {
var _a, _b, _c;
return {
workers: (_a = options === null || options === undefined ? undefined : options.sqlWorkers) !== null && _a !== undefined ? _a : 4,
httpOptions: {
backendType: options === null || options === undefined ? undefined : options.backendType,
maxPageSize: (_b = options === null || options === undefined ? undefined : options.maxSqlPageSize) !== null && _b !== undefined ? _b : 4096,
cacheSize: (_c = options === null || options === undefined ? undefined : options.sqlCacheSize) !== null && _c !== undefined ? _c : 4096
},
};
}
/**
* 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)}
*/
function importMBTiles(opt) {
const pool = 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]] = x.row[1];
return a;
}, {});
debug('Loaded metadata', data);
return data;
}
throw new Error('Could not load metadata');
})
.then((md) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const opts = Object.assign({}, opt);
const format = (_b = (_a = md['format']) === null || _a === undefined ? undefined : _a.toLowerCase) === null || _b === undefined ? undefined : _b.call(_a);
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 = (_c = opt.projection) !== null && _c !== undefined ? _c : 'EPSG:3857';
opts.attributions = ((_d = md.attribution) !== null && _d !== undefined ? _d : md.description);
opts.maxZoom = (_e = opt.maxZoom) !== null && _e !== undefined ? _e : +md['maxzoom'];
opts.minZoom = (_f = opt.minZoom) !== null && _f !== undefined ? _f : +md['minzoom'];
const projExtent = (_h = (_g = get(opts.projection)) === null || _g === undefined ? undefined : _g.getExtent) === null || _h === undefined ? undefined : _h.call(_g);
const bounds = md['bounds'];
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 = (_j = formats[format].mime) !== null && _j !== undefined ? _j : format;
opts.mime = mime;
opts.tileGrid = new TileGrid({
origin: [projExtent[0], projExtent[2]],
extent,
minZoom: opts.minZoom,
resolutions
});
}
else {
const vectorOpts = opts;
// 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)));
}
class MBTilesFormat extends FeatureFormat {
constructor(options) {
var _a, _b, _c;
super();
options = options ? options : {};
this.dataProjection = new Projection({
code: '',
units: 'tile-pixels',
});
this.featureClass_ = options.featureClass ? options.featureClass : RenderFeature;
this.geometryName_ = (_a = options.geometryName) !== null && _a !== undefined ? _a : 'Geometry';
this.layers_ = (_b = options.layers) !== null && _b !== undefined ? _b : null;
this.idProperty_ = options.idProperty;
this.extent = (_c = options.extent) !== null && _c !== undefined ? _c : 4096;
/*
* As this is the very first time MBTiles will be distributed by HTTP
* there is still no official MIME type
*/
this.supportedMediaTypes = [
'application/vnd-mbtiles'
];
}
readFeature(source, options) {
const properties = source.properties;
let id;
if (!this.idProperty_) {
id = source.id;
}
else {
id = properties[this.idProperty_];
delete properties[this.idProperty_];
}
const points = source.loadGeometry();
const flatCoordinates = [];
const ends = [];
const type = MBTilesFormat.MBTypes[points.length > 1 ? 'multi' : 'mono'][source.type];
if (type === 'Unknown')
return null;
for (let i = 0; i < points.length; i++) {
if (points[i].length == 0)
continue;
for (let j = 0; j < points[i].length; j++) {
flatCoordinates.push(points[i][j].x, points[i][j].y);
}
ends.push(flatCoordinates.length);
}
const feature = new this.featureClass_(type, flatCoordinates, ends, 2, properties, id);
if ((options === null || options === undefined ? undefined : options.dataProjection) && 'transform' in feature)
feature.transform(options === null || options === undefined ? undefined : options.dataProjection);
return feature;
}
readFeatures(source, options) {
const layers = this.layers_;
const features = [];
const tile = new VectorTile(new Protobuf(pako.ungzip(source)));
options = this.adaptOptions(options);
const dataProjection = get(options === null || options === undefined ? undefined : options.dataProjection);
const extent = options === null || options === undefined ? undefined : options.extent;
if (!dataProjection || !options || !extent)
throw new Error('Cannot determine the projection/extent');
dataProjection.setWorldExtent(extent);
dataProjection.setExtent([0, 0, this.extent, this.extent]);
options.dataProjection = dataProjection;
for (const layerName of Object.keys(tile.layers)) {
if (layers && !layers.includes(layerName)) {
continue;
}
const l = tile.layers[layerName];
for (let idx = 0; idx < l.length; idx++) {
const vectorFeature = l.feature(idx);
const feature = this.readFeature(vectorFeature, options);
feature.getProperties().layer = layerName;
features.push(feature);
}
}
return features;
}
readProjection() {
return this.dataProjection;
}
}
MBTilesFormat.MBTypes = {
mono: ['Unknown', 'Point', 'LineString', 'Polygon'],
multi: ['Unknown', 'MultiPoint', 'MultiLineString', 'Polygon']
};
/**
* A tile source in a remote .mbtiles file accessible by HTTP
*
* WARNING
* If your application continuously creates and removes MBTilesSource
* objects, special care must be taken to properly dispose of them.
* An MBTilesSource creates a thread pool that the JS engine is unable to
* automatically garbage-collect unless the dispose() method
* is invoked.
* If you need to dispose a map that can potentially contain
* MBTilesSource objects, check loadExample() in
* https://github.com/mmomtchev/ol-mbtiles/blob/main/examples/index.ts#L15
*/
class MBTilesVectorSource extends VectorTileSource {
/**
* @param {MBTilesVectorOptions} options options
*/
constructor(options) {
var _a;
if (options.url === undefined && options.pool === undefined)
throw new Error('Must specify url');
super(Object.assign(Object.assign({}, options), { url: undefined, format: new MBTilesFormat({
layers: options.layers
}),
// This is required to prevent Openlayers' cache from thinking that all tiles share the same URL
tileUrlFunction: (coords) => `${options.url}#${coords[0]}:${coords[1]}:${coords[2]}` }));
this.setTileLoadFunction(this.tileLoader.bind(this));
this.pool = (_a = options.pool) !== null && _a !== undefined ? _a : createSQLiteHTTPPool(httpPoolOptions(options))
.then((pool) => pool.open(options.url).then(() => pool));
}
tileLoader(_tile, _url) {
const tile = _tile;
debug('loading tile', [tile.tileCoord[0], tile.tileCoord[1], tile.tileCoord[2]]);
tile.setLoader((extent, resolution, projection) => {
this.pool
.then((p) => p.exec('SELECT tile_data FROM tiles WHERE zoom_level = $zoom AND tile_column = $col AND tile_row = $row', {
$zoom: tile.tileCoord[0],
$col: tile.tileCoord[1],
$row: (1 << tile.tileCoord[0]) - 1 - tile.tileCoord[2]
}))
.then((r) => {
if (r && r[0] && r[0].row[0]) {
const format = tile.getFormat();
const features = format.readFeatures(r[0].row[0], {
extent,
featureProjection: projection
});
tile.setFeatures(features);
tile.onLoad(features, projection);
return;
}
throw new Error(`No data for ${tile.tileCoord}`);
})
.catch((e) => {
debug(e);
tile.onError();
});
});
}
disposeInternal() {
return this.pool.then((p) => p.close());
}
}
/**
* A tile source in a remote .mbtiles file accessible by HTTP
*
* WARNING
* If your application continuously creates and removes MBTilesSource
* objects, special care must be taken to properly dispose of them.
* An MBTilesSource creates a thread pool that the JS engine is unable to
* automatically garbage-collect unless the dispose() method
* is invoked.
* If you need to dispose a map that can potentially contain
* MBTilesSource objects, check loadExample() in
* https://github.com/mmomtchev/ol-mbtiles/blob/main/examples/index.ts#L15
*/
class MBTilesRasterSource extends ImageTileSource {
/**
* @param {MBTilesRasterOptions} options options
*/
constructor(options) {
var _a;
if (options.url === undefined && options.pool === undefined)
throw new Error('Must specify url');
super(Object.assign(Object.assign({}, options), { url: undefined,
// This is required to prevent Openlayers' cache from thinking that all tiles share the same URL
tileUrlFunction: (coords) => `${options.url}#${coords[0]}:${coords[1]}:${coords[2]}` }));
this.setTileLoadFunction(this.tileLoader.bind(this));
this.pool = (_a = options.pool) !== null && _a !== undefined ? _a : createSQLiteHTTPPool(httpPoolOptions(options))
.then((pool) => pool.open(options.url).then(() => pool));
this.mime = options.mime;
}
// TODO fix the tile type in Openlayers
tileLoader(tile, _url) {
debug('loading tile', [tile.tileCoord[0], tile.tileCoord[1], tile.tileCoord[2]]);
const image = tile.getImage();
this.pool
.then((p) => p.exec('SELECT tile_data FROM tiles WHERE zoom_level = $zoom AND tile_column = $col AND tile_row = $row', {
$zoom: tile.tileCoord[0],
$col: tile.tileCoord[1],
$row: (1 << tile.tileCoord[0]) - 1 - tile.tileCoord[2]
}))
.then((r) => {
if (r && r[0]) {
if (r[0].row[0] instanceof Uint8Array) {
const blob = new Blob([r[0].row[0]], { type: this.mime });
const imageUrl = URL.createObjectURL(blob);
image.src = imageUrl;
return;
}
}
throw new Error(`No data for ${tile.tileCoord}`);
})
.catch((e) => {
debug(e);
tile.setState(TileState.ERROR);
});
}
disposeInternal() {
return this.pool.then((p) => p.close());
}
}
export { MBTilesFormat, MBTilesRasterSource, MBTilesVectorSource, importMBTiles };
//# sourceMappingURL=index.js.map