log file defaults to standard out.
log format defaults to current behavior ('tiny' for production, 'dev' for dev, and not enabled in test)
452 lines
13 KiB
JavaScript
452 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
process.env.UV_THREADPOOL_SIZE =
|
|
Math.ceil(Math.max(4, require('os').cpus().length * 1.5));
|
|
|
|
var fs = require('fs'),
|
|
path = require('path');
|
|
|
|
var clone = require('clone'),
|
|
cors = require('cors'),
|
|
enableShutdown = require('http-shutdown'),
|
|
express = require('express'),
|
|
handlebars = require('handlebars'),
|
|
mercator = new (require('@mapbox/sphericalmercator'))(),
|
|
morgan = require('morgan');
|
|
|
|
var packageJson = require('../package'),
|
|
serve_font = require('./serve_font'),
|
|
serve_rendered = null,
|
|
serve_style = require('./serve_style'),
|
|
serve_data = require('./serve_data'),
|
|
utils = require('./utils');
|
|
|
|
var isLight = packageJson.name.slice(-6) == '-light';
|
|
if (!isLight) {
|
|
// do not require `serve_rendered` in the light package
|
|
serve_rendered = require('./serve_rendered');
|
|
}
|
|
|
|
function start(opts) {
|
|
console.log('Starting server');
|
|
|
|
var app = express().disable('x-powered-by'),
|
|
serving = {
|
|
styles: {},
|
|
rendered: {},
|
|
data: {},
|
|
fonts: {}
|
|
};
|
|
|
|
app.enable('trust proxy');
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
var defaultLogFormat = process.env.NODE_ENV == 'production' ? 'tiny' : 'dev';
|
|
var logFormat = opts.logFormat || defaultLogFormat;
|
|
app.use(morgan(logFormat, {
|
|
stream: opts.logFile ? fs.createWriteStream(opts.logFile, { flags: 'a' }) : process.stdout,
|
|
skip: function(req, res) { return opts.silent && (res.statusCode == 200 || res.statusCode == 304) }
|
|
}));
|
|
}
|
|
|
|
var config = opts.config || null;
|
|
var configPath = null;
|
|
if (opts.configPath) {
|
|
configPath = path.resolve(opts.configPath);
|
|
try {
|
|
config = clone(require(configPath));
|
|
} catch (e) {
|
|
console.log('ERROR: Config file not found or invalid!');
|
|
console.log(' See README.md for instructions and sample data.');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
if (!config) {
|
|
console.log('ERROR: No config file not specified!');
|
|
process.exit(1);
|
|
}
|
|
|
|
var options = config.options || {};
|
|
var paths = options.paths || {};
|
|
options.paths = paths;
|
|
paths.root = path.resolve(
|
|
configPath ? path.dirname(configPath) : process.cwd(),
|
|
paths.root || '');
|
|
paths.styles = path.resolve(paths.root, paths.styles || '');
|
|
paths.fonts = path.resolve(paths.root, paths.fonts || '');
|
|
paths.sprites = path.resolve(paths.root, paths.sprites || '');
|
|
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
|
|
|
|
var startupPromises = [];
|
|
|
|
var checkPath = function(type) {
|
|
if (!fs.existsSync(paths[type])) {
|
|
console.error('The specified path for "' + type + '" does not exist (' + paths[type] + ').');
|
|
process.exit(1);
|
|
}
|
|
};
|
|
checkPath('styles');
|
|
checkPath('fonts');
|
|
checkPath('sprites');
|
|
checkPath('mbtiles');
|
|
|
|
if (options.dataDecorator) {
|
|
try {
|
|
options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator));
|
|
} catch (e) {}
|
|
}
|
|
|
|
var data = clone(config.data || {});
|
|
|
|
if (opts.cors) {
|
|
app.use(cors());
|
|
}
|
|
|
|
Object.keys(config.styles || {}).forEach(function(id) {
|
|
var item = config.styles[id];
|
|
if (!item.style || item.style.length == 0) {
|
|
console.log('Missing "style" property for ' + id);
|
|
return;
|
|
}
|
|
|
|
if (item.serve_data !== false) {
|
|
startupPromises.push(serve_style(options, serving.styles, item, id, opts.publicUrl,
|
|
function(mbtiles, fromData) {
|
|
var dataItemId;
|
|
Object.keys(data).forEach(function(id) {
|
|
if (fromData) {
|
|
if (id == mbtiles) {
|
|
dataItemId = id;
|
|
}
|
|
} else {
|
|
if (data[id].mbtiles == mbtiles) {
|
|
dataItemId = id;
|
|
}
|
|
}
|
|
});
|
|
if (dataItemId) { // mbtiles exist in the data config
|
|
return dataItemId;
|
|
} else if (fromData) {
|
|
console.log('ERROR: data "' + mbtiles + '" not found!');
|
|
process.exit(1);
|
|
} else {
|
|
var id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
|
|
while (data[id]) id += '_';
|
|
data[id] = {
|
|
'mbtiles': mbtiles
|
|
};
|
|
return id;
|
|
}
|
|
}, function(font) {
|
|
serving.fonts[font] = true;
|
|
}).then(function(sub) {
|
|
app.use('/styles/', sub);
|
|
}));
|
|
}
|
|
if (item.serve_rendered !== false) {
|
|
if (serve_rendered) {
|
|
startupPromises.push(
|
|
serve_rendered(options, serving.rendered, item, id, opts.publicUrl,
|
|
function(mbtiles) {
|
|
var mbtilesFile;
|
|
Object.keys(data).forEach(function(id) {
|
|
if (id == mbtiles) {
|
|
mbtilesFile = data[id].mbtiles;
|
|
}
|
|
});
|
|
return mbtilesFile;
|
|
}
|
|
).then(function(sub) {
|
|
app.use('/styles/', sub);
|
|
})
|
|
);
|
|
} else {
|
|
item.serve_rendered = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
startupPromises.push(
|
|
serve_font(options, serving.fonts).then(function(sub) {
|
|
app.use('/', sub);
|
|
})
|
|
);
|
|
|
|
Object.keys(data).forEach(function(id) {
|
|
var item = data[id];
|
|
if (!item.mbtiles || item.mbtiles.length == 0) {
|
|
console.log('Missing "mbtiles" property for ' + id);
|
|
return;
|
|
}
|
|
|
|
startupPromises.push(
|
|
serve_data(options, serving.data, item, id, serving.styles, opts.publicUrl).then(function(sub) {
|
|
app.use('/data/', sub);
|
|
})
|
|
);
|
|
});
|
|
|
|
app.get('/styles.json', function(req, res, next) {
|
|
var result = [];
|
|
var query = req.query.key ? ('?key=' + req.query.key) : '';
|
|
Object.keys(serving.styles).forEach(function(id) {
|
|
var styleJSON = serving.styles[id];
|
|
result.push({
|
|
version: styleJSON.version,
|
|
name: styleJSON.name,
|
|
id: id,
|
|
url: utils.getPublicUrl(opts.publicUrl, req) +
|
|
'/styles/' + id + '/style.json' + query
|
|
});
|
|
});
|
|
res.send(result);
|
|
});
|
|
|
|
var addTileJSONs = function(arr, req, type) {
|
|
Object.keys(serving[type]).forEach(function(id) {
|
|
var info = clone(serving[type][id]);
|
|
var path = '';
|
|
if (type == 'rendered') {
|
|
path = 'styles/' + id;
|
|
} else {
|
|
path = type + '/' + id;
|
|
}
|
|
info.tiles = utils.getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, {
|
|
'pbf': options.pbfAlias
|
|
});
|
|
arr.push(info);
|
|
});
|
|
return arr;
|
|
};
|
|
|
|
app.get('/rendered.json', function(req, res, next) {
|
|
res.send(addTileJSONs([], req, 'rendered'));
|
|
});
|
|
app.get('/data.json', function(req, res, next) {
|
|
res.send(addTileJSONs([], req, 'data'));
|
|
});
|
|
app.get('/index.json', function(req, res, next) {
|
|
res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
|
|
});
|
|
|
|
//------------------------------------
|
|
// serve web presentations
|
|
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
|
|
|
var templates = path.join(__dirname, '../public/templates');
|
|
var serveTemplate = function(urlPath, template, dataGetter) {
|
|
var templateFile = templates + '/' + template + '.tmpl';
|
|
if (template == 'index') {
|
|
if (options.frontPage === false) {
|
|
return;
|
|
} else if (options.frontPage &&
|
|
options.frontPage.constructor === String) {
|
|
templateFile = path.resolve(paths.root, options.frontPage);
|
|
}
|
|
}
|
|
startupPromises.push(new Promise(function(resolve, reject) {
|
|
fs.readFile(templateFile, function(err, content) {
|
|
if (err) {
|
|
err = new Error('Template not found: ' + err.message);
|
|
reject(err);
|
|
return;
|
|
}
|
|
var compiled = handlebars.compile(content.toString());
|
|
|
|
app.use(urlPath, function(req, res, next) {
|
|
var data = {};
|
|
if (dataGetter) {
|
|
data = dataGetter(req);
|
|
if (!data) {
|
|
return res.status(404).send('Not found');
|
|
}
|
|
}
|
|
data['server_version'] = packageJson.name + ' v' + packageJson.version;
|
|
data['public_url'] = opts.publicUrl || '/';
|
|
data['is_light'] = isLight;
|
|
data['key_query_part'] =
|
|
req.query.key ? 'key=' + req.query.key + '&' : '';
|
|
data['key_query'] = req.query.key ? '?key=' + req.query.key : '';
|
|
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
|
return res.status(200).send(compiled(data));
|
|
});
|
|
resolve();
|
|
});
|
|
}));
|
|
};
|
|
|
|
serveTemplate('/$', 'index', function(req) {
|
|
var styles = clone(config.styles || {});
|
|
Object.keys(styles).forEach(function(id) {
|
|
var style = styles[id];
|
|
style.name = (serving.styles[id] || serving.rendered[id] || {}).name;
|
|
style.serving_data = serving.styles[id];
|
|
style.serving_rendered = serving.rendered[id];
|
|
if (style.serving_rendered) {
|
|
var center = style.serving_rendered.center;
|
|
if (center) {
|
|
style.viewer_hash = '#' + center[2] + '/' +
|
|
center[1].toFixed(5) + '/' +
|
|
center[0].toFixed(5);
|
|
|
|
var centerPx = mercator.px([center[0], center[1]], center[2]);
|
|
style.thumbnail = center[2] + '/' +
|
|
Math.floor(centerPx[0] / 256) + '/' +
|
|
Math.floor(centerPx[1] / 256) + '.png';
|
|
}
|
|
|
|
var tiles = utils.getTileUrls(
|
|
req, style.serving_rendered.tiles,
|
|
'styles/' + id, style.serving_rendered.format, opts.publicUrl);
|
|
style.xyz_link = tiles[0];
|
|
}
|
|
});
|
|
var data = clone(serving.data || {});
|
|
Object.keys(data).forEach(function(id) {
|
|
var data_ = data[id];
|
|
var center = data_.center;
|
|
if (center) {
|
|
data_.viewer_hash = '#' + center[2] + '/' +
|
|
center[1].toFixed(5) + '/' +
|
|
center[0].toFixed(5);
|
|
}
|
|
data_.is_vector = data_.format == 'pbf';
|
|
if (!data_.is_vector) {
|
|
if (center) {
|
|
var centerPx = mercator.px([center[0], center[1]], center[2]);
|
|
data_.thumbnail = center[2] + '/' +
|
|
Math.floor(centerPx[0] / 256) + '/' +
|
|
Math.floor(centerPx[1] / 256) + '.' + data_.format;
|
|
}
|
|
|
|
var tiles = utils.getTileUrls(
|
|
req, data_.tiles, 'data/' + id, data_.format, opts.publicUrl, {
|
|
'pbf': options.pbfAlias
|
|
});
|
|
data_.xyz_link = tiles[0];
|
|
}
|
|
if (data_.filesize) {
|
|
var suffix = 'kB';
|
|
var size = parseInt(data_.filesize, 10) / 1024;
|
|
if (size > 1024) {
|
|
suffix = 'MB';
|
|
size /= 1024;
|
|
}
|
|
if (size > 1024) {
|
|
suffix = 'GB';
|
|
size /= 1024;
|
|
}
|
|
data_.formatted_filesize = size.toFixed(2) + ' ' + suffix;
|
|
}
|
|
});
|
|
return {
|
|
styles: Object.keys(styles).length ? styles : null,
|
|
data: Object.keys(data).length ? data : null
|
|
};
|
|
});
|
|
|
|
serveTemplate('/styles/:id/$', 'viewer', function(req) {
|
|
var id = req.params.id;
|
|
var style = clone((config.styles || {})[id]);
|
|
if (!style) {
|
|
return null;
|
|
}
|
|
style.id = id;
|
|
style.name = (serving.styles[id] || serving.rendered[id]).name;
|
|
style.serving_data = serving.styles[id];
|
|
style.serving_rendered = serving.rendered[id];
|
|
return style;
|
|
});
|
|
|
|
/*
|
|
app.use('/rendered/:id/$', function(req, res, next) {
|
|
return res.redirect(301, '/styles/' + req.params.id + '/');
|
|
});
|
|
*/
|
|
serveTemplate('/styles/:id/wmts.xml', 'wmts', function(req) {
|
|
var id = req.params.id;
|
|
var wmts = clone((config.styles || {})[id]);
|
|
if (!wmts) {
|
|
return null;
|
|
}
|
|
if (wmts.hasOwnProperty("serve_rendered") && !wmts.serve_rendered) {
|
|
return null;
|
|
}
|
|
wmts.id = id;
|
|
wmts.name = (serving.styles[id] || serving.rendered[id]).name;
|
|
wmts.baseUrl = (req.get('X-Forwarded-Protocol')?req.get('X-Forwarded-Protocol'):req.protocol) + '://' + req.get('host');
|
|
return wmts;
|
|
});
|
|
|
|
serveTemplate('/data/:id/$', 'data', function(req) {
|
|
var id = req.params.id;
|
|
var data = clone(serving.data[id]);
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
data.id = id;
|
|
data.is_vector = data.format == 'pbf';
|
|
return data;
|
|
});
|
|
|
|
var startupComplete = false;
|
|
var startupPromise = Promise.all(startupPromises).then(function() {
|
|
console.log('Startup complete');
|
|
startupComplete = true;
|
|
});
|
|
app.get('/health', function(req, res, next) {
|
|
if (startupComplete) {
|
|
return res.status(200).send('OK');
|
|
} else {
|
|
return res.status(503).send('Starting');
|
|
}
|
|
});
|
|
|
|
var server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() {
|
|
var address = this.address().address;
|
|
if (address.indexOf('::') === 0) {
|
|
address = '[' + address + ']'; // literal IPv6 address
|
|
}
|
|
console.log('Listening at http://%s:%d/', address, this.address().port);
|
|
});
|
|
|
|
// add server.shutdown() to gracefully stop serving
|
|
enableShutdown(server);
|
|
|
|
return {
|
|
app: app,
|
|
server: server,
|
|
startupPromise: startupPromise
|
|
};
|
|
}
|
|
|
|
module.exports = function(opts) {
|
|
var running = start(opts);
|
|
|
|
running.startupPromise.catch(function(err) {
|
|
console.error(err.message);
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on('SIGINT', function() {
|
|
process.exit();
|
|
});
|
|
|
|
process.on('SIGHUP', function() {
|
|
console.log('Stopping server and reloading config');
|
|
|
|
running.server.shutdown(function() {
|
|
for (var key in require.cache) {
|
|
delete require.cache[key];
|
|
}
|
|
|
|
var restarted = start(opts);
|
|
running.server = restarted.server;
|
|
running.app = restarted.app;
|
|
});
|
|
});
|
|
|
|
return running;
|
|
};
|