diff --git a/bin/mbcheck b/bin/mbcheck new file mode 100755 index 0000000..935ee40 --- /dev/null +++ b/bin/mbcheck @@ -0,0 +1,162 @@ +#!/usr/bin/env node + +var options = argv = require('optimist').argv, + _ = require('underscore'), + Step = require('step'), + utils = require('..').utils, + MBTiles = require('..').MBTiles, + mbtiles, + tables = [], + zooms = [], + tiles = []; + +// Show help. +if (argv.help || argv._.length === 0) { + console.warn('Usage: mbcheck [OPTIONS] [FILE]'); + utils.table([ + ['--help', 'Show help.'] + ]); + 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(''); + console.warn(' Metadata'); + utils.table([ + ['name', name], + ['type', type], + ['description', description], + ['vesion', 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/index.js b/index.js index 6854d6d..bdd2d1a 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,10 @@ var MBTiles = require('./lib/mbtiles'), + utils = require('./lib/utils'), Step = require('step'); module.exports = { MBTiles: MBTiles, + utils: utils, pool: function(datasource) { return { create: function(callback) { diff --git a/lib/mbtiles.js b/lib/mbtiles.js index 51f87bb..7039fc5 100644 --- a/lib/mbtiles.js +++ b/lib/mbtiles.js @@ -34,6 +34,17 @@ MBTiles.prototype.exists = function(table, callback) { } }; +// DB integrity check. +MBTiles.prototype.integrity = function(callback) { + this.db.get('PRAGMA quick_check(1)', function(err, row) { + if (!(row && row.integrity_check && row.integrity_check === 'ok')) { + callback(new Error('Corrupted database.')); + } else { + callback(null, true); + } + }); +}; + // Setup schema, indices, views for a new mbtiles database. // Sets the synchronous flag to OFF for (much) faster inserts. // See http://www.sqlite3.org/pragma.html#pragma_synchronous diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..0c6e540 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,21 @@ +var utils = {}; + +utils.table = function(fields) { + if (!fields[0]) return; + var lengths = fields[0].map(function(val, i) { + return Math.max.apply(Math, fields.map(function(field) { + if (field[i] === undefined) field[i] = ''; + return field[i].toString().length; + })); + }); + fields.forEach(function(field) { + console.warn( + ' ' + field.map(function(val, i) { + if (i >= lengths.length - 1) return val; + return val + Array(lengths[i] - val.toString().length + 1).join(' '); + }).join(' ') + ); + }); +}; + +module.exports = utils; diff --git a/package.json b/package.json index 6d01799..b46807c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "Young Hahn " ], "dependencies": { + "optimist": "0.2.x", "sqlite3": "2.0.x", "step": "0.0.x", "underscore": "1.1.x",