UNPKG

ol-mbtiles

Version:

MBTiles format reader for Openlayers

321 lines (314 loc) 14.4 kB
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