关于javascript:从零打造一个Web地图引擎

95次阅读

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

说到地图,大家肯定很相熟,平时应该都应用过百度地图、高德地图、腾讯地图等,如果波及到地图相干的开发需要,也有很多抉择,比方后面的几个地图都会提供一套 js API,此外也有一些开源地图框架能够应用,比方OpenLayersLeaflet 等。

那么大家有没有想过这些地图是怎么渲染进去的呢,为什么依据一个经纬度就能显示对应的地图呢,不晓得没关系,本文会带各位从零实现一个简略的地图引擎,来帮忙大家理解 GIS 基础知识及 Web 地图的实现原理。

选个经纬度

首先咱们去高德地图上选个经纬度,作为咱们前期的地图中心点,关上高德坐标拾取工具,轻易抉择一个点:

笔者抉择了杭州的雷峰塔,经纬度为:[120.148732,30.231006]

瓦片 url 剖析

地图瓦片咱们应用高德的在线瓦片,地址如下:

https://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8

目前各大地图厂商的瓦片服务遵循的规定是有不同的:

谷歌 XYZ 标准:谷歌地图、OpenStreetMap、高德地图、geoq、天地图,坐标原点在左上角

TMS 标准:腾讯地图,坐标原点在左下角

WMTS 标准:原点在左上角,瓦片不是正方形,而是矩形,这个应该是官网规范

百度地图比拟特立独行,投影、分辨率、坐标系都跟其余厂商不一样,原点在经纬度都为 0 的地位,也就是两头,向右为 X 正方向,向上为 Y 正方向

谷歌和 TMS 的瓦片区别能够通过该地址可视化的查看:地图瓦片。

尽管标准不同,但原理根本是统一的,都是把地球投影成一个微小的正方形世界平面图,而后依照四叉树进行分层切割,比方第一层,只有一张瓦片,显示整个世界的信息,所以根本只能看到洲和海的名称和边界线,第二层,切割成四张瓦片,显示信息略微多了一点,以此类推,就像一个金字塔一样,底层分辨率最高,显示的细节最多,瓦片数也最多,顶层分辨率最低,显示的信息很少,瓦片数量绝对也起码:

每一层的瓦片数量计算公式:

Math.pow(Math.pow(2, n), 2)// 行 * 列:2^n * 2^n

十八层就须要 68719476736 张瓦片,所以一套地图瓦片整体数量是十分宏大的。

瓦片切好当前,通过行列号和缩放层级来保留,所以能够看到瓦片地址中有三个变量:xyz

x:行号
y:列号
z:分辨率,个别为 0 -18

通过这三个变量就能够定位到一张瓦片,比方上面这个地址,行号为109280,列号为53979,缩放层级为17

https://webrd01.is.autonavi.com/appmaptile?x=109280&y=53979&z=17&lang=zh_cn&size=1&scale=1&style=8

对应的瓦片为:

对于瓦片的更多信息能够浏览瓦片地图原理。

坐标系简介

高德地图应用的是 GCJ-02 坐标系,也称火星坐标系,由中国国家测绘局在 02 年公布,是在 GPS 坐标(WGS-84 坐标系)根底上经加密后而来,也就是减少了非线性的偏移,让你摸不准实在地位,为了国家平安,国内地图服务商都须要应用GCJ-02 坐标系

WGS-84坐标系是国内通用的规范,EPSG编号为 EPSG:4326,通常 GPS 设施获取到的原始经纬度和国外的地图厂商应用的都是WGS-84 坐标系。

这两种坐标系都是天文坐标系,球面坐标,单位为 ,这种坐标不便在地球上定位,然而不不便展现和进行面积间隔计算,咱们印象中的地图都是立体的,所以就有了另外一种立体坐标系,立体坐标系是通过投影的形式从天文坐标系中转换过去,所以也称为投影坐标系,通常单位为 ,投影坐标系依据投影形式的不同存在多种,在 Web 开发的场景里通常应用的是 Web 墨卡托投影,编号为EPSG:3857,它基于 墨卡托投影 ,把WGS-84 坐标系投影成正方形:

这是通过舍弃了南北 85.051129 纬度 以上的地区实现的,因为它是正方形,所以一个大的正方形能够很不便的被宰割为更小的正方形。

坐标系更具体的信息可参考 GIS 之坐标零碎,EPSG:3857的详细信息可参考 EPSG:3857。

经纬度定位行列号

上一节里咱们简略介绍了一下坐标系,依照 Web 地图的规范,咱们的地图引擎也抉择反对 EPSG:3857 投影,然而咱们通过高德工具获取到的是火星坐标系的经纬度坐标,所以第一步要把经纬度坐标转换为 Web 墨卡托 投影坐标,这里为了简略,先间接把火星坐标当做 WGS-84 坐标,前面再来看这个问题。

转换方法网上一搜就有:

// 角度转弧度
const angleToRad = (angle) => {return angle * (Math.PI / 180)
}

// 弧度转角度
const radToAngle = (rad) => {return rad * (180 / Math.PI)
}

// 地球半径
const EARTH_RAD = 6378137

// 4326 转 3857
const lngLat2Mercator = (lng, lat) => {
    // 经度先转弧度,而后因为 弧度 = 弧长 / 半径,失去弧长为 弧长 = 弧度 * 半径 
    let x = angleToRad(lng) * EARTH_RAD; 
    // 纬度先转弧度
    let rad = angleToRad(lat)
    // 上面我就看不懂了,各位随便。。。let sin = Math.sin(rad)
    let y = EARTH_RAD / 2 * Math.log((1 + sin) / (1 - sin))
    return [x, y]
}

// 3857 转 4326
const mercatorTolnglat = (x, y) => {let lng = radToAngle(x) / EARTH_RAD
    let lat = radToAngle((2 * Math.atan(Math.exp(y / EARTH_RAD)) - (Math.PI / 2)))
    return [lng, lat]
}

3857坐标有了,它的单位是 ,那么怎么转换成瓦片的行列号呢,这就波及到 分辨率 的概念了,即地图上一像素代表理论多少米,分辨率如果能从地图厂商的文档里获取是最好的,如果找不到,也能够简略计算一下(如果应用计算出来的也不行,那就只能求助搜索引擎了),咱们晓得地球半径是 6378137 米,3857坐标系把地球当做正圆球体来解决,所以能够算出地球周长,投影是贴着地球赤道的:

所以投影成正方形的世界平面图后的边长代表的就是地球的周长,后面咱们也晓得了每一层级的瓦片数量的计算形式,而一张瓦片的大小个别是 256*256 像素,所以用地球周长除以开展后的世界平面图的边长就晓得了地图上每像素代表理论多少米:

// 地球周长
const EARTH_PERIMETER = 2 * Math.PI * EARTH_RAD
// 瓦片像素
const TILE_SIZE = 256

// 获取某一层级下的分辨率
const getResolution = (n) => {const tileNums = Math.pow(2, n)
    const tileTotalPx = tileNums * TILE_SIZE
    return EARTH_PERIMETER / tileTotalPx
}

地球周长算进去是 40075016.68557849,能够看到OpenLayers 就是这么计算的:

3857坐标的单位是 ,那么把坐标除以分辨率就能够失去对应的像素坐标,再除以256,就能够失去瓦片的行列号:

函数如下:

// 依据 3857 坐标及缩放层级计算瓦片行列号
const getTileRowAndCol = (x, y, z) => {let resolution = getResolution(z)
    let row = Math.floor(x / resolution / TILE_SIZE)
    let col = Math.floor(y / resolution / TILE_SIZE)
    return [row, col]
}

接下来咱们把层级固定为 17,那么分辨率resolution 就是 1.194328566955879,雷峰塔的经纬度转成3857 的坐标为:[13374895.665697495, 3533278.205310311],应用下面的函数计算出来行列号为:[43744, 11556],咱们把这几个数据代入瓦片的地址里进行拜访:

https://webrd01.is.autonavi.com/appmaptile?x=43744&y=11556&z=17&lang=zh_cn&size=1&scale=1&style=8

一片空白,这是为啥呢,其实是因为原点不一样,43263857 坐标系的原点在赤道和本初子午线相交点,非洲边上的海里,而瓦片的原点在左上角:

再来看下图会更容易了解:

3857坐标系的原点相当于在世界平面图的两头,向右为 x 轴正方向,向上为 y 轴正方向,而瓦片地图的原点在左上角,所以咱们须要依据图上【绿色虚线】的间隔计算出【橙色实线】的间隔,这也很简略,程度坐标就是程度绿色虚线的长度加上世界平面图的一半,垂直坐标就是世界平面图的一半减去垂直绿色虚线的长度,世界平面图的一半也就是地球周长的一半,批改 getTileRowAndCol 函数:

const getTileRowAndCol = (x, y, z) => {
  x += EARTH_PERIMETER / 2     // ++
  y = EARTH_PERIMETER / 2 - y  // ++
  let resolution = getResolution(z)
  let row = Math.floor(x / resolution / TILE_SIZE)
  let col = Math.floor(y / resolution / TILE_SIZE)
  return [row, col]
}

这次计算出来的瓦片行列号为[109280, 53979],代入瓦片地址:

https://webrd01.is.autonavi.com/appmaptile?x=109280&y=53979&z=17&lang=zh_cn&size=1&scale=1&style=8

后果如下:

能够看到雷峰塔进去了。

瓦片显示地位计算

咱们当初能依据一个经纬度找到对应的瓦片,然而这还不够,咱们的指标是要能在浏览器上显示进去,这就须要解决两个问题,一个是加载多少块瓦片,二是计算每一块瓦片的显示地位。

渲染瓦片咱们应用 canvas 画布,模板如下:

<template>
  <div class="map" ref="map">
    <canvas ref="canvas"></canvas>
  </div>
</template>

地图画布容器 map 的大小咱们很容易获取:

// 容器大小
let {width, height} = this.$refs.map.getBoundingClientRect()
this.width = width
this.height = height
// 设置画布大小
let canvas = this.$refs.canvas
canvas.width = width
canvas.height = height
// 获取绘图上下文
this.ctx = canvas.getContext('2d')

地图中心点咱们设在画布两头,另外中心点的经纬度 center 和缩放层级 zoom 因为都是咱们本人设定的,所以也是已知的,那么咱们能够计算出核心坐标对应的瓦片:

// 中心点对应的瓦片
let centerTile = getTileRowAndCol(...lngLat2Mercator(...this.center),// 4326 转 3857
    this.zoom// 缩放层级
)

缩放层级还是设为17,中心点还是应用雷峰塔的经纬度,那么对应的瓦片行列号后面咱们曾经计算过了,为[109280, 53979]

核心坐标对应的瓦片行列号晓得了,那么该瓦片左上角在世界平面图中的像素地位咱们也就晓得了:

// 核心瓦片左上角对应的像素坐标
let centerTilePos = [centerTile[0] * TILE_SIZE, centerTile[1] * TILE_SIZE]

计算出来为[27975680, 13818624]。这个坐标怎么转换到屏幕上呢,请看下图:

核心经纬度的瓦片咱们计算出来了,瓦片左上角的像素坐标也晓得了,而后咱们再计算出核心经纬度自身对应的像素坐标,那么和瓦片左上角的差值就能够计算出来,最初咱们把画布的原点挪动到画布两头(画布默认原点为左上角,x 轴正方向向右,y 轴正方向向下),也就是把核心经纬度作为坐标原点,那么核心瓦片的显示地位就是这个差值。

补充一下将经纬度转换成像素的办法:

// 计算 4326 经纬度对应的像素坐标
const getPxFromLngLat = (lng, lat, z) => {let [_x, _y] = lngLat2Mercator(lng, lat)// 4326 转 3857
  // 转成世界平面图的坐标
  _x += EARTH_PERIMETER / 2
  _y = EARTH_PERIMETER / 2 - _y
  let resolution = resolutions[z]// 该层级的分辨率
  // 米 / 分辨率失去像素
  let x = Math.floor(_x / resolution)
  let y = Math.floor(_y / resolution)
  return [x, y]
}

计算中心经纬度对应的像素坐标:

// 中心点对应的像素坐标
let centerPos = getPxFromLngLat(...this.center, this.zoom)

计算差值:

// 核心像素坐标距核心瓦片左上角的差值
let offset = [centerPos[0] - centerTilePos[0],
    centerPos[1] - centerTilePos[1]
]

最初通过 canvas 来把核心瓦片渲染进去:

// 挪动画布原点到画布两头
this.ctx.translate(this.width / 2, this.height / 2)
// 加载瓦片图片
let img = new Image()
// 拼接瓦片地址
img.src = getTileUrl(...centerTile, this.zoom)
img.onload = () => {
    // 渲染到 canvas
    this.ctx.drawImage(img, -offset[0], -offset[1])
}

这里先来看看 getTileUrl 办法的实现:

// 拼接瓦片地址
const getTileUrl = (x, y, z) => {let domainIndexList = [1, 2, 3, 4]
  let domainIndex =
    domainIndexList[Math.floor(Math.random() * domainIndexList.length)]
  return `https://webrd0${domainIndex}.is.autonavi.com/appmaptile?x=${x}&y=${y}&z=${z}&lang=zh_cn&size=1&scale=1&style=8`
}

这里随机了四个子域:webrd01webrd02webrd03webrd04,这是因为浏览器对于同一域名同时申请的资源是有数量限度的,而当地图层级变大后须要加载的瓦片数量会比拟多,那么平均扩散到各个子域上来申请能够更快的渲染出所有瓦片,缩小排队等待时间,根本所有地图厂商的瓦片服务地址都反对多个子域。

为了不便看到中心点的地位,咱们再额定渲染两条核心辅助线,成果如下:

能够看到中心点的确是雷峰塔,当然这只是渲染了核心瓦片,咱们要的是瓦片铺满整个画布,对于其余瓦片咱们都能够依据核心瓦片计算出来,比方核心瓦片右边的一块,它的计算如下:

// 瓦片行列号,行号减 1,列号不变
let leftTile = [centerTile[0] - 1, centerTile[1]]
// 瓦片显示坐标,x 轴减去一个瓦片的大小,y 轴不变
let leftTilePos = [offset[0] - TILE_SIZE * 1,
    offset[1]
]

所以咱们只有计算出核心瓦片四个方向各须要几块瓦片,而后用一个双重循环即可计算出画布须要的所有瓦片,计算须要的瓦片数量很简略,请看下图:

画布宽高的一半减去核心瓦片占据的空间即可失去该方向残余的空间,而后除以瓦片的尺寸就晓得须要几块瓦片了:

// 计算瓦片数量
let rowMinNum = Math.ceil((this.width / 2 - offset[0]) / TILE_SIZE)// 左
let colMinNum = Math.ceil((this.height / 2 - offset[1]) / TILE_SIZE)// 上
let rowMaxNum = Math.ceil((this.width / 2 - (TILE_SIZE - offset[0])) / TILE_SIZE)// 右
let colMaxNum = Math.ceil((this.height / 2 - (TILE_SIZE - offset[1])) / TILE_SIZE)// 下

咱们把核心瓦片作为原点,坐标为[0, 0],来个双重循环扫描一遍即可渲染出所有瓦片:

// 从上到下,从左到右,加载瓦片
for (let i = -rowMinNum; i <= rowMaxNum; i++) {for (let j = -colMinNum; j <= colMaxNum; j++) {
        // 加载瓦片图片
        let img = new Image()
        img.src = getTileUrl(centerTile[0] + i,// 行号
            centerTile[1] + j,// 列号
            this.zoom
        )
        img.onload = () => {
            // 渲染到 canvas
            this.ctx.drawImage(
                img, 
                i * TILE_SIZE - offset[0], 
                j * TILE_SIZE - offset[1]
            )
        }
    }
}

成果如下:

很完满。

拖动

拖动能够这么思考,后面曾经实现了渲染指定经纬度的瓦片,当咱们按住进行拖动时,能够晓得鼠标滑动的间隔,而后把该间隔,也就是像素转换成经纬度的数值,最初咱们再更新以后中心点的经纬度,并清空画布,调用之前的办法从新渲染,不停重绘造成是在挪动的视觉假象。

监听鼠标相干事件:

<canvas ref="canvas" @mousedown="onMousedown"></canvas>
export default {data(){
        return {isMousedown: false}
    },
    mounted() {window.addEventListener("mousemove", this.onMousemove);
        window.addEventListener("mouseup", this.onMouseup);
    },
    methods: {
        // 鼠标按下
        onMousedown(e) {if (e.which === 1) {this.isMousedown = true;}
        },

        // 鼠标挪动
        onMousemove(e) {if (!this.isMousedown) {return;}
            // ...
        },

        // 鼠标松开
        onMouseup() {this.isMousedown = false;}
    }
}

onMousemove 办法里计算拖动后的核心经纬度及从新渲染画布:

// 计算本次拖动的间隔对应的经纬度数据
let mx = e.movementX * resolutions[this.zoom];
let my = e.movementY * resolutions[this.zoom];
// 把以后中心点经纬度转成 3857 坐标
let [x, y] = lngLat2Mercator(...this.center);
// 更新拖动后的中心点经纬度
center = mercatorToLngLat(x - mx, my + y);

movementXmovementY 属性能获取本次和上一次鼠标事件中的挪动值,兼容性不是很好,不过本人计算该值也很简略,具体请移步 MDN。乘以以后分辨率把 像素 换算成 ,而后把以后中心点经纬度也转成 3857 坐标,偏移本次挪动的间隔,最初再转回 4326 的经纬度坐标作为更新后的中心点即可。

为什么 x 是减,y是加呢,很简略,咱们鼠标向右和向下挪动时间隔是正的,相应的地图会向右或向下挪动,4326坐标系向右和向上为正方向,那么地图向右挪动时,中心点显然是相对来说是向左移了,因为向右为正方向,所以中心点经度方向就是缩小了,所以是减去挪动的间隔,而地图向下挪动,中心点相对来说是向上移了,因为向上为正方向,所以中心点纬度方向就是减少了,所以加上挪动的间隔。

更新完核心经纬度,而后清空画布从新绘制:

// 清空画布
this.clear();
// 从新绘制,renderTiles 办法就是上一节的代码逻辑封装
this.renderTiles();

成果如下:

能够看到曾经凌乱了,这是为啥呢,其实是因为图片加载是一个异步的过程,咱们鼠标挪动过程中,会一直的计算出要加载的瓦片进行加载,然而可能上一批瓦片还没加载实现,鼠标曾经挪动到新的地位了,又计算出一批新的瓦片进行加载,此时上一批瓦片可能加载实现并渲染进去了,然而这些瓦片有些可能曾经被移除画布,不须要显示,有些可能还在画布内,然而应用的还是之前的地位,渲染进去也是不对的,同时新的一批瓦片可能也加载实现并渲染进去,天然导致了最终显示的错乱。

晓得起因就简略了,首先咱们加个缓存对象,因为在拖动过程中,很多瓦片只是地位变了,不须要从新加载,同一个瓦片加载一次,后续只更新它的地位即可;另外再设置一个对象来记录以后画布上应该显示的瓦片,避免不应该呈现的瓦片渲染进去:

{
    // 缓存瓦片
    tileCache: {},
    // 记录以后画布上须要的瓦片
    currentTileCache: {}}

因为须要记录瓦片的地位、加载状态等信息,咱们创立一个瓦片类:

// 瓦片类
class Tile {constructor(opt = {}) {
    // 画布上下文
    this.ctx = ctx
    // 瓦片行列号
    this.row = row
    this.col = col
    // 瓦片层级
    this.zoom = zoom
    // 显示地位
    this.x = x
    this.y = y
    // 一个函数,判断某块瓦片是否应该渲染
    this.shouldRender = shouldRender
    // 瓦片 url
    this.url = ''
    // 缓存 key
    this.cacheKey = this.row + '_' + this.col + '_' + this.zoom
    // 图片
    this.img = null
    // 图片是否加载实现
    this.loaded = false

    this.createUrl()
    this.load()}
    
  // 生成 url
  createUrl() {this.url = getTileUrl(this.row, this.col, this.zoom)
  }

  // 加载图片
  load() {this.img = new Image()
    this.img.src = this.url
    this.img.onload = () => {
      this.loaded = true
      this.render()}
  }

  // 将图片渲染到 canvas 上
  render() {if (!this.loaded || !this.shouldRender(this.cacheKey)) {return}
    this.ctx.drawImage(this.img, this.x, this.y)
  }
    
  // 更新地位
  updatePos(x, y) {
    this.x = x
    this.y = y
    return this
  }
}

而后批改之前的双重循环渲染瓦片的逻辑:

this.currentTileCache = {}// 清空缓存对象
for (let i = -rowMinNum; i <= rowMaxNum; i++) {for (let j = -colMinNum; j <= colMaxNum; j++) {
        // 以后瓦片的行列号
        let row = centerTile[0] + i
        let col = centerTile[1] + j
        // 以后瓦片的显示地位
        let x = i * TILE_SIZE - offset[0]
        let y = j * TILE_SIZE - offset[1]
        // 缓存 key
        let cacheKey = row + '_' + col + '_' + this.zoom
        // 记录画布以后须要的瓦片
        this.currentTileCache[cacheKey] = true
        // 该瓦片已加载过
        if (this.tileCache[cacheKey]) {
            // 更新到以后地位
            this.tileCache[cacheKey].updatePos(x, y).render()} else {
            // 未加载过
            this.tileCache[cacheKey] = new Tile({
                ctx: this.ctx,
                row,
                col,
                zoom: this.zoom,
                x,
                y,
                // 判断瓦片是否在以后画布缓存对象上,是的话则代表须要渲染
                shouldRender: (key) => {return this.currentTileCache[key]
                },
            })
        }
    }
}

成果如下:

能够看到,拖动曾经失常了,当然,上述实现还是很毛糙的,须要优化的中央很多,比方:

1. 个别会先排个序,优先加载核心瓦片

2. 缓存的瓦片越来越多必定也会影响性能,所以还须要一些革除策略

这些问题有趣味的能够自行思考。

缩放

拖动是实时更新中心点经纬度,那么缩放天然更新缩放层级就行了:

export default {data() {
        return {
            // 缩放层级范畴
            minZoom: 3,
            maxZoom: 18,
            // 防抖定时器
            zoomTimer: null
        }
    },
    mounted() {window.addEventListener('wheel', this.onMousewheel)
    },
    methods: {
        // 鼠标滚动
        onMousewheel(e) {if (e.deltaY > 0) {
                // 层级变小
                if (this.zoom > this.minZoom) this.zoom--
            } else {
                // 层级变大
                if (this.zoom < this.maxZoom) this.zoom++
            }
            // 加个防抖,避免疾速滚动加载两头过程的瓦片
            this.zoomTimer = setTimeout(() => {this.clear()
                this.renderTiles()}, 300)
        }
    }
}

成果如下:

性能是有了,不过成果很个别,因为咱们平时应用的地图缩放都是有一个放大或放大的过渡动画,而这个是间接空白而后从新渲染,不认真看都不晓得是放大还是放大。

所以咱们无妨加个过渡成果,当咱们鼠标滚动后,先将画布放大或放大,动画完结后再依据最终的缩放值来渲染须要的瓦片。

画布默认缩放值为 1,放大则在此基础上乘以2 倍,放大则除以 2,而后动画到目标值,动画期间设置画布的缩放值及清空画布,从新绘制画布上的已有瓦片,达到放大或放大的视觉效果,动画完结后再调用renderTiles 从新渲染最终缩放值须要的瓦片。

// 动画应用 popmotion 库,https://popmotion.io/
import {animate} from 'popmotion'

export default {data() {
        return {
            lastZoom: 0,
            scale: 1,
            scaleTmp: 1,
            playback: null,
        }
    },
    methods: {
        // 鼠标滚动
        onMousewheel(e) {if (e.deltaY > 0) {
                // 层级变小
                if (this.zoom > this.minZoom) this.zoom--
            } else {
                // 层级变大
                if (this.zoom < this.maxZoom) this.zoom++
            }
            // 层级未产生扭转
            if (this.lastZoom === this.zoom) {return}
            this.lastZoom = this.zoom
            // 更新缩放比例,也就是指标缩放值
            this.scale *= e.deltaY > 0 ? 0.5 : 2
            // 进行上一次动画
            if (this.playback) {this.playback.stop()
            }
            // 开启动画
            this.playback = animate({
                from: this.scaleTmp,// 以后缩放值
                to: this.scale,// 指标缩放值
                onUpdate: (latest) => {
                    // 实时更新以后缩放值
                    this.scaleTmp = latest
                    // 保留画布之前状态,起因有二:// 1.scale 办法是会在之前的状态上叠加的,比方初始是 1,第一次执行 scale(2,2),第二次执行 scale(3,3),最终缩放值不是 3,而是 6,所以每次缩放完就复原状态,那么就相当于每次都是从初始值 1 开始缩放,成果就对了
                    // 2. 保障缩放成果只对从新渲染已有瓦片失效,不会对最初的 renderTiles()造成影响
                    this.ctx.save()
                    this.clear()
                    this.ctx.scale(latest, latest)
                    // 刷新以后画布上的瓦片
                    Object.keys(this.currentTileCache).forEach((tile) => {this.tileCache[tile].render()})
                    // 复原到画布之前状态
                    this.ctx.restore()},
                onComplete: () => {
                    // 动画实现后将缩放值重置为 1
                    this.scale = 1
                    this.scaleTmp = 1
                    // 依据最终缩放值从新计算须要的瓦片并渲染
                    this.renderTiles()},
            })
        }
    }
}

成果如下:

尽管成果还是个别,不过至多能看进去是在放大还是放大。

坐标系转换

后面还遗留了一个小问题,即咱们把高德工具上选出的经纬度间接当做 4326 经纬度,后面也讲过,它们之间是存在偏移的,比方手机 GPS 获取到的经纬度个别都是 84 坐标,间接在高德地图显示,会发现和你理论地位不一样,所以就须要进行一个转换,有一些工具能够帮你做些事件,比方 Gcoord、coordtransform 等。

总结

上述成果看着比拟个别,其实只有在下面的根底上略微加一点瓦片的淡出动画,成果就会好很多,目前个别都是应用 canvas 来渲染 2D 地图,如果本人实现动画不太不便,也有一些弱小的 canvas 库能够抉择,笔者最初应用 Konva.js 库重做了一版,退出了瓦片淡出动画,最终成果如下:

另外只有搞清楚各个地图的瓦片规定,就能稍加批改反对更多的地图瓦片:

具体实现限于篇幅不再开展,有趣味的能够浏览本文源码。

本文具体的介绍了一个简略的 web 地图开发过程,上述实现原理仅是笔者的集体思路,不代表 openlayers 等框架的原理,因为笔者也是 GIS 的初学者,所以难免会有问题,或更好的实现,欢送指出。

在线demo:https://wanglin2.github.io/web_map_demo/

残缺源码:https://github.com/wanglin2/web_map_demo

正文完
 0