将栅格瓦片部署在服务容器中,向外提供基础底图服务是专网环境下最常采用的方法。专网也就意味着栅格瓦片的来源只能通过拷贝文件的方式导入到客户的服务器中,并不能通过爬虫直接从互联网上爬取。问题就来了,当栅格瓦片的数量越来越多时,拷贝的时间就越来越长(虽然单张瓦片的大小很小,但是瓦片数量很多)。那通过打成压缩包,到现场再解压,这种方式呢?打压缩包的效率也不高,若打成rar格式,客户现场是Linux系统还得安装解压软件,并且增量的栅格瓦片如何融入进这压缩包内也是个问题(显然重新打包代价太高)。
作者为了方便部署,曾经做了一个小的Linux系统,上面存放瓦片同时向外提供服务,但是在迁移这个系统镜像时发现,迁移速度依然不够理想。
今天作者想介绍一种办法,作为迁移大量栅格瓦片的尝试实验。
1.将瓦片打包成MBTiles
如果你已经有了离线的栅格瓦片,直接借助于MBUtil,生成MBTiles。当然,在使用前,先确认一下本地的sqllite版本。
当生成MBTiles后,迁移就变得很容易,所有的离线瓦片会组织成MBTiles文件,只需要拷贝这一个文件即可。
2.部署瓦片服务
有了MBTiles文件,你就需要一个瓦片服务,它可以根据前端请求,从MBTiles中查询对应的资源并返回。
你可以使用TileStream,或者MBTiles Server。
作者使用的是TileStream,这里要注意有两个坑。
1.node的版本需要是0.10.x或者0.8.x
2.需要修改node_module中connect模块的static.js文件(/node_modules/connect/lib/middleware/static.js)
/*!
* Connect - staticProvider
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, path = require('path')
, join = path.join
, basename = path.basename
, normalize = path.normalize
, utils = require('../utils')
, Buffer = require('buffer').Buffer
, parse = require('url').parse
, mime = require('mime');
/**
* Static file server with the given `root` path.
*
* Examples:
*
* var oneDay = 86400000;
*
* connect(
* connect.static(__dirname + '/public')
* ).listen(3000);
*
* connect(
* connect.static(__dirname + '/public', { maxAge: oneDay })
* ).listen(3000);
*
* Options:
*
* - `maxAge` Browser cache maxAge in milliseconds. defaults to 0
* - `hidden` Allow transfer of hidden files. defaults to false
* - `redirect` Redirect to trailing "/" when the pathname is a dir
*
* @param {String} root
* @param {Object} options
* @return {Function}
* @api public
*/
exports = module.exports = function static(root, options){
options = options || {};
// root required
if (!root) throw new Error('static() root path required');
options.root = root;
return function static(req, res, next) {
options.path = req.url;
options.getOnly = true;
send(req, res, next, options);
};
};
/**
* Expose mime module.
*/
exports.mime = mime;
/**
* Respond with 416 "Requested Range Not Satisfiable"
*
* @param {ServerResponse} res
* @api private
*/
function invalidRange(res) {
var body = 'Requested Range Not Satisfiable';
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', body.length);
res.statusCode = 416;
res.end(body);
}
/**
* Attempt to tranfer the requseted file to `res`.
*
* @param {ServerRequest}
* @param {ServerResponse}
* @param {Function} next
* @param {Object} options
* @api private
*/
var send = exports.send = function(req, res, next, options){
options = options || {};
if (!options.path) throw new Error('path required');
// setup
var maxAge = options.maxAge || 0
, ranges = req.headers.range
, head = 'HEAD' == req.method
, get = 'GET' == req.method
, root = options.root ? normalize(options.root) : null
, redirect = false === options.redirect ? false : true
, getOnly = options.getOnly
, fn = options.callback
, hidden = options.hidden
, done;
// replace next() with callback when available
if (fn) next = fn;
// ignore non-GET requests
if (getOnly && !get && !head) return next();
// parse url
var url = parse(options.path)
, path = decodeURIComponent(url.pathname)
, type;
// null byte(s)
if (~path.indexOf('\0')) return utils.badRequest(res);
// when root is not given, consider .. malicious
if (!root && ~path.indexOf('..')) return utils.forbidden(res);
// join / normalize from optional root dir
path = normalize(join(root, path));
// malicious path
if (root && 0 != path.indexOf(root)) return fn
? fn(new Error('Forbidden'))
: utils.forbidden(res);
// index.html support
if (normalize('/') == path[path.length - 1]) path += 'index.html';
// "hidden" file
if (!hidden && '.' == basename(path)[0]) return next();
fs.stat(path, function(err, stat){
// mime type
type = mime.getType(path);
// ignore ENOENT
if (err) {
if (fn) return fn(err);
return 'ENOENT' == err.code
? next()
: next(err);
// redirect directory in case index.html is present
} else if (stat.isDirectory()) {
if (!redirect) return next();
res.statusCode = 301;
res.setHeader('Location', url.pathname + '/');
res.end('Redirecting to ' + url.pathname + '/');
return;
}
// header fields
if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000));
if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
if (!res.getHeader('content-type')) {
var charset = mime.getType(type);
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
}
res.setHeader('Accept-Ranges', 'bytes');
// conditional GET support
if (utils.conditionalGET(req)) {
if (!utils.modified(req, res)) {
req.emit('static');
return utils.notModified(res);
}
}
var opts = {};
var chunkSize = stat.size;
// we have a Range request
if (ranges) {
ranges = utils.parseRange(stat.size, ranges);
// valid
if (ranges) {
// TODO: stream options
// TODO: multiple support
opts.start = ranges[0].start;
opts.end = ranges[0].end;
chunkSize = opts.end - opts.start + 1;
res.statusCode = 206;
res.setHeader('Content-Range', 'bytes '
+ opts.start
+ '-'
+ opts.end
+ '/'
+ stat.size);
// invalid
} else {
return fn
? fn(new Error('Requested Range Not Satisfiable'))
: invalidRange(res);
}
}
res.setHeader('Content-Length', chunkSize);
// transfer
if (head) return res.end();
// stream
var stream = fs.createReadStream(path, opts);
req.emit('static', stream);
stream.pipe(res);
// callback
if (fn) {
function callback(err) { done || fn(err); done = true }
req.on('close', callback);
stream.on('end', callback);
}
});
};
主要是lookup方法已经在新版本中取消了。
3.启动后台服务
启动后台服务时,会在用户目录下新建~/Documents/MapBox/tiles/文件夹,将MBTiles文件拷贝至此,再启动服务,并指定服务器ip或域名。
./index.js start --host="*.*.*.*"
Started [Server Tile].
Started [Server Core:8888].
4.查看服务是否发布成果
通过浏览器访问服务管理页面http://.../map/[you_mbtilesfilename]
如果能出这样的页面就说明发布成果
为什么图片不可见,因为我在生成mbtiles文件时xy行列号整反了
5.如何使用服务
点击管理页面上的info按钮,在TILE URL中即为其瓦片的调用地址,这里可以颠倒一下xy,应用就能看到瓦片!
image.png
6.有待探索
该服务的健壮性、响应速度、吞吐量、高可用等有待进一步验证
网友评论