博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
How it works(13) Tileserver-GL源码阅读(B) 栅格瓦片的渲染
阅读量:4303 次
发布时间:2019-05-27

本文共 17883 字,大约阅读时间需要 59 分钟。

serve_rendered.js

是什么使tileserver如此的无可替代?

是他的栅格瓦片渲染.

TilestrataTilestache还在用需要复杂配置文件的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}); });}

rendered方法

先来回顾一下,server_render函数是在什么环境下调用的:

server.js

Object.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/

你可能感兴趣的文章
TB创建公式应用dll失败 请检查用户权限,终极解决方案
查看>>
python绘制k线图(蜡烛图)报错 No module named 'matplotlib.finance
查看>>
talib均线大全
查看>>
期货市场技术分析06_长期图表和商品指数
查看>>
期货市场技术分析07_摆动指数和相反意见理论
查看>>
满屏的指标?删了吧,手把手教你裸 K 交易!
查看>>
不吹不黑 | 聊聊为什么要用99%精度的数据回测
查看>>
X 分钟速成 Python
查看>>
对于模拟交易所引发的思考
查看>>
高频交易的几种策略
查看>>
量化策略回测TRIXKDJ
查看>>
量化策略回测唐安奇通道
查看>>
CTA策略如何过滤部分震荡行情?
查看>>
量化策略回测DualThrust
查看>>
量化策略回测BoolC
查看>>
量化策略回测DCCV2
查看>>
mongodb查询优化
查看>>
五步git操作搞定Github中fork的项目与原作者同步
查看>>
git 删除远程分支
查看>>
删远端分支报错remote refs do not exist或git: refusing to delete the current branch解决方法
查看>>