說到地圖,大家一定很熟悉,平時應該都使用過百度地圖、高德地圖、騰訊地圖等,如果涉及到地圖相關的開發需求,也有很多選擇,比如前面的幾個地圖都會提供一套js API,此外也有一些開源地圖框架可以使用,比如OpenLayers、Leaflet等,
那么大家有沒有想過這些地圖是怎么渲染出來的呢,為什么根據一個經緯度就能顯示對應的地圖呢,不知道沒關系,本文會帶各位從零實作一個簡單的地圖引擎,來幫助大家了解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張瓦片,所以一套地圖瓦片整體數量是非常龐大的,
瓦片切好以后,通過行列號和縮放層級來保存,所以可以看到瓦片地址中有三個變數:x、y、z
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

一片空白,這是為啥呢,其實是因為原點不一樣,4326和3857坐標系的原點在赤道和本初子午線相交點,非洲邊上的海里,而瓦片的原點在左上角:

再來看下圖會更容易理解:

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 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 = https://www.cnblogs.com/wanglinmantan/p/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`
}
這里隨機了四個子域:webrd01、webrd02、webrd03、webrd04,這是因為瀏覽器對于同一域名同時請求的資源是有數量限制的,而當地圖層級變大后需要加載的瓦片數量會比較多,那么均勻分散到各個子域下去請求可以更快的渲染出所有瓦片,減少排隊等待時間,基本所有地圖廠商的瓦片服務地址都支持多個子域,
為了方便看到中心點的位置,我們再額外渲染兩條中心輔助線,效果如下:

可以看到中心點確實是雷峰塔,當然這只是渲染了中心瓦片,我們要的是瓦片鋪滿整個畫布,對于其他瓦片我們都可以根據中心瓦片計算出來,比如中心瓦片左邊的一塊,它的計算如下:
// 瓦片行列號,行號減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 = https://www.cnblogs.com/wanglinmantan/p/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: {
// 滑鼠按下
onm ousedown(e) {
if (e.which === 1) {
this.isMousedown = true;
}
},
// 滑鼠移動
onm ousemove(e) {
if (!this.isMousedown) {
return;
}
// ...
},
// 滑鼠松開
onm ouseup() {
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);
movementX和movementY屬性能獲取本次和上一次滑鼠事件中的移動值,兼容性不是很好,不過自己計算該值也很簡單,詳細請移步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 = https://www.cnblogs.com/wanglinmantan/p/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: {
// 滑鼠滾動
onm ousewheel(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: {
// 滑鼠滾動
onm ousewheel(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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/415297.html
標籤:JavaScript
