• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

VUE实现线框风大屏地图开发

武飞扬头像
juejin
帮助530

📖阅读本文,你将

  • 学会收集省市区各级别的地理数据(GeoJSON
  • 学会组织数据,并开发一款 线框风 大屏地图
  • 提供源码

需要注意的是,在任何公共场所展示中国地图,需要严格注意南海九段线、群岛、藏南地区等各种地图细节,容易踩坑。因此本文不进行全局中国地图的展示,会以 湖北省 作为用例展示 线框风 开发示例。

(但是没问题,我会提供中国地图的开发注意事项,保证你开发出能实际生产使用的中国地图的 线框风 大屏地图)

前情提要

在本专栏的上一篇文章《大屏地图:从瓦片到引擎,到手把手实战》 中,我们提到:

在各种各样场景的大屏开发中,关于地图的展示,一般存在两种常见的玩法: 瓦片风线框风

上一篇文章也详细介绍了如何用 maplibre-gl 这个库进行瓦片风地图的开发,那么,本篇文章将引领你进行 线框风 地图的实战开发。

一、分析:如何开发 线框风 地图

让我们看一看,目前市面上常见的 线框风 地图大多长什么样子吧:

当然,更多的是一整张中国地图。

你会发现,线框风地图是 没有瓦片细节 的。

对于它们而言,真正核心的是 区域的形状

而在之前课程里面,我们学习过,地图上形状表达的文件格式是:GeoJSON

是的,凡你所见的每一个多边形,都是 GeoJSON 所描述的点勾勒而成。

每一个你能看到的多边形,把它转换为 GeoJSON 对象,都可以理解为是一个多边形 (Polygon) 类别或者 MultiPolygon 类别的 Feature

因此,这一张线框风地图,其实是一个 FeatureCollection 类别的 GeoJson 对象。

有了这个认识,我们就可以从容梳理出,渲染一个基础版本的 线框风 的大屏地图,其实只需要这两部:

  1. 找到准确适合的 GeoJSON
  2. GeoJSON 渲染出来。

二、找到准确适合的 GeoJSON

2.1 统一行政区划代码

在哪里能找到我国所有区域,最新最准确最全面的 统一行政区划代码?

答案当然是:《中华人民共和国民政部-2020年1月中华人民共和国县以上行政区划代码》

这是我找到的 2020/01 发布的标准;但在此之后,相关标准有过变动,作为生产使用时,需要参照这个页面的相关变更:

2021年中华人民共和国行政区划代码 变更参照

但是,为了获取最新最准确的列表,我们也可以直接访问 github ,通过这个仓库获得全部数据:github.com/modood/Admi…

因为本文我们以 湖北省 为例,我随手整理出了如下的 行政区划代码列表

// code  name  parentCode
420000 湖北省 100000
420100 武汉市 420000
420200 黄石市 420000
420300 十堰市 420000
420500 宜昌市 420000
420600 襄阳市 420000
420700 鄂州市 420000
420800 荆门市 420000
420900 孝感市 420000
421000 荆州市 420000
421100 黄冈市 420000
421200 咸宁市 420000
421300 随州市 420000
422800 恩施土家族苗族自治州 420000
429004 仙桃市 420000
429005 潜江市 420000
429006 天门市 420000
429021 神农架林区 420000

当然,这只是 湖北省 一级子区域(市、自治州)级区域。

然后,每个 市、自治州 也拥有自己的 县、地级市 区域,比如武汉:

// code  name  parentCode
420102 江岸区 420100
420103 江汉区 420100
420104 硚口区 420100
420105 汉阳区 420100
420106 武昌区 420100
420107 青山区 420100
420111 洪山区 420100
420112 东西湖区 420100
420113 汉南区 420100
420114 蔡甸区 420100
420115 江夏区 420100
420116 黄陂区 420100
420117 新洲区 420100

由于数据量过于庞大,不在此进行枚举。

但基本能确定一个事实:

通过这种方式,我们能遍历出湖北省区域树上的所有 标准行政区划代码

有了这个代码清单,我们才可以进行 寻找GeoJSON 的下一步。

2.2 去哪儿找 GeoJSON

当然,你首先可以访问: github,通过搜索 china geojson 关键词,寻找相关开源分享。

除此之外。

难道还有比 商用 数据更香的吗?

访问此链接:https://geo.datav.aliyun.com/areas_v3/bound/100000.json 你能看到由 aliyun dataV 团队提供的 中国地理边界 GeoJSON 静态数据。

同理,如果我们访问 https://geo.datav.aliyun.com/areas_v3/bound/420000.json 就能访问到 湖北省地理边界GeoJSON 数据服务。

而我们此前已经得到了 湖北省 所有子孙区域的行政区划代码清单,不难想象,我们可以:

  • 直接使用该源的 GeoJSON
  • 或者手动 (或写脚本) 抓取相关信息完成私有化部署

当然,爬取商用数据是有法律风险的,因此,我个人,以及本篇文章并不推荐和建议你去写爬取脚本。

下面,我们只是出于代码学习探讨的目的,简单列举一个基于 node.js的 "爬取脚本" 应该如何实现:

新建 scripts/fetch-geojson.js:

const axios = require('axios');

// 这个库需要 yarn add fs-extra 进行安装
const fs = require('fs-extra');
const path = require('path')

const codes = [
  420000,
  420102,
  420103
  // 这里是你要爬取的区域代码列表
]

// 下载后的存储文件夹
const ORIGIN_GEOJSON_DIR = path.resolve(__dirname, '../.temp/origin-geojson')

// 请求实现
const getGeojson = async (code) => {
  const res = await axios.get(`https://geo.datav.aliyun.com/areas_v3/bound/${code}.json`)
  return res.data
}

// 写入实现
const writeGeojson = async (code, geojson) => {
  const fileName = `${code}.json`
  const filePath = path.resolve(ORIGIN_GEOJSON_DIR, fileName)
  await fs.writeJSON(filePath, geojson)
}

// 任务组织
const begin = async () => {
  await fs.remove(ORIGIN_GEOJSON_DIR)
  await fs.ensureDir(ORIGIN_GEOJSON_DIR)
  const tasks = codes.map(t => getGeojson(t))
  const jsons = await Promise.all(tasks)
  const writeTasks = jsons.map((json, index) => writeGeojson(codes[index], json))
  await Promise.all(writeTasks)
}

// 开始
begin()

然后再在 package.json 中添加如下代码:

{
  "scripts": {
    "fetch-geojson": "node scripts/fetch-geojson.js"
  }
}

然后执行命令:

yarn fetch-geojson

就能完成相关 GeoJSON 的下载了。

理论上:我很强。于是,按照自己这套方法论,我去拉了下 antvGeoJSON 数据,结果却发现:

antv 公开的数据里,421088 监利市的数据居然是缺失的。

当然,正统的做法,是去找它轮廓的 GeoJSON。但本文是出于演示教学的目的,我直接偷懒给它注释掉了。

2.3 如何组织 GeoJSON 格式?

有了 GeoJSON 其实并不足以完全支撑我们在业务上的开发,只有当 GeoJSON 被良好地组织起来之后,才能使用得更加顺手。

前面我们获取到的诸多 GeoJSON,它们的格式长这样:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "adcode": 420000,
        "name": "湖北省",
        "center": [
          114.298572,
          30.584355
        ],
        "centroid": [
          112.271301,
          30.987527
        ],
        "childrenNum": 17,
        "level": "province",
        "acroutes": [
          100000
        ],
        "parent": {
          "adcode": 100000
        }
      },
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
          // 省略
        ]
      }
    }
  ]
}

也就是说,虽然它是一个 FeatureCollection,但它只包含一个 Feature,能描述的也只是 湖北省 的外轮廓。

如果要满足如上所示的效果,这个 GeoJSON 是不足够的。

那如果要满足如上效果,最理想的 GeoJSON 应该是什么样的呢?

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "武汉市"
        // 其他属性省略
      }
      // 其他属性省略
    },
    {
      "type": "Feature",
      "properties": {
        "name": "宜昌市"
        // 其他属性省略
      }
      // 其他属性省略
    },
    // ...以及其他所有市
  ]
}

对的,我们要渲染 "湖北省" 的地图时,恰恰并不需要 "湖北省" 本身的轮廓,而是需要它所有子节点的轮廓,这样就能完成拼接,最终渲染出 "湖北省" 的轮廓,以及它各个区域的轮廓;

因此,针对获取到的 原始 GeoJSON, 进行一些 merge 操作,就显得非常必要了。

现在我们有了各个区域的结构树、有了各个区域本身轮廓的 GeoJSON,生成我们需要的结构并不困难,写一个脚本就能完成。

文末会开源脚本代码,此处不赘述。

到这一步,我们就已经获得了 合适的 GeoJSON 了。

三、基础地渲染 GeoJSON 线框

“渲染 GeoJSON 是几乎所有地图引擎都具备的能力,因为上一篇文章是通过 maplibre-gl 进行的 “瓦片风”地图 开发,那么本文我们将继续使用 maplibre-gl 进行 线框风 地图开发。

因此,轮廓的渲染,是简单的,还记得我们在上一篇文章《大屏地图:从瓦片到引擎,到手把手实战》 中所做的总结吗?

  • 得到地图实例
  • 先添加资源
  • 再添加图层

这一次,我们的思路,依旧如初。

3.1 得到地图实例

因为这一次,我们没有 瓦片 的需求,因此地图实例非常容易:

我们无需任何初始的资源及图层。

<template>
  <div ref="mapEl" class="map"></div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import maplibregl from 'maplibre-gl'
const mapEl = ref(null)
const initOption = {
  style: {
    "version": 8,
    "id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
    "sources": {},
    "layers": [
    ]
  },
}

onMounted(() => {
  const map = new maplibregl.Map({
    container: mapEl.value,
    ...initOption,
  });
})
</script>

这样,我们虽然得到了一个地图实例,但它依然不会显示任何内容。

此时,我们要做的,就是 获取到json、添加为资源,显示到图层

先定义一个通过网络获取 GeoJSON 的方法:

const fetchGeoJSON = async (areaCode) => {
  // 嗯的,如你所见,这是我的个人CDN,你最好不要在生产上使用它
  const response = await fetch(`https://pic.zhangshichun.top/geojson/merged/${areaCode}.json`, {
    headers: {
      'Content-Type': 'application/json'
    }
  })
  return response.json()
}

然后添加资源和图层:

const hubeiGeoJSON = await fetchGeoJSON(420000)
map.addSource('bound-source', {
  type: 'geojson',
  data: hubeiGeoJSON
})
map.addLayer({
  id: 'areas-line',
  type: 'line',
  source: 'bound-source',
  paint: {
    'line-color': '#fff',
    'line-width': 2
  }
})

就能看到如下效果了:

3.2 添加区域文本

在上面组织 GeoJSON 的时候,除了得到各省市区的轮廓之外,我还手动组织了各市区的名称及定位。

因此,通过 获取到json、添加为资源,显示到图层 三步走,我也能轻易地把地图标注显示出来:

  const areaNames = await fetchGeoJSON('420000-area-names')
  map.addSource('names-source', {
    type: 'geojson',
    data: areaNames
  })
  map.addLayer({
    id: 'areas-name',
    source: 'names-source',
    "type": "symbol",
    "layout": {
      "text-field": '{name}',
      "text-size": 8,
    },
    "paint": {
      "text-color": "white",
      "text-halo-color": "rgba(0, 0, 0, 0)"
    },
    filter: ['==', 'parentCode', 420000] // 这里的意思是,我只要湖北省地级市及自治区这一层的名称显示
  })

看效果:

当然,我也贴心地为了你准备了 码上掘金 的示例:

在示例中,你可以轻松地旋转、放大、缩小,因为它的本质不是图片,而是在地图引擎上的矢量渲染。

四、实现下钻

以上代码虽然已经实现了基础线框的加载,但它既没有交互,也没有下钻。

接下来,让我思考,如何做 地图下钻 这一功能:

  • 监听某个区域的点击事件
  • 获取到这个区域的 Code,获取到对应的 GeoJSON 数据
  • 把新 GeoJSON 显示到地图上。

ok,先说 点击事件,这个属于 框架API,只要是个健全的地图引擎,一定就具备这种能力,查查文档就能查到,不赘述。

再说 通过 Code 请求数据,这个上面一节已经实现了,也没啥好说的。

那么,怎么 更新GeoJSON到地图 呢?

maplibre-gl 或者 mapbox-glAPI 思路里,其实很简单:

更新 资源Resource)。

因此,原来的三步走,其实可以稍微改变一下,变成四步走:

这种代码组织方式的最大优势就在于:

  1. 获取 GeoJSON 这一异步操作不阻碍图层和资源的注册。
  2. 3/4 这两步的操作是可以不停更新执行的,也就是:支持下钻了。

因此,我们封装一个方法:loadArea,用来动态加载某个区域:

const loadArea = async (code) => {
  const geoJSON = await fetchGeoJSON(code)
  map?.getSource('bound-source').setData(geoJSON)
  map?.on('click', fullLayerKey, eventFn);
}

再给区域绑定点击事件,就实际上完成最粗糙的 下钻

map.on('click', 'areas-surface', (e) => {
  const feature = e.features?.[0];
  const code = feature?.properties?.adcode;
  loadArea(code)
})

到码上掘金看看效果:

粗糙吗?

粗糙,但思路确定好之后,剩下的无非是缝缝补补。

  • 缝补点A:下钻后地图应该自动放大,到适应边框。
  • 缝补点B:下钻后,地理信息标注应该更新到下面一级。

都不难。

自动放大 的核心代码:

const fitBounds = (feature, options = {}) => {
  const bboxResult = turf.bbox(feature);
  const [a, b, c, d] = bboxResult;
  map?.fitBounds(
    [
      [a, b],
      [c, d],
    ],
    options,
  );
}

上面用到了库 turf.js,本专栏的这篇文章介绍过了:juejin.cn/post/717086…

更新标注的核心代码:

map?.setFilter('areas-name', ['==', 'parentCode', code]);

看看缝补之后的效果?

或者亲自动手试试:

五、鼠标悬浮高亮?

学会了下钻,高亮 实在容易:

思路:新增一个高亮图层,通过筛选,让它显示出来即可。

新增代码如下:

map.addLayer({
  id: 'areas-surface-hight',
  type: 'fill',
  source: 'bound-source',
  layout: {},
  paint: {
    'fill-color': 'orange',
    'fill-opacity': 1,
  },
  filter: ['==', 'adcode', '']
})

map.on('mousemove', 'areas-surface', (e) => {
  const feature = e.features?.[0];
  const code = feature?.properties?.adcode;
  map?.setFilter('areas-surface-hight', ['==', 'adcode', Number(code)]);
})

就可以看到效果:

以及,亲自动手试试:

六、总结

至此,我们就一步步,完成了以下内容:

  • 分析如何做 线框风地图
  • 如何收集并组织 GeoJSON
  • 如何开始进行简单渲染
  • 如何交互联动
  • 如何下钻
  • 如何高亮

至此,你对框架及思路,都有了一个较为系统的体验,相信其他需求也一定难不倒你。

七、源码

以上内容,均提供源码:

渲染相关的源码:github.com/zhangshichu…

相关 demo: zhangshichun.github.io/windstorm-u…

GeoJSON 的获取和合并: github.com/zhangshichu…

在项目中执行以下命令可自行抓取合并 GeoJSON:

npm run geojson:fetch
npm run geojson:merge

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanfbaik
系列文章
更多 icon
同类精品
更多 icon
继续加载