大屏地圖:從瓦片到引擎,到手把手實(shí)戰(zhàn)
大廠技術(shù) 堅(jiān)持周更 精選好文
??閱讀本文,你將
了解 mapbox-gl和maplibre-gl這兩款地圖引擎的長短了解 天地圖 這一權(quán)威地圖平臺(tái)的使用 進(jìn)行一個(gè) 瓦片風(fēng) 地圖的開發(fā)實(shí)戰(zhàn)
關(guān)于錘子的隱喻
有人說:
“手里捏著錘子的人,看什么都像釘子。”
雖然有點(diǎn)挖苦的意思,但其實(shí)也可以理解為一種解決問題的方法和思路:“先把各種難題轉(zhuǎn)換為自己熟悉的問題,然后就可以用自己熟悉的方式解決問題了?!?/p>
當(dāng)然,這思路可能有 緣木求魚 的挖苦意味在里面。
但是,有沒有一種可能:
“我手里是一把多功能錘子!”
噢,對于我而言,mapbox-gl/maplibre-gl 就是我面對各種大屏地圖開發(fā)需求的那把 多功能錘子。
讓我們看看這把錘子,究竟如何。
一、地圖引擎的選擇
談到 GIS,就很難繞開 Mapbox 這家公司,畢竟目前世界上最廣泛使用的 矢量瓦片標(biāo)準(zhǔn) MAPBOX CECTOR TILE SPECIFICATION 正是這家公司發(fā)布制定的。
除此之外, mapbox 還提供了非常完全的地理信息服務(wù)、非常多的地圖開發(fā)工具,其中就包括一款在前端開發(fā)者圈中非常熱門的地圖渲染引擎:
mapbox-gl。
這也是我日常進(jìn)行地圖開發(fā),所選擇的地圖引擎。
1.1 認(rèn)識(shí) mapbox-gl
mapbox-gl 是一款開源地圖引擎。
它的 npmjs 地址:https://www.npmjs.com/package/mapbox-gl
它的 github 官網(wǎng):https://github.com/mapbox/mapbox-gl-js
它的使用文檔:https://docs.mapbox.com/mapbox-gl-js/
首先,我們要認(rèn)識(shí)這個(gè)庫,就要認(rèn)識(shí)它的能力和邊界,以下是我的個(gè)人使用總結(jié):
mapbox是一款地圖引擎,它能做什么?
能通過各種投影系進(jìn)行地圖瓦片的投影。 支持在地圖瓦片上疊加各種圖層,支持 geojson、圖片、文本 等多種信息在圖層上進(jìn)行加載顯示。支持自定義 Style(矢量瓦片)支持 2.5D視角旋轉(zhuǎn)及顯示支持加載 3D模型支持通過 DOM的方式添加HTML元素支持 web-gl能力進(jìn)行圖形渲染支持進(jìn)行 3D形式的球星地理渲染和星空背景渲染
尤其是其 "2.5D 視角旋轉(zhuǎn)及顯示"、"加載 3D 模型" 這兩點(diǎn),是非常亮眼的,相比于 OpenLayers 和 Leaflet 這兩款競品,這也是它最為吸引人的地方所在。
但也不能盲目樂觀,我也總結(jié)了使用中感受到不足的點(diǎn):
無法支持 地下管網(wǎng)開挖 這種形式的頁面展示(相比于 Cesium)3D支持上能力比較弱(相比于Cesium)不夠 open
“不夠 open ?” 想必你也有這樣的困惑吧,為什么我會(huì)這樣說?
mapbox-gl開源,但很可惜,它也不是純粹的 開源作品,雖然它確實(shí) 開源。
這得從它的 accessToken 和賬號(hào)注冊 說起。
1.2 使用 mapbox?可能沒那么容易
不久前,我曾在掘金發(fā)過一篇文章介紹 mapbox-gl: 《【一庫】mapbox-gl!一款開箱即用的地圖引擎》
但文章發(fā)布后,卻收到很多小伙伴的反饋:"注冊 mapbox 賬號(hào)居然需要國際信用卡..."
我去試了試:還真是!
這是
mapbox在2022年6月新出的規(guī)定,注冊賬號(hào)必須綁定一張國際信用卡。這個(gè)要求,就讓很多國內(nèi)小伙伴想試用的成本大大提升了。
那么,可能有人就會(huì)問了:“mapbox 不是開源產(chǎn)品嗎?不注冊它們官方的賬號(hào),難道用不了嗎?”問的很好,也很合理。
但是:
抱歉,真的用不了。
納尼?引用一段 stackoverflow.com 上小伙伴對其的評價(jià)吧:
Mapbox have now changed mapbox-gl-js in version 2 to no longer be Open, you will have to have a key going forward.
翻譯一下:
Mapbox 在
[email protected]版本開始,已經(jīng)不再開放。你必須有它家的accessToken才能進(jìn)行下一步。
沒錯(cuò),沒有國際信用卡,不能注冊 mapbox,沒有 mapbox 賬號(hào)用不了 mapbox-gl的 v2 版本。
好家伙,它是懂資本的。

那么?我的意思是:別用 mapbox-gl 了嗎?
并不是,我只是要推薦一下它的孿生弟弟:
maplibre-gl
1.3 maplibre-gl:我比哥哥更開放
如果你想嘗試 mapbox-gl 的各種炫酷能力,但你不想(能)注冊 Mapbox 官網(wǎng)賬號(hào),現(xiàn)在,有了一個(gè)更好的選擇:
maplibre-gl
它的 npmjs 地址:https://www.npmjs.com/package/maplibre-gl
它的 github 官網(wǎng):https://github.com/maplibre/maplibre-gl-js
簡單介紹一下:它就是 mapbox-gl 倉庫 fork 出來的開放版本,無需 accessToken 就能品嘗 mapbox-gl 的強(qiáng)大能力。
其他介紹?不用了,參照本文關(guān)于 mapbox-gl 的相關(guān)介紹即可。
1.4 一個(gè)簡單的選擇原則
到底是用 mapbox-gl 還是使用 maplibre-gl? 我提供一個(gè)我自己的簡單原則:
如果你希望使用 Mapbox官方提供的瓦片服務(wù),那選mapbox-gl就完事了。如果你只是希望使用其地圖引擎的相關(guān)能力,并不打算使用 Mapbox官方的瓦片服務(wù),很好,你可以選擇maplibre-gl這款更加Open的開源引擎。
按照這個(gè)原則,本系列涉及到的各類 Demo 都會(huì)以 maplibre-gl 作為地圖引擎進(jìn)行開發(fā)。
二、 大屏的地圖一般怎么玩?
在各種各樣場景的大屏開發(fā)中,關(guān)于地圖的展示,一般存在兩種常見的玩法:
線框風(fēng)格 地圖

瓦片風(fēng)格 地圖

一款大屏到底選取哪種風(fēng)格作為地圖樣式,通常是由 業(yè)務(wù)特點(diǎn) 決定的:
如果業(yè)務(wù)方并不在意具體的業(yè)務(wù)地理位置,只在乎自己在每個(gè)省的營收關(guān)系、投資情況等粗粒度的數(shù)據(jù)展示及分析,那天然適合 線框風(fēng) 地圖。沒有瓦片帶來的地理信息細(xì)節(jié)干擾,展示上也更加清爽明白。
但如果業(yè)務(wù)方非常在意實(shí)際的地理業(yè)務(wù)數(shù)據(jù),關(guān)心自己的轄區(qū)在
XX街道XX區(qū)域,區(qū)域與區(qū)域之間的關(guān)聯(lián),事件在地理位置上的準(zhǔn)確顯示,那則適合選用 瓦片風(fēng) 地圖,提供精準(zhǔn)的參考和地理信息。
maplibre-gl 最擅長的便是 瓦片風(fēng)格 的地圖,但不必?fù)?dān)心,作為一款 多功能錘子,它也能輕松駕馭 線框風(fēng) 的地圖場景。
三、通過 "天地圖" 獲取在線瓦片服務(wù)
"天地圖" 是由 "國家基礎(chǔ)地理信息中心" 提供的一個(gè)地理信息服務(wù)平臺(tái)。
通過 "天地圖",我們能夠獲得免費(fèi)、權(quán)威的地理信息數(shù)據(jù),也是很多人獲取地圖瓦片的首選方案。
官網(wǎng):https://www.tianditu.gov.cn/
注冊完成后,訪問控制臺(tái)(https://console.tianditu.gov.cn/api/key),申請 稱為個(gè)人開發(fā)者,然后注冊一個(gè)應(yīng)用。

這樣,你就能夠獲得一個(gè)自動(dòng)生成的 key(密鑰)。
這個(gè) key 就是你后期請求瓦片的一個(gè)重要憑證。
// 在文本后續(xù)的代碼引用中,我都會(huì)用全局變量 MY_KEY 來代替我申請到的這個(gè) `key`,這是為了避免你圖方便把它用到了項(xiàng)目中。那對你而言是一件危險(xiǎn)的事情。
window.MY_KEY = '88******************2030'
有了這個(gè)密鑰后,訪問 地圖服務(wù)清單(http://lbs.tianditu.gov.cn/server/MapService.html),查看天地圖提供的各類地圖服務(wù):
各類地圖瓦片、標(biāo)注瓦片,應(yīng)有盡有。
通過這些提供的瓦片,你將可以快速搭建一個(gè)完全免費(fèi)、且完全權(quán)威的地圖頁面,并且把業(yè)務(wù)數(shù)據(jù)展示其上。
四、用引擎顯示地圖
3.1 安裝地圖引擎
按照本文第 1.4 節(jié)【一個(gè)簡單的選擇原則】中所說,我們要使用 天地圖 的瓦片,因此我們選用 maplibre-gl:
yarn add maplibre-gl@latest
或者通過 cdn 的形式完成代碼引入。
<script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />
3.1 渲染天地圖瓦片的地圖
在 mapbox 的設(shè)計(jì)思路中,“地圖” 是一個(gè)對象,你可以通過使用如下 API 快速初始化一個(gè)地圖實(shí)例:
<template>
<div ref="mapEl" class="map"></div>
</template>
<script setup>
import mapboxgl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { onMounted, ref } from 'vue'
const mapEl = ref(null)
const initOption = {
style: {
"version": 8,
"id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
"sources": {
"tdt-vec": {
"type": "raster",
"tiles": [`https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${MY_KEY}`],
"tileSize": 256
}
},
"layers": [{
"id": "tdt-tiles-layer",
"type": "raster",
"source": "tdt-vec",
}]
},
}
onMounted(() => {
const map = new mapboxgl.Map({
container: mapEl.value,
...initOption,
});
})
</script>
<style lang="scss" scoped>
.map {
width: 600px;
height: 300px;
}
</style>
通過以上代碼,就能快速渲染一個(gè)基于 墨卡托投影、天地圖瓦片 的平面 瓦片風(fēng) 二維地圖。

發(fā)現(xiàn)沒,不僅可以正確加載天地圖的瓦片服務(wù),還可以完成 2.5D 的視角傾斜。
上面代碼中,所做的,正是簡單生成了一個(gè)地圖實(shí)例,其中最核心的代碼在這里:
"tiles": [`https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${MY_KEY}`],
聲明了天地圖瓦片資源的請求方式。
在碼上掘金中你也可以試試:
代碼片段
效果實(shí)現(xiàn)了,代碼有了,但你想必還是一臉懵逼:
為什么要這么寫呢?
這要說到 mapbox 系框架的基本 API 思路了:圖層與資源。

圖層(
Layers): 我們所能看到的絕大部分內(nèi)容都屬于圖層,這和PhotoShop里的圖層概念很相似,圖層間有層級(jí)關(guān)系;圖層上可以設(shè)置各種布局(layout)屬性和繪制(paint)屬性,用來規(guī)定自己的顯示特點(diǎn)。但歸根結(jié)底,一張圖層上顯示什么,還是取決于它所引用的 **資源(source)**。資源(
Sources): 瓦片是資源,GeoJSON是資源,圖片也是資源。資源是影響顯示的第一要素。
所以,我們可以理解,如果在 mapbox 系中,要顯示一個(gè)內(nèi)容,起碼需要兩步:
// step 1:添加資源
map.addSource(...)
// step 2:添加圖層
map.addLayer(...)
當(dāng)然,上面生效的這段代碼,是通過在初始化階段把 資源 和 圖層 注入到了地圖實(shí)例當(dāng)中,我們完全可以換一種寫法,同樣能實(shí)現(xiàn)相關(guān)功能:
map.on('load', () => {
map.addSource('tdt-vec', {
"type": "raster",
"tiles": [`https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${MY_KEY}`],
"tileSize": 256
})
map.addLayer({
"id": 'tdt-tiles-layer',
"type": "raster",
"source": "tdt-vec",
})
})
思路上是一致,只是添加資源及圖層的時(shí)機(jī)不同罷了。
3.2 添加標(biāo)注層
只有地理瓦片,對于很多人而言依然不足以表達(dá)出足夠的地理信息,比如:
當(dāng)前看到的是什么省、什么市、什么街道?
因此,在一張健全的地圖上,地圖標(biāo)注 也是必要而關(guān)鍵的。
在 3.1 節(jié)示例代碼的基礎(chǔ)上,我們按照解釋說明的思路,再添加 一個(gè)標(biāo)注資源 和 一個(gè)標(biāo)注圖層:
"sources": {
// ... 上一節(jié)內(nèi)容省略
"tdt-cva": {
"type": "raster",
"tiles": [`https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${MY_KEY}`],
"tileSize": 256
}
},
"layers": [
//... 上一節(jié)內(nèi)容省略
{
"id": "tdt-cva-layer",
"type": "raster",
"source": "tdt-cva",
},
]
這樣一來,我們的地圖就不再單調(diào)了:

在碼上掘金里親手嘗試吧:
代碼片段
3.3 對地圖顏色進(jìn)行微調(diào)
通常來說,大屏是以深色作為主色調(diào)的,目前市面上最常見的大屏主題,前三排名為:
科技藍(lán) 科技藍(lán) 還TM是科技藍(lán)
因此,如果地圖底色過于鮮亮,可能會(huì)和 科技藍(lán) 風(fēng)格不搭,此時(shí),你可以選擇通過 layers.raster.paint 提供的一些配置,進(jìn)行色相轉(zhuǎn)換,滿足自己的審美訴求。
比如,修改底圖 layer 為:
{
"id": "tdt-tiles-layer",
"type": "raster",
"source": "tdt-vec",
"paint": {
"raster-brightness-max": 0.7, // 最大亮度
"raster-brightness-min": 0.3, // 最小亮度
"raster-hue-rotate": 20, // 色相變換的角度
"raster-saturation": 0.7 // 飽和度
}
},

如果這種風(fēng)格還不能滿足你的訴求,你可以選擇 "天地圖 影像底圖" 作為背景進(jìn)行展示,修改底圖和標(biāo)注的來源為:
"tdt-vec": {
"type": "raster",
"tiles": [`https://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${MY_KEY}`],
"tileSize": 256
},
"tdt-cva": {
"type": "raster",
"tiles": [`https://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${MY_KEY}`],
"tileSize": 256
}
兩相對比:

很顯然,影像底圖會(huì)具備更好的在大屏上展示的效果。
四、加載業(yè)務(wù)信息
甲方要的不是世界地圖,而是業(yè)務(wù)地圖。
沒有業(yè)務(wù)屬性的地圖,對于甲方而言,并無價(jià)值。
4.1 加載多邊形塊
假設(shè)我在地圖上繪制了兩個(gè)多邊形,形成了一個(gè) FeatureCollection 的 GeoJSON 數(shù)據(jù)。
你問我什么是
GeoJSON? 你是不是還沒看過上一篇基礎(chǔ)知識(shí)篇?看緊去補(bǔ)補(bǔ):《前端開發(fā)大屏地圖?必知必會(huì)的基本知識(shí)》
那么,我應(yīng)該如何把它們在地圖上繪制出來,表現(xiàn)出兩塊區(qū)域的形狀呢?
map.on('style.load', () => {
map.addSource('geojson-area-source', {
type: 'geojson',
data: geojsonArea // 你得到的geojson
})
map.addLayer({
id: 'geojson-area-layer',
type: 'fill',
source: 'geojson-area-source',
layout: {},
paint: {
'fill-color': 'red',
'fill-opacity': 0.5,
},
})
})
沒錯(cuò),就是這么容易,還是我們之前總結(jié)的兩步走:
添加資源 添加圖層

4.2 加載圖標(biāo)及文本
假設(shè),我們現(xiàn)在又 3 位靚仔正在地圖上玩躲貓貓,我們希望標(biāo)注出他們的位置,以及名稱,我們應(yīng)該怎么做?
記住兩步走的法則:先加資源,再加圖層。
資源1:頭像
分別創(chuàng)建了三個(gè)人的頭像:
{
zhuren: 'https://pic.zhangshichun.top/pic/20221129-12.png'
bao: 'https://pic.zhangshichun.top/pic/20221129-10.png'
nan: 'https://pic.zhangshichun.top/pic/20221129-11.png'
}
資源2:三位靚仔的坐標(biāo)和信息
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "德育處主任",
"icon": "zhuren"
},
"geometry": {
"coordinates": [
114.34495622042738,
30.51879704948628
],
"type": "Point"
}
},
{
"type": "Feature",
"properties": {
"name": "戰(zhàn)場小包",
"icon": "bao"
},
"geometry": {
"coordinates": [
114.46248908403493,
30.52385942598788
],
"type": "Point"
}
},
{
"type": "Feature",
"properties": {
"name": "南方者",
"icon": "nan"
},
"geometry": {
"coordinates": [
114.4188340204089,
30.481906063384173
],
"type": "Point"
}
}
]
}
開始編碼!
首先,先定義一個(gè)方法,簡化 maplibre 的掛在圖片的邏輯:
// 注冊圖片的方法
const loadImages = async (imgs) => {
await Promise.all(
Object.entries(imgs).map(
([key, url]) =>
new Promise((resolve) => {
map.loadImage(url, (error, res) => {
if (error) throw error;
map.addImage(key, res);
resolve(res);
});
}),
),
);
};
然后,兩步走(先加資源,再加圖層):
// 加載圖片
await loadImages(images)
// 添加位置資源
map.addSource('boys-source', {
type: 'geojson',
data: boys
})
// 添加ICON圖層
map.addLayer({
id: 'boys-icon-layer',
type: 'symbol',
source: 'boys-source',
layout: {
'icon-image': '{icon}',
'icon-size': 0.2,
'icon-anchor': 'center',
'icon-rotation-alignment': 'viewport',
'icon-allow-overlap': true
}
})
// 添加名字圖層
map.addLayer({
id: 'boys-name-layer',
"type": "symbol",
source: 'boys-source',
"layout": {
"text-field": '{name}',
"text-size": 14,
'text-offset': [0, 2.4], // 名字要設(shè)置便宜,避免被頭像擋住
'text-allow-overlap': true
},
"paint": {
"text-color": "white",
},
})
效果達(dá)成:
可以在碼上掘金里親自嘗試:
代碼片段
總體上來說,業(yè)務(wù)信息的加載,都是同樣的邏輯,只要記住兩步走的基本方針,就能完成絕大多數(shù)的業(yè)務(wù)需求。
五、總結(jié)
在本篇文章,我們系統(tǒng)性地了解了:
mapbox-gl和maplibre-gl兩個(gè)庫的使用范疇。學(xué)習(xí)了天地圖的使用方法 并且實(shí)戰(zhàn)了幾個(gè)簡單的業(yè)務(wù)場景
碰到 瓦片風(fēng) 的大屏地圖開發(fā),想必不會(huì)再難倒你了。
以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。

從零搭建全棧可視化大屏制作平臺(tái)V6.Dooring
點(diǎn)個(gè)在看你最好看

