├── 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": "",
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 | [](https://travis-ci.org/DenisCarriere/mbtiles-offline)
4 | [](https://coveralls.io/github/DenisCarriere/mbtiles-offline?branch=master)
5 | [](https://badge.fury.io/js/mbtiles-offline)
6 | [](https://raw.githubusercontent.com/DenisCarriere/mbtiles-offline/master/LICENSE)
7 |
8 |
9 |
10 | [](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 |
--------------------------------------------------------------------------------