UNPKG

@alvarcarto/tilewarm

Version:

A command-line tool to warm up your tile server cache

229 lines (196 loc) 5.78 kB
const _ = require('lodash'); const yargs = require('yargs'); const fs = require('fs'); const VERSION = require('../package.json').version; const defaultOpts = { buffer: '0km', zoom: '3-9', list: false, input: null, method: 'GET', headers: {}, concurrency: '5', verbose: false, maxRetries: '5', retryBaseTimeout: '5000', // 10 min requestTimeout: 10 * 60 * 1000, }; function getOpts(argv) { const userOpts = getUserOpts(); const opts = _.merge(defaultOpts, userOpts); return validateAndTransformOpts(opts); } function getUserOpts() { const userOpts = yargs .usage( 'Usage: $0 <url> [options]\n\n' + '<url> Tile URL template\n' ) .example('tilewarm http://tileserver.com/{z}/{x}/{y}.png --point 62.31,23.12 --buffer 10km') .demand(1) .option('point', { describe: 'Center of region (use with -b)', default: defaultOpts.point, type: 'string' }) .alias('p', 'point') .option('buffer', { describe: 'Buffer point/geometry by an amount. Affix units at end: mi,km', default: defaultOpts.buffer, type: 'string' }) .alias('b', 'buffer') .option('zoom', { describe: 'Zoom levels (comma separated or range)', default: defaultOpts.zoom, type: 'string' }) .alias('z', 'zoom') .option('list', { describe: 'Don\'t perform any requests, just list all tile URLs', default: defaultOpts.list, type: 'boolean' }) .alias('l', 'list') .option('input', { describe: 'GeoJSON input file', default: defaultOpts.input, type: 'string' }) .alias('i', 'input') .option('request-timeout', { describe: 'Timeout for individual tile request in ms', default: defaultOpts.requestTimeout, type: 'integer' }) .option('verbose', { describe: 'Increase logging', default: defaultOpts.verbose, type: 'boolean' }) .option('max-retries', { describe: 'How many times to retry the tile request. The first request is not counted as a retry. Accepts integer or function that will get zoom as `z` parameter.', default: defaultOpts.maxRetries, type: 'string' }) .option('retry-base-timeout', { describe: 'Base timeout defines how many ms to wait before retrying a request. The final wait time is calculated with retryIndex * retryBaseTimeout. Accepts integer or function that will get zoom as `z` parameter.', default: defaultOpts.retryBaseTimeout, type: 'string' }) .option('concurrency', { describe: 'How many concurrent requests to execute. Accepts integer or function which gets zoom level as z parameter. For example "z < 8 ? 2 : z * 2"', default: defaultOpts.concurrency, type: 'string' }) .alias('c', 'concurrency') .option('method', { describe: 'Which HTTP method to use in requests', default: defaultOpts.method, type: 'string' }) .alias('m', 'method') .help('h') .alias('h', 'help') .alias('v', 'version') .version(VERSION) .argv; userOpts.url = userOpts._[0]; return userOpts; } function validateAndTransformOpts(opts) { if (opts.point && !opts.buffer) { throwArgumentError('When --point is set, --buffer must also be set'); } if (!/^((\d+\-\d+)|(\d+(,\d+)*))$/.test(opts.zoom)) throwArgumentError('Invalid "zoom" argument'); assertTemplateUrl(opts.url); return _.merge({}, opts, { buffer: parseBuffer(opts.buffer), zoom: parseZoomRange(opts.zoom), point: parsePoint(opts.point), input: parseInput(opts.input), concurrency: parseNumberOrZoomFunction(opts.concurrency), maxRetries: parseNumberOrZoomFunction(opts.maxRetries), retryBaseTimeout: parseNumberOrZoomFunction(opts.retryBaseTimeout), }); } function parseNumberOrZoomFunction(val, message) { let newVal; const concurrencyIsNumber = /^\d+$/.test(val); if (concurrencyIsNumber) { const number = assertNumber(val, message); newVal = (z) => number; } else { const func = new Function('z', `return ${val}`); newVal = func; } return newVal; } function assertNumber(val, message) { const number = Number(val); if (!_.isFinite(number)) { throwArgumentError(message); } return number; } function throwArgumentError(message) { const err = new Error(message); err.argumentError = true; throw err; } function parsePoint(point) { const arr = String(point).split(','); const nums = _.map(arr, i => parseFloat(i)); return { lat: nums[0], lng: nums[1], }; } function parseBuffer(buffer) { const radius = parseFloat(buffer); const unit = /mi$/.test(buffer) ? 'miles' : 'kilometers'; return { radius, unit, }; } function parseZoomRange(zoom) { if (zoom.indexOf('-') > -1) { const parts = zoom.split('-'); const min = Number(parts[0]); const max = Number(parts[1]); return _.range(min, max + 1); } const nums = _.map(zoom.split(','), s => Number(s)); return _.sortBy(nums); } function parseInput(input) { if (!input) { return null; } const content = fs.readFileSync(input, { encoding: 'utf8'}); let obj; try { obj = JSON.parse(content); } catch (e) { throwArgumentError('Invalid JSON'); } return obj; } function assertTemplateUrl(template) { if (!/^https?\:/.test(template)) { throwArgumentError('Invalid url'); } assertTemplateUrlParameter(template, '{x}'); assertTemplateUrlParameter(template, '{y}'); assertTemplateUrlParameter(template, '{z}'); } function assertTemplateUrlParameter(template, param) { if (template.indexOf(param) === -1) { throwArgumentError(`Template url is missing parameter: ${param}`); } }; module.exports = { getOpts: getOpts };