├── test ├── out │ ├── .gitignore │ ├── metadata-corrupt.json │ ├── metadata-blank.json │ ├── metadata-empty.json │ ├── metadata-with spaces.json │ ├── metadata-world.json │ ├── metadata-save.json │ ├── metadata-plain_3.json │ ├── metadata-MOBAC.json │ ├── metadata-plain_1.json │ ├── metadata-unindexed.json │ ├── metadata-corrupt_null_tile.json │ ├── metadata-plain_2.json │ ├── metadata-plain_4.json │ ├── metadata-geocoder_data.json │ ├── metadata-geocoder_legacy.json │ └── metadata-some-empty-tiles.json └── in │ ├── MOBAC.mbtiles │ ├── blank.mbtiles │ ├── empty.mbtiles │ ├── save.mbtiles │ ├── world.mbtiles │ ├── corrupt.mbtiles │ ├── images │ ├── 0 │ │ └── 0 │ │ │ └── 0.png │ └── 1 │ │ ├── 0 │ │ ├── 0.png │ │ └── 1.png │ │ └── 1 │ │ ├── 0.png │ │ └── 1.png │ ├── plain_1.mbtiles │ ├── plain_2.mbtiles │ ├── plain_3.mbtiles │ ├── plain_4.mbtiles │ ├── unindexed.mbtiles │ ├── geocoder_data.mbtiles │ ├── with spaces.mbtiles │ ├── geocoder_legacy.mbtiles │ ├── corrupt_null_tile.mbtiles │ ├── some-empty-tiles.mbtiles │ └── extent.json ├── utils ├── index.js ├── universalify.test.js ├── utils.test.js └── utils.js ├── schema ├── INDEX │ ├── metadata.sql │ └── tiles.sql ├── TABLE │ ├── metadata.sql │ └── tiles.sql └── index.js ├── .travis.yml ├── .gitignore ├── LICENSE ├── package.json ├── types.ts ├── CHANGELOG.md ├── index.d.ts ├── test.js ├── README.md └── index.js /test/out/.gitignore: -------------------------------------------------------------------------------- 1 | *.mbtiles -------------------------------------------------------------------------------- /test/out/metadata-corrupt.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils') 2 | module.exports = utils 3 | -------------------------------------------------------------------------------- /test/out/metadata-blank.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer" 4 | } 5 | -------------------------------------------------------------------------------- /schema/INDEX/metadata.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX IF NOT EXISTS index_metadata ON metadata (name, value); -------------------------------------------------------------------------------- /schema/TABLE/metadata.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS metadata ( 2 | name text, 3 | value text 4 | ); -------------------------------------------------------------------------------- /test/in/MOBAC.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/MOBAC.mbtiles -------------------------------------------------------------------------------- /test/in/blank.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/blank.mbtiles -------------------------------------------------------------------------------- /test/in/empty.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/empty.mbtiles -------------------------------------------------------------------------------- /test/in/save.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/save.mbtiles -------------------------------------------------------------------------------- /test/in/world.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/world.mbtiles -------------------------------------------------------------------------------- /schema/INDEX/tiles.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX IF NOT EXISTS index_tiles ON tiles (zoom_level, tile_row, tile_column); -------------------------------------------------------------------------------- /test/in/corrupt.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/corrupt.mbtiles -------------------------------------------------------------------------------- /test/in/images/0/0/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/images/0/0/0.png -------------------------------------------------------------------------------- /test/in/images/1/0/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/images/1/0/0.png -------------------------------------------------------------------------------- /test/in/images/1/0/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/images/1/0/1.png -------------------------------------------------------------------------------- /test/in/images/1/1/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/images/1/1/0.png -------------------------------------------------------------------------------- /test/in/images/1/1/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/images/1/1/1.png -------------------------------------------------------------------------------- /test/in/plain_1.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/plain_1.mbtiles -------------------------------------------------------------------------------- /test/in/plain_2.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/plain_2.mbtiles -------------------------------------------------------------------------------- /test/in/plain_3.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/plain_3.mbtiles -------------------------------------------------------------------------------- /test/in/plain_4.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/plain_4.mbtiles -------------------------------------------------------------------------------- /test/out/metadata-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer", 4 | "name": "empty" 5 | } 6 | -------------------------------------------------------------------------------- /test/in/unindexed.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/unindexed.mbtiles -------------------------------------------------------------------------------- /test/in/geocoder_data.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/geocoder_data.mbtiles -------------------------------------------------------------------------------- /test/in/with spaces.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/with spaces.mbtiles -------------------------------------------------------------------------------- /test/in/geocoder_legacy.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/geocoder_legacy.mbtiles -------------------------------------------------------------------------------- /test/in/corrupt_null_tile.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/corrupt_null_tile.mbtiles -------------------------------------------------------------------------------- /test/in/some-empty-tiles.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/HEAD/test/in/some-empty-tiles.mbtiles -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | - 8 6 | notifications: 7 | email: 8 | - carriere.denis@gmail.com 9 | -------------------------------------------------------------------------------- /schema/TABLE/tiles.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS tiles ( 2 | zoom_level integer, 3 | tile_column integer, 4 | tile_row integer, 5 | tile_data blob 6 | ); -------------------------------------------------------------------------------- /test/out/metadata-with spaces.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "type": "baselayer", 4 | "json": "{\"level1\":{\"level2\":\"property\"},\"custom\":[\"custom list\"]}" 5 | } 6 | -------------------------------------------------------------------------------- /test/out/metadata-world.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer", 4 | "minzoom": 0, 5 | "maxzoom": 0, 6 | "format": "jpg", 7 | "bounds": [ 8 | -180, 9 | -85.051129, 10 | 180, 11 | 85.051129 12 | ], 13 | "center": [ 14 | 0, 15 | 0 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test/out/metadata-save.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer", 4 | "minzoom": 1, 5 | "maxzoom": 3, 6 | "description": "Bar", 7 | "name": "Foo", 8 | "bounds": [ 9 | -110, 10 | -40, 11 | 95, 12 | 50 13 | ], 14 | "center": [ 15 | -7.5, 16 | 5 17 | ], 18 | "format": "png" 19 | } 20 | -------------------------------------------------------------------------------- /test/out/metadata-plain_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "type": "baselayer", 4 | "minzoom": 4, 5 | "maxzoom": 8, 6 | "name": "plain_3", 7 | "bounds": [ 8 | -12.480468747741963, 9 | 34.59704151068267, 10 | 42.53906249240259, 11 | 71.52490903141549 12 | ], 13 | "center": [ 14 | 15.029297, 15 | 53.060975 16 | ], 17 | "format": "png" 18 | } 19 | -------------------------------------------------------------------------------- /test/out/metadata-MOBAC.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer", 4 | "minzoom": 2, 5 | "maxzoom": 4, 6 | "description": "Unnamed atlas created on 2017-01-25 14:08:12 by MOBAC", 7 | "name": "Unnamed atlas", 8 | "bounds": [ 9 | -67.5, 10 | 41.046, 11 | -45.088, 12 | 55.777 13 | ], 14 | "center": [ 15 | -56.294, 16 | 48.4115 17 | ], 18 | "format": "png" 19 | } 20 | -------------------------------------------------------------------------------- /test/out/metadata-plain_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer", 4 | "minzoom": 0, 5 | "maxzoom": 4, 6 | "description": "demo description", 7 | "name": "plain_1", 8 | "bounds": [ 9 | -179.9999999749438, 10 | -69.99999999526695, 11 | 179.9999999749438, 12 | 84.99999999782301 13 | ], 14 | "center": [ 15 | 0, 16 | 7.5 17 | ], 18 | "format": "png", 19 | "json": "{\"level1\":{\"level2\":\"property\"},\"version\":\"2.0.0\"}" 20 | } 21 | -------------------------------------------------------------------------------- /test/out/metadata-unindexed.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer", 4 | "minzoom": 0, 5 | "maxzoom": 4, 6 | "description": "demo description", 7 | "name": "plain_1", 8 | "bounds": [ 9 | -179.9999999749438, 10 | -69.99999999526695, 11 | 179.9999999749438, 12 | 84.99999999782301 13 | ], 14 | "center": [ 15 | 0, 16 | 7.5 17 | ], 18 | "format": "png", 19 | "json": "{\"level1\":{\"level2\":\"property\"},\"version\":\"2.0.0\"}" 20 | } 21 | -------------------------------------------------------------------------------- /test/out/metadata-corrupt_null_tile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "type": "baselayer", 4 | "minzoom": 0, 5 | "maxzoom": 4, 6 | "description": "demo description", 7 | "name": "corrupt_null", 8 | "bounds": [ 9 | -179.9999999749438, 10 | -69.99999999526695, 11 | 179.9999999749438, 12 | 84.99999999782301 13 | ], 14 | "center": [ 15 | 0, 16 | 7.5 17 | ], 18 | "format": "png", 19 | "json": "{\"level1\":{\"level2\":\"property\"},\"version\":\"2.0.0\"}" 20 | } 21 | -------------------------------------------------------------------------------- /test/out/metadata-plain_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "type": "baselayer", 4 | "minzoom": 0, 5 | "maxzoom": 4, 6 | "name": "plain_2", 7 | "bounds": [ 8 | -179.9999999749438, 9 | -69.99999999526695, 10 | 179.9999999749438, 11 | 79.99999999662558 12 | ], 13 | "center": [ 14 | 0, 15 | 5 16 | ], 17 | "format": "png", 18 | "formatter": "function(options, data) { if (options.format === 'full') { return '' + data.NAME + ' (Population: ' + data.POP2005 + ')'; } else { return '' + data.NAME + ''; } }" 19 | } 20 | -------------------------------------------------------------------------------- /test/out/metadata-plain_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "type": "baselayer", 4 | "minzoom": 0, 5 | "maxzoom": 4, 6 | "name": "plain_4", 7 | "bounds": [ 8 | -179.9999999749438, 9 | -69.99999999526695, 10 | 179.9999999749438, 11 | 79.99999999662558 12 | ], 13 | "center": [ 14 | 0, 15 | 5 16 | ], 17 | "format": "png", 18 | "formatter": "function(options, data) { if (options.format === 'full') { return '' + data.NAME + ' (Population: ' + data.POP2005 + ')'; } else { return '' + data.NAME + ''; } }" 19 | } 20 | -------------------------------------------------------------------------------- /schema/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | /** 5 | * SQL Schema 6 | */ 7 | module.exports = { 8 | TABLE: { 9 | metadata: fs.readFileSync(path.join(__dirname, 'TABLE', 'metadata.sql'), 'utf8'), 10 | tiles: fs.readFileSync(path.join(__dirname, 'TABLE', 'tiles.sql'), 'utf8') 11 | }, 12 | INDEX: { 13 | metadata: fs.readFileSync(path.join(__dirname, 'INDEX', 'metadata.sql'), 'utf8'), 14 | tiles: fs.readFileSync(path.join(__dirname, 'INDEX', 'tiles.sql'), 'utf8') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/out/metadata-geocoder_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "type": "baselayer", 4 | "minzoom": 8, 5 | "maxzoom": 8, 6 | "name": "ne-countries", 7 | "bounds": [ 8 | -180, 9 | -85.0511, 10 | 180, 11 | 85.0511 12 | ], 13 | "center": [ 14 | -74, 15 | 40.75, 16 | 8 17 | ], 18 | "template": "{{#__location__}}{{/__location__}}{{#__teaser__}}
Name:   {{name}}\nSearch: {{search}}\nPop'n:  {{population}}\nLon:    {{lon}}\nLat:    {{lat}}\nBounds: {{bounds}}
{{/__teaser__}}{{#__full__}}{{/__full__}}" 19 | } 20 | -------------------------------------------------------------------------------- /test/out/metadata-geocoder_legacy.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "type": "baselayer", 4 | "minzoom": 0, 5 | "maxzoom": 4, 6 | "name": "plain_2", 7 | "bounds": [ 8 | -179.9999999749438, 9 | -69.99999999526695, 10 | 179.9999999749438, 11 | 79.99999999662558 12 | ], 13 | "center": [ 14 | 0, 15 | 5 16 | ], 17 | "format": "png", 18 | "formatter": "function(options, data) { if (options.format === 'full') { return '' + data.NAME + ' (Population: ' + data.POP2005 + ')'; } else { return '' + data.NAME + ''; } }" 19 | } 20 | -------------------------------------------------------------------------------- /test/in/extent.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [ 12 | -80, 13 | 45 14 | ], 15 | [ 16 | -75, 17 | 45 18 | ], 19 | [ 20 | -75, 21 | 50 22 | ], 23 | [ 24 | -80, 25 | 50 26 | ], 27 | [ 28 | -80, 29 | 45 30 | ] 31 | ] 32 | ] 33 | } 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | types.js 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history -------------------------------------------------------------------------------- /test/out/metadata-some-empty-tiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "type": "baselayer", 4 | "minzoom": 1, 5 | "maxzoom": 2, 6 | "name": "US Debt Held By Foreign Nations", 7 | "bounds": [ 8 | -180, 9 | -85.05112877980659, 10 | 180, 11 | 85.05112877980659 12 | ], 13 | "center": [ 14 | 0, 15 | 0 16 | ], 17 | "format": "png", 18 | "legend": "
\n Total US foreign held debt:
\n $4.45 trillion
\n\nSource: The Washington Post, 2011\n\n
", 19 | "template": "{{#__teaser__}}{{Country}}{{/__teaser__}}", 20 | "spec": "1.2" 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Denis Carriere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/universalify.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const universalify = require('universalify') 3 | 4 | function fnPromise (message) { 5 | return new Promise((resolve, reject) => { 6 | return resolve(message) 7 | }) 8 | } 9 | 10 | function fnCallback (message, callback) { 11 | return callback(null, message) 12 | } 13 | 14 | test('universalify -- fromCallback', t => { 15 | const fn = universalify.fromCallback(fnCallback) 16 | fn('bar') 17 | .then(message => t.equal(message, 'bar', 'fromCallback - promise')) 18 | .catch(() => t.error('promise error')) 19 | 20 | fn('bar', (error, message) => { 21 | if (error) t.error('callback error') 22 | t.equal(message, 'bar', 'fromCallback - callback') 23 | }) 24 | t.end() 25 | }) 26 | 27 | test('universalify -- fromPromise', t => { 28 | const fn = universalify.fromPromise(fnPromise) 29 | fn('bar') 30 | .then(message => t.equal(message, 'bar', 'fromPromise - promise')) 31 | .catch(() => t.error('promise error')) 32 | 33 | fn('bar', (error, message) => { 34 | if (error) t.error('callback error') 35 | t.equal(message, 'bar', 'fromPromise - callback') 36 | }) 37 | t.end() 38 | }) 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mbtiles-offline", 3 | "version": "4.0.1", 4 | "description": "MBTiles binding for NodeJS 4+ using Callbacks and/or Promises.", 5 | "main": "index.js", 6 | "types": "index.d.js", 7 | "files": [ 8 | "index.js", 9 | "index.d.ts", 10 | "utils", 11 | "schema" 12 | ], 13 | "scripts": { 14 | "types": "tsc index.d.ts types.ts --lib es6", 15 | "lint": "standard", 16 | "pretest": "npm run types && npm run lint", 17 | "test": "tap test.js utils/utils.test.js --coverage", 18 | "posttest": "tsc types.ts --lib es6 && node types.js", 19 | "docs": "documentation readme index.js --shallow --section=API" 20 | }, 21 | "author": "Denis Carriere <@DenisCarriere>", 22 | "license": "MIT", 23 | "keywords": [ 24 | "mbtiles", 25 | "sqlite3", 26 | "offline" 27 | ], 28 | "dependencies": { 29 | "@mapbox/tiletype": "*", 30 | "@turf/bbox": "*", 31 | "bbox-dateline": "*", 32 | "debug": "*", 33 | "global-mercator": "*", 34 | "lodash.omit": "*", 35 | "sqlite3-offline": "*", 36 | "universalify": "*" 37 | }, 38 | "devDependencies": { 39 | "@types/geojson": "*", 40 | "@types/node": "*", 41 | "coveralls": "*", 42 | "documentation": "*", 43 | "fs-extra": "*", 44 | "load-json-file": "*", 45 | "standard": "*", 46 | "tap": "*", 47 | "tape": "*", 48 | "typescript": "*", 49 | "write-json-file": "*" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | import * as MBTiles from './' 4 | import {Tile, Bounds, MBTilesStatic} from './' 5 | 6 | interface Container { 7 | [name: string]: MBTilesStatic 8 | } 9 | 10 | const image = fs.readFileSync(path.join(__dirname, 'test', 'in', 'images', '0', '0', '0.png')) 11 | const bounds: Bounds = [-110, -40, 95, 50] 12 | const options = { 13 | bounds, 14 | format: 'jpg', 15 | type: 'overlay', 16 | name: 'Foo', 17 | description: 'Bar', 18 | minzoom: 1, 19 | maxzoom: 3, 20 | } 21 | 22 | // Default (TMS/XYZ Schema) 23 | async function main() { 24 | const tile: Tile = [0, 0, 0] 25 | const db = new MBTiles(path.join(__dirname, 'test', 'in', 'plain_1.mbtiles'), 'tms') 26 | // Class properties 27 | db.db 28 | db.uri 29 | // Class methods 30 | await db.metadata() 31 | await db.count() 32 | await db.tables() 33 | await db.findAll() 34 | await db.findOne(tile) 35 | await db.index() 36 | await db.save(tile, image) 37 | await db.update(options) 38 | await db.delete(tile) 39 | const tiles: Tile[] = await db.findAll() 40 | const hashes = await db.hashes() 41 | hashes.size 42 | const container: Container = {} 43 | container['foo'].findOne(tile) 44 | 45 | // Sync functions 46 | db.metadataSync((error, metadata) => {}) 47 | db.findOneSync(tile, (error, image) => {}) 48 | } 49 | 50 | // Quadkey Schema 51 | async function quadkey() { 52 | const tile = '1' 53 | const db = new MBTiles('foo', 'quadkey') 54 | await db.findOne(tile) 55 | await db.save(tile, image) 56 | await db.update(options) 57 | await db.delete(tile) 58 | const tiles: string[] = await db.findAll() 59 | const hashes = await db.hashes() 60 | hashes.size 61 | const container: Container = {} 62 | container['foo'].findOne(tile) 63 | } -------------------------------------------------------------------------------- /utils/utils.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const test = require('tape') 3 | const utils = require('./') 4 | 5 | test('Utils -- getFiles', t => { 6 | t.assert(utils.getFiles(path.join(__dirname, '..', 'test', 'in')), 'getFiles') 7 | t.end() 8 | }) 9 | 10 | // http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/ 11 | test('Utils -- Tile Parser -- Quadkey', t => { 12 | const parser = utils.getTileParser('quadkey') 13 | t.deepEqual(parser.schemaToTile('00'), [0, 3, 2]) 14 | t.deepEqual(parser.tileToSchema([0, 3, 2]), '00') 15 | t.end() 16 | }) 17 | 18 | test('Utils -- Tile Parser -- XYZ', t => { 19 | const parser = utils.getTileParser('xyz') 20 | t.deepEqual(parser.schemaToTile([0, 3, 2]), [0, 0, 2]) 21 | t.deepEqual(parser.tileToSchema([0, 0, 2]), [0, 3, 2]) 22 | t.deepEqual(parser.tileToSchema([15, 9, 4]), [15, 6, 4]) 23 | t.end() 24 | }) 25 | 26 | test('Utils -- Tile Parser -- TMS', t => { 27 | const parser = utils.getTileParser('tms') 28 | t.deepEqual(parser.schemaToTile([0, 3, 2]), [0, 3, 2]) 29 | t.deepEqual(parser.tileToSchema([0, 3, 2]), [0, 3, 2]) 30 | t.end() 31 | }) 32 | 33 | test('Utils -- parseMetadata', t => { 34 | t.deepEqual(utils.parseMetadata([{name: 'center', value: '110,30,800'}]), {center: [110, 30, 800]}, 'center [x,y,z]') 35 | t.deepEqual(utils.parseMetadata([{name: 'center', value: '110,30'}]), {center: [110, 30]}, 'center [x,y]') 36 | t.deepEqual(utils.parseMetadata([{name: 'type', value: 'baselayer'}]), {type: 'baselayer'}, 'type') 37 | t.deepEqual(utils.parseMetadata([{name: 'name', value: 'foo'}]), {name: 'foo'}, 'name') 38 | t.deepEqual(utils.parseMetadata([{name: 'bounds', value: '-110,-20,130,30'}]), {bounds: [-110, -20, 130, 30]}, 'bounds') 39 | t.deepEqual(utils.parseMetadata([{name: 'format', value: 'png'}]), {format: 'png'}, 'format png') 40 | t.deepEqual(utils.parseMetadata([{name: 'format', value: 'jpeg'}]), {format: 'jpeg'}, 'format jpeg') 41 | t.deepEqual(utils.parseMetadata([{name: 'version', value: '1.1.0'}]), {version: '1.1.0'}, 'version') 42 | t.deepEqual(utils.parseMetadata([{name: 'minzoom', value: '10'}]), {minzoom: 10}, 'minzoom') 43 | t.deepEqual(utils.parseMetadata([{name: 'maxzoom', value: '18'}]), {maxzoom: 18}, 'maxzoom') 44 | t.deepEqual(utils.parseBounds([-20, -30, 20, 30]), [-20, -30, 20, 30], 'bounds single') 45 | t.deepEqual(utils.parseBounds([[-20, -30, 20, 30], [-110, -30, 120, 80]]), [-110, -30, 120, 80], 'bounds multiple') 46 | t.deepEqual(utils.parseBounds(require(path.join(__dirname, '..', 'test', 'in', 'extent.json'))), [-80, 45, -75, 50], 'bounds geojson') 47 | t.end() 48 | }) 49 | 50 | test('Utils -- throws', t => { 51 | t.throws(() => utils.getTileParser(''), /schema is required/) 52 | t.throws(() => utils.getTileParser('foo'), 'invalid tile parser') 53 | t.end() 54 | }) 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ## 4.0.0 - 2017-08-28 5 | 6 | - Convert entire library to ES5+ (NodeJS 4 compatible) 7 | - Upgrade `sqlite3-offline` to NodeJS 4 compatible 8 | - Drop classes in favor of ES5 syntax 9 | 10 | ## 3.2.0 - 2017-07-30 11 | 12 | - Added new methods `findOneSync` & `metadataSync` using callbacks 13 | - Improve `Promise.reject` handling 14 | - Update Typescript definition - added `mbtiles.db` & `mbtiles.uri` 15 | - Updated documentation 16 | 17 | ## 3.1.0 - 2017-07-18 18 | 19 | - Dropped automatic conversion of JPG to JPEG (MBTiles spec uses `jpg`) 20 | 21 | ## 3.0.3 - 2017-07-5 22 | 23 | - Update `sqlite3-offline` to support the 3 latest versions of Electron 24 | - Update typescript definintions to easily declare MBTiles in a container/Object/{} 25 | 26 | ## 3.0.0 - 2017-07-1 27 | 28 | - Change tests to Tape/Tap instead of Jest 29 | - Add Schema parameters -- allows the user to enter TMS, Quadkey or XYZ as tile format and returns that format as well. 30 | - **BREAKING CHANGE**: Default Tile Schema is XYZ (before it was TMS) 31 | - **BREAKING CHANGE**: `hashes()` output => `Promise` 32 | 33 | ## 2.9.0 - 2017-06-28 34 | 35 | - Update sqlite-offline to support NodeJS v8 36 | 37 | ## 2.8.0 - 2017-04-04 38 | 39 | - Switch to `debug` instead of `console.log` 40 | 41 | ## 2.7.0 - 2017-3-24 42 | 43 | - Use `jpeg` in metadata instead of `jpg` (bug in GeoServer, cannot be detected as valid MBTiles) 44 | 45 | ## 2.5.0 - 2017-3-16 46 | 47 | - Add `bbox-dateline` to support malformed bbox extents (+/-180 longitudes "Fiji extent") 48 | 49 | ## 2.4.0 - 2017-2-25 50 | 51 | - Test for corrupt MBTiles 52 | - Add more error detection 53 | - Remove undefined, blank & null values from metadata() & update() 54 | 55 | ## 2.3.0 - 2017-2-22 56 | 57 | - Update `bounds` Typescript definition 58 | - Drop `extent` input, now `bounds` can handle BBox|BBox[]|GeoJSON 59 | 60 | ## 2.2.0 - 2017-2-20 61 | 62 | - Allow multiple extent inputs (BBox & BBox[] & GeoJSON) 63 | 64 | ## 2.1.0 - 2017-2-17 65 | 66 | - Update SQLite3-offline dependency 67 | - Add validation method `validate()` 68 | - Auto detect Image format (png/jpg) `format()` 69 | - Auto detect min & max zoom level `getMinZoom()` & `getMaxZoom()` 70 | - Auto detect bounding box `getBounds(zoom)` 71 | 72 | ## 2.0.0 - 2017-2-2 73 | 74 | - Update Typescript definitions 75 | - Add tiles as param for count 76 | - Add tiles index 77 | - Add tiles @param to findAll & hashes 78 | - Improve operations on blank mbtiles 79 | - Add hashes to quickly build an index 80 | - Removed Sequelize for SQLite3 (offline) 81 | - Entire rewrite of methods: 82 | - metadata 83 | - count 84 | - update 85 | - tables 86 | - save 87 | 88 | ## 1.3.0 - 2017-1-25 89 | 90 | - Overhaul module to be Standard JS 91 | - Removed await by `async` 92 | 93 | ## 1.2.0 - 2017-1-9 94 | 95 | - Add options to `findAll` (queue, limit, offset) 96 | 97 | ## 1.1.0 - 2017-1-2 98 | 99 | - Fixed overwrite save function `Cannot read property 'destroy' of null` 100 | - Added `count`, `findOne` & `findAll` functions 101 | 102 | ## 1.0.2 - 2017-1-1 103 | 104 | - Handle metadata via class attributes 105 | - Detect image type using tiletype 106 | 107 | ## 1.0.0 - 2016-12-30 108 | 109 | - Initialize library 110 | -------------------------------------------------------------------------------- /utils/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const sqlite3 = require('sqlite3-offline') 3 | const turfBBox = require('@turf/bbox') 4 | const mercator = require('global-mercator') 5 | const dateline = require('bbox-dateline') 6 | 7 | /** 8 | * Get Tile Parser from schema 9 | * http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/ 10 | * 11 | * @param {string} schema Tile schema 12 | * @return {[Function, Function]} Tile schema parsing functions [Schema to Tile, Tile to Schema] 13 | */ 14 | module.exports.getTileParser = (schema) => { 15 | if (!schema) throw new Error('schema is required') 16 | 17 | switch (schema.toLowerCase()) { 18 | case 'osm': 19 | case 'arcgis': 20 | case 'google': 21 | case 'xyz': 22 | return {schemaToTile: mercator.googleToTile, tileToSchema: mercator.tileToGoogle} 23 | case 'quadkey': 24 | case 'quadtree': 25 | return {schemaToTile: mercator.quadkeyToTile, tileToSchema: mercator.tileToQuadkey} 26 | case 'tms': 27 | return {schemaToTile: (tile) => tile, tileToSchema: (tile) => tile} 28 | default: 29 | throw new Error(schema + ' unknown Tile schema') 30 | } 31 | } 32 | 33 | /** 34 | * Get all files that match regex 35 | * 36 | * @param {string} path 37 | * @param {string} regex 38 | * @returns {string[]} matching files 39 | * getFiles('/home/myfiles') 40 | * //=['map', 'test'] 41 | */ 42 | module.exports.getFiles = (path, regex) => { 43 | regex = regex || /\.mbtiles$/ 44 | var mbtiles = fs.readdirSync(path).filter(value => value.match(regex)) 45 | mbtiles = mbtiles.map(data => data.replace(regex, '')) 46 | mbtiles = mbtiles.filter(name => !name.match(/^_.*/)) 47 | return mbtiles 48 | } 49 | 50 | /** 51 | * Connect to SQL MBTiles DB 52 | * 53 | * @param {string} uri 54 | * @returns {Sqlite3} Sqlite3 connection 55 | */ 56 | module.exports.connect = (uri) => { 57 | return new sqlite3.Database(uri) 58 | } 59 | 60 | /** 61 | * Parse Metadata 62 | * @param {ParseMetadata[]} data 63 | * @returns Metadata 64 | */ 65 | module.exports.parseMetadata = (data) => { 66 | const metadata = {} 67 | if (data) { 68 | data.forEach(item => { 69 | const name = item.name.toLowerCase() 70 | const value = item.value 71 | 72 | if (value === null || value === undefined || value === '') return 73 | 74 | switch (name) { 75 | case 'minzoom': 76 | case 'maxzoom': 77 | metadata[name] = Number(value) 78 | break 79 | case 'name': 80 | case 'attribution': 81 | case 'description': 82 | metadata[name] = value 83 | break 84 | case 'bounds': 85 | const bounds = value.split(',').map(i => Number(i)) 86 | metadata.bounds = [bounds[0], bounds[1], bounds[2], bounds[3]] 87 | break 88 | case 'center': 89 | const center = value.split(',').map(i => Number(i)) 90 | switch (center.length) { 91 | case 2: 92 | metadata.center = [center[0], center[1]] 93 | break 94 | case 3: 95 | metadata.center = [center[0], center[1], center[2]] 96 | } 97 | break 98 | case 'type': 99 | switch (value) { 100 | case 'overlay': 101 | case 'baselayer': 102 | metadata[name] = value 103 | } 104 | break 105 | case 'format': 106 | switch (value.toLowerCase()) { 107 | case 'png': 108 | metadata[name] = value 109 | break 110 | case 'jpeg': 111 | case 'jpg': 112 | metadata[name] = 'jpeg' 113 | } 114 | break 115 | case 'version': 116 | switch (value) { 117 | case '1.0.0': 118 | case '1.1.0': 119 | case '1.2.0': 120 | metadata[name] = value 121 | } 122 | break 123 | default: 124 | metadata[name] = value 125 | } 126 | }) 127 | } 128 | return metadata 129 | } 130 | 131 | /** 132 | * Parse Bounds - Retrieves maximum extent 133 | * 134 | * @param {BBox|BBox[]|GeoJSON.FeatureCollection|GeoJSON.Feature} extent 135 | * @returns BBox Maximum extent 136 | */ 137 | module.exports.parseBounds = (extent) => { 138 | // GeoJSON.FeatureCollection 139 | if (extent.type === 'FeatureCollection' || extent.type === 'Feature') { 140 | return dateline.bbox(turfBBox(extent)) 141 | } 142 | return dateline.bbox(mercator.maxBBox(extent)) 143 | } 144 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {Database} from 'sqlite3-offline' 2 | 3 | type Tile = MBTiles.Tile 4 | type Metadata = MBTiles.Metadata 5 | type UpdateMetadata = MBTiles.UpdateMetadata 6 | type Bounds = MBTiles.Bounds 7 | type BBox = MBTiles.BBox 8 | type Center = MBTiles.Center 9 | type Formats = 'jpg' | 'png' | 'pbf' | 'webp' 10 | type Types = 'overlay' | 'baselayer' 11 | type Schema = 'xyz' | 'tms' | 'quadkey' 12 | 13 | interface SchemaType { 14 | xyz: Tile 15 | tms: Tile 16 | quadkey: string 17 | } 18 | 19 | declare class MBTilesClass { 20 | // Metadata properties 21 | type: string 22 | version: string 23 | name?: string 24 | minzoom?: number 25 | maxzoom?: number 26 | format?: string 27 | bounds?: BBox 28 | center?: Center 29 | description?: string 30 | attribution?: string 31 | url?: string 32 | 33 | // Extra properties 34 | db?: Database 35 | uri?: string 36 | errors?: any[] 37 | ok?: boolean 38 | schema?: string 39 | 40 | constructor(uri: string, schema?: Schema) 41 | 42 | /** 43 | * Save buffer data to individual Tile 44 | */ 45 | save(tile: T, image: Buffer): Promise 46 | 47 | /** 48 | * Retrieves Metadata from MBTiles 49 | */ 50 | metadata(): Promise 51 | 52 | /** 53 | * Sync: Retrieves Metadata from MBTiles 54 | */ 55 | metadataSync(callback: (error: Error, metadata: Metadata) => void): void 56 | 57 | /** 58 | * Delete individual Tile 59 | */ 60 | delete(tile: T): Promise 61 | 62 | /** 63 | * Count the amount of Tiles 64 | */ 65 | count(tiles?: T[]): Promise 66 | 67 | 68 | /** 69 | * Update Metadata 70 | */ 71 | update(metadata: UpdateMetadata): Promise 72 | 73 | /** 74 | * Finds all Tile unique hashes 75 | */ 76 | findAll(tiles?: T[]): Promise 77 | 78 | /** 79 | * Finds one Tile and returns Buffer 80 | */ 81 | findOne(tile: T): Promise 82 | 83 | /** 84 | * Sync: Finds one Tile and returns Buffer 85 | */ 86 | findOneSync(tile: T, callback: (error: Error, tile: Buffer) => void): void 87 | 88 | /** 89 | * Build SQL tables 90 | */ 91 | tables(): Promise 92 | 93 | /** 94 | * Build SQL index 95 | */ 96 | index(): Promise 97 | 98 | /** 99 | * Creates hash from a single Tile 100 | */ 101 | hash(tile: T): number 102 | 103 | /** 104 | * Creates a hash table for all tiles 105 | */ 106 | hashes(tiles?: T[]): Promise 107 | 108 | /** 109 | * Retrieves Minimum Zoom level 110 | */ 111 | getMinZoom(): Promise 112 | 113 | /** 114 | * Retrieves Maximum Zoom level 115 | */ 116 | getMaxZoom(): Promise 117 | 118 | /** 119 | * Retrieves Image Format 120 | */ 121 | getFormat(): Promise 122 | 123 | /** 124 | * Retrieves Bounds 125 | */ 126 | getBounds(zoom?: number): Promise 127 | 128 | /** 129 | * Validate MBTiles according to the specifications 130 | */ 131 | validate(): Promise 132 | } 133 | 134 | declare namespace MBTiles { 135 | export type Center = [number, number] 136 | export type Tile = [number, number, number] 137 | export type BBox = [number, number, number, number] 138 | export type Formats = 'png' | 'jpeg' | 'jpeg' | 'pbf' 139 | export type Types = 'baselayer' | 'overlay' 140 | export type Versions = '1.0.0' | '1.1.0' | '1.2.0' 141 | export type Polygon = GeoJSON.Feature 142 | export type Polygons = GeoJSON.FeatureCollection 143 | export type MultiPolygon = GeoJSON.Feature 144 | export type MultiPolygons = GeoJSON.FeatureCollection 145 | export type Bounds = BBox | BBox[] | Polygon | Polygons | MultiPolygon | MultiPolygons 146 | export type Hashes = Set 147 | export type MBTilesStatic = MBTilesClass 148 | export type MBTilesTile = MBTilesClass 149 | export type MBTilesQuadkey = MBTilesClass 150 | 151 | export interface Metadata extends BaseMetadata { 152 | bounds?: BBox 153 | } 154 | 155 | export interface UpdateMetadata extends BaseMetadata { 156 | bounds?: Bounds 157 | } 158 | 159 | export interface BaseMetadata { 160 | name: string 161 | description: string 162 | minzoom?: number 163 | maxzoom?: number 164 | format?: string 165 | type?: string 166 | version?: string 167 | center?: Center 168 | attribution?: string 169 | url?: string 170 | } 171 | } 172 | 173 | interface MBTiles { 174 | new (uri: string, schema: 'quadkey'): MBTilesClass; 175 | new (uri: string, schema?: 'xyz' | 'tms'): MBTilesClass; 176 | } 177 | 178 | declare const MBTiles: MBTiles; 179 | export = MBTiles 180 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const load = require('load-json-file') 4 | const test = require('tape') 5 | const write = require('write-json-file') 6 | const copySync = require('fs-extra').copySync 7 | const MBTiles = require('./') 8 | 9 | const options = { 10 | name: 'Foo', 11 | description: 'Bar', 12 | minzoom: 1, 13 | maxzoom: 3, 14 | format: 'png', 15 | center: [-7.5, 5], 16 | bounds: [-110, -40, 95, 50] 17 | } 18 | 19 | const baseMetadata = { 20 | type: 'baselayer', 21 | version: '1.1.0' 22 | } 23 | 24 | const metadata = Object.assign({}, baseMetadata, options) 25 | 26 | const metadataPlain1 = { 27 | bounds: [ -179.9999999749438, -69.99999999526695, 179.9999999749438, 84.99999999782301 ], 28 | center: [ 0, 7.5 ], 29 | description: 'demo description', 30 | format: 'png', 31 | json: '{"level1":{"level2":"property"},"version":"2.0.0"}', 32 | maxzoom: 4, 33 | minzoom: 0, 34 | name: 'plain_1', 35 | type: 'baselayer', 36 | version: '1.1.0' 37 | } 38 | 39 | const directories = { 40 | out: path.join(__dirname, 'test', 'out') + path.sep, 41 | in: path.join(__dirname, 'test', 'in') + path.sep 42 | } 43 | 44 | const image = fs.readFileSync(path.join(directories.in, 'images', '0', '0', '0.png')) 45 | const fixtures = fs.readdirSync(directories.in).filter(filename => filename.match(/\.mbtiles/)) 46 | 47 | test('MBTiles -- plain_1', t => { 48 | const db = new MBTiles(directories.in + 'plain_1.mbtiles') 49 | 50 | Promise.all([ 51 | db.metadata().then(metadata => t.deepEqual(metadata, metadataPlain1, 'metadata')), 52 | db.tables().then(status => t.assert(status, 'tables')), 53 | db.count().then(count => t.equal(count, 285, 'count')), 54 | db.findAll().then(tiles => t.equal(tiles.length, 285, 'findAll')), 55 | db.findOne([0, 0, 0]).then(image => t.equal(image.byteLength, 7072, 'findOne')), 56 | db.findOne([15, 9, 4]).then(image => t.equal(image.byteLength, 1167, 'findOne - resolves correctly')), 57 | db.hashes().then(hashes => t.equal(hashes.size, 285, 'hashes')) 58 | ]).then(() => { 59 | t.equal(db.hash([0, 0, 0]), 1, 'hash') 60 | t.end() 61 | }) 62 | }) 63 | 64 | test('MBTiles -- save', t => { 65 | copySync(directories.in + 'save.mbtiles', directories.out + 'save.mbtiles') 66 | const db = new MBTiles(directories.out + 'save.mbtiles') 67 | 68 | Promise.all([ 69 | db.index().then(status => t.true(status, 'index')), 70 | db.save([0, 0, 0], image).then(status => t.true(status, 'save - [0, 0, 0]')), 71 | db.save([1, 1, 1], image).then(status => t.true(status, 'save - [1, 1, 1]')), 72 | db.delete([1, 1, 1]).then(status => t.true(status, 'delete - [1, 1, 1]')), 73 | db.update(options).then(updatedMetadata => t.deepEqual(updatedMetadata, metadata, 'update')) 74 | ]).then(() => { 75 | t.end() 76 | }) 77 | }) 78 | 79 | test('MBTiles -- delete quadkey', t => { 80 | const db = new MBTiles(directories.out + 'delete.mbtiles', 'quadkey') 81 | Promise.all([ 82 | db.save('031', Buffer.from([0, 1])), 83 | db.count().then(count => t.equal(count, 1, 'save count')), 84 | db.delete('031'), 85 | db.count().then(count => t.equal(count, 0, 'delete count')) 86 | ]).then(() => { 87 | t.end() 88 | }) 89 | }) 90 | 91 | test('MBTiles -- hashes', t => { 92 | const db1 = new MBTiles(directories.out + 'hashes-quadkey.mbtiles', 'quadkey') 93 | const db2 = new MBTiles(directories.out + 'hashes-tms.mbtiles', 'tms') 94 | const db3 = new MBTiles(directories.out + 'hashes-xyz.mbtiles', 'xyz') 95 | 96 | Promise.all([ 97 | db1.save('021', Buffer.from([0, 1])), 98 | db2.save([1, 5, 3], Buffer.from([0, 1])), 99 | db3.save([1, 2, 3], Buffer.from([0, 1])) 100 | ]).then(() => { 101 | db1.hashes().then(hashes1 => { 102 | db2.hashes().then(hashes2 => { 103 | db3.hashes().then(hashes3 => { 104 | t.true(hashes1.has(hashes2.values().next().value), 'hashes1 contains hashes2') 105 | t.true(hashes2.has(hashes3.values().next().value), 'hashes2 contains hashes2') 106 | }) 107 | }) 108 | }) 109 | }) 110 | 111 | t.end() 112 | }) 113 | 114 | test('MBTiles -- blank', t => { 115 | const db = new MBTiles(directories.in + 'blank.mbtiles') 116 | 117 | Promise.all([ 118 | db.metadata().then(metadata => t.deepEqual(metadata, baseMetadata, 'metadata')), 119 | db.count().then(count => t.equal(count, 0, 'count')), 120 | db.tables().then(status => t.true(status, 'tables')), 121 | db.findAll().then(tiles => t.equal(tiles.length, 0, 'findAll')), 122 | db.findOne([0, 0, 0]).then(image => t.not(image, 'findOne')), 123 | db.findOne([10, 0, 0]).then(image => t.not(image, 'findOne')), 124 | db.hashes().then(hashes => t.equal(hashes.size, 0, 'hashes')) 125 | ]) 126 | .then(() => { 127 | t.equal(db.hash([0, 0, 0]), 1, 'hash') 128 | t.end() 129 | }) 130 | }) 131 | 132 | test('MBTiles -- metadata', t => { 133 | for (const filename of fixtures) { 134 | const name = path.parse(filename).name 135 | const db = new MBTiles(directories.in + filename) 136 | 137 | db.metadata().then(metadata => { 138 | const output = path.join(directories.out, `metadata-${name}.json`) 139 | if (process.env.REGEN) write.sync(output, metadata) 140 | t.deepEqual(metadata, load.sync(output), filename) 141 | }).catch(error => console.error(error)) 142 | } 143 | t.end() 144 | }) 145 | 146 | test('MBTiles -- jpg metadata (not JPEG)', t => { 147 | const db = new MBTiles(directories.out + 'format-jpg.mbtiles') 148 | db.update({format: 'jpg'}).then(metadata => t.equal(metadata.format, 'jpg')) 149 | t.end() 150 | }) 151 | 152 | test('MBTiles -- findOneSync', t => { 153 | const db = new MBTiles(directories.in + 'plain_1.mbtiles') 154 | db.findOneSync([1, 1, 1], (error, image) => { 155 | if (error) t.fail(error) 156 | t.equal(image.byteLength, 2450) 157 | }) 158 | t.end() 159 | }) 160 | 161 | test('MBTiles -- metadata', t => { 162 | const db = new MBTiles(directories.in + 'plain_1.mbtiles') 163 | db.metadata((error, metadata) => { 164 | if (error) t.fail(error) 165 | t.deepEqual(metadata, metadataPlain1) 166 | }) 167 | t.end() 168 | }) 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MBTiles Offline 2 | 3 | [![Build Status](https://travis-ci.org/DenisCarriere/mbtiles-offline.svg?branch=master)](https://travis-ci.org/DenisCarriere/mbtiles-offline) 4 | [![Coverage Status](https://coveralls.io/repos/github/DenisCarriere/mbtiles-offline/badge.svg?branch=master)](https://coveralls.io/github/DenisCarriere/mbtiles-offline?branch=master) 5 | [![npm version](https://badge.fury.io/js/mbtiles-offline.svg)](https://badge.fury.io/js/mbtiles-offline) 6 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/master/LICENSE) 7 | 8 | 9 | 10 | [![Standard - JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 11 | 12 | > MBTiles binding for NodeJS 4+ using Callbacks and/or Promises. 13 | 14 | ## Install 15 | 16 | ```bash 17 | $ npm install --save mbtiles-offline 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```javascript 23 | const MBTiles = require('mbtiles-offline') 24 | const db = new MBTiles('example.mbtiles') 25 | 26 | db.metadata() 27 | //= Promise{ JSON } 28 | 29 | db.save([1, 2, 3], Buffer([0, 1])) 30 | db.save([2, 2, 3], Buffer([3, 4])) 31 | 32 | db.findOne([1, 2, 3]) 33 | //= Promise { } 34 | 35 | db.findAll() 36 | //= Promise{ [[1, 2, 3], [2, 2, 3]] } 37 | 38 | db.count() 39 | //= Promise { 2 } 40 | 41 | db.delete([1, 2, 3]) 42 | db.delete([2, 2, 3]) 43 | 44 | db.count() 45 | //= Promise { 0 } 46 | ``` 47 | 48 | ## Features 49 | 50 | | Name | Description | 51 | | ------------------------- | :----------------------------------------------- | 52 | | [metadata](#metadata) | Retrieve Metadata from MBTiles | 53 | | [update](#update) | Update Metadata | 54 | | [save](#save) | Save buffer data to individual Tile | 55 | | [delete](#delete) | Delete individual Tile | 56 | | [count](#count) | Count total tiles | 57 | | [findOne](#findone) | Finds one Tile and returns buffer | 58 | | [findAll](#findall) | Finds all Tiles | 59 | | [tables](#tables) | Build SQL Tables | 60 | | [index](#index) | Build SQL Index | 61 | | [getMinZoom](#getminzoom) | Retrieves Minimum Zoom level | 62 | | [getMaxZoom](#getmaxzoom) | Retrieves Maximum Zoom level | 63 | | [getFormat](#getformat) | Retrieves Image Format | 64 | | [getBounds](#getbounds) | Retrieves Bounds | 65 | | [validate](#getformat) | Validate MBTiles according to the specifications | 66 | | [hash](#hash) | Creates hash from a single Tile | 67 | | [hashes](#getformat) | Creates a hash table for all tiles | 68 | 69 | ## NodeJS Support 70 | 71 | Windows, MacOSX, Linux & Electron 72 | 73 | - Node.js v8 74 | - Node.js v7 75 | - Node.js v6 76 | - Node.js v5 77 | - Node.js v4 78 | 79 | ## Schemas 80 | 81 | ### XYZ 82 | 83 | Slippy Map is the most commonly used Tile schema for service maps as tiles, providers such as Google/ArcGIS & OpenStreetMap use this schema. 84 | 85 | ```javascript 86 | const tile1 = [1, 2, 3] 87 | const tile2 = [2, 2, 3] 88 | const tile3 = [1, 3, 3] 89 | const tile4 = [2, 3, 3] 90 | const img = Buffer([0, 1]) 91 | const db = new MBTiles('xyz.mbtiles', 'xyz') 92 | 93 | db.save(tile1, img) 94 | db.save(tile1, img) 95 | db.findOne(tile1) 96 | //= Promise { } 97 | db.findAll() 98 | //= Promise{ [[1, 2, 3], [2, 2, 3]] } 99 | ``` 100 | 101 | ### TMS 102 | 103 | Tile Map Service is an OGC protocol for serving maps as tiles. MBTiles uses TMS as it's internal tile storage schema (TMS has an inverse Y compared to the XYZ schema). 104 | 105 | ```javascript 106 | const tile1 = [1, 5, 3] 107 | const tile2 = [2, 5, 3] 108 | const tile3 = [1, 4, 3] 109 | const tile4 = [2, 4, 3] 110 | const img = Buffer([0, 1]) 111 | const db = new MBTiles('tms.mbtiles', 'tms') 112 | 113 | db.save(tile1, img) 114 | db.save(tile2, img) 115 | db.findOne(tile1) 116 | //= Promise { } 117 | db.findAll() 118 | //= Promise{ [[1, 5, 3], [2, 5, 3]] } 119 | ``` 120 | 121 | ### Quadkey 122 | 123 | [Bing Map Tile System](https://msdn.microsoft.com/en-us/library/bb259689.aspx), a quadkey is defined as a string which represent a Tile [x,y,z]. 124 | 125 | ```javascript 126 | const tile1 = '021' 127 | const tile2 = '030' 128 | const tile3 = '023' 129 | const tile4 = '032' 130 | const img = Buffer([0, 1]) 131 | const db = new MBTiles('quadkey.mbtiles', 'quadkey') 132 | 133 | db.save(tile1, img) 134 | db.save(tile2, img) 135 | db.findOne(tile1) 136 | //= Promise { } 137 | db.findAll() 138 | //= Promise { ['021', '030'] } 139 | ``` 140 | 141 | ## API 142 | 143 | 144 | 145 | ### index 146 | 147 | MBTiles 148 | 149 | #### constructor 150 | 151 | MBTiles 152 | 153 | **Parameters** 154 | 155 | - `uri` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Path to MBTiles 156 | - `schema` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Tile schema ('xyz', 'tms', 'quadkey') (optional, default `'xyz'`) 157 | 158 | **Examples** 159 | 160 | ```javascript 161 | const db = new MBTiles('example.mbtiles') 162 | //= mbtiles 163 | ``` 164 | 165 | Returns **MBTiles** MBTiles 166 | 167 | #### save 168 | 169 | Save buffer data to individual Tile 170 | 171 | **Parameters** 172 | 173 | - `tile` **Tile** Tile [x, y, z] 174 | - `image` **[Buffer](https://nodejs.org/api/buffer.html)** Tile image 175 | 176 | **Examples** 177 | 178 | ```javascript 179 | db.save([x, y, z], buffer).then(status => { 180 | //= status 181 | }).catch(error => { 182 | //= error 183 | }) 184 | ``` 185 | 186 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** true/false 187 | 188 | #### metadata 189 | 190 | Retrieves Metadata from MBTiles 191 | 192 | **Examples** 193 | 194 | ```javascript 195 | db.metadata().then(metadata => { 196 | //= metadata 197 | }).catch(error => { 198 | //= error 199 | }) 200 | ``` 201 | 202 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<Metadata>** Metadata as an Object 203 | 204 | #### metadataSync 205 | 206 | Sync: Retrieves Metadata from MBTiles 207 | 208 | **Parameters** 209 | 210 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** a method that takes (error: {Error}, metadata: {Object}) 211 | 212 | **Examples** 213 | 214 | ```javascript 215 | db.metadata((error, metadata) => { 216 | //= error 217 | //= metadata 218 | }) 219 | ``` 220 | 221 | Returns **void** 222 | 223 | #### delete 224 | 225 | Delete individual Tile 226 | 227 | **Parameters** 228 | 229 | - `tile` **Tile** Tile [x, y, z] 230 | 231 | **Examples** 232 | 233 | ```javascript 234 | db.delete([x, y, z]).then(status => { 235 | //= status 236 | }).catch(error => { 237 | //= error 238 | }) 239 | ``` 240 | 241 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** true/false 242 | 243 | #### getMinZoom 244 | 245 | Retrieves Minimum Zoom level 246 | 247 | **Examples** 248 | 249 | ```javascript 250 | db.getMinZoom().then(minZoom => { 251 | //= minZoom 252 | }).catch(error => { 253 | //= error 254 | }) 255 | ``` 256 | 257 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>** 258 | 259 | #### getMaxZoom 260 | 261 | Retrieves Maximum Zoom level 262 | 263 | **Examples** 264 | 265 | ```javascript 266 | db.getMaxZoom().then(maxZoom => { 267 | //= maxZoom 268 | }).catch(error => { 269 | //= error 270 | }) 271 | ``` 272 | 273 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>** 274 | 275 | #### getFormat 276 | 277 | Retrieves Image Format 278 | 279 | **Examples** 280 | 281 | ```javascript 282 | db.getFormat().then(format => { 283 | //= format 284 | }).catch(error => { 285 | //= error 286 | }) 287 | ``` 288 | 289 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<Formats>** 290 | 291 | #### getBounds 292 | 293 | Retrieves Bounds 294 | 295 | **Parameters** 296 | 297 | - `zoom` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Zoom level 298 | 299 | **Examples** 300 | 301 | ```javascript 302 | db.getBounds().then(bbox => { 303 | //= bbox 304 | }).catch(error => { 305 | //= error 306 | }) 307 | ``` 308 | 309 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<BBox>** 310 | 311 | #### count 312 | 313 | Count the amount of Tiles 314 | 315 | **Parameters** 316 | 317 | - `tiles` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<Tile>?** Only find given tiles 318 | 319 | **Examples** 320 | 321 | ```javascript 322 | db.count().then(count => { 323 | //= count 324 | }).catch(error => { 325 | //= error 326 | }) 327 | ``` 328 | 329 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>** 330 | 331 | #### update 332 | 333 | Update Metadata 334 | 335 | **Parameters** 336 | 337 | - `metadata` **Metadata** Metadata according to MBTiles spec 1.1.0 (optional, default `{}`) 338 | - `metadata.attribution` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Attribution 339 | - `metadata.bounds` **BBox** BBox [west, south, east, north] or Polygon GeoJSON 340 | - `metadata.center` **Center** Center [lng, lat] or [lng, lat, height] 341 | - `metadata.description` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Description 342 | - `metadata.format` **Formats** Format 'png' | 'jpg' | 'webp' | 'pbf' 343 | - `metadata.minzoom` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Minimum zoom level 344 | - `metadata.maxzoom` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Maximum zoom level 345 | - `metadata.name` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Name 346 | - `metadata.url` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** URL source or tile scheme 347 | - `metadata.type` **Types** Type 'baselayer' | 'overlay' (optional, default `'baselayer'`) 348 | - `metadata.version` **Versions** Version '1.0.0' | '1.1.0' | '1.2.0' (optional, default `'1.1.0'`) 349 | 350 | **Examples** 351 | 352 | ```javascript 353 | const options = { 354 | name: 'Foo', 355 | description: 'Bar', 356 | minzoom: 1, 357 | maxzoom: 3, 358 | format: 'png', 359 | bounds: [-110, -40, 95, 50] 360 | } 361 | db.update(options).then(metadata => { 362 | //= metadata 363 | }).catch(error => { 364 | //= error 365 | }) 366 | ``` 367 | 368 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<Metadata>** Metadata 369 | 370 | #### validate 371 | 372 | Validate MBTiles according to the specifications 373 | 374 | **Examples** 375 | 376 | ```javascript 377 | db.validate().then(status => { 378 | //= status 379 | }).catch(error => { 380 | //= error 381 | }) 382 | ``` 383 | 384 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** true/false 385 | 386 | #### findAll 387 | 388 | Finds all Tile unique hashes 389 | 390 | **Parameters** 391 | 392 | - `tiles` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<Tile>?** Only find given tiles (optional, default `[]`) 393 | 394 | **Examples** 395 | 396 | ```javascript 397 | const tile1 = [33, 40, 6] 398 | const tile2 = [20, 50, 7] 399 | db.findAll([tile1, tile2]).then(tiles => { 400 | //= tiles 401 | }).catch(error => { 402 | //= error 403 | }) 404 | ``` 405 | 406 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<Tile>>** An array of Tiles [x, y, z] 407 | 408 | #### findOneSync 409 | 410 | Sync: Finds one Tile and returns Buffer 411 | 412 | **Parameters** 413 | 414 | - `tile` **Tile** Tile [x, y, z] 415 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** a method that takes (image: {Buffer}) 416 | 417 | **Examples** 418 | 419 | ```javascript 420 | db.findOneSync([x, y, z], (error, image) => { 421 | //= error 422 | //= image 423 | }) 424 | ``` 425 | 426 | Returns **void** 427 | 428 | #### findOne 429 | 430 | Finds one Tile and returns Buffer 431 | 432 | **Parameters** 433 | 434 | - `tile` **Tile** Tile [x, y, z] 435 | 436 | **Examples** 437 | 438 | ```javascript 439 | db.findOne([x, y, z]).then(image => { 440 | //= image 441 | }).catch(error => { 442 | //= error 443 | }) 444 | ``` 445 | 446 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Buffer](https://nodejs.org/api/buffer.html)>** Tile Data 447 | 448 | #### tables 449 | 450 | Build SQL tables 451 | 452 | **Examples** 453 | 454 | ```javascript 455 | db.tables().then(status => { 456 | //= status 457 | }).catch(error => { 458 | //= error 459 | }) 460 | ``` 461 | 462 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** true/false 463 | 464 | #### index 465 | 466 | Build SQL index 467 | 468 | **Examples** 469 | 470 | ```javascript 471 | db.index().then(status => { 472 | //= status 473 | }).catch(error => { 474 | //= error 475 | }) 476 | ``` 477 | 478 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** true/false 479 | 480 | #### hash 481 | 482 | Creates hash from a single Tile 483 | 484 | **Parameters** 485 | 486 | - `tile` **Tile** 487 | 488 | **Examples** 489 | 490 | ```javascript 491 | const hash = db.hash([5, 25, 12]) 492 | //= 16797721 493 | ``` 494 | 495 | Returns **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** hash 496 | 497 | #### hashes 498 | 499 | Creates a hash table for all tiles 500 | 501 | **Parameters** 502 | 503 | - `tiles` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<Tile>?** Only find given tiles 504 | 505 | **Examples** 506 | 507 | ```javascript 508 | await db.save([0, 0, 3], Buffer([0, 1])) 509 | await db.save([0, 1, 3], Buffer([2, 3])) 510 | db.hashes() 511 | //= Promise { Set { 64, 65 } } 512 | ``` 513 | 514 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)<[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>>** hashes 515 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const mercator = require('global-mercator') 2 | const tiletype = require('@mapbox/tiletype') 3 | const omit = require('lodash.omit') 4 | const utils = require('./utils') 5 | const schema = require('./schema') 6 | const warning = require('debug')('mbtiles-offline:warning') 7 | 8 | // Globals 9 | const EXCLUDE = ['_table', '_index', 'db', 'schema', 'uri', 'ok', 'errors', 'schemaToTile', 'tileToSchema'] 10 | 11 | /** 12 | * MBTiles 13 | * 14 | * @param {string} uri Path to MBTiles 15 | * @param {string} [schema='xyz'] Tile schema ('xyz', 'tms', 'quadkey') 16 | * @returns {MBTiles} MBTiles 17 | * @example 18 | * const db = new MBTiles('example.mbtiles') 19 | * //= mbtiles 20 | */ 21 | function MBTiles (uri, schema) { 22 | this.db = utils.connect(uri) 23 | this.uri = uri 24 | this.version = '1.1.0' 25 | this.type = 'baselayer' 26 | this.errors = [] 27 | this.ok = true 28 | this.schema = schema || 'xyz' 29 | const parser = utils.getTileParser(this.schema) 30 | this.schemaToTile = parser.schemaToTile 31 | this.tileToSchema = parser.tileToSchema 32 | } 33 | 34 | /** 35 | * Save buffer data to individual Tile 36 | * 37 | * @param {Tile} tile Tile [x, y, z] 38 | * @param {Buffer} image Tile image 39 | * @returns {Promise} true/false 40 | * @example 41 | * db.save([x, y, z], buffer).then(status => { 42 | * //= status 43 | * }).catch(error => { 44 | * //= error 45 | * }) 46 | */ 47 | MBTiles.prototype.save = function (tile, image) { 48 | tile = this.schemaToTile(tile) 49 | const x = tile[0] 50 | const y = tile[1] 51 | const z = tile[2] 52 | return new Promise((resolve, reject) => { 53 | this.tables().then(() => { 54 | const query = 'INSERT INTO tiles (tile_column, tile_row, zoom_level, tile_data) VALUES (?, ?, ?, ?)' 55 | this.db.run(query, [x, y, z, image], error => { 56 | if (error) { 57 | warning(error) 58 | this.errors.push(error) 59 | this.ok = false 60 | return resolve(false) 61 | } 62 | return resolve(true) 63 | }) 64 | }) 65 | }) 66 | } 67 | 68 | /** 69 | * Retrieves Metadata from MBTiles 70 | * 71 | * @returns {Promise} Metadata as an Object 72 | * @example 73 | * db.metadata().then(metadata => { 74 | * //= metadata 75 | * }).catch(error => { 76 | * //= error 77 | * }) 78 | */ 79 | MBTiles.prototype.metadata = function () { 80 | return new Promise((resolve, reject) => { 81 | this.metadataSync((error, metadata) => { 82 | if (error) return reject(error) 83 | return resolve(metadata) 84 | }) 85 | }) 86 | } 87 | 88 | /** 89 | * Sync: Retrieves Metadata from MBTiles 90 | * 91 | * @param {Function} callback a method that takes (error: {Error}, metadata: {Object}) 92 | * @returns {void} 93 | * @example 94 | * db.metadata((error, metadata) => { 95 | * //= error 96 | * //= metadata 97 | * }) 98 | */ 99 | MBTiles.prototype.metadataSync = function (callback) { 100 | this.db.serialize(() => { 101 | this.db.run(schema.TABLE.metadata, error => { 102 | if (error) { 103 | warning(error) 104 | this.errors.push(error) 105 | this.ok = false 106 | return callback(error, {}) 107 | } 108 | }) 109 | this.db.all('SELECT * FROM metadata', (error, rows) => { 110 | if (error) { 111 | warning(error) 112 | this.errors.push(error) 113 | this.ok = false 114 | return callback(error, {}) 115 | } 116 | 117 | const metadata = utils.parseMetadata(rows) 118 | this.minzoom = metadata.minzoom 119 | this.maxzoom = metadata.maxzoom 120 | this.attribution = metadata.attribution || this.attribution 121 | this.description = metadata.description || this.description 122 | this.name = metadata.name || this.name 123 | this.type = metadata.type || this.type 124 | this.version = metadata.version || this.version 125 | this.url = metadata.url || this.url 126 | const bounds = metadata.bounds || this.bounds 127 | if (bounds) { 128 | const bbox = utils.parseBounds(bounds) 129 | this.bounds = bbox 130 | this.center = mercator.bboxToCenter(bbox) 131 | } 132 | 133 | this.getFormat().then(format => { 134 | this.getMinZoom().then(minZoom => { 135 | this.getMaxZoom().then(maxZoom => { 136 | this.getBounds().then(bounds => { 137 | const json = omit(Object.assign(this, metadata), EXCLUDE) 138 | const results = JSON.parse(JSON.stringify(json)) 139 | return callback(error, results) 140 | }) 141 | }) 142 | }) 143 | }) 144 | }) 145 | }) 146 | } 147 | 148 | /** 149 | * Delete individual Tile 150 | * 151 | * @param {Tile} tile Tile [x, y, z] 152 | * @returns {Promise} true/false 153 | * @example 154 | * db.delete([x, y, z]).then(status => { 155 | * //= status 156 | * }).catch(error => { 157 | * //= error 158 | * }) 159 | */ 160 | MBTiles.prototype.delete = function (tile) { 161 | tile = this.schemaToTile(tile) 162 | return new Promise((resolve, reject) => { 163 | this.tables().then(() => { 164 | const query = 'DELETE FROM tiles WHERE tile_column=? AND tile_row=? AND zoom_level=?' 165 | this.db.run(query, tile, error => { 166 | if (error) { 167 | warning(error) 168 | this.errors.push(error) 169 | this.ok = false 170 | resolve(false) 171 | } 172 | return resolve(true) 173 | }) 174 | }) 175 | }) 176 | } 177 | 178 | /** 179 | * Retrieves Minimum Zoom level 180 | * 181 | * @returns {Promise} 182 | * @example 183 | * db.getMinZoom().then(minZoom => { 184 | * //= minZoom 185 | * }).catch(error => { 186 | * //= error 187 | * }) 188 | */ 189 | MBTiles.prototype.getMinZoom = function () { 190 | return new Promise((resolve, reject) => { 191 | if (this.minzoom !== undefined) { return resolve(this.minzoom) } 192 | this.tables().then(() => { 193 | this.db.get('SELECT MIN(zoom_level) FROM tiles', (error, row) => { 194 | if (error) { 195 | warning(error) 196 | this.errors.push(error) 197 | this.ok = false 198 | return resolve(undefined) 199 | } 200 | if (row === undefined || row === null) return resolve(undefined) 201 | 202 | const minzoom = row['MIN(zoom_level)'] 203 | if (minzoom === undefined || minzoom === null) return resolve(undefined) 204 | this.minzoom = minzoom 205 | return resolve(minzoom) 206 | }) 207 | }) 208 | }) 209 | } 210 | 211 | /** 212 | * Retrieves Maximum Zoom level 213 | * 214 | * @returns {Promise} 215 | * @example 216 | * db.getMaxZoom().then(maxZoom => { 217 | * //= maxZoom 218 | * }).catch(error => { 219 | * //= error 220 | * }) 221 | */ 222 | MBTiles.prototype.getMaxZoom = function () { 223 | return new Promise((resolve, reject) => { 224 | if (this.db === undefined) return resolve(undefined) 225 | if (this.maxzoom !== undefined) return resolve(this.maxzoom) 226 | this.tables().then(() => { 227 | this.db.get('SELECT MAX(zoom_level) FROM tiles', (error, row) => { 228 | if (error) { 229 | warning(error) 230 | this.errors.push(error) 231 | this.ok = false 232 | return resolve(undefined) 233 | } 234 | if (row === undefined || row === null) return resolve(undefined) 235 | 236 | const maxzoom = row['MAX(zoom_level)'] 237 | if (maxzoom === null || maxzoom === undefined) return resolve(undefined) 238 | this.maxzoom = maxzoom 239 | return resolve(maxzoom) 240 | }) 241 | }) 242 | }) 243 | } 244 | 245 | /** 246 | * Retrieves Image Format 247 | * 248 | * @returns {Promise} 249 | * @example 250 | * db.getFormat().then(format => { 251 | * //= format 252 | * }).catch(error => { 253 | * //= error 254 | * }) 255 | */ 256 | MBTiles.prototype.getFormat = function () { 257 | return new Promise((resolve, reject) => { 258 | if (this.format !== undefined) return resolve(this.format) 259 | this.tables().then(() => { 260 | this.db.get('SELECT tile_data FROM tiles LIMIT 1', (error, row) => { 261 | if (error) { 262 | warning(error) 263 | this.errors.push(error) 264 | this.ok = false 265 | return resolve(undefined) 266 | } 267 | if (row === undefined || row === null) return resolve(undefined) 268 | 269 | var format = tiletype.type(row['tile_data']) 270 | if (format === undefined || format === null) return resolve(undefined) 271 | this.format = format 272 | return resolve(format) 273 | }) 274 | }) 275 | }) 276 | } 277 | 278 | /** 279 | * Retrieves Bounds 280 | * 281 | * @param {number} zoom Zoom level 282 | * @returns {Promise} 283 | * @example 284 | * db.getBounds().then(bbox => { 285 | * //= bbox 286 | * }).catch(error => { 287 | * //= error 288 | * }) 289 | */ 290 | MBTiles.prototype.getBounds = function (zoom) { 291 | return new Promise((resolve, reject) => { 292 | if (this.bounds !== undefined) return resolve(this.bounds) 293 | this.tables().then(() => { 294 | this.getMaxZoom().then(maxzoom => { 295 | this.getMinZoom().then(minzoom => { 296 | // Validate zoom input based on Min & Max zoom levels 297 | var zoomLevel = (zoom === undefined) ? maxzoom : zoom 298 | if (zoom > maxzoom) zoomLevel = maxzoom 299 | if (zoom < minzoom) zoomLevel = minzoom 300 | 301 | const query = 'SELECT MIN(tile_column), MIN(tile_row), MAX(tile_column), MAX(tile_row) FROM tiles WHERE zoom_level=?' 302 | this.db.get(query, zoomLevel, (error, row) => { 303 | if (error) { 304 | warning(error) 305 | this.errors.push(error) 306 | this.ok = false 307 | return resolve(undefined) 308 | } 309 | if (row === undefined || row === null) return resolve(undefined) 310 | if (zoomLevel === undefined || zoomLevel === null) return resolve(undefined) 311 | 312 | // Retrieve BBox from southwest & northeast tiles 313 | const southwest = mercator.tileToBBox([row['MIN(tile_column)'], row['MIN(tile_row)'], zoomLevel]) 314 | const northeast = mercator.tileToBBox([row['MAX(tile_column)'], row['MAX(tile_row)'], zoomLevel]) 315 | const west = southwest[0] 316 | const south = southwest[1] 317 | const east = northeast[2] 318 | const north = northeast[3] 319 | 320 | const bbox = [west, south, east, north] 321 | this.bounds = bbox 322 | this.center = mercator.bboxToCenter(bbox) 323 | return resolve(bbox) 324 | }) 325 | }) 326 | }) 327 | }) 328 | }) 329 | } 330 | 331 | /** 332 | * Count the amount of Tiles 333 | * 334 | * @param {Tile[]} [tiles] Only find given tiles 335 | * @returns {Promise} 336 | * @example 337 | * db.count().then(count => { 338 | * //= count 339 | * }).catch(error => { 340 | * //= error 341 | * }) 342 | */ 343 | MBTiles.prototype.count = function (tiles) { 344 | // Tile schema will be converted by findAll 345 | return new Promise(resolve => { 346 | this.findAll(tiles).then(existingTiles => { 347 | return resolve(existingTiles.length) 348 | }) 349 | }) 350 | } 351 | 352 | /** 353 | * Update Metadata 354 | * 355 | * @param {Metadata} [metadata={}] Metadata according to MBTiles spec 1.1.0 356 | * @param {string} metadata.attribution Attribution 357 | * @param {BBox} metadata.bounds BBox [west, south, east, north] or Polygon GeoJSON 358 | * @param {Center} metadata.center Center [lng, lat] or [lng, lat, height] 359 | * @param {string} metadata.description Description 360 | * @param {Formats} metadata.format Format 'png' | 'jpg' | 'webp' | 'pbf' 361 | * @param {number} metadata.minzoom Minimum zoom level 362 | * @param {number} metadata.maxzoom Maximum zoom level 363 | * @param {string} metadata.name Name 364 | * @param {string} metadata.url URL source or tile scheme 365 | * @param {Types} [metadata.type='baselayer'] Type 'baselayer' | 'overlay' 366 | * @param {Versions} [metadata.version='1.1.0'] Version '1.0.0' | '1.1.0' | '1.2.0' 367 | * @returns {Promise} Metadata 368 | * @example 369 | * const options = { 370 | * name: 'Foo', 371 | * description: 'Bar', 372 | * minzoom: 1, 373 | * maxzoom: 3, 374 | * format: 'png', 375 | * bounds: [-110, -40, 95, 50] 376 | * } 377 | * db.update(options).then(metadata => { 378 | * //= metadata 379 | * }).catch(error => { 380 | * //= error 381 | * }) 382 | */ 383 | MBTiles.prototype.update = function (metadata) { 384 | metadata = metadata || {} 385 | return new Promise((resolve, reject) => { 386 | this.metadata().then(currentMetadata => { 387 | this.db.serialize(() => { 388 | this.db.run(schema.TABLE.metadata, error => { 389 | if (error) { 390 | warning(error) 391 | this.errors.push(error) 392 | this.ok = false 393 | return resolve(undefined) 394 | } 395 | }) 396 | this.db.run('DELETE FROM metadata', error => { 397 | if (error) { 398 | warning(error) 399 | this.errors.push(error) 400 | this.ok = false 401 | return resolve(undefined) 402 | } 403 | }) 404 | if (metadata.bounds) metadata.bounds = utils.parseBounds(metadata.bounds) 405 | const results = Object.assign(currentMetadata, metadata) 406 | 407 | // Load Metadata to database 408 | const stmt = this.db.prepare('INSERT INTO metadata VALUES (?, ?)') 409 | Object.keys(results).forEach(name => { 410 | const value = results[name] 411 | 412 | // String or Number 413 | var query 414 | if (typeof value !== 'object') query = [name, String(value)] 415 | // Array 416 | else if (value.length) query = [name, value.join(',')] 417 | // JSON 418 | else query = [name, JSON.stringify(value)] 419 | 420 | stmt.run(query, error => { 421 | if (error) { 422 | warning(error) 423 | this.errors.push(error) 424 | this.ok = false 425 | return resolve(undefined) 426 | } 427 | }) 428 | }) 429 | stmt.finalize(error => { 430 | if (error) { 431 | warning(error) 432 | this.errors.push(error) 433 | this.ok = false 434 | return resolve(undefined) 435 | } 436 | return resolve(results) 437 | }) 438 | }) 439 | }) 440 | }) 441 | } 442 | 443 | /** 444 | * Validate MBTiles according to the specifications 445 | * 446 | * @returns {Promise} true/false 447 | * @example 448 | * db.validate().then(status => { 449 | * //= status 450 | * }).catch(error => { 451 | * //= error 452 | * }) 453 | */ 454 | MBTiles.prototype.validate = function () { 455 | return new Promise((resolve, reject) => { 456 | this.metadata().then(metadata => { 457 | if (this.errors.length) throw new Error(this.errors) 458 | 459 | // MBTiles spec 1.0.0 460 | if (metadata.name === undefined) throw new Error('Metadata is required') 461 | if (metadata.format === undefined) throw new Error('Metadata is required') 462 | if (metadata.version === undefined) throw new Error('Metadata is required') 463 | if (metadata.type === undefined) throw new Error('Metadata is required') 464 | 465 | // MBTiles spec 1.1.0 466 | if (metadata.version === '1.1.0') { 467 | if (metadata.bounds === undefined) throw new Error('Metadata is required') 468 | } 469 | }) 470 | return resolve(true) 471 | }) 472 | } 473 | 474 | /** 475 | * Finds all Tile unique hashes 476 | * 477 | * @param {Tile[]} [tiles] Only find given tiles 478 | * @returns {Promise} An array of Tiles [x, y, z] 479 | * @example 480 | * const tile1 = [33, 40, 6] 481 | * const tile2 = [20, 50, 7] 482 | * db.findAll([tile1, tile2]).then(tiles => { 483 | * //= tiles 484 | * }).catch(error => { 485 | * //= error 486 | * }) 487 | */ 488 | MBTiles.prototype.findAll = function (tiles) { 489 | tiles = tiles || [] 490 | tiles = tiles.map(tile => this.schemaToTile(tile)) 491 | return new Promise(resolve => { 492 | this.tables().then(() => { 493 | // Search all - Not optimized for memory leaks 494 | if (tiles.length === 0) { 495 | const query = 'SELECT tile_column, tile_row, zoom_level FROM tiles' 496 | this.db.all(query, (error, rows) => { 497 | if (error) { 498 | warning(error) 499 | this.errors.push(error) 500 | this.ok = false 501 | return resolve(undefined) 502 | } 503 | const tiles = rows.map(row => this.tileToSchema([row.tile_column, row.tile_row, row.zoom_level])) 504 | return resolve(tiles) 505 | }) 506 | } 507 | 508 | // Searh by defined set of tiles 509 | const levels = {} 510 | tiles.forEach(tile => { 511 | const x = tile[0] 512 | const y = tile[1] 513 | const z = tile[2] 514 | if (levels[z] === undefined) { 515 | levels[z] = { 516 | west: x, 517 | south: y, 518 | east: x, 519 | north: y, 520 | tiles: [], 521 | index: {} 522 | } 523 | } 524 | if (x < levels[z].west) { levels[z].west = x } 525 | if (x > levels[z].east) { levels[z].east = x } 526 | if (y < levels[z].south) { levels[z].south = y } 527 | if (y > levels[z].north) { levels[z].north = y } 528 | levels[z].tiles.push(tile) 529 | levels[z].index[mercator.hash(tile)] = true 530 | }) 531 | 532 | // Execute SQL queries 533 | const results = [] 534 | this.db.serialize(() => { 535 | const stmt = this.db.prepare(` 536 | SELECT tile_column, tile_row, zoom_level 537 | FROM tiles 538 | WHERE zoom_level=? 539 | AND tile_column>=? 540 | AND tile_column<=? 541 | AND tile_row>=? 542 | AND tile_row<=?`) 543 | Object.keys(levels).forEach(zoom => { 544 | const level = levels[zoom] 545 | const west = level.west 546 | const south = level.south 547 | const east = level.east 548 | const north = level.north 549 | const index = level.index 550 | 551 | stmt.all([zoom, west, east, south, north], (error, rows) => { 552 | if (error) { 553 | warning(error) 554 | this.errors.push(error) 555 | this.ok = false 556 | return resolve(undefined) 557 | } 558 | 559 | // Remove any extra tiles 560 | rows.forEach(row => { 561 | const tile = [row.tile_column, row.tile_row, row.zoom_level] 562 | const hash = mercator.hash(tile) 563 | if (index[hash]) results.push(this.tileToSchema(tile)) 564 | }) 565 | }) 566 | }) 567 | stmt.finalize(() => { 568 | return resolve(results) 569 | }) 570 | }) 571 | }) 572 | }) 573 | } 574 | 575 | /** 576 | * Sync: Finds one Tile and returns Buffer 577 | * 578 | * @param {Tile} tile Tile [x, y, z] 579 | * @param {Function} callback a method that takes (image: {Buffer}) 580 | * @return {void} 581 | * @example 582 | * db.findOneSync([x, y, z], (error, image) => { 583 | * //= error 584 | * //= image 585 | * }) 586 | */ 587 | MBTiles.prototype.findOneSync = function (tile, callback) { 588 | tile = this.schemaToTile(tile) 589 | const query = 'SELECT tile_data FROM tiles WHERE tile_column=? AND tile_row=? AND zoom_level=?' 590 | this.db.get(query, tile, (error, row) => { 591 | if (error) { 592 | warning(error) 593 | this.errors.push(error) 594 | this.ok = false 595 | callback(error, null) 596 | } else if (row) { 597 | callback(error, row.tile_data) 598 | } else { 599 | warning(' not found') 600 | callback(error, null) 601 | } 602 | }) 603 | } 604 | 605 | /** 606 | * Finds one Tile and returns Buffer 607 | * 608 | * @param {Tile} tile Tile [x, y, z] 609 | * @return {Promise} Tile Data 610 | * @example 611 | * db.findOne([x, y, z]).then(image => { 612 | * //= image 613 | * }).catch(error => { 614 | * //= error 615 | * }) 616 | */ 617 | MBTiles.prototype.findOne = function (tile) { 618 | return new Promise((resolve, reject) => { 619 | this.findOneSync(tile, (error, image) => { 620 | if (error) return reject(error) 621 | return resolve(image) 622 | }) 623 | }) 624 | } 625 | 626 | /** 627 | * Build SQL tables 628 | * 629 | * @returns {Promise} true/false 630 | * @example 631 | * db.tables().then(status => { 632 | * //= status 633 | * }).catch(error => { 634 | * //= error 635 | * }) 636 | */ 637 | MBTiles.prototype.tables = function () { 638 | return new Promise((resolve, reject) => { 639 | if (this._table) return resolve(true) 640 | this.db.serialize(() => { 641 | this.db.run(schema.TABLE.metadata, error => { 642 | if (error) { 643 | warning(error) 644 | this.errors.push(error) 645 | this.ok = false 646 | return reject(error) 647 | } 648 | }) 649 | this.db.run(schema.TABLE.tiles, error => { 650 | if (error) { 651 | warning(error) 652 | this.errors.push(error) 653 | this.ok = false 654 | return reject(error) 655 | } 656 | this._table = true 657 | return resolve(true) 658 | }) 659 | }) 660 | }) 661 | } 662 | 663 | /** 664 | * Build SQL index 665 | * 666 | * @returns {Promise} true/false 667 | * @example 668 | * db.index().then(status => { 669 | * //= status 670 | * }).catch(error => { 671 | * //= error 672 | * }) 673 | */ 674 | MBTiles.prototype.index = function () { 675 | return new Promise((resolve, reject) => { 676 | if (this._index) { return resolve(true) } 677 | this.tables().then(() => { 678 | this.db.serialize(() => { 679 | this.db.run(schema.INDEX.tiles, error => { 680 | if (error) { 681 | warning(error) 682 | this.errors.push(error) 683 | this.ok = false 684 | return reject(error) 685 | } 686 | }) 687 | this.db.run(schema.INDEX.metadata, error => { 688 | if (error) { 689 | warning(error) 690 | this.errors.push(error) 691 | this.ok = false 692 | return reject(new Error('index')) 693 | } 694 | this._index = true 695 | return resolve(true) 696 | }) 697 | }) 698 | }) 699 | }) 700 | } 701 | 702 | /** 703 | * Creates hash from a single Tile 704 | * 705 | * @param {Tile} tile 706 | * @return {number} hash 707 | * @example 708 | * const hash = db.hash([5, 25, 12]) 709 | * //= 16797721 710 | */ 711 | MBTiles.prototype.hash = function (tile) { 712 | tile = this.schemaToTile(tile) 713 | return mercator.hash(tile) 714 | } 715 | 716 | /** 717 | * Creates a hash table for all tiles 718 | * 719 | * @param {Tile[]} [tiles] Only find given tiles 720 | * @return {Promise>} hashes 721 | * @example 722 | * await db.save([0, 0, 3], Buffer([0, 1])) 723 | * await db.save([0, 1, 3], Buffer([2, 3])) 724 | * db.hashes() 725 | * //= Promise { Set { 64, 65 } } 726 | */ 727 | MBTiles.prototype.hashes = function (tiles) { 728 | return new Promise((resolve, reject) => { 729 | this.findAll(tiles).then(existingTiles => { 730 | const index = new Set() 731 | existingTiles.forEach(tile => index.add(this.hash(tile))) 732 | if (Object.keys(index).length === 0) warning(' is empty') 733 | return resolve(index) 734 | }) 735 | }) 736 | } 737 | 738 | module.exports = MBTiles 739 | --------------------------------------------------------------------------------