How it works(13) Tileserver-GL源码阅读(B) 栅格瓦片的渲染

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;
});
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值