diff --git a/CHANGELOG.md b/CHANGELOG.md index 1252e26..8ffb0ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.3.0 + +- `step`, `underscore`, `optimist` dependencies removed. +- Tests upgraded to use `mocha`. +- Commands removed. + # 0.2.7 - `_metadata` now returns null if the metadata table or the requested field is diff --git a/README.md b/README.md index f08da04..8b53f7a 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,5 @@ Utilities and [tilelive][1] integration for the [MBTiles][2] format. npm install mbtiles -### Commandline tools - -Several commandline tools are included in `bin` for manipulating existing -MBTiles files. - - mbcheck [FILE] - Check an mbtiles for missing metadata and tiles. - - mbcompact [FILE] - Eliminate duplicate images to reduce mbtiles filesize. - - mbpipe [COMMAND] [FILE] - Pipe each tile to stdin of the specified command and - write stdout into the mbtiles file. - - mbrekey [FILE] - Rekey a compacted mbtiles to save space. - - [1]: https://github.com/mapbox/tilelive.js [2]: http://mbtiles.org diff --git a/bin/mbcheck b/bin/mbcheck deleted file mode 100755 index f8d5966..0000000 --- a/bin/mbcheck +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env node - -var options = argv = require('optimist').argv, - _ = require('underscore'), - Step = require('step'), - utils = require('..').utils, - MBTiles = require('..'), - mbtiles, - zooms = []; - -// Show help. -if (argv.help || argv._.length === 0) { - console.warn('Usage: mbcheck [FILE]'); - console.warn(' Check an mbtiles for missing metadata and tiles.'); - console.warn('Examples:'); - console.warn(' mbcheck world.mbtiles'); - process.exit(); -} - -// Wishlist: -// --------- -// - check that tiles exist for specified bounds - -Step( - function() { mbtiles = new MBTiles(argv._[0], this); }, - function() { mbtiles._integrity(this); }, - function(err) { - if (err) throw err; - mbtiles._exists('tiles', function(err, exists) { - if (!exists) throw new Error('`tiles` table does not exist.'); - this(); - }.bind(this)); - }, - function(err) { - if (err) throw err; - mbtiles._exists('metadata', function(err, exists) { - if (!exists) throw new Error('`metadata` table does not exist.'); - this(); - }.bind(this)); - }, - function(err) { - if (err) throw err; - mbtiles._metadata('name', this.parallel()); - mbtiles._metadata('type', this.parallel()); - mbtiles._metadata('desciption', this.parallel()); - mbtiles._metadata('version', this.parallel()); - mbtiles._metadata('format', this.parallel()); - mbtiles._metadata('bounds', this.parallel()); - }, - function(err, name, type, description, version, format, bounds) { - console.warn('Metadata'); - utils.table([ - ['name', name], - ['type', type], - ['description', description], - ['version', version], - ['format', format], - ['bounds', bounds] - ]); - this(); - }, - function(err) { - if (err) throw err; - Step( - function() { - var group = this.group(), - query = mbtiles._db.prepare( - 'SELECT zoom_level AS zoom, ' + - 'COUNT(zoom_level) AS count ' + - 'FROM tiles ' + - 'WHERE zoom_level = ?' - ); - for (var i = 0; i < 30; i++) { - query.get(i, group()); - } - query.finalize(); - }, - function(err, rows) { - if (err) throw err; - zooms = rows; - do { - var last = zooms.pop(); - if (last.count) zooms.push(last); - } while(!last.count); - do { - var first = zooms.shift(); - if (first.count) zooms.unshift(first); - } while(!first.count); - this(); - }.bind(this) - ); - }, - function(err) { - if (err) throw err; - Step( - function() { - var group = this.group(), - query = mbtiles._db.prepare( - 'SELECT MAX(tile_column) AS maxx, ' + - 'MIN(tile_column) AS minx, ' + - 'MAX(tile_row) AS maxy, ' + - 'MIN(tile_row) AS miny, ' + - 'zoom_level AS zoom ' + - 'FROM tiles ' + - 'WHERE zoom_level = ?' - ); - for (var i = 0; i < zooms.length; i++) { - query.get(zooms[i].zoom, group()); - } - query.finalize(); - }, - function(err, rows) { - if (err) throw err; - - var group = this.group(), - query = mbtiles._db.prepare( - 'SELECT zoom_level AS zoom, ' + - 'COUNT(zoom_level) AS count, ' + - '? AS expected ' + - 'FROM tiles ' + - 'WHERE zoom_level = ? AND ' + - 'tile_column >= ? AND ' + - 'tile_column <= ? AND ' + - 'tile_row >= ? AND ' + - 'tile_row <= ?' - ); - for (var i = 0; i < rows.length; i++) { - var expected = (rows[i].maxx - rows[i].minx + 1) * (rows[i].maxy - rows[i].miny + 1); - query.get( - expected, - rows[i].zoom, - rows[i].minx, - rows[i].maxx, - rows[i].miny, - rows[i].maxy, - group() - ); - } - query.finalize(); - }, - function(err, rows) { - if (err) throw err; - console.warn(''); - var output = []; - output.push(['Z', 'Tiles', 'Missing']); - _(rows).each(function(row, index) { - output.push([ - row.zoom, - zooms[index].count, - row.expected - row.count - ]); - }); - utils.table(output); - this(); - }.bind(this) - ); - } -); diff --git a/bin/mbcompact b/bin/mbcompact deleted file mode 100755 index 0bd8f9f..0000000 --- a/bin/mbcompact +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node - -var options = argv = require('optimist').argv, - _ = require('underscore'), - Step = require('step'), - crypto = require('crypto'), - fs = require('fs'), - sys = require('sys'), - spawn = require('child_process').spawn, - MBTiles = require('..'), - utils = require('..').utils, - mbtiles, - hits = 0, - ids = []; - -// Show help. -if (argv.help || argv._.length < 1) { - console.warn('Usage: mbcompact [FILE]'); - console.warn(' Eliminate duplicate images to reduce mbtiles filesize.'); - console.warn('Examples:'); - console.warn(' mbcompact world.mbtiles'); - process.exit(); -} - -// Grab args. -var filename = argv._[0]; -Step( - function() { mbtiles = new MBTiles(filename, this); }, - function(err) { - if (err) throw err; - mbtiles._db.all('SELECT name, type ' - + 'FROM sqlite_master ' - + 'WHERE type IN (?, ?)', - 'table', - 'view', - function(err, rows) { - if (err) throw err; - if (_(rows).any(function(row) { return row.name === 'tiles' && row.type === 'view' })) { - throw new Error('Table is already compacted.'); - } - if (!_(rows).any(function(row) { return row.name === 'tiles' && row.type === 'table' })) { - throw new Error('Tiles table does not exist.'); - } - this(); - }.bind(this) - ); - }, - function(err) { - if (err) throw err; - mbtiles._setup(this); - }, - function(err) { - if (err) throw err; - mbtiles._db.get('SELECT COUNT(*) AS total FROM tiles', this.parallel()); - mbtiles._db.run('PRAGMA locking_mode=EXCLUSIVE', this.parallel()); - mbtiles._db.run('PRAGMA journal_mode=TRUNCATE', this.parallel()); - }, - function(err, row) { - if (err) throw err; - if (!row.total) throw new Error('No tiles found'); - - var total = row.total; - var printed = 0; - var done = this; - var doit = function(limit, offset) { - process.nextTick(function() { - mbtiles._db - .prepare('SELECT tile_data AS tile_data, zoom_level AS z, tile_column AS x, tile_row AS y FROM tiles LIMIT ? OFFSET ?') - .all(limit, offset, function(err, rows) { - var images = []; - var map = []; - for (var i = 0; i < rows.length; i++) { - var tile_id = crypto - .createHash('md5') - .update(rows[i].tile_data) - .digest('hex'); - if (!_(ids).include(tile_id)) { - ids.unshift(tile_id); - images.push({ - tile_id: tile_id, - tile_data: rows[i].tile_data - }); - } else { - hits++; - } - map.push({ - tile_id: tile_id, - zoom_level: rows[i].z, - tile_column: rows[i].x, - tile_row: rows[i].y - }); - } - Step( - function() { - mbtiles._insert('images', images, this.parallel()); - mbtiles._insert('map', map, this.parallel()); - }, - function(err) { - if (err) throw err; - - // If IDs has grown over threshold, trim back down. - (ids.length > 1000) && (ids = ids.slice(0, 1000 - 900)); - - var progress = Math.floor(offset / total * 40); - if (progress > printed) { - sys.print((new Array(progress - printed + 1)).join('#')); - printed = progress; - } - if (rows.length === limit) { - doit(limit, offset + limit); - } else { - done(); - } - } - ); - }) - .finalize(); - }); - }; - console.warn('00 -------------- 50 -------------- 100'); - sys.print(''); - doit(1000, 0); - }, - function(err) { - if (err) throw err; - mbtiles._db.run('DROP TABLE tiles', this); - }, - function(err) { - if (err) throw err; - mbtiles._db.run('CREATE VIEW IF NOT EXISTS tiles AS ' - + 'SELECT map.zoom_level AS zoom_level, ' - + 'map.tile_column AS tile_column, ' - + 'map.tile_row AS tile_row, ' - + 'images.tile_data AS tile_data ' - + 'FROM map JOIN images ON images.tile_id = map.tile_id;', this.parallel()); - mbtiles._db.run('VACUUM', this.parallel()); - }, - function(err) { - if (err) throw err; - sys.print('\n'); - console.warn('Compact hits %s.', hits); - console.warn('Compaction complete.'); - } -); - diff --git a/bin/mbpipe b/bin/mbpipe deleted file mode 100755 index f06d8b2..0000000 --- a/bin/mbpipe +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node - -var options = argv = require('optimist').argv, - _ = require('underscore'), - Step = require('step'), - sys = require('sys'), - spawn = require('child_process').spawn, - MBTiles = require('..'), - utils = require('..').utils, - mbtiles, - table, - total; - -// @TODO: support specifying a table to be processed, e.g. for processing -// grids or grid data. - -// Show help. -if (argv.help || argv._.length < 2) { - console.warn('Usage: mbpipe [COMMAND] [FILE]'); - console.warn(' Pipe each tile to stdin of the specified command and'); - console.warn(' write stdout into the mbtiles file.'); - console.warn('Examples:'); - console.warn(' mbpipe "pngquant 32" world.mbtiles'); - process.exit(); -} - -// Grab args. -var filename = argv._[1], - command = { - command: argv._[0].split(' ')[0], - args: argv._[0].split(' ').slice(1) - }; - -Step( - function() { mbtiles = new MBTiles(filename, this); }, - function(err) { - if (err) throw err; - Step( - function() { mbtiles._db.run('PRAGMA synchronous=0', this); }, - function() { mbtiles._db.run('PRAGMA locking_mode=EXCLUSIVE', this); }, - function() { mbtiles._db.run('PRAGMA journal_mode=TRUNCATE', this); }, - this - ); - }, - function(err) { - if (err) throw err; - mbtiles._exists('tiles', function(err, exists) { - if (exists) table = 'tiles'; - this(); - }.bind(this)); - }, - function(err) { - if (err) throw err; - mbtiles._exists('images', function(err, exists) { - if (exists) table = 'images'; - this(); - }.bind(this)); - }, - function(err) { - if (err) throw err; - if (!table) throw new Error('No usable image table found.'); - mbtiles._db.get('SELECT COUNT(tile_data) AS total FROM ' + table, function(err, row) { - total = row.total; - this(); - }.bind(this)); - }, - function(err) { - if (err) throw err; - if (!total) throw new Error('No tiles found'); - - var printed = 0; - var done = this; - var doit = function(table, limit, offset) { - process.nextTick(function() { - var query = table === 'images' - ? mbtiles._db.prepare('SELECT tile_data AS tile_data, tile_id AS id FROM images ORDER BY tile_id ASC LIMIT ? OFFSET ?') - : mbtiles._db.prepare('SELECT tile_data AS tile_data, zoom_level AS z, tile_column AS x, tile_row AS y FROM tiles ORDER BY z, x, y LIMIT ? OFFSET ?'); - query.all(limit, offset, function(err, rows) { - Step( - function() { - var group = this.group(); - var exec = function(row, callback) { - row.piped = new Buffer(0); - var child = spawn(command.command, command.args); - var stream = function(chunk) { - var joined = (new Buffer(row.piped.length + chunk.length)); - row.piped.copy(joined, 0, 0); - chunk.copy(joined, row.piped.length, 0); - row.piped = joined; - }; - child.stdin.write(row.tile_data); - child.stdin.end(); - child.stdout.on('data', stream); - child.stderr.on('data', stream); - child.on('exit', function(code) { - if (code) return callback(null, null); - if (table === 'images') { - mbtiles._db.run( - 'INSERT OR REPLACE INTO images (tile_id, tile_data) VALUES(?, ?)', - row.id, - row.piped, - callback - ); - } else { - mbtiles._db.run( - 'INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES(?, ?, ?, ?)', - row.z, - row.x, - row.y, - row.piped, - callback - ); - } - }); - }; - for (var i = 0; i < rows.length; i++) { - exec(rows[i], group()); - } - }, - function(err) { - var progress = Math.floor(offset / total * 40); - if (progress > printed) { - sys.print((new Array(progress - printed + 1)).join('#')); - printed = progress; - } - if (rows.length === limit) { - doit(table, limit, offset + limit); - } else { - mbtiles._db.run('VACUUM', function() { - sys.print('\n'); - console.warn('Pipe complete.'); - done(); - }); - } - } - ); - }); - query.finalize(); - }); - }; - console.warn('00 -------------- 50 -------------- 100'); - doit(table, 255, 0); - } -); - diff --git a/bin/mbrekey b/bin/mbrekey deleted file mode 100755 index 6e5ee3f..0000000 --- a/bin/mbrekey +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env node - -var options = argv = require('optimist').argv; -var _ = require('underscore'); -var sqlite3 = require('sqlite3'); -var utils = require('..').utils; - -// Show help. -if (argv.help || argv._.length < 1) { - console.warn('Usage: mbrekey [FILE]'); - console.warn(' Rekey a compacted mbtiles to save space.'); - console.warn('Examples:'); - console.warn(' mbrekey world.mbtiles'); - process.exit(); -} - -// Grab args. -var filename = argv._[0]; - -var db = new sqlite3.Database(filename); - -db.exec("PRAGMA journal_mode = OFF; PRAGMA locking_mode = EXCLUSIVE; PRAGMA cache_size = 400000; PRAGMA synchronous = OFF; PRAGMA temp_store = MEMORY;"); - -var tables = {}; -db.all("SELECT tbl_name FROM sqlite_master WHERE TYPE = 'table'", function(err, map) { - if (err) throw err; - map.forEach(function(row) { - tables[row.tbl_name] = true; - }); -}); - -var tileQueue = new utils.Queue(changeTileID, 1); -var gridQueue = new utils.Queue(changeGridID, 1); - -db.run('CREATE INDEX IF NOT EXISTS "temp_tile_id_idx" ON "map" ("tile_id")', function(err) { - if (err) throw err; - console.warn('Created temporary index.'); - db.run('CREATE TEMP TABLE "tile_hash_id" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "hash" TEXT UNIQUE NOT NULL)', function(err) { - if (err) throw err; - db.run('REPLACE INTO tile_hash_id (hash) SELECT tile_id FROM images', function(err) { - if (err) throw err; - db.get('SELECT MAX(id) AS max FROM tile_hash_id', function(err, max) { - if (err) throw err; - if (!max.max) return tileQueue.emit('empty'); - max = max.max; - console.warn('Starting tile update... (%d total)', max); - - for (var i = 1; i < max; i += 10000) { - tileQueue.add(i); - } - }); - }); - }); -}) - -tileQueue.on('empty', function() { - if (!tables.grid_utfgrid) { - deleteTempKey(); - console.warn("MBtiles file doesn't have interactivity"); - return; - } - - db.run('CREATE TEMP TABLE "grid_hash_id" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "hash" TEXT UNIQUE NOT NULL)', function(err) { - if (err) throw err; - db.run('REPLACE INTO grid_hash_id (hash) SELECT grid_id FROM grid_utfgrid', function(err) { - if (err) throw err; - db.get('SELECT MAX(id) AS max FROM grid_hash_id', function(err, max) { - if (err) throw err; - if (!max.max) return gridQueue.emit('empty'); - max = max.max; - console.warn('Starting grid update... (%d total)', max); - for (var i = 1; i < max; i += 10000) { - gridQueue.add(i); - } - }); - }); - }); -}); - -gridQueue.on('empty', deleteTempKey); - -var changedTiles = 0; - -function changeTileID(start, done) { - db.all('SELECT id, hash FROM tile_hash_id WHERE id >= ? AND id < ?', start, start + 10000, function(err, rows) { - if (err) throw err; - db.serialize(function() { - db.run('BEGIN'); - var st1 = db.prepare('UPDATE images SET tile_id = ? WHERE tile_id = ?'); - var st2 = db.prepare('UPDATE map SET tile_id = ? WHERE tile_id = ?'); - for (var i = 0; i < rows.length; i++) { - st1.run(rows[i].id, rows[i].hash); - st2.run(rows[i].id, rows[i].hash); - } - db.run('COMMIT', function(err) { - if (err) throw err; - st1.finalize(function(err) { - st2.finalize(function(err) { - changedTiles += rows.length; - console.warn('Updated %d tiles.', changedTiles); - done(); - })}); - }); - }); - }); -}; - - -var changedGrids = 0; -function changeGridID(start, done) { - db.all('SELECT id, hash FROM grid_hash_id WHERE id >= ? AND id < ?', start, start + 10000, function(err, rows) { - if (err) throw err; - db.serialize(function() { - db.run('BEGIN'); - rows.forEach(function(row) { - db.run('UPDATE grid_key SET grid_id = ? WHERE grid_id = ?', row.id, row.hash); - db.run('UPDATE grid_utfgrid SET grid_id = ? WHERE grid_id = ?', row.id, row.hash); - db.run('UPDATE map SET grid_id = ? WHERE grid_id = ?', row.id, row.hash); - }); - db.run('COMMIT', function(err) { - if (err) throw err; - changedGrids += rows.length; - console.warn('Updated %d grids.', changedGrids); - done(); - }); - }); - }); -}; - -function deleteTempKey() { - db.run('DROP INDEX "temp_tile_id_idx"', function(err) { - if (err) throw err; - console.warn('Vacuuming...'); - db.run('VACUUM;', function(err) { - if (err) throw err; - db.close(); - }); - }); -} diff --git a/lib/mbtiles.js b/lib/mbtiles.js index 701d904..43c9f62 100644 --- a/lib/mbtiles.js +++ b/lib/mbtiles.js @@ -1,16 +1,12 @@ -var _ = require('underscore'), - fs = require('fs'), - Step = require('step'), - crypto = require('crypto'), - zlib = require('zlib'), - path = require('path'), - url = require('url'), - qs = require('querystring'), - Buffer = require('buffer').Buffer, - sm = new (require('sphericalmercator')), - sqlite3 = require('sqlite3'); - -if (process.env.NODE_ENV === 'test') sqlite3.verbose(); +var fs = require('fs'); +var crypto = require('crypto'); +var zlib = require('zlib'); +var path = require('path'); +var url = require('url'); +var qs = require('querystring'); +var Buffer = require('buffer').Buffer; +var sm = new (require('sphericalmercator')); +var sqlite3 = require('sqlite3'); function noop(err) { if (err) throw err; @@ -53,16 +49,14 @@ function MBTiles(uri, callback) { this.setMaxListeners(0); this.filename = uri.pathname; this._batchSize = +uri.query.batch; - Step(function() { - mbtiles._db = new sqlite3.Database(mbtiles.filename, this); - }, function(err) { - if (err) throw err; - fs.stat(mbtiles.filename, this); - }, function(err, stat) { + mbtiles._db = new sqlite3.Database(mbtiles.filename, function(err) { if (err) return callback(err); - mbtiles._stat = stat; - mbtiles.open = true; - callback(null, mbtiles); + fs.stat(mbtiles.filename, function(err, stat) { + if (err) return callback(err); + mbtiles._stat = stat; + mbtiles.open = true; + callback(null, mbtiles); + }); }); return undefined; @@ -102,24 +96,18 @@ MBTiles.findID = function(filepath, id, callback) { MBTiles.prototype._exists = function(table, callback) { if (typeof callback !== 'function') callback = noop; - if (this._schema) { - return callback(null, _(this._schema).include(table)); - } else { - this._db.all( - 'SELECT name FROM sqlite_master WHERE type IN (?, ?)', - 'table', - 'view', - function(err, rows) { - if (err) return callback(err); - this._schema = _(rows).pluck('name'); - this._exists(table, callback); - }.bind(this) - ); - } + if (this._schema) return callback(null, this._schema.indexOf(table) !== -1); + + var sql = 'SELECT name FROM sqlite_master WHERE type IN ("table", "view")'; + var mbtiles = this; + this._db.all(sql, function(err, rows) { + if (err) return callback(err); + mbtiles._schema = rows.map(function(r) { return r.name }); + mbtiles._exists(table, callback); + }); }; // DB integrity check. -// // - @param {Function(err)} callback MBTiles.prototype._integrity = function(callback) { if (typeof callback !== 'function') callback = noop; @@ -159,24 +147,22 @@ MBTiles.prototype.getTile = function(z, x, y, callback) { // Flip Y coordinate because MBTiles files are TMS. y = (1 << z) - 1 - y; + var sql = 'SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?'; var mbtiles = this; - this._db.get('SELECT tile_data FROM tiles WHERE ' + - 'zoom_level = ? AND tile_column = ? AND tile_row = ?', - z, x, y, - function(err, row) { - if (!row || (err && err.errno == 1)) { - return callback(new Error('Tile does not exist')); - } else if (err) { - return callback(err); - } else { - var options = { - 'Content-Type': MBTiles.utils.getMimeType(row.tile_data), - 'Last-Modified': new Date(mbtiles._stat.mtime).toUTCString(), - 'ETag': mbtiles._stat.size + '-' + Number(mbtiles._stat.mtime) - }; - return callback(null, row.tile_data, options); - } - }); + this._db.get(sql, z, x, y, function(err, row) { + if ((!err && !row) || (err && err.errno == 1)) { + return callback(new Error('Tile does not exist')); + } else if (err) { + return callback(err); + } else { + var options = { + 'Content-Type': MBTiles.utils.getMimeType(row.tile_data), + 'Last-Modified': new Date(mbtiles._stat.mtime).toUTCString(), + 'ETag': mbtiles._stat.size + '-' + Number(mbtiles._stat.mtime) + }; + return callback(null, row.tile_data, options); + } + }); }; // Select a grid and its data from an mbtiles database. Scheme is XYZ. @@ -192,190 +178,38 @@ MBTiles.prototype.getGrid = function(z, x, y, callback) { // Flip Y coordinate because MBTiles files are TMS. y = (1 << z) - 1 - y; - var that = this; - Step( - function() { - that._db.get('SELECT grid FROM grids WHERE ' + - 'zoom_level = ? AND tile_column = ? AND tile_row = ?', - z, x, y, - this.parallel() - ); - that._db.all('SELECT key_name, key_json FROM grid_data WHERE ' + - 'zoom_level = ? AND tile_column = ? AND tile_row = ?', - z, x, y, - this.parallel() - ); - }, - function(err, row, rows) { - if ((!row || !row.grid) || (err && err.errno == 1)) { - return callback(new Error('Grid does not exist')); - } - if (err) return callback(err); + var sqlgrid = 'SELECT grid FROM grids WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?'; + var sqljson = 'SELECT key_name, key_json FROM grid_data WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?'; - zlib.inflate(!Buffer.isBuffer(row.grid) - ? new Buffer(row.grid, 'binary') - : row.grid, - function(err,buffer) { - if (err) return callback(new Error('Grid is invalid:' + err.message)); - try { - var data = rows.reduce(function(memo, r) { - memo[r.key_name] = JSON.parse(r.key_json); - return memo; - }, {}); - var result = JSON.parse(buffer.toString()); - } catch(err) { - return callback(new Error('Grid is invalid:' + err.message)); - } - var options = { - 'Content-Type': 'text/javascript', - 'Last-Modified': new Date(that._stat.mtime).toUTCString(), - 'ETag': that._stat.size + '-' + Number(that._stat.mtime) - }; - callback(null, _(result).extend({data:data}), options); + var mbtiles = this; + mbtiles._db.get(sqlgrid, z, x, y, function(err, row) { + if (err && err.errno !== 1) return callback(err); + if (!row || !row.grid || err) return callback(new Error('Grid does not exist')); + zlib.inflate(!Buffer.isBuffer(row.grid) ? new Buffer(row.grid, 'binary') : row.grid, function(err, buffer) { + if (err) return callback(new Error('Grid is invalid:' + err.message)); + try { var grid = JSON.parse(buffer); } + catch(err) { return callback(new Error('Grid is invalid:' + err.message)) }; + mbtiles._db.all(sqljson, z, x, y, function(err, rows) { + if (err) return callback(err); + grid.data = grid.data || {}; + for (var i = 0; i < rows.length; i++) { + try { grid.data[rows[i].key_name] = JSON.parse(rows[i].key_json); } + catch(err) { return callback(new Error('Grid is invalid:' + err.message)) }; + } + callback(null, grid, { + 'Content-Type': 'text/javascript', + 'Last-Modified': new Date(mbtiles._stat.mtime).toUTCString(), + 'ETag': mbtiles._stat.size + '-' + Number(mbtiles._stat.mtime) }); - } - ); -}; - -// Select a metadata value from the database. -// -// - @param {Function} callback -MBTiles.prototype._metadata = function(key, callback) { - if (typeof callback !== 'function') callback = noop; - - this._db.get('SELECT value FROM metadata WHERE name = ?', - key, - function(err, row) { - // If the metadata table or the requested field is missing return - // null and allow the caller handle it. - if (!row || (err && err.errno == 1)) return callback(null, null); - else if (err) return callback(err); - else return callback(null, row.value); + }); }); -}; - - -MBTiles.prototype._close = function(callback) { - this._db.close(callback); + }); }; MBTiles.prototype.close = function(callback) { - this._close(callback); + this._db.close(callback); }; -MBTiles.prototype._getInfo = function(callback) { - var mbtiles = this; - var info = {}; - info.scheme = 'tms'; - info.basename = path.basename(mbtiles.filename); - info.id = path.basename(mbtiles.filename, path.extname(mbtiles.filename)); - info.filesize = mbtiles._stat.size; - Step(function() { - var end = this; - mbtiles._db.all('SELECT name, value FROM metadata', function(err, rows) { - if (rows) for (var i = 0; i < rows.length; i++) { - info[rows[i].name] = rows[i].value; - } - end(err); - }); - }, - // Determine min/max zoom if needed - function(err) { - if (err && err.errno !== 1) return callback(err); - if (info.maxzoom !== undefined - && info.minzoom !== undefined) return this(); - - var step = this; - var zoomquery = mbtiles._db.prepare('SELECT zoom_level FROM tiles ' + - 'WHERE zoom_level = ? LIMIT 1', function(err) { - if (err) { - if (err.errno === 1) step(); - else throw new Error(err); - } else { - var group = step.group(); - for (var i = 0; i < 30; i++) { - zoomquery.get(i, group()); - } - zoomquery.finalize(); - } - }); - }, - function(err, rows) { - if (err) return callback(err); - if (rows) { - var zooms = _(rows).chain() - .reject(_.isUndefined) - .pluck('zoom_level') - .value(); - info.minzoom = zooms.shift(); - info.maxzoom = zooms.length ? zooms.pop() : info.minzoom; - } - this(); - }, - // Determine bounds if needed - function(err) { - if (err) return callback(err); - if (info.bounds) return this(); - if (typeof info.minzoom === 'undefined') return this(); - - var next = this; - Step( - function() { - mbtiles._db.get( - 'SELECT MAX(tile_column) AS maxx, ' + - 'MIN(tile_column) AS minx, MAX(tile_row) AS maxy, ' + - 'MIN(tile_row) AS miny FROM tiles ' + - 'WHERE zoom_level = ?', - info.minzoom, - this - ); - }, - function(err, row) { - if (!err && row) { - // @TODO this breaks a little at zoom level zero - var urTile = sm.bbox(row.maxx, row.maxy, info.minzoom, true); - var llTile = sm.bbox(row.minx, row.miny, info.minzoom, true); - // @TODO bounds are limited to "sensible" values here - // as sometimes tilesets are rendered with "negative" - // and/or other extremity tiles. Revisit this if there - // are actual use cases for out-of-bounds bounds. - info.bounds = [ - llTile[0] > -180 ? llTile[0] : -180, - llTile[1] > -90 ? llTile[1] : -90, - urTile[2] < 180 ? urTile[2] : 180, - urTile[3] < 90 ? urTile[3] : 90 - ].join(','); - } - next(); - } - ); - }, - // Return info - function(err) { - if (err) return callback(err); - var range = parseInt(info.maxzoom, 10) - parseInt(info.minzoom, 10); - info.minzoom = parseInt(info.minzoom, 10); - if (isNaN(info.minzoom) || typeof info.minzoom !== 'number') delete info.minzoom; - info.maxzoom = parseInt(info.maxzoom, 10); - if (isNaN(info.maxzoom) || typeof info.maxzoom !== 'number') delete info.maxzoom; - - info.bounds = _((info.bounds || '').split(',')).map(parseFloat); - if (info.bounds.length !== 4 || info.bounds[0] === null) delete info.bounds; - - if (info.center) info.center = _((info.center).split(',')).map(parseFloat); - if ((!info.center || info.center.length !== 3) && info.bounds) info.center = [ - (info.bounds[2] - info.bounds[0]) / 2 + info.bounds[0], - (info.bounds[3] - info.bounds[1]) / 2 + info.bounds[1], - (range <= 1) ? info.maxzoom : Math.floor(range * 0.5) + info.minzoom - ]; - if (info.center && (info.center.length !== 3 || info.center[0] === null || isNaN(info.center[2]))) { - delete info.center; - } - - return callback(null, info); - }); -} - // Obtain metadata from the database. Performing fallback queries if certain // keys(like `bounds`, `minzoom`, `maxzoom`) have not been provided. // @@ -384,11 +218,114 @@ MBTiles.prototype.getInfo = function(callback) { if (typeof callback !== 'function') throw new Error('Callback needed'); if (!this.open) return callback(new Error('MBTiles not yet loaded')); if (this._info) return callback(null, this._info); + var mbtiles = this; - mbtiles._getInfo(function(err, info) { - mbtiles._info = info; - return callback(err, info); + var info = {}; + info.scheme = 'tms'; + info.basename = path.basename(mbtiles.filename); + info.id = path.basename(mbtiles.filename, path.extname(mbtiles.filename)); + info.filesize = mbtiles._stat.size; + mbtiles._db.all('SELECT name, value FROM metadata', function(err, rows) { + if (err && err.errno !== 1) return callback(err); + if (rows) rows.forEach(function(row) { + switch (row.name) { + // The special "json" key/value pair allows JSON to be serialized + // and merged into the metadata of an MBTiles based source. This + // enables nested properties and non-string datatypes to be + // captured by the MBTiles metadata table. + case 'json': + try { var jsondata = JSON.parse(row.value); } + catch (err) { return callback(err); } + Object.keys(jsondata).reduce(function(memo, key) { + memo[key] = memo[key] || jsondata[key]; + return memo; + }, info); + break; + case 'minzoom': + case 'maxzoom': + info[row.name] = parseInt(row.value, 10); + break; + case 'center': + case 'bounds': + info[row.name] = row.value.split(',').map(parseFloat); + break; + default: + info[row.name] = row.value; + break; + } + }); + ensureZooms(info, function(err, info) { + if (err) return callback(err); + ensureBounds(info, function(err, info) { + if (err) return callback(err); + ensureCenter(info, function(err, info) { + if (err) return callback(err); + mbtiles._info = info; + return callback(null, info); + }); + }); + }); }); + function ensureZooms(info, callback) { + if ('minzoom' in info && 'maxzoom' in info) return callback(null, info); + var remaining = 30; + var zooms = []; + var query = mbtiles._db.prepare('SELECT zoom_level FROM tiles WHERE zoom_level = ? LIMIT 1', function(err) { + if (err) return callback(err.errno === 1 ? null : err, info); + for (var i = 0; i < remaining; i++) query.get(i, function(err, row) { + if (err) return (remaining = 0) && callback(err); + if (row) zooms.push(row.zoom_level); + if (--remaining === 0) { + if (!zooms.length) return callback(null, info); + zooms.sort(function(a,b) { return a < b ? -1 : 1 }); + info.minzoom = zooms[0]; + info.maxzoom = zooms.pop(); + return callback(null, info); + } + }); + query.finalize(); + }); + }; + function ensureBounds(info, callback) { + if ('bounds' in info) return callback(null, info); + if (!('minzoom' in info)) return callback(null, info); + mbtiles._db.get( + 'SELECT MAX(tile_column) AS maxx, ' + + 'MIN(tile_column) AS minx, MAX(tile_row) AS maxy, ' + + 'MIN(tile_row) AS miny FROM tiles ' + + 'WHERE zoom_level = ?', + info.minzoom, + function(err, row) { + if (err) return callback(err); + if (!row) return callback(null, info); + + // @TODO this breaks a little at zoom level zero + var urTile = sm.bbox(row.maxx, row.maxy, info.minzoom, true); + var llTile = sm.bbox(row.minx, row.miny, info.minzoom, true); + // @TODO bounds are limited to "sensible" values here + // as sometimes tilesets are rendered with "negative" + // and/or other extremity tiles. Revisit this if there + // are actual use cases for out-of-bounds bounds. + info.bounds = [ + llTile[0] > -180 ? llTile[0] : -180, + llTile[1] > -90 ? llTile[1] : -90, + urTile[2] < 180 ? urTile[2] : 180, + urTile[3] < 90 ? urTile[3] : 90 + ]; + return callback(null, info); + }); + }; + function ensureCenter(info, callback) { + if ('center' in info) return callback(null, info); + if (!('bounds' in info) || !('minzoom' in info) || !('maxzoom' in info)) return callback(null, info); + var range = info.maxzoom - info.minzoom; + info.center = [ + (info.bounds[2] - info.bounds[0]) / 2 + info.bounds[0], + (info.bounds[3] - info.bounds[1]) / 2 + info.bounds[1], + range <= 1 ? info.maxzoom : Math.floor(range * 0.5) + info.minzoom + ]; + return callback(null, info); + }; }; // Puts the MBTiles tilestore into write mode. @@ -400,31 +337,36 @@ MBTiles.prototype.startWriting = function(callback) { var mbtiles = this; mbtiles._clearCaches(); - Step(function() { - mbtiles._setup(this); - }, function(err) { - if (err) throw err; + mbtiles._setup(function(err) { + if (err) return callback(err); + if (mbtiles._isWritable) return callback(); + // Sets the synchronous flag to OFF for (much) faster inserts. // See http://www.sqlite3.org/pragma.html#pragma_synchronous - if (!mbtiles._isWritable) { - mbtiles._isWritable = 1; - mbtiles._db.run('PRAGMA synchronous=OFF', this); - } else { - mbtiles._isWritable++; - this(); - } - }, function(err) { - return callback(err); + mbtiles._isWritable = 1; + mbtiles._db.run('PRAGMA synchronous=OFF', callback); }); }; MBTiles.prototype._clearCaches = function() { this._pending = 0; - this._tileCache = {}; - this._gridCache = {}; - this._keyCache = {}; - this._dataCache = {}; - this._mapCache = {}; + this._writes = {}; +}; + +// Queue a row to be written to a table. +MBTiles.prototype.write = function(table, id, row, callback) { + callback = callback || function() {}; + + this._writes = this._writes || {}; + this._writes[table] = this._writes[table] || {}; + this._writes[table][id] = this._writes[table][id] || {}; + + // Merge row data. + for (var key in row) this._writes[table][id][key] = row[key]; + + return ++this._pending >= this._batchSize + ? this._commit(callback) + : callback(); }; // (private) Commits the cached changes to the database. @@ -435,77 +377,51 @@ MBTiles.prototype._commit = function(callback) { mbtiles._db.serialize(function() { mbtiles._db.run('BEGIN'); - if (Object.keys(mbtiles._tileCache)) { - // Insert images table. - var images = mbtiles._db.prepare('REPLACE INTO images (tile_id, tile_data) VALUES (?, ?)'); - for (var id in mbtiles._tileCache) { - images.run(id, mbtiles._tileCache[id]); + var statements = {}; + Object.keys(mbtiles._writes).forEach(function(table) { + switch (table) { + case 'map': + // Insert map table. This has to be so complicate due to a design flaw + // in the tables. + // TODO: This should be remedied when we upgrade the MBTiles schema. + var sql = '\ + REPLACE INTO map (zoom_level, tile_column, tile_row, tile_id, grid_id)\ + VALUES (?, ?, ?,\ + COALESCE(?, (SELECT tile_id FROM map WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?)),\ + COALESCE(?, (SELECT grid_id FROM map WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?)))'; + statements['map'] = mbtiles._db.prepare(sql); + for (var id in mbtiles._writes[table]) { + var row = mbtiles._writes[table][id]; + statements['map'].run( + row.zoom_level, row.tile_column, row.tile_row, + row.tile_id, row.zoom_level, row.tile_column, row.tile_row, + row.grid_id, row.zoom_level, row.tile_column, row.tile_row); + } + statements['map'].finalize(); + break; + default: + var rows = []; + var args = []; + var fields = []; + for (var id in mbtiles._writes[table]) { + var record = mbtiles._writes[table][id]; + var row = []; + for (var field in record) { + row.push(record[field]); + if (fields.indexOf(field) === -1) { + fields.push(field); + args.push('?'); + } + } + rows.push(row); + } + var sql = 'REPLACE INTO ' + table + ' ( ' + fields.join(',') + ' ) VALUES(' + args.join(',') + ')'; + statements[table] = mbtiles._db.prepare(sql); + while (rows.length) statements[table].run.apply(statements[table], rows.shift()); + statements[table].finalize(); + break; } - images.finalize(); - } - - - if (Object.keys(mbtiles._gridCache)) { - // Insert grid_utfgrid table. - var grids = mbtiles._db.prepare('REPLACE INTO grid_utfgrid (grid_id, grid_utfgrid) VALUES (?, ?)'); - for (var id in mbtiles._gridCache) { - grids.run(id, mbtiles._gridCache[id]); - } - grids.finalize(); - } - - - if (Object.keys(mbtiles._keyCache)) { - // Insert grid_key. - var keys = mbtiles._db.prepare('INSERT OR IGNORE INTO grid_key (grid_id, key_name) VALUES (?, ?)'); - for (var id in mbtiles._keyCache) { - mbtiles._keyCache[id].forEach(function(key) { - keys.run(id, key); - }); - } - keys.finalize(); - } - - - if (Object.keys(mbtiles._dataCache)) { - // Insert keymap table. - var keymap = mbtiles._db.prepare('REPLACE INTO keymap (key_name, key_json) VALUES (?, ?)'); - for (var key in mbtiles._dataCache) { - keymap.run(key, JSON.stringify(mbtiles._dataCache[key])); - } - keymap.finalize(); - } - - // Insert map table. This has to be so complicate due to a design flaw - // in the tables. - // TODO: This should be remedied when we upgrade the MBTiles schema. - var mapBoth = mbtiles._db.prepare('REPLACE INTO map (zoom_level, ' + - 'tile_column, tile_row, tile_id, grid_id) VALUES (?, ?, ?, ?, ?)'); - var mapTile = mbtiles._db.prepare('REPLACE INTO map (zoom_level, ' + - 'tile_column, tile_row, tile_id, grid_id) VALUES (?, ?, ?, ?, ' + - '(SELECT grid_id FROM map WHERE zoom_level = ? ' + - 'AND tile_column = ? AND tile_row = ?))'); - var mapGrid = mbtiles._db.prepare('REPLACE INTO map (zoom_level, ' + - 'tile_column, tile_row, tile_id, grid_id) VALUES (?, ?, ?, ' + - '(SELECT tile_id FROM map WHERE zoom_level = ? ' + - 'AND tile_column = ? AND tile_row = ?), ?)'); - for (var coords in mbtiles._mapCache) { - var map = mbtiles._mapCache[coords]; - - if (typeof map.grid_id === 'undefined') { - // Only the tile_id is defined. - mapTile.run(map.z, map.x, map.y, map.tile_id, map.z, map.x, map.y); - } else if (typeof map.tile_id === 'undefined') { - // Only the grid_id is defined. - mapGrid.run(map.z, map.x, map.y, map.z, map.x, map.y, map.grid_id); - } else { - // Both tile_id and grid_id are defined. - mapBoth.run(map.z, map.x, map.y, map.tile_id, map.grid_id); - } - } - mapBoth.finalize(); - mapTile.finalize(); - mapGrid.finalize(); + }); mbtiles._db.run('COMMIT', callback); mbtiles._clearCaches(); @@ -520,14 +436,13 @@ MBTiles.prototype.stopWriting = function(callback) { if (!this.open) return callback(new Error('MBTiles not yet loaded')); var mbtiles = this; - if (this._isWritable) this._isWritable--; - this._commit(function(err) { + mbtiles._commit(function(err) { if (err) return callback(err); - if (!mbtiles._isWritable) { - mbtiles._db.run('PRAGMA synchronous=NORMAL', callback); - } else { - return callback(null); - } + mbtiles._db.run('PRAGMA synchronous=NORMAL', function(err) { + if (err) return callback(err); + mbtiles._isWritable = false; + return callback(); + }); }); }; @@ -542,7 +457,6 @@ MBTiles.prototype.putTile = function(z, x, y, data, callback) { if (typeof callback !== 'function') throw new Error('Callback needed'); if (!this.open) return callback(new Error('MBTiles not yet loaded')); if (!this._isWritable) return callback(new Error('MBTiles not in write mode')); - if (!Buffer.isBuffer(data)) return callback(new Error('Image needs to be a Buffer')); // Flip Y coordinate because MBTiles files are TMS. @@ -554,17 +468,18 @@ MBTiles.prototype.putTile = function(z, x, y, data, callback) { ? String(data.key) : crypto.createHash('md5').update(data).digest('hex'); - // This corresponds to the images table. - if (!this._tileCache[id]) this._tileCache[id] = data; - - // This corresponds to the map table. + // Queue writes for images, map table. var coords = hash(z, x, y); - if (!this._mapCache[coords]) this._mapCache[coords] = { z: z, x: x, y: y }; - this._mapCache[coords].tile_id = id; - - // Only commit when we can insert at least batchSize rows. - if (++this._pending >= this._batchSize) return this._commit(callback); - else return callback(null); + this.write('images', id, { + tile_id: id, + tile_data: data + }); + this.write('map', coords, { + zoom_level: z, + tile_column: x, + tile_row: y, + tile_id: id + }, callback); }; // Inserts a grid into the MBTiles store. Scheme is XYZ. @@ -591,30 +506,31 @@ MBTiles.prototype.putGrid = function(z, x, y, data, callback) { ? String(data.key) : crypto.createHash('md5').update(json).digest('hex'); - // This corresponds to the map table. var coords = hash(z, x, y); - if (!this._mapCache[coords]) this._mapCache[coords] = { z: z, x: x, y: y }; - this._mapCache[coords].grid_id = id; - var mbtiles = this; - Step(function() { - if (mbtiles._gridCache[id]) return this(); - else zlib.deflate(new Buffer(json, 'utf8'), this); - }, function(err, buffer) { - if (err) throw err; - if (mbtiles._gridCache[id]) return this(); - // grid_utfgrid table. - mbtiles._gridCache[id] = buffer; - // grid_key table. - mbtiles._keyCache[id] = Object.keys(data.data || {}); - // keymap table. - _(mbtiles._dataCache).extend(data.data || {}); - this(); - }, function(err) { + + this.write('map', coords, { + zoom_level: z, + tile_column: x, + tile_row: y, + grid_id: id + }); + zlib.deflate(new Buffer(json, 'utf8'), function(err, buffer) { if (err) return callback(err); - // Only commit when we can insert at least batchSize rows. - if (++this._pending >= this._batchSize) return this._commit(callback); - else return callback(null); + Object.keys(data.data || {}).forEach(function(key) { + mbtiles.write('grid_key', id + '_' + key, { + grid_id: id, + key_name: key + }); + mbtiles.write('keymap', key, { + key_name: key, + key_json: JSON.stringify(data.data[key]) + }); + }); + mbtiles.write('grid_utfgrid', id, { + grid_id: id, + grid_utfgrid: buffer + }, callback); }); }; @@ -623,23 +539,31 @@ MBTiles.prototype.putInfo = function(data, callback) { if (!this.open) return callback(new Error('MBTiles not yet loaded')); if (!this._isWritable) return callback(new Error('MBTiles not in write mode')); - // Valid keys. - var keys = ['name', 'type', 'description', 'version', 'formatter', 'template', - 'bounds', 'center', 'minzoom', 'maxzoom', 'legend', 'attribution']; - + var jsondata; var stmt = this._db.prepare('REPLACE INTO metadata (name, value) VALUES (?, ?)'); stmt.on('error', callback); for (var key in data) { - if (keys.indexOf(key) !== -1) stmt.run(key, String(data[key])); + // If a data property is a javascript hash/object, slip it into + // the 'json' field which contains stringified JSON to be merged + // in at read time. Allows nested/deep metadata to be recorded. + var nested = typeof data[key] === 'object' && + key !== 'bounds' && + key !== 'center'; + if (nested) { + jsondata = jsondata || {}; + jsondata[key] = data[key]; + } else { + stmt.run(key, String(data[key])); + } } + if (jsondata) stmt.run('json', JSON.stringify(jsondata)); var mbtiles = this; stmt.finalize(function(err) { if (err) return callback(err); - mbtiles._getInfo(function(err, info) { - if (err) return callback(err); - mbtiles._info = info; - if (callback) callback(null); + delete mbtiles._info; + mbtiles.getInfo(function(err, info) { + return callback(err, null); }); }); }; diff --git a/package.json b/package.json index 2013989..bca7552 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mbtiles", - "version": "0.2.8", + "version": "0.3.2", "description": "Utilities and tilelive integration for the MBTiles format.", "url": "http://github.com/mapbox/node-mbtiles", "author": { @@ -11,12 +11,6 @@ "keywords": ["map", "mbtiles"], "licenses": [{ "type": "BSD" }], "main": "./lib/mbtiles", - "bin": { - "mbcheck": "./bin/mbcheck", - "mbcompact": "./bin/mbcompact", - "mbpipe": "./bin/mbpipe", - "mbrekey": "./bin/mbrekey" - }, "repository": { "type": "git", "url": "http://github.com/mapbox/node-mbtiles.git" @@ -29,19 +23,16 @@ "Konstantin Käfer " ], "dependencies": { - "underscore": "~1.3.3", - "step": "~0.0.5", - "optimist": "~0.6.0", - "sqlite3": "~2.1.1", + "sqlite3": "~2.1.12", "sphericalmercator": "~1.0.1" }, "devDependencies": { - "expresso": "~0.9.0" + "mocha": "~1.8.2" }, "engines": { "node": ">= 0.6.0" }, "scripts": { - "test": "expresso" + "test": "mocha" } } diff --git a/test/fixtures/plain_1.mbtiles b/test/fixtures/plain_1.mbtiles index 2ca8645..5fdce65 100644 Binary files a/test/fixtures/plain_1.mbtiles and b/test/fixtures/plain_1.mbtiles differ diff --git a/test/info.test.js b/test/info.test.js index c6467b2..193ec66 100644 --- a/test/info.test.js +++ b/test/info.test.js @@ -1,98 +1,98 @@ -process.env.NODE_ENV = 'test'; +require('sqlite3').verbose(); var fs = require('fs'); -var Step = require('step'); var MBTiles = require('..'); - +var assert = require('assert'); var fixtures = { plain_1: __dirname + '/fixtures/plain_1.mbtiles', empty: __dirname + '/fixtures/empty.mbtiles' }; -try { fs.unlink(fixtures.empty); } catch (err) {} - - -exports['get metadata'] = function(beforeExit, assert) { - var completed = false; - - new MBTiles(fixtures.plain_1, function(err, mbtiles) { - if (err) throw err; - - mbtiles.getInfo(function(err, data) { - completed = true; - if (err) throw err; - - assert.deepEqual({ - name: 'plain_1', - description: 'demo description', - version: '1.0.3', - scheme: 'tms', - minzoom: 0, - maxzoom: 4, - formatter: null, - center: [ 0, 7.500000001278025, 2 ], - bounds: [ -179.9999999749438, -69.99999999526695, 179.9999999749438, 84.99999999782301 ], - - // These aren't part of TileJSON, but exist in an MBTiles file. - filesize: 561152, - type: 'baselayer', - id: 'plain_1', - basename: 'plain_1.mbtiles' - }, data); - }) +describe('info', function() { + before(function(done) { + try { fs.unlinkSync(fixtures.empty); } catch (err) {} + done(); }); + it('get metadata', function(done) { + new MBTiles(fixtures.plain_1, function(err, mbtiles) { + assert.ifError(err); - beforeExit(function() { - assert.ok(completed); + mbtiles.getInfo(function(err, data) { + assert.ifError(err); + + assert.deepEqual({ + name: 'plain_1', + description: 'demo description', + version: '1.0.3', + scheme: 'tms', + minzoom: 0, + maxzoom: 4, + formatter: null, + center: [ 0, 7.500000001278025, 2 ], + bounds: [ -179.9999999749438, -69.99999999526695, 179.9999999749438, 84.99999999782301 ], + // Test that json data is merged in. + level1: { level2: 'property' }, + // These aren't part of TileJSON, but exist in an MBTiles file. + filesize: 561152, + type: 'baselayer', + id: 'plain_1', + basename: 'plain_1.mbtiles' + }, data); + + done(); + }); + }); }); -}; + it('get/put metadata from empty file', function(done) { + this.timeout(10e3); -exports['get/put metadata from empty file'] = function(beforeExit, assert) { - var completion = {}; + var info = { + version: '1.0.0', + level1: { level2: 'property' }, + custom: [ 'custom list' ] + }; - new MBTiles(fixtures.empty, function(err, mbtiles) { - if (err) throw err; - completion.open = true; + new MBTiles(fixtures.empty, function(err, mbtiles) { + assert.ifError(err); - mbtiles.getInfo(function(err, data) { - if (err) throw err; - completion.info = true; + mbtiles.getInfo(function(err, data) { + assert.ifError(err); - assert.deepEqual({ - basename: "empty.mbtiles", - filesize: 0, - id: "empty", - scheme: "tms" - }, data); + assert.deepEqual({ + basename: "empty.mbtiles", + filesize: 0, + id: "empty", + scheme: "tms" + }, data); - mbtiles.putInfo({ version: '1.0.0' }, function(err) { - assert.ok(err); - assert.equal(err.message, 'MBTiles not in write mode'); - completion.putFail = true; + mbtiles.putInfo(info, function(err) { + assert.ok(err); + assert.equal(err.message, 'MBTiles not in write mode'); - mbtiles.startWriting(function(err) { - if (err) throw err; - completion.startWriting = true; + mbtiles.startWriting(function(err) { + assert.ifError(err); - mbtiles.putInfo({ version: '1.0.0' }, function(err) { - if (err) throw err; - completion.written = true; + mbtiles.putInfo(info, function(err) { + assert.ifError(err); - mbtiles.stopWriting(function(err) { - if (err) throw err; - completion.stopWriting = true; + mbtiles.stopWriting(function(err) { + assert.ifError(err); - mbtiles.getInfo(function(err, data) { - if (err) throw err; - completion.updatedInfo = true; + mbtiles.getInfo(function(err, data) { + assert.ifError(err); - assert.deepEqual({ - basename: "empty.mbtiles", - filesize: 0, - id: "empty", - scheme: "tms", - version: "1.0.0" - }, data); + assert.deepEqual({ + basename: "empty.mbtiles", + filesize: 0, + id: "empty", + scheme: "tms", + version: "1.0.0", + level1: { level2: "property" }, + custom: [ 'custom list' ] + }, data); + + done(); + }); }); }); }); @@ -100,17 +100,4 @@ exports['get/put metadata from empty file'] = function(beforeExit, assert) { }); }); }); - - beforeExit(function() { - assert.deepEqual(completion, { - info: true, - open: true, - putFail: true, - startWriting: true, - stopWriting: true, - updatedInfo: true, - written: true - }); - }); -}; - +}); diff --git a/test/list.test.js b/test/list.test.js index 845f142..92fa1c8 100644 --- a/test/list.test.js +++ b/test/list.test.js @@ -1,23 +1,23 @@ -process.env.NODE_ENV = 'test'; +require('sqlite3').verbose(); var fs = require('fs'); -var Step = require('step'); var MBTiles = require('..'); - +var assert = require('assert'); var fixtures = { doesnotexist: __dirname + '/doesnotexist' }; -try { fs.unlink(fixtures.doesnotexist); } catch (err) {} - - -exports['list'] = function(beforeExit, assert) { - var completed = false; beforeExit(function() { assert.ok(completed); }); - - MBTiles.list(fixtures.doesnotexist, function(err, list) { - completed = true; - assert.equal(err, null); - assert.deepEqual(list, {}); +describe('list', function() { + before(function(done) { + try { fs.unlinkSync(fixtures.doesnotexist); } catch (err) {} + done(); }); -}; + it('list', function(done) { + MBTiles.list(fixtures.doesnotexist, function(err, list) { + assert.ifError(err); + assert.deepEqual(list, {}); + done(); + }); + }); +}); diff --git a/test/read.test.js b/test/read.test.js index b0f4ac5..ce0bf55 100644 --- a/test/read.test.js +++ b/test/read.test.js @@ -1,8 +1,8 @@ -process.env.NODE_ENV = 'test'; +require('sqlite3').verbose(); var fs = require('fs'); var MBTiles = require('..'); - +var assert = require('assert'); var fixtures = { plain_1: __dirname + '/fixtures/plain_1.mbtiles', @@ -13,244 +13,133 @@ var fixtures = { corrupt: __dirname + '/fixtures/corrupt.mbtiles' }; -try { fs.unlink(fixtures.non_existent); } catch (err) {} - -function yieldsError(assert, status, error, msg) { +function yieldsError(assert, error, msg, callback) { return function(err) { assert.ok(err); var re = new RegExp( "^" + msg, "i"); assert.ok(err.message.match(re)); - status[error]++; + if (callback) callback(); }; } +describe('read', function() { + var loaded = {}; -exports['get tiles'] = function(beforeExit, assert) { - var status = { - success: 0, - error: 0 - }; + before(function(done) { + try { fs.unlinkSync(fixtures.non_existent); } catch (err) {} + done(); + }); + before(function(done) { + var queue = Object.keys(fixtures); + var load = function() { + if (!queue.length) return done(); + var key = queue.shift(); + new MBTiles(fixtures[key], function(err, mbtiles) { + if (err) throw err; + loaded[key] = mbtiles; + load(); + }); + }; + load(); + }); - new MBTiles(fixtures.plain_1, function(err, mbtiles) { - if (err) throw err; - fs.readdirSync(__dirname + '/fixtures/images/').forEach(function(file) { - var coords = file.match(/^plain_1_(\d+)_(\d+)_(\d+).png$/); - if (coords) { - // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. - coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; - mbtiles.getTile(coords[3] | 0, coords[1] | 0, coords[2] | 0, function(err, tile, headers) { - if (err) throw err; - assert.deepEqual(tile, fs.readFileSync(__dirname + '/fixtures/images/' + file)); - assert.equal(headers['Content-Type'], 'image/png'); - assert.ok(!isNaN(Date.parse(headers['Last-Modified']))); - assert.ok(/\d+-\d+/.test(headers['ETag'])); - status.success++; - }); - } + fs.readdirSync(__dirname + '/fixtures/images/').forEach(function(file) { + var coords = file.match(/^plain_1_(\d+)_(\d+)_(\d+).png$/); + if (!coords) return; + + // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. + coords = [ coords[3], coords[1], coords[2] ]; + coords[2] = Math.pow(2, coords[0]) - 1 - coords[2]; + it('tile ' + coords.join('/'), function(done) { + loaded.plain_1.getTile(coords[0] | 0, coords[1] | 0, coords[2] | 0, function(err, tile, headers) { + if (err) throw err; + assert.deepEqual(tile, fs.readFileSync(__dirname + '/fixtures/images/' + file)); + assert.equal(headers['Content-Type'], 'image/png'); + assert.ok(!isNaN(Date.parse(headers['Last-Modified']))); + assert.ok(/\d+-\d+/.test(headers['ETag'])); + done(); + }); }); - - mbtiles.getTile(0, 1, 0, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(-1, 0, 0, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(0, 0, 1, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(3, 1, -1, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(2, -3, 3, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(18, 2, 262140, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(4, 0, 15, yieldsError(assert, status, 'error', 'Tile does not exist')); - }); - - - beforeExit(function() { - assert.equal(status.success, 285); - assert.equal(status.error, 7); - }); -}; - -exports['get grids'] = function(beforeExit, assert) { - var status = { - success: 0, - error: 0 - }; - - new MBTiles(fixtures.plain_2, function(err, mbtiles) { - if (err) throw err; - fs.readdirSync(__dirname + '/fixtures/grids/').forEach(function(file) { - var coords = file.match(/^plain_2_(\d+)_(\d+)_(\d+).json$/); - if (coords) { - // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. - coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; - mbtiles.getGrid(coords[3] | 0, coords[1] | 0, coords[2] | 0, function(err, grid, headers) { - if (err) throw err; - assert.deepEqual(JSON.stringify(grid), fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8')); - assert.equal(headers['Content-Type'], 'text/javascript'); - assert.ok(!isNaN(Date.parse(headers['Last-Modified']))); - assert.ok(/\d+-\d+/.test(headers['ETag'])); - status.success++; - }); - } + it('grid ' + coords.join('/'), function(done) { + loaded.plain_1.getGrid(coords[0] | 0, coords[1] | 0, coords[2] | 0, yieldsError(assert, 'error', 'Grid does not exist', done)); }); - - mbtiles.getGrid(0, 1, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(-1, 0, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(0, 0, 1, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 1, -1, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(2, -3, 3, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(18, 2, 262140, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 0, 15, yieldsError(assert, status, 'error', 'Grid does not exist')); }); - - - beforeExit(function() { - assert.equal(status.success, 241); - assert.equal(status.error, 7); - }); -}; - - -exports['get grids from file without interaction'] = function(beforeExit, assert) { - var status = { - success: 0, - error: 0 - }; - - new MBTiles(fixtures.plain_1, function(err, mbtiles) { - if (err) throw err; - mbtiles.getGrid(0, 1, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(-1, 0, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(0, 0, -1, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 1, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(2, -3, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(18, 2, 3, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 0, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 3, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 4, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 5, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 13, 4, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 0, 14, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 0, 7, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 6, 2, yieldsError(assert, status, 'error', 'Grid does not exist')); - }); - - beforeExit(function() { - assert.equal(status.success, 0); - assert.equal(status.error, 14); - }); -}; - -exports['get grids with different schema'] = function(beforeExit, assert) { - var status = { - success: 0, - error: 0 - }; - - new MBTiles(fixtures.plain_4, function(err, mbtiles) { - if (err) throw err; - fs.readdirSync(__dirname + '/fixtures/grids/').forEach(function(file) { - var coords = file.match(/^plain_2_(\d+)_(\d+)_(\d+).json$/); - if (coords) { - // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. - coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; - mbtiles.getGrid(coords[3] | 0, coords[1] | 0, coords[2] | 0, function(err, grid) { - if (err) throw err; - assert.deepEqual(JSON.stringify(grid), fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8')); - status.success++; - }); - } + [ [0,1,0], + [-1,0,0], + [0,0,1], + [3,1,-1], + [2,-3,3], + [18,2,262140], + [4,0,15] + ].forEach(function(coords) { + it('tile ' + coords.join('/'), function(done) { + loaded.plain_1.getTile(coords[0], coords[1], coords[2], yieldsError(assert, 'error', 'Tile does not exist', done)); }); - - mbtiles.getGrid(0, 1, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(-1, 0, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(0, 0, 1, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 1, -1, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(2, -3, 3, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(18, 2, 262140, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 0, 15, yieldsError(assert, status, 'error', 'Grid does not exist')); }); - beforeExit(function() { - assert.equal(status.success, 241); - assert.equal(status.error, 7); + fs.readdirSync(__dirname + '/fixtures/grids/').forEach(function(file) { + var coords = file.match(/^plain_2_(\d+)_(\d+)_(\d+).json$/); + if (!coords) return; + + // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. + coords = [ coords[3], coords[1], coords[2] ]; + coords[2] = Math.pow(2, coords[0]) - 1 - coords[2]; + it('grid ' + coords.join('/'), function(done) { + loaded.plain_2.getGrid(coords[0] | 0, coords[1] | 0, coords[2] | 0, function(err, grid, headers) { + if (err) throw err; + assert.deepEqual(JSON.stringify(grid), fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8')); + assert.equal(headers['Content-Type'], 'text/javascript'); + assert.ok(!isNaN(Date.parse(headers['Last-Modified']))); + assert.ok(/\d+-\d+/.test(headers['ETag'])); + done(); + }); + }); + it('grid alt ' + coords.join('/'), function(done) { + loaded.plain_4.getGrid(coords[0] | 0, coords[1] | 0, coords[2] | 0, function(err, grid, headers) { + if (err) throw err; + assert.deepEqual(JSON.stringify(grid), fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8')); + assert.equal(headers['Content-Type'], 'text/javascript'); + assert.ok(!isNaN(Date.parse(headers['Last-Modified']))); + assert.ok(/\d+-\d+/.test(headers['ETag'])); + done(); + }); + }); }); -}; - - -exports['get grids from file without interaction'] = function(beforeExit, assert) { - var status = { - success: 0, - error: 0 - }; - - new MBTiles(fixtures.plain_1, function(err, mbtiles) { - if (err) throw err; - mbtiles.getGrid(0, 1, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(-1, 0, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(0, 0, -1, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 1, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(2, -3, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(18, 2, 3, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 0, 0, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 3, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 4, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 5, 8, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 13, 4, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(4, 0, 14, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 0, 7, yieldsError(assert, status, 'error', 'Grid does not exist')); - mbtiles.getGrid(3, 6, 2, yieldsError(assert, status, 'error', 'Grid does not exist')); + [ [0,1,0], + [-1,0,0], + [0,0,1], + [3,1,-1], + [2,-3,3], + [18,2,262140], + [4,0,15] + ].forEach(function(coords) { + it('grid ' + coords.join('/'), function(done) { + loaded.plain_2.getGrid(coords[0], coords[1], coords[2], yieldsError(assert, 'error', 'Grid does not exist', done)); + }); + it('grid alt ' + coords.join('/'), function(done) { + loaded.plain_4.getGrid(coords[0], coords[1], coords[2], yieldsError(assert, 'error', 'Grid does not exist', done)); + }); }); - - beforeExit(function() { - assert.equal(status.success, 0); - assert.equal(status.error, 14); + [ [0,1,0], + [-1,0,0], + [0,0,-1], + [3,1,8], + [2,-3,0], + [18,2,3], + [4,0,0], + [4,3,8], + [4,4,8], + [4,5,8], + [4,13,4], + [4,0,14], + [3,0,7], + [3,6,2] + ].forEach(function(coords) { + it('dne ' + coords.join('/'), function(done) { + loaded.non_existent.getTile(coords[0], coords[1], coords[2], yieldsError(assert, 'error', 'Tile does not exist', done)); + }); + it('corrupt ' + coords.join('/'), function(done) { + loaded.corrupt.getTile(coords[0], coords[1], coords[2], yieldsError(assert, 'error', 'SQLITE_CORRUPT: database disk image is malformed', done)); + }); }); -}; - -exports['get tiles from non-existent file'] = function(beforeExit, assert) { - var status = { - success: 0, - error: 0 - }; - - new MBTiles(fixtures.non_existent, function(err, mbtiles) { - if (err) throw err; - mbtiles.getTile(0, 1, 0, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(-1, 0, 0, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(0, 0, -1, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(3, 1, 8, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(2, -3, 0, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(18, 2, 3, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(4, 0, 0, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(4, 3, 8, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(4, 4, 8, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(4, 5, 8, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(4, 13, 4, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(4, 0, 14, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(3, 0, 7, yieldsError(assert, status, 'error', 'Tile does not exist')); - mbtiles.getTile(3, 6, 2, yieldsError(assert, status, 'error', 'Tile does not exist')); - }); - - beforeExit(function() { - assert.equal(status.success, 0); - assert.equal(status.error, 14); - }); -}; - -exports['get tiles from corrupt file'] = function(beforeExit, assert) { - var status = { - success: 0, - error: 0 - }; - var error; - new MBTiles(fixtures.corrupt, function(err, mbtiles) { - error = err; - }); - - beforeExit(function() { - assert.throws( - function() { - throw err; - }, - Error - ); - }); -}; - - +}); diff --git a/test/reloading.test.js b/test/reloading.test.js deleted file mode 100644 index ae46856..0000000 --- a/test/reloading.test.js +++ /dev/null @@ -1,107 +0,0 @@ -process.env.NODE_ENV = 'test'; - -var fs = require('fs'); -var MBTiles = require('..'); - -var fixtureDir = __dirname + '/fixtures/output'; -var fixtures = { - source: __dirname + '/fixtures/plain_1.mbtiles', - destination: fixtureDir + '/write_3.mbtiles' -}; - -// Load entire database as buffer. -var file = fs.readFileSync(fixtures.source); - -// Recreate output directory to remove previous tests. -try { fs.unlinkSync(fixtures.destination); } catch(err) {} -try { fs.mkdirSync(fixtureDir, 0755); } catch(err) {} - -exports['test file reloading during copying'] = function(beforeExit, assert) { - var completed = false; - var status = { - success: 0, - error: 0 - }; - - var tiles = [ - [ 0, 0, 0 ], - [ 1, 0, 1 ], - [ 4, 0, 5 ], - [ 4, 0, 4 ], - [ 1, 0, 0 ], - [ 3, 6, 3 ], - [ 4, 8, 6 ], - [ 4, 9, 1 ], - [ 4, 9, 10 ], - [ 4, 9, 7 ], - [ 4, 9, 6 ] - ]; - - var fd = fs.openSync(fixtures.destination, 'w'); - // Start copying the file. Write first 100 KB and last 100 KB, then wait. - fs.writeSync(fd, file, 0, 100000, 0); - fs.writeSync(fd, file, 461152, 100000, 461152); - - function writeRest() { - setTimeout(function() { - fs.writeSync(fd, file, 100000, 461152, 100000); - fs.closeSync(fd); - - setTimeout(function() { - new MBTiles(fixtures.destination, function(err, mbtiles) { - var returned = 0; - tiles.forEach(function(c) { - mbtiles.getTile(c[0], c[1], c[2], function(err, tile) { - if (err) assert.ok(false, "Couldn't load tile " + c[0] + '/' + c[1] + '/' + c[2]); - else status.success++; - }); - }); - }); - - }, 2000); - }, 1000); - } - - // Try reading. - new MBTiles(fixtures.destination, function(err, mbtiles) { - if (err) throw err; - mbtiles.getInfo(function(err, data) { - completed = true; - if (err) throw err; - - assert.deepEqual({ - name: 'plain_1', - description: 'demo description', - version: '1.0.3', - scheme: 'tms', - minzoom: 0, - maxzoom: 4, - formatter: null, - center: [ 0, 7.500000001278025, 2 ], - bounds: [ -179.9999999749438, -69.99999999526695, 179.9999999749438, 84.99999999782301 ], - - // These aren't part of TileJSON, but exist in an MBTiles file. - filesize: 561152, - type: 'baselayer', - id: 'write_3', - basename: 'write_3.mbtiles' - }, data); - }); - - var returned = 0; - tiles.forEach(function(c) { - mbtiles.getTile(c[0], c[1], c[2], function(err, tile) { - if (++returned === tiles.length) writeRest(); - if (err) status.error++; - else assert.ok(false, "Could unexpectedly load tile " + c[0] + '/' + c[1] + '/' + c[2]); - }); - }); - }); - - - beforeExit(function() { - assert.ok(completed); - assert.equal(status.error, 11); - assert.equal(status.success, 11); - }); -}; diff --git a/test/write.test.js b/test/write.test.js index bfc2666..bfb1239 100644 --- a/test/write.test.js +++ b/test/write.test.js @@ -1,72 +1,68 @@ -process.env.NODE_ENV = 'test'; +require('sqlite3').verbose(); var fs = require('fs'); var assert = require('assert'); var MBTiles = require('..'); - var fixtureDir = __dirname + '/fixtures/output'; -// Recreate output directory to remove previous tests. -try { fs.unlinkSync(fixtureDir + '/write_1.mbtiles'); } catch(err) {} -try { fs.mkdirSync(fixtureDir, 0755); } catch(err) {} +describe('write', function() { + before(function(done) { + // Recreate output directory to remove previous tests. + try { fs.unlinkSync(fixtureDir + '/write_1.mbtiles'); } catch(err) {} + try { fs.mkdirSync(fixtureDir, 0755); } catch(err) {} + done(); + }); + it('test mbtiles file creation', function(done) { + this.timeout(20e3); -exports['test mbtiles file creation'] = function(beforeExit, assert) { - var completed = { written: 0, read: 0 }; - new MBTiles(fixtureDir + '/write_1.mbtiles', function(err, mbtiles) { - completed.open = true; - if (err) throw err; - - mbtiles.startWriting(function(err) { - completed.started = true; + var completed = { written: 0, read: 0 }; + new MBTiles(fixtureDir + '/write_1.mbtiles', function(err, mbtiles) { + completed.open = true; if (err) throw err; - fs.readdirSync(__dirname + '/fixtures/images/').forEach(insertTile); - }); - - function insertTile(file) { - var coords = file.match(/^plain_1_(\d+)_(\d+)_(\d+).png$/); - if (!coords) return; - - // Flip Y coordinate because file names are TMS, but .putTile() expects XYZ. - coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; - - var tile = fs.readFileSync(__dirname + '/fixtures/images/' + file); - mbtiles.putTile(coords[3] | 0, coords[1] | 0, coords[2] | 0, tile, function(err) { + mbtiles.startWriting(function(err) { + completed.started = true; if (err) throw err; - completed.written++; - if (completed.written === 285) { - mbtiles.stopWriting(function(err) { - completed.stopped = true; - if (err) throw err; - verifyWritten(); - }); - } - }); - } - function verifyWritten() { - fs.readdirSync(__dirname + '/fixtures/images/').forEach(function(file) { + fs.readdirSync(__dirname + '/fixtures/images/').forEach(insertTile); + }); + + function insertTile(file) { var coords = file.match(/^plain_1_(\d+)_(\d+)_(\d+).png$/); - if (coords) { - // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. - coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; - mbtiles.getTile(coords[3] | 0, coords[1] | 0, coords[2] | 0, function(err, tile) { - if (err) throw err; - assert.deepEqual(tile, fs.readFileSync(__dirname + '/fixtures/images/' + file)); - completed.read++; - }); - } - }); - } - }); + if (!coords) return; - beforeExit(function() { - assert.deepEqual({ - open: true, - started: true, - written: 285, - read: 285, - stopped: true - }, completed); - }) -}; + // Flip Y coordinate because file names are TMS, but .putTile() expects XYZ. + coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; + + var tile = fs.readFileSync(__dirname + '/fixtures/images/' + file); + mbtiles.putTile(coords[3] | 0, coords[1] | 0, coords[2] | 0, tile, function(err) { + if (err) throw err; + completed.written++; + if (completed.written === 285) { + mbtiles.stopWriting(function(err) { + completed.stopped = true; + if (err) throw err; + verifyWritten(); + }); + } + }); + } + + function verifyWritten() { + fs.readdirSync(__dirname + '/fixtures/images/').forEach(function(file) { + var coords = file.match(/^plain_1_(\d+)_(\d+)_(\d+).png$/); + if (coords) { + // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. + coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; + mbtiles.getTile(coords[3] | 0, coords[1] | 0, coords[2] | 0, function(err, tile) { + if (err) throw err; + assert.deepEqual(tile, fs.readFileSync(__dirname + '/fixtures/images/' + file)); + completed.read++; + if (completed.read === 285) done(); + }); + } + }); + } + }); + }); +}); diff --git a/test/write_grids.test.js b/test/write_grids.test.js index a7a453e..5584ce7 100644 --- a/test/write_grids.test.js +++ b/test/write_grids.test.js @@ -1,71 +1,67 @@ -process.env.NODE_ENV = 'test'; +require('sqlite3').verbose(); var fs = require('fs'); +var assert = require('assert'); var MBTiles = require('..'); - var fixtureDir = __dirname + '/fixtures/output'; -// Recreate output directory to remove previous tests. -try { fs.unlinkSync(fixtureDir + '/write_2.mbtiles'); } catch(err) {} -try { fs.mkdirSync(fixtureDir, 0755); } catch(err) {} - -exports['test mbtiles file creation'] = function(beforeExit, assert) { - var completed = { written: 0, read: 0 }; - new MBTiles(fixtureDir + '/write_2.mbtiles', function(err, mbtiles) { - completed.open = true; - if (err) throw err; - - mbtiles.startWriting(function(err) { - completed.started = true; +describe('write grids', function() { + before(function(done) { + // Recreate output directory to remove previous tests. + try { fs.unlinkSync(fixtureDir + '/write_2.mbtiles'); } catch(err) {} + try { fs.mkdirSync(fixtureDir, 0755); } catch(err) {} + done(); + }); + it('test mbtiles file creation', function(done) { + this.timeout(20e3); + var completed = { written: 0, read: 0 }; + new MBTiles(fixtureDir + '/write_2.mbtiles', function(err, mbtiles) { + completed.open = true; if (err) throw err; - fs.readdirSync(__dirname + '/fixtures/grids/').forEach(insertGrid); - }); - - function insertGrid(file) { - var coords = file.match(/^plain_2_(\d+)_(\d+)_(\d+).json$/); - if (!coords) return; - - // Flip Y coordinate because file names are TMS, but .putGrid() expects XYZ. - coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; - - var grid = fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8'); - mbtiles.putGrid(coords[3] | 0, coords[1] | 0, coords[2] | 0, JSON.parse(grid), function(err) { + mbtiles.startWriting(function(err) { + completed.started = true; if (err) throw err; - completed.written++; - if (completed.written === 241) { - mbtiles.stopWriting(function(err) { - completed.stopped = true; - if (err) throw err; - verifyWritten(); - }); - } - }); - } - function verifyWritten() { - fs.readdirSync(__dirname + '/fixtures/grids/').forEach(function(file) { + fs.readdirSync(__dirname + '/fixtures/grids/').forEach(insertGrid); + }); + + function insertGrid(file) { var coords = file.match(/^plain_2_(\d+)_(\d+)_(\d+).json$/); - if (coords) { - // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. - coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; - mbtiles.getGrid(coords[3] | 0, coords[1] | 0, coords[2] | 0, function(err, grid) { - if (err) throw err; - assert.deepEqual(JSON.stringify(grid), fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8')); - completed.read++; - }); - } - }); - } - }); + if (!coords) return; - beforeExit(function() { - assert.deepEqual({ - open: true, - started: true, - written: 241, - read: 241, - stopped: true - }, completed); - }) -}; + // Flip Y coordinate because file names are TMS, but .putGrid() expects XYZ. + coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; + + var grid = fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8'); + mbtiles.putGrid(coords[3] | 0, coords[1] | 0, coords[2] | 0, JSON.parse(grid), function(err) { + if (err) throw err; + completed.written++; + if (completed.written === 241) { + mbtiles.stopWriting(function(err) { + completed.stopped = true; + if (err) throw err; + verifyWritten(); + }); + } + }); + } + + function verifyWritten() { + fs.readdirSync(__dirname + '/fixtures/grids/').forEach(function(file) { + var coords = file.match(/^plain_2_(\d+)_(\d+)_(\d+).json$/); + if (coords) { + // Flip Y coordinate because file names are TMS, but .getTile() expects XYZ. + coords[2] = Math.pow(2, coords[3]) - 1 - coords[2]; + mbtiles.getGrid(coords[3] | 0, coords[1] | 0, coords[2] | 0, function(err, grid) { + if (err) throw err; + assert.deepEqual(JSON.stringify(grid), fs.readFileSync(__dirname + '/fixtures/grids/' + file, 'utf8')); + completed.read++; + if (completed.read === 241) done(); + }); + } + }); + } + }); + }); +});