内容太多所以分成高德地图篇(上)、D3js篇(下)两个部分,本篇是对高德地图使用的总结。
国内Web端用到的地图引擎,基本是高德、百度和腾讯三家中的一个。
我最后选择了高德,虽然我司不是阿里系,但是腾讯的文档不完善,接口也不够丰富。至于百度嘛,不知道为啥,做的时候根本没有想起来要去对它做调研,就这样忘记它吧~
完成这次项目用到了:
- 地图生命周期和状态
- 基础类:Pixel、LngLat、Bounds
- 覆盖物:Marker、Polygon、Rectangle、OverlayGroup
- 信息窗体:InfoWindow
- 图层:MassMarks、DistrictLayer
- 自建图层:CustomLayer
- 搜索服务:Autocomplete、DistrictSearch
- 地图控件:Scale、ToolBar
通过看官方文档就能上手的部分就不啰嗦了,这里面值得一提的是图层、自建图层和搜索服务。
1. 麻点图层 MassMarks
需求说明:
将3000多个数据,以图标的方式渲染到地图上;
图标的颜色和形状取决于接口返回的字段;
图标的尺寸根据地图当前的缩放级别动态调整。
因为高德地图已经有高效渲染海量点数据的 API,直接用就是了。
需要自己稍微处理一下的部分就是图标的形状、颜色和尺寸。
1.1 图标尺寸
设置尺寸很容易,根据缩放级别设置相应的图标尺寸,修改配置项中 style
下的 size
属性即可(size支持数组或者AMap.Size
类)。
缩放比例对应图标尺寸配置
1 2 3 4 5 6 7 8 9 10 11 12
| const MASS_STYLE_MAPPING = { 9: [4, 4], 10: [6, 6], 11: [10, 10], 12: [10, 10], 13: [20, 20], 14: [20, 20], 15: [22, 22], 16: [22, 22], 17: [26, 26], 18: [26, 26], }
|
1.2 图标形状
设置图标形状对应配置项中 style
下的 url
属性。
文档中只说 url 是图标地址,string 类型,看示例用的是网络图片地址。
经过测试摸索后发现支持 svg 字符串,为了方便配置图标颜色,svg 当然是比图片灵活许多。
最后的实现方案:
- 接口返回的数据包含该点的图标名称
- 前端静态维护名称和 path 的映射
- 渲染图标的时候,根据图标名称找到对应的 path,动态生成 svg 字符串
贴一下前端动态生成 svg 字符串的代码:
动态生成 svg 字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const SVG_MAPPING = { user: { viewBox: '0 0 20 20', path: 'M19 19h-18v-1c0-5 4-9 9-9s9 4 9 9v1zM3.1 17h13.9c-0.5-3.4-3.4-6-6.9-6s-6.5 2.6-7 6z M10 11c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5zM10 3c-1.7 0-3 1.3-3 3s1.3 3 3 3 3-1.3 3-3-1.3-3-3-3z' } }
const getSvg = (iconName, color, bgType) => { const svg = SVG_MAPPING[iconName] if (!svg) { return '' } let bg = '' if (bgType === 'rect') { const box = svg.viewBox.split(' ') bg = `%3Cpath fill='%23fff' d='M0 0h${box[2]}v${box[3]}H0z'/%3E` } else if (bgType === 'circle') { const r = svg.viewBox.split(' ')[2] / 2 bg = `%3Ccircle fill='%23fff' cx='${r}' cy='${r}' r='${r}'/%3E` } return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='${svg.viewBox}'%3E${bg}%3Cpath fill='${color}' d='${svg.path}'/%3E%3C/svg%3E` }
|
1.3 图标颜色
图标是什么颜色是接口返回的,在动态生成 svg 字符串的时候,同时传入颜色字段就可以了。
2. 行政区图层 DistrictLayer
需求说明:
页面可以进行城市切换;
地图上显示当前城市地图,并绘制行政区域边界线。
2.1 覆盖物
最开始的方案,是先用 DistrictSearch 根据城市名称获得城市的 adcode,再用城市 adcode 和 level 去查询下面的行政区边界,最后绘制成AMap.Polygon
类。
优点:可以自定义边界线的粗细程度。
缺点:耗费时间,大概要 600ms。
2.2 图层
后来发现高德 2018-09-12 就发布了简易行政区图层插件。
这个插件用起来很简单,重点是特别快,大概 200ms 完成绘制。
但是它真的太简单,只支持到大部分市级的行政区边界,太仓、辛集都没有,重庆绘制出来不包括郊县。
优点:绘制高效,调用简单。
缺点:覆盖的地区不全,不能调整边界线的粗细程度。
2.3 覆盖物结合图层
综合上面两种方式的优缺点,最后采用二合一方式,贴一个简单的代码:
绘制行政区域边界线
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const map = new AMap.Map('amap') const districtLayer = new AMap.DistrictLayer.Province({ styles: { fill: 'rgba(204,243,255,0.3)', 'county-stroke': '#CC66CC', } }) const districtSearcher = new AMap.DistrictSearch() const drawDistrictBounds = async cityName => { const city = await getAMapCity(districtSearcher, cityName) if (city.level === 'district') { drawDistrictBoundsByCustom(city.adcode, city.level) return } if (city.adcode === '500100') { districtLayer.setDistricts([city.adcode, 500200]) } else { districtLayer.setDistricts([city.adcode]) } map.add(districtLayer) } const drawDistrictBoundsByCustom = async (keyword, level = 'city') => { const group = await districtSearch(districtSearcher, { keyword, level }) const districtGroup = new AMap.OverlayGroup(group) districtGroup.setOptions({ strokeWeight: 1, fillOpacity: 0.3, fillColor: '#CCF3FF', strokeColor: '#CC66CC', }) districtGroup.setMap(map) }
|
3. 自建图层 CustomLayer
需求说明:
自定义图层内容;
图层状态可以同步地图的缩放、平移、尺寸变化状态。
高德的自定义图层功能必须给点个赞,这个功能给地图产品带来更多的可能。除了 CustomLayer,还有 TileLayer、ImageLayer、CanvasLayer、VideoLayer。不是很明白细化这些图层的原因是什么,感觉有个万能的 CustomLayer 就够了。
使用起来也简单:
- 加载CustomLayer插件
- 创建CustomLayer实例,初始化render方法,此方法会在地图状态变化时被调用
- 添加自定义图层到地图实例
需要注意的是,render 方法会有被频繁调用可能,最好是做一个防抖判断。
4. 搜索服务 DistrictSearch
这个服务提供行政区信息的查询,使用该服务可以获取到行政区域的区号、城市编码、中心点、边界、下辖区域等详细信息,为基于行政区域的地图功能提供支持。
要注意两个点:
- 高德的行政级别划分跟我们的认知不一致。
举个例子,重庆市我认为是市级别,但是高德是省级;太仓市我认为是市级别,实际又是区级。
- 个别省市的行政区划分很特殊,目前发现的是重庆和苏州,重庆由重庆郊县和主城区组成,苏州由苏州工业园区和主城区组成。
封装了两个搜索的方法,自我感觉并不是很满意,再慢慢优化吧。
下面的代码引入了ramda工具库,缩写为R。
4.1 搜索城市信息
- searcher:DistrictSearch 实例
- keyword:关键字(城市名称或 adcode)
- level:搜索级别
搜索城市信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const getAMapCity = (searcher, keyword, level = 'city') => { return new Promise((resolve, reject) => { searcher.setExtensions('base') searcher.setSubdistrict(0) searcher.search(keyword, (status, result) => { if (status !== 'complete') { reject(result) return } const district = R.find(R.propEq('level', level))(result.districtList) || result.districtList[0] if (!district) { reject(result) return } resolve(district) }) }) }
|
4.2 搜索区域边界线
- searcher:DistrictSearch 实例
- others:更多配置
- keyword:关键字(城市名称或 adcode)
- level:搜索级别
- subDistrict:是否获取下一级的行政区边界 1-返回,0-不返回
搜索区域边界线
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| const districtSearch = (searcher, others = {}) => { return new Promise((resolve, reject) => { const { keyword, level, subDistrict = 1 } = others searcher.setExtensions('all') searcher.setSubdistrict(subDistrict) searcher.search(keyword, async (status, result) => { if (status !== 'complete') { reject(result) return } const district = R.find(R.propEq('level', level))(result.districtList) if (!district) { reject(result) return } const { boundaries, districtList = [] } = district if (level === 'city' && !R.isEmpty(districtList)) { const next = districtList.map(item => { return districtSearch(searcher, { keyword: item.adcode, level: item.level }) }) const group = await Promise.all(next) if (district.adcode === '500100') { const { districtList: districtJiao } = R.find(R.propEq('adcode', '500200'))(result.districtList) const nextJiao = districtJiao.map(item => { return districtSearch(searcher, { keyword: item.adcode, level: item.level }) }) Promise.all(nextJiao).then(jiaoGroup => { resolve(group.concat([jiaoGroup])) }) return } if (district.adcode === '320500') { const districtPark = R.find(R.propEq('adcode', '320571'))(result.districtList) const industrialPark = districtPark.boundaries.map(item => { return new AMap.Polygon({ path: item }) }) resolve(group.concat([industrialPark])) return } resolve(group) return } resolve(boundaries.map(item => (new AMap.Polygon({ path: item })))) }) }) }
|
5. 问题碎片
有些琐碎的、我觉得挺坑的点,也顺便记录在这里。
OverlayGroup 的作用很鸡肋
我还以为AMap.OverlayGroup
是批量绘制覆盖物的对象,实际测试下来并不是,我估计它的底层只是代替开发者做了一个循环,因为对性能没有一点儿帮助。
获取 Bounds 中心点存在问题
创建一个AMap.Rectangle
时需要传入矩形的 bounds 参数,文档是说用东北-西南角坐标。亲测下来,这样会在创建完矩形后,获取 bounds 中心点时,经度为负数。
解决办法就是使用西北-东南角坐标。
moveend 事件触发的时机和文档不符
给地图绑定事件的时候,意外发现 ToolBar 控制、键盘控制地图缩放时,moveend 事件没有被触发,但是鼠标滚轮控制缩放时就会触发 moveend 事件,真的是奇怪。
解决办法是全局添加一个标记值 isZoomEnd,绑定 zoomend 事件,触发时置 isZoomEnd 为 true,在 moveend 回调中阻止重复处理,并改 isZoomEnd 为 false。
clearMap接口效率奇低
清除地图上所有覆盖物的API clearMap
,能把人逼疯。我们的业务场景下地图上绘制了成千上万的覆盖物,切换城市时这些覆盖物需要一次性清除掉,绘制新的。8000 个覆盖物要耗费十几秒!实在恼火,且无能为力。
绘制Polygon传入的path参数会被更改
创建一个AMap.Polygon
时需要传入多边形的 path 参数,讲道理,函数不应该直接修改传引用的参数,亲测发现这里 path 会被改写。
解决办法就是 path 传入时使用副本。
绘制多边形使用 path 副本
1 2 3 4 5 6 7 8 9 10 11 12 13
| const drawPolygon = (data) => { const group = data.map(item => { const polygon = R.map(p => ([p.lng, p.lat]), item.boundaries) return new AMap.Polygon({ path: R.clone(polygon), extData: { polygon, ...R.omit(['blockIds', 'boundaries'], item) }, }) }) }
|
高德只支持了点数据的海量绘制,需要海量绘制覆盖物的时候,性能就下滑得难以让人接受了,不得不另寻出路。
借助 CustomLayer,就可以自定义覆盖物的绘制方式,canvas 也好,svg 也好。
下面一篇文就继续总结高德地图配合 D3js 的开发经验。