大屏地图:从瓦片到引擎,到手把手实战
共 15870字,需浏览 32分钟
·
2023-01-09 14:01
大厂技术 坚持周更 精选好文
📖阅读本文,你将
了解 mapbox-gl
和maplibre-gl
这两款地图引擎的长短了解 天地图 这一权威地图平台的使用 进行一个 瓦片风 地图的开发实战
关于锤子的隐喻
有人说:
“手里捏着锤子的人,看什么都像钉子。”
虽然有点挖苦的意思,但其实也可以理解为一种解决问题的方法和思路:“先把各种难题转换为自己熟悉的问题,然后就可以用自己熟悉的方式解决问题了。”
当然,这思路可能有 缘木求鱼 的挖苦意味在里面。
但是,有没有一种可能:
“我手里是一把多功能锤子!”
噢,对于我而言,mapbox-gl/maplibre-gl
就是我面对各种大屏地图开发需求的那把 多功能锤子。
让我们看看这把锤子,究竟如何。
一、地图引擎的选择
谈到 GIS
,就很难绕开 Mapbox
这家公司,毕竟目前世界上最广泛使用的 矢量瓦片标准 MAPBOX CECTOR TILE SPECIFICATION
正是这家公司发布制定的。
除此之外, mapbox
还提供了非常完全的地理信息服务、非常多的地图开发工具,其中就包括一款在前端开发者圈中非常热门的地图渲染引擎:
mapbox-gl
。
这也是我日常进行地图开发,所选择的地图引擎。
1.1 认识 mapbox-gl
mapbox-gl
是一款开源地图引擎。
它的 npmjs
地址:https://www.npmjs.com/package/mapbox-gl
它的 github
官网:https://github.com/mapbox/mapbox-gl-js
它的使用文档:https://docs.mapbox.com/mapbox-gl-js/
首先,我们要认识这个库,就要认识它的能力和边界,以下是我的个人使用总结:
mapbox
是一款地图引擎,它能做什么?
能通过各种投影系进行地图瓦片的投影。 支持在地图瓦片上叠加各种图层,支持 geojson
、图片、文本 等多种信息在图层上进行加载显示。支持自定义 Style
(矢量瓦片)支持 2.5D
视角旋转及显示支持加载 3D
模型支持通过 DOM
的方式添加HTML
元素支持 web-gl
能力进行图形渲染支持进行 3D
形式的球星地理渲染和星空背景渲染
尤其是其 "2.5D
视角旋转及显示"、"加载 3D
模型" 这两点,是非常亮眼的,相比于 OpenLayers
和 Leaflet
这两款竞品,这也是它最为吸引人的地方所在。
但也不能盲目乐观,我也总结了使用中感受到不足的点:
无法支持 地下管网开挖 这种形式的页面展示(相比于 Cesium
)3D
支持上能力比较弱(相比于Cesium
)不够 open
“不够 open
?” 想必你也有这样的困惑吧,为什么我会这样说?
mapbox-gl
开源,但很可惜,它也不是纯粹的 开源作品,虽然它确实 开源。
这得从它的 accessToken
和账号注册 说起。
1.2 使用 mapbox
?可能没那么容易
不久前,我曾在掘金发过一篇文章介绍 mapbox-gl
: 《【一库】mapbox-gl!一款开箱即用的地图引擎》
但文章发布后,却收到很多小伙伴的反馈:"注册 mapbox
账号居然需要国际信用卡..."
我去试了试:还真是!
这是
mapbox
在2022年6月
新出的规定,注册账号必须绑定一张国际信用卡。这个要求,就让很多国内小伙伴想试用的成本大大提升了。
那么,可能有人就会问了:“mapbox
不是开源产品吗?不注册它们官方的账号,难道用不了吗?”问的很好,也很合理。
但是:
抱歉,真的用不了。
纳尼?引用一段 stackoverflow.com
上小伙伴对其的评价吧:
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 在
mapbox-gl-js@2.0
版本开始,已经不再开放。你必须有它家的accessToken
才能进行下一步。
没错,没有国际信用卡,不能注册 mapbox
,没有 mapbox
账号用不了 mapbox-gl
的 v2
版本。
好家伙,它是懂资本的。
那么?我的意思是:别用 mapbox-gl
了吗?
并不是,我只是要推荐一下它的孪生弟弟:
maplibre-gl
1.3 maplibre-gl
:我比哥哥更开放
如果你想尝试 mapbox-gl
的各种炫酷能力,但你不想(能)注册 Mapbox
官网账号,现在,有了一个更好的选择:
maplibre-gl
它的 npmjs
地址:https://www.npmjs.com/package/maplibre-gl
它的 github
官网:https://github.com/maplibre/maplibre-gl-js
简单介绍一下:它就是 mapbox-gl
仓库 fork
出来的开放版本,无需 accessToken
就能品尝 mapbox-gl
的强大能力。
其他介绍?不用了,参照本文关于 mapbox-gl
的相关介绍即可。
1.4 一个简单的选择原则
到底是用 mapbox-gl
还是使用 maplibre-gl
? 我提供一个我自己的简单原则:
如果你希望使用 Mapbox
官方提供的瓦片服务,那选mapbox-gl
就完事了。如果你只是希望使用其地图引擎的相关能力,并不打算使用 Mapbox
官方的瓦片服务,很好,你可以选择maplibre-gl
这款更加Open
的开源引擎。
按照这个原则,本系列涉及到的各类 Demo
都会以 maplibre-gl
作为地图引擎进行开发。
二、 大屏的地图一般怎么玩?
在各种各样场景的大屏开发中,关于地图的展示,一般存在两种常见的玩法:
线框风格 地图
瓦片风格 地图
一款大屏到底选取哪种风格作为地图样式,通常是由 业务特点 决定的:
如果业务方并不在意具体的业务地理位置,只在乎自己在每个省的营收关系、投资情况等粗粒度的数据展示及分析,那天然适合 线框风 地图。没有瓦片带来的地理信息细节干扰,展示上也更加清爽明白。
但如果业务方非常在意实际的地理业务数据,关心自己的辖区在
XX街道XX区域
,区域与区域之间的关联,事件在地理位置上的准确显示,那则适合选用 瓦片风 地图,提供精准的参考和地理信息。
maplibre-gl
最擅长的便是 瓦片风格 的地图,但不必担心,作为一款 多功能锤子,它也能轻松驾驭 线框风 的地图场景。
三、通过 "天地图" 获取在线瓦片服务
"天地图" 是由 "国家基础地理信息中心" 提供的一个地理信息服务平台。
通过 "天地图",我们能够获得免费、权威的地理信息数据,也是很多人获取地图瓦片的首选方案。
官网:https://www.tianditu.gov.cn/
注册完成后,访问控制台(https://console.tianditu.gov.cn/api/key),申请 称为个人开发者,然后注册一个应用。
这样,你就能够获得一个自动生成的 key
(密钥)。
这个 key
就是你后期请求瓦片的一个重要凭证。
// 在文本后续的代码引用中,我都会用全局变量 MY_KEY 来代替我申请到的这个 `key`,这是为了避免你图方便把它用到了项目中。那对你而言是一件危险的事情。
window.MY_KEY = '88******************2030'
有了这个密钥后,访问 地图服务清单(http://lbs.tianditu.gov.cn/server/MapService.html),查看天地图提供的各类地图服务:
各类地图瓦片、标注瓦片,应有尽有。
通过这些提供的瓦片,你将可以快速搭建一个完全免费、且完全权威的地图页面,并且把业务数据展示其上。
四、用引擎显示地图
3.1 安装地图引擎
按照本文第 1.4
节【一个简单的选择原则】中所说,我们要使用 天地图 的瓦片,因此我们选用 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
的设计思路中,“地图” 是一个对象,你可以通过使用如下 API
快速初始化一个地图实例:
<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>
通过以上代码,就能快速渲染一个基于 墨卡托投影、天地图瓦片 的平面 瓦片风 二维地图。
发现没,不仅可以正确加载天地图的瓦片服务,还可以完成 2.5D
的视角倾斜。
上面代码中,所做的,正是简单生成了一个地图实例,其中最核心的代码在这里:
"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}`],
声明了天地图瓦片资源的请求方式。
在码上掘金中你也可以试试:
代码片段
效果实现了,代码有了,但你想必还是一脸懵逼:
为什么要这么写呢?
这要说到 mapbox
系框架的基本 API
思路了:图层与资源。
图层(
Layers
): 我们所能看到的绝大部分内容都属于图层,这和PhotoShop
里的图层概念很相似,图层间有层级关系;图层上可以设置各种布局(layout
)属性和绘制(paint
)属性,用来规定自己的显示特点。但归根结底,一张图层上显示什么,还是取决于它所引用的 **资源(source
)**。资源(
Sources
): 瓦片是资源,GeoJSON
是资源,图片也是资源。资源是影响显示的第一要素。
所以,我们可以理解,如果在 mapbox
系中,要显示一个内容,起码需要两步:
// step 1:添加资源
map.addSource(...)
// step 2:添加图层
map.addLayer(...)
当然,上面生效的这段代码,是通过在初始化阶段把 资源 和 图层 注入到了地图实例当中,我们完全可以换一种写法,同样能实现相关功能:
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",
})
})
思路上是一致,只是添加资源及图层的时机不同罢了。
3.2 添加标注层
只有地理瓦片,对于很多人而言依然不足以表达出足够的地理信息,比如:
当前看到的是什么省、什么市、什么街道?
因此,在一张健全的地图上,地图标注 也是必要而关键的。
在 3.1
节示例代码的基础上,我们按照解释说明的思路,再添加 一个标注资源 和 一个标注图层:
"sources": {
// ... 上一节内容省略
"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": [
//... 上一节内容省略
{
"id": "tdt-cva-layer",
"type": "raster",
"source": "tdt-cva",
},
]
这样一来,我们的地图就不再单调了:
在码上掘金里亲手尝试吧:
代码片段
3.3 对地图颜色进行微调
通常来说,大屏是以深色作为主色调的,目前市面上最常见的大屏主题,前三排名为:
科技蓝 科技蓝 还TM是科技蓝
因此,如果地图底色过于鲜亮,可能会和 科技蓝 风格不搭,此时,你可以选择通过 layers.raster.paint
提供的一些配置,进行色相转换,满足自己的审美诉求。
比如,修改底图 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 // 饱和度
}
},
如果这种风格还不能满足你的诉求,你可以选择 "天地图 影像底图" 作为背景进行展示,修改底图和标注的来源为:
"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
}
两相对比:
很显然,影像底图会具备更好的在大屏上展示的效果。
四、加载业务信息
甲方要的不是世界地图,而是业务地图。
没有业务属性的地图,对于甲方而言,并无价值。
4.1 加载多边形块
假设我在地图上绘制了两个多边形,形成了一个 FeatureCollection
的 GeoJSON
数据。
你问我什么是
GeoJSON
? 你是不是还没看过上一篇基础知识篇?看紧去补补:《前端开发大屏地图?必知必会的基本知识》
那么,我应该如何把它们在地图上绘制出来,表现出两块区域的形状呢?
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,
},
})
})
没错,就是这么容易,还是我们之前总结的两步走:
添加资源 添加图层
4.2 加载图标及文本
假设,我们现在又 3
位靓仔正在地图上玩躲猫猫,我们希望标注出他们的位置,以及名称,我们应该怎么做?
记住两步走的法则:先加资源,再加图层。
资源1:头像
分别创建了三个人的头像:
{
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:三位靓仔的坐标和信息
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "德育处主任",
"icon": "zhuren"
},
"geometry": {
"coordinates": [
114.34495622042738,
30.51879704948628
],
"type": "Point"
}
},
{
"type": "Feature",
"properties": {
"name": "战场小包",
"icon": "bao"
},
"geometry": {
"coordinates": [
114.46248908403493,
30.52385942598788
],
"type": "Point"
}
},
{
"type": "Feature",
"properties": {
"name": "南方者",
"icon": "nan"
},
"geometry": {
"coordinates": [
114.4188340204089,
30.481906063384173
],
"type": "Point"
}
}
]
}
开始编码!
首先,先定义一个方法,简化 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], // 名字要设置便宜,避免被头像挡住
'text-allow-overlap': true
},
"paint": {
"text-color": "white",
},
})
效果达成:
可以在码上掘金里亲自尝试:
代码片段
总体上来说,业务信息的加载,都是同样的逻辑,只要记住两步走的基本方针,就能完成绝大多数的业务需求。
五、总结
在本篇文章,我们系统性地了解了:
mapbox-gl
和maplibre-gl
两个库的使用范畴。学习了天地图的使用方法 并且实战了几个简单的业务场景
碰到 瓦片风 的大屏地图开发,想必不会再难倒你了。
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
点个在看你最好看