关于mapbox:mapboxgl-互联网地图纠偏插件二

13次阅读

共计 7718 个字符,预计需要花费 20 分钟才能阅读完成。

前段时间写的 mapboxgl 互联网地图纠偏插件(一)存在地图旋转时瓦片错位的问题。

这次没有再跟 mapboxgl 的变换矩阵较劲,而是另辟蹊径应用 mapboxgl 的自定义图层,从新写了一套加载瓦片的办法来实现地图纠偏。

上面把我这次打怪降级的心路历程分享一下,或者对你也有启发。

文中波及一些 webgl 的常识细节,没有接触过 webgl 的同学,能够参考看上一次给大家举荐的电子书《WebGL 编程指南》,这次再附上一个蕴含书中所有示例的 github 库,会很有帮忙。

书接上回

在钻研偏移矩阵问题束手无策时,发现用天地图的栅格瓦片没有偏移的问题,因为天地图是大地 2000 坐标,能够间接在 wgs84 坐标地图上应用,根本没有误差。

尝试后感觉,能够倒是能够,但就是配色有点丑,能够先作为一个保底计划,高德瓦片的纠偏还要持续钻研。

话说《WebGL 编程指南》这本书看完后,始终想写个读书笔记,但又感觉光写笔记太干燥,就想着联合地图看无能点啥。

mapboxgl 通过自定图层接口反对 webgl 的扩大,这个接口的益处是,对简单的变换矩阵进行了封装,对外应用大家相熟的 web 墨卡托坐标,并提供了经纬度坐标和 web 墨卡托坐标转换的接口。

查看 mapboxgl 的官网示例时,忽然来了灵感,能够用这个接口本人写个加载栅格瓦片的程序,这样就能绕开 mapboxgl 简单的框架,更容易实现对瓦片纠偏,呈现问题也更好解决,对整体更有掌控感。

技术路线剖析:

用这个思路来实现纠偏,要搞定两大问题,一个是如何用 webgl 实现显示瓦片的性能,另一个是如何计算瓦片在屏幕上的显示地位。

如何用 webgl 显示瓦片

在 webgl 中,图形的根底是三角形,要绘制正方形的瓦片,须要用两个三角形拼成一个正方形,再把图片贴到这个正方形上,就能实现地图瓦片的显示。这个过程中,图片被称为纹理,贴图被称为纹理贴图。实现成果如下(图片地位是轻易写的):

这里有两点要留神:

1、要留神图片的跨域问题,须要通过设置图片的跨域属性来解决。

2、要留神顶点坐标的程序,正确的程序为:左上、左下、右上、右下,不然图片会像穿衣服一样,各种穿反,前后反,左右反

外围代码如下:

    var picLoad = false;
    var tileLayer = {
        id: 'tileLayer',
        type: 'custom',

        // 增加图层时调用
        onAdd: function (map, gl) {var vertexSource = ""+"uniform mat4 u_matrix;"+"attribute vec2 a_pos;"+"attribute vec2 a_TextCoord;"+"varying vec2 v_TextCoord;"+"void main() {"+"   gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0);"+"   v_TextCoord = a_TextCoord;"+"}";

            var fragmentSource = ""+"precision mediump float;"+"uniform sampler2D u_Sampler; "+"varying vec2 v_TextCoord; "+"void main() {"+"    gl_FragColor = texture2D(u_Sampler, v_TextCoord);"+"}";

            // 初始化顶点着色器
            var vertexShader = gl.createShader(gl.VERTEX_SHADER);
            gl.shaderSource(vertexShader, vertexSource);
            gl.compileShader(vertexShader);
            // 初始化片元着色器
            var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
            gl.shaderSource(fragmentShader, fragmentSource);
            gl.compileShader(fragmentShader);
            // 初始化着色器程序
            var program = this.program = gl.createProgram();
            gl.attachShader(this.program, vertexShader);
            gl.attachShader(this.program, fragmentShader);
            gl.linkProgram(this.program);

            
            // 获取顶点地位变量
            var a_Pos = gl.getAttribLocation(this.program, "a_pos");
            var a_TextCoord = gl.getAttribLocation(this.program, 'a_TextCoord');
            // 设置图形顶点坐标
            var leftTop = mapboxgl.MercatorCoordinate.fromLngLat({lng: 110,lat: 40});
            var rightTop = mapboxgl.MercatorCoordinate.fromLngLat({lng: 120,lat: 40});
            var leftBottom = mapboxgl.MercatorCoordinate.fromLngLat({lng: 110,lat: 30});
            var rightBottom = mapboxgl.MercatorCoordinate.fromLngLat({lng: 120,lat: 30});
            // 顶点坐标放入 webgl 缓冲区中
            var attrData = new Float32Array([
                leftTop.x, leftTop.y, 0.0, 1.0,
                leftBottom.x, leftBottom.y, 0.0, 0.0,
                rightTop.x, rightTop.y, 1.0, 1.0,
                rightBottom.x, rightBottom.y, 1.0, 0.0
            ])
            var FSIZE = attrData.BYTES_PER_ELEMENT;
            this.buffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
            gl.bufferData(gl.ARRAY_BUFFER, attrData, gl.STATIC_DRAW);
            // 设置从缓冲区获取顶点数据的规定
            gl.vertexAttribPointer(a_Pos, 2, gl.FLOAT, false, FSIZE * 4, 0);
            gl.vertexAttribPointer(a_TextCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
            // 激活顶点数据缓冲区
            gl.enableVertexAttribArray(a_Pos);
            gl.enableVertexAttribArray(a_TextCoord);

            var _this = this;
            var img = this.img = new Image();
            img.onload = () => {
                 // 创立纹理对象
                 _this.texture = gl.createTexture();
                // 向 target 绑定纹理对象
                gl.bindTexture(gl.TEXTURE_2D, _this.texture);
                // 对纹理进行 Y 轴反转
                gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
                // 配置纹理图像
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.img);

                picLoad = true;
            };
            img.crossOrigin = true;    // 设置容许跨域
            img.src = "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x=843&y=386&z=10";
        },

        // 渲染,地图界面变动时会调用这个办法, 会调用若干次(变动时的每一帧都调用)render: function (gl, matrix) {if(picLoad){
                // 利用着色程序
                // 必须写到这里,不能写到 onAdd 中,不然 gl 中的着色程序可能不是下面写的,会导致上面的变量获取不到
                gl.useProgram(this.program);

                // 向 target 绑定纹理对象
                gl.bindTexture(gl.TEXTURE_2D, this.texture);
                // 开启 0 号纹理单元
                gl.activeTexture(gl.TEXTURE0);
                // 配置纹理参数
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
                // 获取纹理的存储地位
                var u_Sampler = gl.getUniformLocation(this.program, 'u_Sampler');
                // 将 0 号纹理传递给着色器
                gl.uniform1i(u_Sampler, 0);                

                // 给地位变换矩阵赋值
                gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);
                // 绘制图形
                gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
            }            
        }
    };

    map.on('load', function () {map.addLayer(tileLayer);
    });

下面是加载一个瓦片,上面看一下如何加载多个瓦片,这个问题看似简略,但对于 webgl 不相熟的同学有可能会走弯路,我本人在钻研时,就遇到了上面几个问题:

第一个问题:

自定义图层必须要有 onAdd 办法和 render 办法,onadd办法在加载图层时会被调用一次,render办法在地图平移、缩放、旋转时会被调用若干次,来实现平滑过渡的成果。

那么问题来了,哪些 webgl 代码应该放在 onadd 中,哪些应该放在 render 中?

上面是 webglfundamentals 网站给出的解释,在这里,onadd办法就是初始化阶段,render办法就是渲染阶段。

第二个问题:

顶点坐标是一个瓦片用一个缓冲区,还是所有坐标都放在一个缓冲区中,而后定义规定来取?

答案是,一个瓦片就是一个独立的图形,一个图形对应一套本人的顶点坐标,坐标前面能够跟渲染相干的属性,如色彩、纹理坐标等,但多个图形的顶点坐标不能放在一起,放一起会被绘制为一个图形。多个图形应该应用多个缓冲区对象别离存储本人的顶点坐标。

第三个问题:

如何应用多个缓冲区?

webgl 是面向过程式的,平时用惯了面向对象的开发语言,刚接触这个时有点不适应,起初就缓缓相熟了。

咱们能够把 webgl 的设想成一台老式的机械印刷机,它依据模板印刷,一次只能只应用一个模板,如果想要印刷出多个不同的图案,就须要筹备多个不同图案的模板,而后在印刷时一直的更换模板。

webgl 中的着色器、缓冲区对象、纹理对象三者的组合就像是这个模板,模板和它们都蕴含了绘制图形的参数。更换模板,就是在更换着色器、缓冲区对象和纹理对象,不同的是,相比印刷机,电脑中切换这些只是一瞬间的事件,工夫能够忽略不计。

webgl 在理论工作时就是像下面的印刷机一样在不停的更换模板而后印刷,再更换模板再印刷,直到全副图像绘制实现,整个过程也是一瞬间的事件。

在 webgl 中,”印刷的机器“只有一个,但“模板”你能够创立很多,它的下限取决于你的电脑性能。

咱们要做的就是为每一个瓦片创立一个“模板”,而后在绘制时动静切换这些“模板”。

下面三个问题搞明确当前,我胜利的加载了 2 个瓦片。效果图:

如何计算瓦片在屏幕上的显示地位

外围还是用的上篇文章中提到的经纬度和瓦片编号互转算法

原理是:先获取以后显示范畴四个角的经纬度,再依据互转算法计算出四个角对应的瓦片编号,这样就能统计出以后地图范畴所有瓦片的瓦片编号。

而后遍历以后范畴内的所有瓦片编号。

遍历时,依据互转算法,将遍历到的每个瓦片编号转为瓦片左上角的经纬度,再用它相邻的右方、下方、右下方 3 个瓦片的左上角经纬度,组成瓦片的 4 个顶点坐标。

在这一步退出对顶点坐标的纠偏算法,实现对瓦片的纠偏。

最初再去监听地图扭转的事件,当地图产生平移、缩放、旋转时都要反复下面的计算,更新瓦片。

这里遇到个问题:纠偏后也呈现了上一篇中边缘空白的状况。于是对下面的算法优化了一下,在获取到以后显示范畴的四个角经纬度坐标后,对这 4 个坐标也进行纠偏,这样问题就解决了。

当初瓦片的地图的框架搭起来了,也可能浏览查看瓦片地图了,这一刻还真有点小兴奋的呢

但和最终想要的成果还有些差距,还有很多细节须要优化

细节优化

1、缓存瓦片

把申请过的瓦片放到存到变量中,这样申请过的瓦片能够防止反复申请,显示速度会更快,体验更好。

2、缓存网格经纬度

对立计算瓦片网格的经纬度并缓存起来,免得每次都进行反复计算。

3、瓦片加载的程序从两头向周围

当初的程序是从左到右,有种刷屏的感觉,须要对瓦片编号排一下序,让凑近两头的先加载,凑近边缘的后加载。

4、个别瓦片不显示问题

每次地图范畴变换时,为了实现平滑的成果 render 办法会被执行几十次,工夫大略在 1 秒左右,

如果瓦片不能在这期间加载实现,就会被落下,导致不显示。

须要把最初一次执行 render 办法时的 matrix 变换矩阵记录下来,在瓦片加载实现后被动调用 render 办法绘制。

5、影像图注记白底的问题

在加载影像图时,影像和注记是离开的,须要叠加显示,注记层在没有文字的中央是通明的。

但叠加到一起当前注记层在本该是通明的中央却是不通明的红色

起因一,因为在读取纹理像素数据时的配置有问题,要应用 gl.RGBA,如果应用的是gl.RGB 丢掉了透明度A,就会缺失透明度信息,导致不通明。

起因二,因为在绘制前没有对 webgl 开启阿尔法混合(阿尔法在这里能够了解为透明度),在 webgl 中如果要实现通明成果,这个选项是必须要开启的。

解决后的成果:

6、影像图注记白底的问题还是会偶然呈现

按上一条批改后,白底问题呈现的频率明显降低,但偶然还是会呈现。

钻研法则,当注记瓦片加载的工夫稍长时就会呈现,呈现后,只有稍稍拖动一下地图就会失常,曾经浏览过的区域没有这个问题。

揣测,影像和注记是分图层绘制,当个别注记瓦片加载的工夫长,去被动调用 render 办法从新绘制时,注记的图层会全部重绘,但影像图层不会绘制,这可能就导致两个图层无奈动静的混合。

目前的解决办法是,对于注记图层如果加载慢了,就不被动调用 render 办法从新绘制了。因为缺一小块注记不影响大局,而且下一步操作时它也会主动变失常。

地图抖动问题

一些列优化实现后,当初地图也纠偏了,旋转时也不再错位了,原本认为程序曾经很完满了,但当我叠上我的项目实在数据后,发现了一个很要命的问题,自定义图层在大比例尺时会呈现抖动的问题。

这个问题最开始就留神到了,但没太在意,认为影响不大,但叠加上业务数据后,发现基本没法用,那种感觉就像是,坐到了行驶在乡间小路的拖拉机上,~ 颠 ~ 颠 ~ 颠 ~ 颠 ~ 颠 ~ 颠 ~ 颠 ~

起初还认为是瓦片编号和经纬度互转导致的问题,起初发现 mapboxgl 官网的自定义图层示例也有这个问题,看来是 mapboxgl 的 bug 无疑了。

帮 mapboxgl 找问题,最终定位在了 render 办法的 matrix 变换矩阵上,这个参数是 mapboxgl 传来的,用于将 web 墨卡托坐标转为 webgl 坐标,并对瓦片进行缩放和旋转。

当只对地图进行渺小的平移时,地图会动,matrix矩阵却没有变,matrix矩阵不变,自定义图层也就不会变,当地图平移的范畴加大时,matrix 矩阵才会跟着变。

翻看 mapboxgl 的源码,自定义图层和底图用的不是一个变换矩阵,所以只有自定义图层有问题。

尝试了 mapboxgl 的最新版本 v2.3.1 也有这个问题。

唉!原本认为纠偏这事儿要翻篇儿了,这么看来还要再钻研一阵子了。

启发、思路、感触

在应用自定义图层的过程中有了一些启发,上篇文章中纠偏写在了变换矩阵中,这种写法在地图旋转时会呈现瓦片错位的问题。

本篇文章中纠偏是对 a_pos 变量 web 墨卡托坐标进行纠偏,在旋转时就没有呈现错位的状况。

按这个思路,是不是在上篇文章中,也对 a_pos 变量纠偏,地图旋转时就不会呈现错位问题了?值得一试。

所以,接下来两个思路:一、钻研如何进步自定义图层变换矩阵的精度,让它不再抖动。二、钻研如何对 mapboxgl 源码中的 a_pos 变量进行纠偏。

最初说一下应用 mapboxgl 自定义图层的感触,应用 mapboxgl 自定义图层 + webgl 扩大,就感觉关上了 GIS 世界的另一扇窗户,本人能够去实现各种炫酷高大上的性能了,感觉有了有限可能。

代码、示例

在线示例:http://gisarmory.xyz/blog/index.html?demo=mapboxglMapCorrection2

插件代码:http://gisarmory.xyz/blog/index.html?source=mapboxglMapCorrection2

总结

  1. 这次尝试用 maboxgl 的自定义图层性能,本人写了一个加载互联网瓦片的程序,来实现瓦片纠偏
  2. 本人写加载瓦片的程序要搞定两大问题,一个是如何用 webgl 实现显示瓦片的性能,二个是如何计算瓦片在屏幕上的显示地位
  3. webgl 显示瓦片的原理就是绘制个正方形再给正方形贴图片纹理
  4. 计算瓦片在屏幕上的显示地位,外围是应用瓦片号和经纬度的互转算法,在这个过程中对瓦片进行纠偏
  5. 还要进行一些细节优化,比方瓦片的加载程序等
  6. 最终实现了对高德瓦片进行纠偏,并且旋转时也不会呈现错位的状况
  7. 但这种形式有个问题,mapboxgl 的 render 办法中传过来的变换矩阵的精度不够,在大比例尺时会呈现瓦片抖动的状况,这应该是 mapboxgl 的 bug
  8. 在应用自定义图层的过程中有了一些启发,接下来两个思路:一、钻研如何进步自定义图层变换矩阵的精度。二、钻研如何对 mapboxgl 源码中的 a_pos 变量进行纠偏。
  9. 目前的保底计划是应用天地图的瓦片,高德地图的瓦片还要持续钻研。


原文地址:http://gisarmory.xyz/blog/index.html?blog=mapboxglMapCorrection2

关注《GIS 兵器库》,只给你网上搜不到的 GIS 常识技能。

本文章采纳 常识共享署名 - 非商业性应用 - 雷同形式共享 4.0 国内许可协定 进行许可。欢送转载、应用、从新公布,但务必保留文章署名《GIS 兵器库》(蕴含链接:http://gisarmory.xyz/blog/),不得用于商业目标,基于本文批改后的作品务必以雷同的许可公布。

正文完
 0