24

记一次前端"揭开绘制地图的神秘面纱"分享会

记录了我在组内的技术分享, 有同样需求的同学可以参考一下
分享全程下来时间大约 70分钟

image

一. 为什么要分享前端相关的"地图"知识

  1. (大屏展示)很多公司都会有相应的大屏幕展示系统, 例如中国或者全世界的客户与资产分布图.
  2. (生动描绘)用地图的角度来展示地理方面的关系, 让人看着比单纯的文字更直观
  3. (场景多)比如今年的各类疫情严重情况的分布图.
  4. 总的来说还是看起来比较炫酷, 可以提升一点点b格, 并且这个只是也是属于前端的范畴, 那么我们就有必要弄懂它.

二. 做地图相关技术简介

这里我只介绍几款我常用的

** 百度地图
这个名气太大了, 功能很多并且现在对3d的支持也很不错, 注意GL版v1.0 与之前 v2.0版本地图的api有点不一样别掉坑里.
缺点也比较明显, 比如你想要一份干干净净的地图, 上面没有店铺没有任何标识的时候我就建议你用echarts来玩了, 因为百度地图带的东西比较多.
想要使用百度地图的同学可以看这里, 超级简单就可以完成注册用玩耍.
使用非常简单

** hcharts
非常牛非常好用, 但是它部分功能是要收费的, 使用之前要让公司帮你买好相应的功能才能用于商用哦.
由于我们公司地图库是自己研发的最后也就没有这种网上付费的.
详情地址

** echarts
这个库前端无人不知了, 在需求很简单的情况下建议用这个技术来做, 大部分时候项目中需要绘制柱状图或折线图的时候已经引入了echarts此时不用重复引用来节省空间.
echarts画的地图

** 我们公司自己的2d, 3d地图组件库
这个在这里就不做过多详细介绍了, 一些公司也会有自主研发的地图组件, 设计的思想上可能与上面三个不太相同, 接下来我也会聊到.

三.echarts实现基础地图

以echarts为例是因为这个最好弄...

这里我新建了一个vue工程

<template>
  <div class="home">
    <div id="map"></div>
  </div>
</template>

<script>
import echarts from "echarts";
import mapData from "./geo";

export default {
  name: "Home",
  data() {
    return {
      myChart: null,
    };
  },
  methods: {
    initMap() {
      this.myChart = echarts.init(document.getElementById("map"));
      echarts.registerMap("world", mapData); // 定义名称下面要用, 这样做的好处就是可以很方便的实现切换地图的效果
      this.myChart.setOption({
        series: [
          {
            type: "map",
            mapType: "world", // 自定义扩展图表类型
            label: {
              show: false,
            },
          },
        ],
      });
    },
  },
  mounted() {
    this.initMap();
  }
};
</script>
  1. 像我们平时使用echarts一样先初始化
  2. 接下来有点不同需要echarts.registerMap("world", mapData); 可以理解为把这个数据命名为'world', 方便以后的切换(这里的数据我下面会讲).
  3. 在option的配置里面设置类型是地图, 使用上面定义好的'world'类型.

效果图
image

我们可以看得出来, 地图的绘制也没什么'特殊'的, 最主要的就是那个 mapData数据, 这个数据一般叫它geojson数据, 那么接下来我们认识一下它.

四.geojson数据到底是什么

  1. geojson是用json的语法表达和存储地理数据,可以说是json的子集, 它不是专门js使用的这点要清楚.
  2. 地图上有山川, 河流, 海洋等等的地理信息, 那么如何描述一条河? 这个时候就要使用geojson格式的文件来描绘.
  3. 并不是必须用geojson, geojson只是一套规范, 各大解析器用这套规范来解析生成对应的景色, 我们完全可以制定自己的规范来实现这些, 无非是兼容性不好需要自己写绘制的解析器.

五.geojson详细介绍

英语好的可以先撸网站
1. 基本结构

{ // 可以包括点线面, 一个大的集合
  "type": "FeatureCollection", // 定义这个是个geojson文件, 这里还可以是其他值下面会说
  "features": [] // 这里放要绘制的数据
}

以后我们看到"type": "FeatureCollection"这样一行就说明这个文件是geojson规范的文件

2. 描述一个点(Feature)
地图上的打点数据

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",  // 表示这个对象是一个要素
      "properties": {}, // 这里放样式, 后面会专门说
      "geometry": { // 这里面放具体的数据
        "type": "Point",  // 专指画点
        "coordinates": [105.380859375, 31.57853542647338] // 默认是经度与纬度, 三维的话就是xyz三个值, 当然这里也不一定是经纬度(不同的坐标体系)中会讲为什么
      }
    },
  ]
}

3. 描述多个点(FeatureCollection)
**优点

  1. 写法简洁
  2. 这些点样式可以共用
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "MultiPoint", // 多点, 也就是连续画多个同样的点
        "coordinates": [[105.380859375, 31.57853542647338],
        [105.580859375, 31.52853542647338]
        ]
      }
    },
  ]
}

4. 描述一条线(LineString)

  1. 这里还是描绘每一个点, 但这些点会连接在一起形成线
  2. 地图上的连线数据
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "LineString", // 这里所有的点会连接在一起形成线
        "coordinates": [[105.6005859375, 30.65681556429287],
        [107.95166015624999, 31.98944183792288],
        [109.3798828125, 30.031055426540206],
        [107.7978515625, 29.935895213372444]]
      }
    },
  ]
}

5. 描述多条线(MultiLineString)

  1. 这里第二组与第一组的线, 可以分隔开不会首尾相连.
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "MultiLineString",
        "coordinates":
          [
            [
              [105.6005859375, 30.65681556429287],
              [107.95166015624999, 31.98944183792288],
              [109.3798828125, 30.031055426540206],
              [107.7978515625, 29.935895213372444]
            ],
            [
              [109.3798828125, 30.031055426540206],
              [107.1978515625, 31.235895213372444]
            ]
          ]
      }
    },
  ]
}

6. 描述一个面(Polygon, 也叫多边形)

  1. 第一个点与最后一个点要相同, 这样才能完成闭环!!
  2. 三维数组的格式需要注意
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "Polygon", // 注意这里是三维数组
        "coordinates": [
          [
            [106.10595703125, 33.33970700424026],
            [106.32568359375, 32.41706632846282],
            [108.03955078125, 32.2313896627376],
            [108.25927734375, 33.15594830078649],
            [106.10595703125, 33.33970700424026]
          ]
        ]
      }
    },
  ]
}

7. 一个面里面有多个面(Polygon)

  1. 这种单一的'Polygon'里面出现多个形状, 会出现中空的情况, 类似布尔运算, 这样就可以在地图中描述那种圈型的国家
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -39.7265625,
              -3.162455530237848
            ],
            [
              127.96875,
              -3.162455530237848
            ],
            [
              127.96875,
              74.1160468394894
            ],
            [
              -39.7265625,
              74.1160468394894
            ],
            [
              -39.7265625,
              -3.162455530237848
            ]
          ],
          [
            [
              -22.5,
              15.961329081596647
            ],
            [
              110.74218749999999,
              15.961329081596647
            ],
            [
              110.74218749999999,
              70.8446726342528
            ],
            [
              -22.5,
              70.8446726342528
            ],
            [
              -22.5,
              15.961329081596647
            ]
          ]
        ]
      }
    }
  ]
}

效果如下:
image

8. 描述多个面(MultiPolygon)
优势:

  1. 写法简洁
  2. 这些点样式可以共用
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
          [
          [
            [
              -39.7265625,
              -3.162455530237848
            ],
            [
              127.96875,
              -3.162455530237848
            ],
            [
              127.96875,
              74.1160468394894
            ],
            [
              -39.7265625,
              74.1160468394894
            ],
            [
              -39.7265625,
              -3.162455530237848
            ]
          ]
        ],
        [
          [
            [
              -22.5,
              15.961329081596647
            ],
            [
              110.74218749999999,
              15.961329081596647
            ],
            [
              110.74218749999999,
              70.8446726342528
            ],
            [
              -22.5,
              70.8446726342528
            ],
            [
              -22.5,
              15.961329081596647
            ]
          ]
        ]
        ]
      }
    }
  ]
}

这里如果重叠了就是颜色的叠加了如图所示:
image

9. 描述一个组(geometries)

  1. 比如我们为了表示一种特定的地貌那么我们可以把这个地貌数据独立起来
{
  "type": "FeatureCollection",
  "features": [
    { // 可以包括点线面, 一个独立的集合
      "type": "GeometryCollection",
      "geometries": [
        {
          "type": "Point",
          "coordinates": [108.62, 31.02819]
        }, {
          "type": "LineString",
          "coordinates": [[108.896484375, 30.1071178870],
          [108.2184375, 30.91717870],
          [109.5184375, 31.2175780]]
        }
      ]
    }
  ]
 }

10. 不同的样式(properties)

{
  "type": "FeatureCollection",
  "features": [
     {
      "type": "Feature",
      "properties": { // 专门放属性
        "stroke": "#fa9661", // 外边颜色
        "stroke-width": 4.1, // 外边宽
        "stroke-opacity": 0.7, // 外边透明度
        "fill": "#9e290c",  // 填充色
        "fill-opacity": 0.7 // 填充色透明度
      },
      "geometry": {
        "type": "Point",  // 画点
        "coordinates": [105.380859375, 31.57853542647338]
      }
    },
  ]
}

六. geojson的相关网站与工具的使用

** 展示干巴巴的数据大家看着不起劲, 这里我推荐一个绘制geojson的超棒网站地址

那么我来介绍一下如何使用这个网站高效的生成, 以及调试geojson
image

  1. 也就是最后生成的geojson, 这里的变化可以实时影响图像, 并且会有错误提示很方便编写.
  2. 绘制直线
  3. 绘制多边形也就是面, 这里注意要首位相连.
  4. 绘制矩形, 这里应该是专门封装的方法绘制矩形.
  5. 绘制点, 这里会为我们在地图上mark一下, 具体的图片需要我们自己在项目中引用.

image

  1. 点击之后进入编辑模式, 鼠标在图形上会出现小手标识, 此时可以拖动图形移动, 操作可以选择是否保留.
  2. 删除模式, 点击可以删除指定图形,操作可以选择是否保留.

image
单击图形可以出现如图所示的操作框.

  1. 添加样式属性, 上方展示的是当前样式属性
  2. 保存你的更改
  3. 删除这个图形

image

  1. 点击open可以使用本地的geojson文件进行导入绘制.
  2. save下面点击geojson可以把生成的代码文件下载到本地.

七.自制geojson解析绘制工具的思路

  1. 我们可以只做一个转换器, 也就是你随便写认为不错的格式, 最后转换成geojson的格式.
  2. 直接用你喜欢的格式来绘制图形
  3. 如果用canvas来实现就是绘制对应的图形就好了, 就是图形叠加那里需要特殊处理一下, 样式直接读取properties里面的数据进行设置.
  4. 绘制经纬度也是个问题, 毕竟在平面上不好计算经纬度(接下地图绘制章节会讲相关知识).
  5. 所以综上看来是不是绘制一张平面版的地图也没那么困难, 只要数据对了就成功一小半了.

八.地图的基本概念 (瓦片地图, 矢量地图)

** 有没有发现咱们使用的地图在放大的时候,区域都是一个方块一个方块的被加载成图像的.
** 如果你打开控制台的network还可以看到有好多png的请求.
** 地图这种超大的数据, 超多细节是如何做到快速渲染的?
** 下面是现在比较主流的两种地图的绘制模式.

栅格瓦片地图

顾名思义图片像是瓦片一样堆叠起来的格子状成为地图, 有点像拼图, 是不是感觉一点也不高大上....
但这里也是有很多问题要解决的, 比如你在俯视世界的视角看地图, 那么出现的就是世界的瓦片图片, 当高度小于一定的数值时就采用另一套相应的瓦片, 在某个高度范围内是采用放大瓦片图片的方式模拟视野的下降, 每次请求瓦片图片都需要传递: 1: 当前视口所在坐标(经纬度) 2: 当前视口宽高 3: 当前视角高度.

栅格瓦片以 256 256 或 512 512 大小的图片为介质,这种技术通常是在服务端预先将图片渲染好,前端根据地图的缩放等级,按需加载图片加以拼接,目前依旧在大规模使用,但这种方式存在一些劣势:

受到网络带宽开销和存储空间限制的影响大,离线化部署成本高,单套主题将近 500 多 G(中国)。
样式编辑完后端渲染需要时间长。
无三维的建筑数据,在 3D 场景中无高度信息。
数据保密性差。

矢量地图

顾名思义就是矢量绘制出图形, 只要不是照片肯定会小很多, 对于矢量为什么轻量并且不失真可以参考的上篇文章svg的分享svg实战

矢量瓦片采用和栅格瓦片相同的分级切割方案,不同的是,瓦片数据传输的是地理数据,包括道路、土地、建筑等,通过在前端做地图的渲染,具有如下优势:

极少占用服务器空间,降低网络开销,本地化部署只需5G空间(中国)。
地图的底图样式更换简单.
因为具有了地理数据本身,可在数据基础上做三维空间的延伸,例如 3D 建筑。
数据保密性强。

九.不同的坐标系

** 地球本身是个椭球体, 要把它以平面的方式绘制在一个矩形上也真的不好办, 现在有不少绘制的方式但是都有各自的优缺点, 感兴趣的朋友可以查查看具体的细节, 我这里就简单介绍下比较常见的方式.

  1. 经纬度EPSG:4326 也就是地图的默认坐标
    现在球体上定义好经纬度, 然后在正方形纸上画出刻度, 对应的绘制
  2. 墨卡托投影(EPSG:3785 )
    把地球放在一个圆筒里面, 假设地球内部有个光源, 那么地球在圆柱上的投影就是地图
  3. 火星坐标系

火星坐标是国家测绘局为了国家安全在原始坐标的基础上进行偏移得到的坐标,基本国内的电子地图、导航设备都是采用的这一坐标系或在这一坐标的基础上进行二次加密得到的。
火星坐标的真实名称应该是 GCJ-02 坐标,基本上所有的国内的电子地图采用的都是火星坐标系甚至 Google 地图中国部分都特意为中国政府做了偏移。

  1. 百度坐标系

火星坐标是在国际标准坐标 WGS-84 上进行的一次加密,由于国内的电子地图都要至少使用火星坐标进行一次加密,百度直接就任性一些,直接自己又研究了一套加密算法,来了个 二次加密,这就是我们所熟知的百度坐标 BD-09,当然只有百度地图使用的是百度坐标

  1. WGS-84 坐标系

GS-84 坐标是一个国际的标准,一般卫星导航,原始的 GPS 设备中的数据都是采用这一坐标系。国外的 GoogleMap、OpenStreetMap、MapBox、OpenLayer 等采用的都是这一坐标。

geojson设置坐标系
由于坐标系的不同, 那么就算绘制一个点的坐标也都不会完全相同了, 那么就需要我们来告诉使用geojson的人按哪种坐标系进行解析

{
  "type": "FeatureCollection",
  "crs": { // 定义坐标系 (如果不写就是使用经纬度的坐标系)   默认为EPSG:4326。
    "type": "name", // "type" 和 "properties"。为强制拥有
    "properties": {
      "name": "urn: ogc: def: crs: EPSG: 54013" // 这里定义具体的规则
    }
  },
  "features": [
     {},
  ]
}

使用上线的规则

{
  "type": "FeatureCollection",
  "crs": {
    "type": "link", // 这里变成了link
    "properties": {
      "href": "http://example.com/crs/42", // 这里是你设置的资源链接
      "type": "proj4" // "proj4","ogcwkt",esriwkt" 只能这三种格式
    }
  },
  "features": [
     {},
  ]
}

十.更快的前端数据 -> WebAssembly

**WebAssembly是一种新的编码方式,文件体积更小,启动速度更快,运行速度也更快,与使用JavaScript构建的Web应用相比,性能提升明显。它是多种编程语言的编译器目标,包括C++、C、Rust等。
WebAssembly 是由主流浏览器厂商组成的 W3C 社区团体 制定的一个新的规范。**

WebAssembly 可以明显的提升计算的速率, 还挺适合用在地图库里面的

  1. WebAssembly 和 JavaScript 结合使用, 短时间并不会替代js
  2. .wasm文件结尾的文件来标识.
  3. WebAssembly 有一套完整的语义,实际上 wasm 是体积小且加载快的二进制格式, 其目标就是充分发挥硬件能力以达到原生执行效率
    WebAssembly 运行在一个沙箱化的执行环境中,甚至可以在现有的 JavaScript 虚拟机中实现。在web环境中,WebAssembly将会严格遵守同源策略以及浏览器安全策略。
    WebAssembly 设计了一个非常规整的文本格式用来、调试、测试、实验、优化、学习、教学或者编写程序。可以以这种文本格式在web页面上查看wasm模块的源码。
    WebAssembly 在 web 中被设计成无版本、特性可测试、向后兼容的。WebAssembly 可以被 JavaScript 调用,进入 JavaScript 上下文,也可以像 Web API 一样调用浏览器的功能。当然,WebAssembly 不仅可以运行在浏览器上,也可以运行在非web环境下。
  4. 解析 - 解码 WebAssembly 比解析 JavaScript 要快

编译和优化 - 编译和优化所需的时间较少,因为在将文件推送到服务器之前已经进行了更多优化,JavaScript 需要为动态类型多次编译代码
重新优化 - WebAssembly 代码不需要重新优化,因为编译器有足够的信息可以在第一次运行时获得正确的代码
执行 - 执行可以更快,WebAssembly 指令更接近机器码
垃圾回收 - 目前 WebAssembly 不直接支持垃圾回收,垃圾回收都是手动控制的,所以比自动垃圾回收效率更高。目前浏览器中的 MVP(最小化可行产品) 已经很快了。在接下来的几年里,随着浏览器的发展和新功能的增加,它将在未来几年内变得更快。

说了这些都是概念, 接下来我们就一起实战一下go

十一. hello 级别的WebAssembly

中文官网
官网的实现还需要配置环境啥的搞得很正式, 入门级别其实我们更想的是尝尝鲜, 只要你会点c++就能用我接下来的方法实现.

在线生成
在线生成

image

  1. 点击转换c++代码为WebAssembly格式
  2. 点击下载转换好的文件
  3. 下载到的是个二进制文件

引用文件

    fetch("/test.wasm")
      .then((res) => res.arrayBuffer()) // 拿到Buffer格式
      .then((bytes) => WebAssembly.compile(bytes)) // 转字节码
      .then((mod) => {
        const instance = new WebAssembly.Instance(mod);
        const exp = instance.exports;
        console.log(exp._Z7showNumv())
      });
  1. exp._Z7showNumv 而不是 exp.showNum, 这个我们可以在Wat那一栏修改一下, 但是代码多了修改起来也不容易应该有禁止转换时修改名称的选项这里就不过多展开了.
  2. 注意这里会跨域, 因为属于文件协议, 你可以本地启个服务.

开发成本

  1. 需要的不只是前端技术了.
  2. bug稍微有点多, 比如不好调试, 还有的同学遇到了每次编译结果不同等问题.
  3. 社区不完善
  4. 建议这门技术先使用在封装度较高, 计算量很大的模块上.

十二.(组内篇)我写的2d与3d工程的代码介绍

这里我在组内展示一下我编写的两个项目的代码结构与遇到的问题, 就不在这里展开了毕竟涉及保密问题, 但大体思路就是把地图分成世界, 国家, 省, 市, 区 几个等级(省市区是中国的分法), 相当于一个状态机, 然后在每个状态下做相应的事比如打点与连线, 每次变换图层状态都会隐藏其他图层展示相应视野的图层.

end.

地图方面也属于前端比较有用的一环, 我今年刚接触地图相关项目也是一脸蒙, 但是详细学习了geojson等知识之后再用地图相关组件库就非常顺畅了.
这次就是这样, 希望和你一起进步.


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者