瓦片(Tile)地图研究,适配各种瓦片图服务的方式

2,934 阅读5分钟

前言

由于公司的一些项目,需要使用别人的地图服务,但我们已经使用高德开发好了,换sdk需要修改的代码很多,因此想的是客户端sdk不变,加载服务数据可不可行(如arcgis、天地图)

瓦片图简介

为何叫瓦片,看看下图就明白了,这是其中一张图片,一般是服务端根据数据生成一张大图,然后切片成一张张小图,一般大小是256x256的图片

image.png

这是加载了部分图片的效果,可以明显看到地图是由很多张图片拼接而来的

image.png

下图这个是不同缩放层级,需要切图的数量是指数级的

image.png

但是现在大家截图看看地图请求,可能是像这样的,这个是矢量瓦片,也就是服务端返回的是路网的数据,客户端根据数据实时渲染出来,也就可以做到缩放层级更大

image.png

加载瓦片图

我们使用高德地图,加载一下zoom=2的时候,可以看到左上角是0,0,一个4x4的正方形

image.png

天地图,加载zoom=2,左上角是0,0, 一个4x2的长方形,经纬度投影

image.png

高德加载示例代码:

    var map = new AMap.Map('container', {
        zoom: 14,
        zooms: [0, 11],
    });

    var layer = new AMap.TileLayer.Flexible({
        cacheSize: 300,
        zIndex: 200,
        createTile: function (x, y, z, success, fail) {
            var c = document.createElement('canvas');
            c.width = c.height = 256;

            var cxt = c.getContext("2d");
            cxt.font = "15px Verdana";
            cxt.fillStyle = "#ff0000";
            cxt.strokeStyle = "#FF0000";
            cxt.strokeRect(0, 0, 256, 256);
            cxt.fillText('(' + ['x:'+x, 'y:'+y, 'zoom:'+z].join(',') + ')', 10, 30);

            success(c);
        }
    });

    layer.setMap(map);

加载天地图的图层

var xyzTileLayer = new AMap.TileLayer({
        'getTileUrl': function (x, y, z) {
 
            return 'http://t6.tianditu.gov.cn/vec_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&tk=&TILECOL='+x+'&TILEROW='+y+'&TILEMATRIX='+z;
        },
        'zooms': [2, 20],
        'zIndex': 10
    });
  
   map.add(xyzTileLayer);

使用高德的瓦片图加载功能

但是在zoom=2的时候可以看到,高德地图下边多了2个格子

image.png

这多了2个格子,肯定有什么问题

地图投影

可以看到,天地图有2种投影,为什么切图,一个是正方形,一个是长方形呢?

这需要知道地图投影的概念

由于地球是一个球体,因此展开到平面的时候,肯定会有拉伸

看看下图2种投影方式

经纬度投影

image.png

墨卡托投影

image.png

表示的范围基本上都是北美洲版块,但是拉伸的结果不一样,至于为什么,可网上查询相关投影算法

除了经纬度投影墨卡托投影投影外,还有其他很多种投影,只是用处不一样,不过一般web地图都是这2种

不同的投影方式,左上角原点表示的经纬度肯定不一样,那应该多少呢?

投影坐标范围

对于2种投影算法,我们的平面坐标系范围是多少呢?

墨卡托投影:

x方向:[-20037508.3427892,20037508.3427892] 对应的地球坐标经度[-180,180]

y方向:[-20037508.3427892,20037508.3427892] 对应地球坐标维度[-85.05112877980659,85.05112877980659]

经纬度投影:

x方向:[-180,180] 对应的地球坐标经度[-180,180]

y方向:[-90,90] 对应地球坐标维度[-90,90]

可以看到2种投影范围是不一样的

有了平面坐标系的范围,我们就可以处理 上面提到的多了2个格子问题

转换坐标

上面我们用高德加载天地图的时候,用的是4x4的格子 加载 4x2 的数据 在平面坐标系 下面表示的位置肯定是不一样的,这里我们就需要转换一下

例如我们有一个marker要显示到地图上

    // 创建一个 Marker 实例:
    var marker = new AMap.Marker({
        position: new AMap.LngLat(116.39, 39.9),   // 经纬度对象,
        title: '北京'
    });

20221114_102758 00_00_00-00_00_30.gif

图片中看到,zoom=2时,在高德地图的画布上,北京在墨卡托投影是格子(3,1),而经纬度投影是格子(3,0)

image.png

我们使用高德sdk来显示北京的marker,传的是北京的经纬度,但是加载天地图的时候,我们看到的图像数据是在(3,0),但是当前的坐标直接显示是在(3,1),所以我们需要转换一下

这里2有个问题:

1.zoom=2时,4x4的格子加载的是4x2的数据,zoom=3时,8x8的格子加载的是8x4的数据,相当于格子高度少了一半

2.平面坐标范围表示的不一样

要想把marker正确的显示到天地图的那个位置,需要如下转换

高德地图坐标-> 天地图平面坐标 -> 高德平面坐标 -> 高德地图坐标(新) -> 新坐标 显示marker

image.png

1.计算坐标对应天地图所在的格子(直接计算经纬度坐标在3,0)

2.计算天地图格子对应的高德地图格子(这里也是3,0, 因为原点都是左上角)

3.计算高德格子对应新的经纬度

4.用新的经纬度显示marker

代码示例

    let lnglat = [116.39, 39.9];
    
    //把经纬度转换到 天地图对应 的格子
    let tiandituPx = lnglatToTiandituPx(lnglat)
   
    //由于这里是一样的,所以不需要转换
    let gaodePx = tiandituPx;
    
    //把高德的px坐标转换成经纬度
    let newLnglat  = gaodePxToLnglat(gaodePx)
    
    // 创建一个 Marker 实例:
    var marker = new AMap.Marker({
        position: gaodePx,   //新的 经纬度对象,
        title: '北京'
    });

转换函数

    function lnglatToTiandituPx([lng,lat],zoom){
        //火星坐标转84坐标
        let gcj02 = gcj02towgs84(lng,lat);
        
        //经纬度转平面坐标,由于是一样的,因此不需要转换
        let plane = {x:gcj02.lng,y:gcj02.lat}
        //墨卡托平面坐标范围
       // x方向:`[-180,180]`
       // y方向:`[-90,90]`
        return {
            x:(plane.x+180) / 180 * 2, //转换坐标到[0-1],便于计算
            y:(plane.y+90) / 90 * 2,
        }
    }

    function gaodePxToLnglat({x,y}){
        //x方向:`[-20037508.3427892,20037508.3427892]`
        // y方向:`[-20037508.3427892,20037508.3427892]`
        let x_1 = x*20037508.3427892*2 - 20037508.3427892; //还原墨卡托坐标
        let y_1 = y*20037508.3427892*2 - 20037508.3427892;
        //平面坐标转经纬度
        let lnglat = mercator2lonlat({x:x_1,y:y_1});
        return [lnglat.x,lnglat.y];
    }

墨卡托平面坐标和经纬度坐标互相转换

function mercator2lonlat(mercator){ 
    var lonlat={x:0,y:0};   
    var x = mercator.x/20037508.3427892*180; 
    var y = mercator.y/20037508.3427892*180; 
    y= 180/Math.PI*(2*Math.atan(Math.exp(y*Math.PI/180))-Math.PI/2);
    lonlat.x = x;   
	lonlat.y = y;   
	return lonlat;
}


function lonlat2mercator(lonlat)
{   
    var mercator={x:0,y:0}; 
    var x = lonlat.x *20037508.3427892/180;  
    var y = Math.log(Math.tan((90+lonlat.y)*Math.PI/360))/(Math.PI/180);
    y = y *20037508.3427892/180; 
    mercator.x = x; 
    mercator.y = y; 
    return mercator ; 
} 

总结

我们这里使用了高德加载天地图,加载其他地图也差不多,如arcgis等,都是用瓦片图,web地图一般都是 墨卡托投影和经纬度投影

另外还有一个点,不同的地图可能坐标系不一样,基本上都是地球坐标系——WGS84的一次加密(转换)