webgis —— 为瓦片构建缓存

前言

了解 webgis 或接触过 webgis 相关应用的童鞋应该知道,webgis 应用有个特点,加载影像地图或者矢量地图是刚需。

相信对于国内的 gis 行业的从业者来说,天地图应该不是一个陌生的存在吧。

对比其他商业地图(高德、百度、腾讯地图等)而言,天地图有几个明显的优点:

  1. 它是官方出品的地图,权威性较高。
  2. 它更新比较及时,每年都会对部分区域的影像进行更新。
  3. 国内热门地方,影像精度较高,达到亚米级别。
  4. 坐标系采用 wgs84 坐标系,无偏差,无加密。

至于缺点嘛,这里就不做过多的介绍了,用过的童鞋自然会明白。

当然,其实这篇文章的出发点,解决的就是天地图的一大缺点——服务不稳定。

这不,最近很长一段时间,天地图的服务都不给力,经常加载极其缓慢,还动不动会出现响应超时的情况。

如果只是自己用用倒也没啥,但是被老板和客户碰到,就闹心了,接受到过很多次反馈。

虽然,这问题有外部的原因,也并不全是我们自己能解决的,但是有没有什么方法,可以作出一定程度的优化呢?

不然,这个问题,极其的影响 webgis 应用的使用体验。

毕竟,公司的业务都或多或少和 webgis 相关。

思考

对于这种问题,其实业界通常的做法,就是加缓存。

就是,经典的——用空间换时间。

当然,对于我们遇到的这个问题来说,其实有两种解决的思路。

一种是,后端做缓存;一种是,前端做缓存。

当然这两种方案,其实都是针对当前问题所作出的妥协下的无奈。

我们知道,一般而言,无论是使用 cesium 还是 openlayers 等 webgis 框架,直接目的都是为了能够在网页上直接使用地理空间数据。

而地理空间数据有个特点,就是空间范围大,时间跨度长,这一特点导致地理空间数据的数据量量普遍都很大。

所以,虽然公司是专门做 gis 方面应用的,也会定期制作发布一些区域的影像图,但是像天地图这种全球性底图,我们也很难做到自己发布一份来使用。

至于其中原因,说起来很简单,因为做这个事情的成本和收益不对等。

况且天地图服务本身质量不错,又可以免费使用,为什么要吃力不讨好的另起炉灶呢?

后端缓存

如果要采用后端缓存的方案,直接用 nginx 做转发即可。

我们知道,对于天地图而言,这是一个瓦片的请求地址(替换成自己的 token 即可):

https://t2.tianditu.gov.cn/img_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=w&TileMatrix=12&TileRow=1665&TileCol=3413&style=default&format=tiles&tk=你的token

对于不同的瓦片而言,请求变化的只是请求地址里面的几个参数:

  1. TileMatrix——层级数
  2. TileRow——行号
  3. TileCol——列号

所以,了解了这些以后,再稍微研究下 nginx 的缓存策略,想要实现后端对天地图的缓存就很简单了。

在这里,我只做一下简单的介绍,不作详细的赘述,有兴趣的童鞋可以自行研究一下。

前端缓存

有的同学可能会说,有了后端缓存的方案,为什么要研究前端缓存呢?

不要着急,让我们先缕一缕,做个分析对比。

后端缓存,实质上是把天地图服务器上的瓦片资源,缓存到我们自己的服务器上,会增加我们自己服务器的存储和带宽;但是它有个优点是,一旦有了缓存,以后高频访问的地方,就可以不依赖天地图的服务了,并且自己所有的项目都能用。

但是前端缓存也有自己的优势,不占用自己服务器的存储和带宽;一旦有了缓存以后,能支持离线加载,速度更快;每个用户都有自己的小型缓存服务,不会相互影像。

当然前端缓存,也不是没有缺点的,其中一个重要的影响就是,会增加代码的复杂度,对开发人员的要求较高;效果比不上浏览器的默认缓存,稍微会影响一些加载效率;对用户电脑的要求较高。

我们姑且不评价采用哪种方式更好,单纯从解决问题的角度出发,来探讨下如何增加一层前端缓存。

我们知道,用 Window.localStorage - Web APIs | MDN 或者 Window.sessionStorage - Web APIs | MDN 肯定是不行,因为它们的容量都有限制,达不到我们的使用要求。

Web SQL 基本已经处于废弃状态,因此,基本上可以确定,我们能选用的方案就是 Using IndexedDB - Web APIs | MDN

当然 IndexedDB 原生的 api 写起来比较复杂,可以用已经封装好的框架,帮助我们开发,可以参考 mdn 页面的推荐:Using IndexedDB - Web APIs | MDN

当然,这篇文章,不太会专注于怎么把数据存储在 IndexedDB 里面,如果你不了解而刚好又感兴趣的话,可以翻翻博主以前的文章,相信会有一些启发。

既然是与 webgis 相关的文章,那么我们当然要从 cesium、openlayers 这些 webgis 常用的框架入手,介绍,如何在这些框架里面实现瓦片的前端缓存。

openlayers

对于 openlayers 来说,想要实现该功能很简单。

加载 wmts 瓦片地图的时候 ,有一个选项 tileLoadFunction ,支持传入一个自定义的加载函数。

OpenLayers v7.1.0 API - Class: WMTS

我们先来看看,该函数的默认值:

function(imageTile, src) {
  imageTile.getImage().src = src;
};

分析可知,其主要干的事情就是给目标瓦片对象赋予 src 值。

而对于某个瓦片的链接来说,我们可以直接从 url 参数中分析出来所有我们需要的信息,因此我们直接在链接上下文章即可。

比如我们在使用 openlayers 加载天地图时候,该函数的第二个参数 src 可能是下面的值:

https://t2.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL=49&TILEROW=28&TILEMATRIX=6&tk=你的token

那么我们就可以直接用正则或者 URLSearchParams - Web API 接口参考 | MDN 对 url 进行处理,得到我们想要的层级、行列号、图层名等信息,作为存储的唯一标识。

接下来,我们需要通过 ajax 获取到图像的二进制数据,然后通过 URL.createObjectURL() - Web APIs | MDN 将二进制文件转成一个链接,直接赋值给 imageTile.getImage().src 即可。

关键代码如下:

const tileLoadFunction = async(imageTile, src) => {
    // 获取 key
    const cname = this._getKeyName(src);
    // 查询下缓存内是否有该数据
    const data = await getOneByName(cname);
    // 拿到目标 image 对象
    const image = imageTile.getImage();
    image.onload = function onload() {
        // 图片加载完,释放一个URL资源.
        window.URL.revokeObjectURL(this.src);
    };
    // 如果存在缓存,用缓存数据
    if (data) {
        image.src = URL.createObjectURL(data.blob);
    } else {
        // 如果不存在缓存,通过 fetch 加载数据
        const res = await fetch(src);
        const newBlob = await res.blob();
        image.src = URL.createObjectURL(newBlob);
        // 加入缓存
        addOneImage({
            name: cname,
            blob: newBlob
        });
    }
}

cesium

要想在 cesium 中使用前端缓存,实质上和在 openlayers 实际上大同小异。

但是比较棘手的是,cesium 并未直接提供改写瓦片请求的接口,因此我们需要通过重载源码的方式,来实现我们的需求。

我们知道,在 cesium 的架构中,ImageryProvider 是所有图层的容器对象,每个 layer 必定会对应着一个该对象的实例。

分析 cesium 的源码可知, ImageryProvider 对象的 loadImage 方法,是图层中加载每个瓦片的方法。

因此,我们在 loadImage 上下手即可:

// 将 ImageryProvider 对象上的 loadImage 方法重命名
ImageryProvider.loadImage2 = ImageryProvider.loadImage;
// 重载 loadImage 方法
ImageryProvider.loadImage = function loadImage(imageryProvider, url) {
    // 符合我们需求的,调用重载的方法
    if (imageryProvider instanceof WebMapTileServiceImageryProvider) {
        // 返回 promise
        return new Promise((resolve) => {
            // 获取唯一的 key
            const {
                layer,
                tilecol,
                tilematrix,
                tilerow,
            } = url.queryParameters;
            const cname = `${layer} ${tilematrix} ${tilerow} ${tilecol}`;

            // 从缓存里获取数据
            getOneByName(cname).then(async (data) => {
                // 存在数据
                if (data) {
                    const imgUrl = URL.createObjectURL(data.blob);
                    const img = new Image();
                    img.src = imgUrl;
                    img.crossOrigin = 'Anonymous';
                    img.onload = () => {
                        // 直接将图片返回
                        resolve(img);
                    };
                } else {
                    // 不存在数据
                    const resource = Resource.createIfNeeded(url);
                    const newBlob = await resource.fetchImage({
                        preferBlob: true,
                        preferImageBitmap: false,
                        flipY: true,
                    });
                    // 加入缓存
                    addOneImage({
                        name: cname,
                        blob: newBlob.blob
                    });
                    // 将 fetchImage 获取到的对象返回
                    resolve(newBlob);
                }
            });
        });
    }
    // 调用默认的方法
    return ImageryProvider.loadImage2.call(this, imageryProvider, url);
};

后记

就像文章前面所说的那样,无论是前端缓存还是后端缓存,都只是一种辅助措施,一种多方考量下的无奈之举,两者都有自己的缺点和优势。

不过话又说回来,web 应用作手动缓存,给人一种多此一举的感觉。

毕竟很多情况下,浏览器为了加快网页的访问速度,已经尽可能的做了很多事情。

而通常情况下,效率最高的方式,肯定是去遵循浏览器的默认缓存策略,从而使得我们的应用达到最好的使用效果。

奇淫巧计有一定的帮助,但是终究作用还是有限,解决问题的同时,势必会引发新的问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容