本文共 17883 字,大约阅读时间需要 59 分钟。
是什么使
tileserver
如此的无可替代?是他的栅格瓦片渲染.
当Tilestrata
和Tilestache
还在用需要复杂配置文件的mapnik
时,tileserver
却将web页面的mapbox
直接搬到了服务端,达到了前后端配置文件与效果的完全统一,在maputnik
的帮助下,样式的调整也变得方便异常.
这就造就了整个tileserver
里最大的模块:server_render.js
,一个接近800行的大模块,比前面的main.js
模块加上render.js
模块还大.
首先看一下都引用了哪些模块:
var advancedPool = require('advanced-pool'),//用于资源调度 fs = require('fs'), path = require('path'), url = require('url'), util = require('util'), zlib = require('zlib');//将二进制流转换成图像var sharp = require('sharp');var Canvas = require('canvas'),//绘制图像 clone = require('clone'),//深拷贝 Color = require('color'),//封装色彩相关方法 express = require('express'), mercator = new (require('@mapbox/sphericalmercator'))(),//封装瓦片计算的相关方法 mbgl = require('@mapbox/mapbox-gl-native'),//渲染图片的核心模块 mbtiles = require('@mapbox/mbtiles'),//操作mbtiles proj4 = require('proj4'),//投影转换 request = require('request');
在正式进入redered名之前,还有一些预定义方法和变量:
//识别小数的正则表达式var FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
//获取请求的dpi级别var getScale = function(scale) { return (scale || '@1x').slice(1, 2) | 0;};
//后缀名转换字典var extensionToFormat = { '.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png', '.webp': 'webp'};
当渲染或其他细节出错时,返回一张有着相近颜色的纯色瓦片相比不返回是更合适的选择:
var cachedEmptyResponses = { '': new Buffer(0)};function createEmptyResponse(format, color, callback) { //当请求的是pbf或未指定格式时,返回空流 if (!format ||coui format === 'pbf') { callback(null, { data: cachedEmptyResponses['']}); return; } if (format === 'jpg') { format = 'jpeg'; } if (!color) { color = 'rgba(255,255,255,0)'; } //如果命中缓存就直接返回缓存 var cacheKey = format + ',' + color; var data = cachedEmptyResponses[cacheKey]; if (data) { callback(null, { data: data}); return; } //否则就构建缓存 var color = new Color(color); var array = color.array(); var channels = array.length == 4 && format != 'jpeg' ? 4 : 3; sharp(new Buffer(array), { raw: { width: 1, height: 1, channels: channels } }).toFormat(format).toBuffer(function(err, buffer, info) { //无误就缓存起来 if (!err) { cachedEmptyResponses[cacheKey] = buffer; } callback(null, { data: buffer}); });}
先来回顾一下,server_render函数是在什么环境下调用的:
server.jsObject.keys(config.styles || { }).forEach(function(id) { var item = config.styles[id]; /*...*/if (item.serve_data !== false) 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); }) );}
面对每一个需要渲染的样式,都会调用一遍这个函数.也就是说,每一个样式之间都是彼此独立的一个复杂对象.
这个巨大的方法可以分为两个阶段:
在整个流程中,可以发现一些与
server.js
相像的步骤,其实server_redered.js
模块相当于将在浏览器那一套搬到了服务端运行,所以所需的资源是一样的:字体,样式,雪碧图等,都会在server_redered.js
里得到体现.
//一如既往的安全措施 var app = express().disable('x-powered-by'); //生成支持的dpi缩放级别 var maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9); var scalePattern = ''; for (var i = 2; i <= maxScaleFactor; i++) { scalePattern += i.toFixed(); } scalePattern = '@[' + scalePattern + ']x'; //服务启动日期,用于缓存过期判断 var lastModified = new Date().toUTCString(); //水印 var watermark = params.watermark || options.watermark; var styleFile = params.style; var map = { renderers: [], sources: { } };
var existingFonts = { }; var fontListingPromise = new Promise(function(resolve, reject) { //遍历字体文件夹 fs.readdir(options.paths.fonts, function(err, files) { if (err) { reject(err); return; } files.forEach(function(file) { fs.stat(path.join(options.paths.fonts, file), function(err, stats) { if (err) { reject(err); return; } //字体都以文件夹存在 //记录存在的字体 if (stats.isDirectory()) { existingFonts[path.basename(file)] = true; } }); }); resolve(); }); });
Klokantech Basic
为例:{ "version": 8, "name": "Klokantech Basic", "metadata": { "mapbox:autocomposite": false, "mapbox:type": "template", "maputnik:renderer": "mbgljs", "openmaptiles:version": "3.x", "openmaptiles:mapbox:owner": "openmaptiles", "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t" }, "center": [ 8.54806714892635, 47.37180823552663 ], "zoom": 12.241790506353492, "bearing": 0, "pitch": 0, "sources": { "openmaptiles": { "type": "vector", "url": "mbtiles://{v3}" } }, "glyphs": "{fontstack}/{range}.pbf", "sprite": "{styleJsonFolder}/sprite", "layers": [......省略全部图层]}
这里的语法与mapbox-gl的配置完全兼容,包括元数据描述以及具体每一个图层的描述.
图层描述完全交给渲染引擎,我们不去管他,但对于某些东西需要我们为渲染引擎准备好:所谓准备好,就是把存于本地的不同文件夹下的这些资源,在引擎需要的时候让引擎能够找到,当然,如果这些资源本就存于网络,直接通过请求就能获取,不用这么麻烦了.
首先是处理字体与雪碧图,需要统一资源路径表达式:
//制作副本,不对原始文件修改var styleJSONPath = path.resolve(options.paths.styles, styleFile);styleJSON = clone(require(styleJSONPath));//如果是网络资源,则无需修改var httpTester = /^(http(s)?:)?\/\//;//否则就替换为protocol形式的资源描述表达式if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { //替换表达式内的动态内容为实际路径 styleJSON.sprite = 'sprites://' + styleJSON.sprite .replace('{style}', path.basename(styleFile, '.json')) .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleJSONPath)));}//对待字体同样替换为protocol形式if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) { styleJSON.glyphs = 'fonts://' + styleJSON.glyphs;
mbtiles格式的地理数据比较复杂,需要专门处理;
var queue = [];//初始化每一个数据源Object.keys(styleJaSON.sources).forEach(function(name) { var source = styleJSON.sources[name]; var url = source.url; //对于那些存储于mbtiles的数据源 if (url && url.lastIndexOf('mbtiles:', 0) === 0) { delete source.url; var mbtilesFile = url.substring('mbtiles://'.length); //支持数据源别名 var fromData = mbtilesFile[0] == '{' && mbtilesFile[mbtilesFile.length - 1] == '}'; if (fromData) { mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2); var mapsTo = (params.mapping || { })[mbtilesFile]; if (mapsTo) { mbtilesFile = mapsTo; } mbtilesFile = dataResolver(mbtilesFile); if (!mbtilesFile) { console.error('ERROR: data "' + mbtilesFile + '" not found!'); process.exit(1); } } //放入异步初始化队列中 queue.push(new Promise(function(resolve, reject) { mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile); var mbtilesFileStats = fs.statSync(mbtilesFile); if (!mbtilesFileStats.isFile() || mbtilesFileStats.size == 0) { throw Error('Not valid MBTiles file: ' + mbtilesFile); } //获取每个mbtiles数据源的信息 map.sources[name] = new mbtiles(mbtilesFile, function(err) { map.sources[name].getInfo(function(err, info) { if (err) { return; } //支持投影转换 if (!dataProjWGStoInternalWGS && info.proj4) { var to3857 = proj4('EPSG:3857'); var toDataProj = proj4(info.proj4); dataProjWGStoInternalWGS = function(xy) { return to3857.inverse(toDataProj.forward(xy)); }; } var type = source.type; //将mbtiles的元信息并入 Object.assign(source, info); source.type = type; source.tiles = [ 'mbtiles://' + name + '/{z}/{x}/{y}.' + (info.format || 'pbf') ]; resolve(); }); }); })); }});
引擎是这样调用protocol格式的资源的:
var createPool = function(ratio, min, max) { var createRenderer = function(ratio, createCallback) { //初始化渲染引擎 var renderer = new mbgl.Map({ //放大倍率,如2.0一般对应高dpi ratio: ratio, request: function(req, callback) { //处理不同的资源类型 var protocol = req.url.split(':')[0]; if (protocol == 'sprites') { var dir = options.paths[protocol]; var file = unescape(req.url).substring(protocol.length + 3); //返回文件流 fs.readFile(path.join(dir, file), function(err, data) { callback(err, { data: data }); }); } else if (protocol == 'fonts') { var parts = req.url.split('/'); var fontstack = unescape(parts[2]); var range = parts[3].split('.')[0]; //这个函数可以将请求的多个字体文件合为个文件返回 utils.getFontsPbf( null, options.paths[protocol], fontstack, range, existingFonts ).then(function(concated) { callback(null, { data: concated}); }, function(err) { callback(err, { data: null}); }); } else if (protocol == 'mbtiles') { var parts = req.url.split('/'); var sourceId = parts[2]; var source = map.sources[sourceId]; var sourceInfo = styleJSON.sources[sourceId]; var z = parts[3] | 0, x = parts[4] | 0, y = parts[5].split('.')[0] | 0, format = parts[5].split('.')[1]; //从mbtiles文件获取瓦片 source.getTile(z, x, y, function(err, data, headers) { //如果获取错误,就返回纯色空瓦片 if (err) { createEmptyResponse(sourceInfo.format, sourceInfo.color, callback); return; } var response = { }; if (headers['Last-Modified']) { response.modified = new Date(headers['Last-Modified']); } if (format == 'pbf') { try { response.data = zlib.unzipSync(data); } catch (err) { console.log("Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf", id, z, x, y); } if (options.dataDecoratorFunc) { response.data = options.dataDecoratorFunc( sourceId, 'data', response.data, z, x, y); } } else { response.data = data; } callback(null, response); }); } else if (protocol == 'http' || protocol == 'https') { //对于一切网络资源(字体,雪碧图,地理数据),都使用请求模式 request({ url: req.url, encoding: null, gzip: true }, function(err, res, body) { var parts = url.parse(req.url); var extension = path.extname(parts.pathname).toLowerCase(); var format = extensionToFormat[extension] || ''; if (err || res.statusCode < 200 || res.statusCode >= 300) { //出错就返回空透明的瓦片(对应地图瓦片请求)或空流(对应字体,雪碧图等) createEmptyResponse(format, '', callback); return; } var response = { }; if (res.headers.modified) { response.modified = new Date(res.headers.modified); } if (res.headers.expires) { response.expires = new Date(res.headers.expires); } if (res.headers.etag) { response.etag = res.headers.etag; } response.data = body; callback(null, response); }); } } }); //引擎加载样式文件 renderer.load(styleJSON); createCallback(null, renderer); }; //advancedPool提供了一种资源调度池: //只能创建给定范围内数量的对象,多了就会排队等池中的资源可用为之 //可以分配计算资源 return new advancedPool.Pool({ min: min, max: max, create: createRenderer.bind(null, ratio), destroy: function(renderer) { renderer.release(); } });};
把资源初始化和引擎调用打包成一个promise:
var renderersReadyPromise = Promise.all(queue).then(function() { //标准dpi缩放和2倍缩放最常用,所以默认给更多的资源 var minPoolSizes = options.minRendererPoolSizes || [8, 4, 2]; var maxPoolSizes = options.maxRendererPoolSizes || [16, 8, 4]; for (var s = 1; s <= maxScaleFactor; s++) { var i = Math.min(minPoolSizes.length - 1, s - 1); var j = Math.min(maxPoolSizes.length - 1, s - 1); var minPoolSize = minPoolSizes[i]; var maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]); //每个dpi级别的渲染都是权重不同的一个资源池 map.renderers[s] = createPool(s, minPoolSize, maxPoolSize); } });
至此,初始化就告一段落了.
如何将对应经纬度的瓦片渲染出来是首要解决的问题:
var respondImage = function(z, lon, lat, bearing, pitch, width, height, scale, format, res, next, opt_overlay) { //参数验证 if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06 || lon != lon || lat != lat) { return res.status(400).send('Invalid center'); } if (Math.min(width, height) <= 0 || Math.max(width, height) * scale > (options.maxSize || 2048) || width != width || height != height) { return res.status(400).send('Invalid size'); } //格式验证 if (format == 'png' || format == 'webp') { } else if (format == 'jpg' || format == 'jpeg') { format = 'jpeg'; } else { return res.status(400).send('Invalid format'); } //从特定的资源池获取瓦片 var pool = map.renderers[scale]; pool.acquire(function(err, renderer) { var mbglZ = Math.max(0, z - 1); var params = { zoom: mbglZ, center: [lon, lat], bearing: bearing, pitch: pitch, width: width, height: height }; //当0级时自动放大2倍,否则就太小了 if (z == 0) { params.width *= 2; params.height *= 2; } //按照参数渲染 renderer.render(params, function(err, data) { //完成后释放资源,让给下一个 pool.release(renderer); if (err) { console.error(err); return; } //生成的二进制流渲染成对应格式与尺寸 var image = sharp(data, { raw: { width: params.width * scale, height: params.height * scale, channels: 4 } }); if (z == 0) { //当0级时,调整图像为512x256,因为这时是一个长条状的世界地图 image.resize(width * scale, height * scale); } //有背景就渲染背景 if (opt_overlay) { image.overlayWith(opt_overlay); } //用node-Canvas绘制文字水印 if (watermark) { var canvas = new Canvas(scale * width, scale * height); var ctx = canvas.getContext('2d'); ctx.scale(scale, scale); ctx.font = '10px sans-serif'; ctx.strokeWidth = '1px'; ctx.strokeStyle = 'rgba(255,255,255,.4)'; ctx.strokeText(watermark, 5, height - 5); ctx.fillStyle = 'rgba(0,0,0,.4)'; ctx.fillText(watermark, 5, height - 5); image.overlayWith(canvas.toBuffer()); } //输出图像 var formatQuality = (params.formatQuality || { })[format] || (options.formatQuality || { })[format]; if (format == 'png') { image.png({ adaptiveFiltering: false}); } else if (format == 'jpeg') { image.jpeg({ quality: formatQuality || 80}); } else if (format == 'webp') { image.webp({ quality: formatQuality || 90}); } image.toBuffer(function(err, buffer, info) { if (!buffer) { return res.status(404).send('Not found'); } res.set({ 'Last-Modified': lastModified, 'Content-Type': 'image/' + format }); return res.status(200).send(buffer); }); }); }); };
接下来是它的调用:
var tilePattern = '/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+)' + ':scale(' + scalePattern + ')?\.:format([\\w]+)';//挂载路由到瓦片请求上app.get(tilePattern, function(req, res, next) { var modifiedSince = req.get('if-modified-since'), cc = req.get('cache-control'); //允许使用浏览器缓存 if (modifiedSince && (!cc || cc.indexOf('no-cache') == -1)) { if (new Date(lastModified) <= new Date(modifiedSince)) { return res.sendStatus(304); } } var z = req.params.z | 0, x = req.params.x | 0, y = req.params.y | 0, scale = getScale(req.params.scale), format = req.params.format; if (z < 0 || x < 0 || y < 0 || z > 20 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) { return res.status(404).send('Out of bounds'); } var tileSize = 256; //行列号转坐标 //再用像素坐标转经纬度 //求出中心点 //相当于((x + 0.5) / Math.pow(2,z)) *256* Math.pow(2,z) var tileCenter = mercator.ll([ ((x + 0.5) / (1 << z)) * (256 << z), ((y + 0.5) / (1 << z)) * (256 << z) ], z); return respondImage(z, tileCenter[0], tileCenter[1], 0, 0, tileSize, tileSize, scale, format, res, next);});
还支持静态瓦片的渲染,由于不常用,暂且不提.
tile_server
还提供了每种样式的元数据接口供第三方查看调用:
var tileJSON = { 'tilejson': '2.0.0', 'name': styleJSON.name, 'attribution': '', 'minzoom': 0, 'maxzoom': 20, 'bounds': [-180, -85.0511, 180, 85.0511], 'format': 'png', 'type': 'baselayer'};Object.assign(tileJSON, params.tilejson || { });tileJSON.tiles = params.domains || options.domains;//修改tilejson的四至为真实值utils.fixTileJSONCenter(tileJSON);//挂载到路由上app.get('/' + id + '.json', function(req, res, next) { var info = clone(tileJSON); //动态生成该样式的调用地址 info.tiles = utils.getTileUrls(req, info.tiles,'styles/' + id, info.format,ublicUrl); return res.send(info);});
至此,庞大的函数就真正完成了使命:
//等待一切初始化完成后返回已经将各种方法挂载完毕的app对象return Promise.all([fontListingPromise, renderersReadyPromise]).then(function() { return app;});
转载地址:http://tgqws.baihongyu.com/