大屏地图:从瓦片到引擎,到手把手实战

趣谈前端

共 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 对地图颜色进行微调

通常来说,大屏是以深色作为主色调的,目前市面上最常见的大屏主题,前三排名为:

  1. 科技蓝
  2. 科技蓝
  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,
      },
    })
  })

没错,就是这么容易,还是我们之前总结的两步走:

  1. 添加资源
  2. 添加图层

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': [02.4], // 名字要设置便宜,避免被头像挡住
    'text-allow-overlap'true
  },
  "paint": {
    "text-color""white",
  },
})

效果达成:

可以在码上掘金里亲自尝试:

代码片段

总体上来说,业务信息的加载,都是同样的逻辑,只要记住两步走的基本方针,就能完成绝大多数的业务需求。

五、总结

在本篇文章,我们系统性地了解了:

  • mapbox-gl 和 maplibre-gl 两个库的使用范畴。
  • 学习了天地图的使用方法
  • 并且实战了几个简单的业务场景

碰到 瓦片风 的大屏地图开发,想必不会再难倒你了。


以上便是本次分享的全部内容,希望对你有所帮助^_^

喜欢的话别忘了 分享、点赞、收藏 三连哦~。


从零搭建全栈可视化大屏制作平台V6.Dooring

从零设计可视化大屏搭建引擎

Dooring可视化搭建平台数据源设计剖析

可视化搭建的一些思考和实践

基于Koa + React + TS从零开发全栈文档编辑器(进阶实战




点个在看你最好看


浏览 147
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报