ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

百度地图商圈边界数据本地化:CityList与多边形绘制优化

百度地图商圈边界数据本地化:CityList与多边形绘制优化 简介面向 JavaScript 开发者的城市行政区域与商圈数据获取工具类基于百度地图 API 1.5主要为房地产、本地服务、交通规划等需要精确地理信息的应用场景提供行政区边界与商圈几何数据支持。主入口类为 CityList开发者通过实例化该类即可获取以坐标点表示的多边形边界进而用于地图绘制、位置搜索、用户行为分析等业务类库将获取数据的接口与函数集中在一个脚本中省去了自行对接地图服务的重复工作。整个压缩包仅含 1 个 js 文件体积约 9KB轻量且聚焦内部结构便于阅读适合直接引入前端项目快速验证功能同时也可作为学习百度地图行政区划与商圈数据调用的参考实现。目前已有 439 人学习下载对于需要处理地理数据的前端或 GIS 开发人员来说这是一个高性价比的入门示例既能帮助你快速搭建地图数据层也能为后续二次封装和扩展提供清晰基础。1. 商圈边界拿不到批量接口就把数据搬到本地地图上画“朝阳区边界”很多人第一反应是调百度地图官方接口。但到商圈级别官方接口既无批量返回也没有稳定的多边形定义边界点几百上千个每次实时拉取体验和流量都扛不住。这个包里的 CityList.js 把城市行政区和商圈的多边形坐标整理成 JavaScript 可直接消费的数据结构配合“2城市商圈及行政区域”数据文件一次加载进页面渲染、判断、聚合全部本地完成。类库主入口是 CityList基于百度地图 API 1.5 的坐标约定设计适合在 Web 端做区域展示、商圈圈选、位置归属判断的 JavaScript 开发者WordPress 地图页和 Qt WebView 场景也能直接复用。2. CityList 的数据组织方式与 BD-09 坐标约定2.1 两个文件数据和逻辑分离解压“2城市商圈及行政区域.zip”后核心是“2城市商圈及行政区域”数据文件和 CityList.js 类文件。数据文件里是城市、行政区、商圈的名称和坐标点数组CityList.js 负责读取、筛选和格式化这些数据。这种分离方式好维护行政区划调整时只换数据文件类库代码不用动反过来类库升级解析逻辑也不碰数据。2.2 商圈与行政区的坐标点存储结构我一般会把数据源组织成下面这种 JSON 形态var cityBoundarySource { 北京市: { districts: [ { name: 朝阳区, points: [ [116.427, 39.921], [116.471, 39.911] ] } ], businessCircles: [ { name: 国贸, district: 朝阳区, points: [ [116.451, 39.908], [116.469, 39.902] ] } ] } };每个区域用一个 Array 存坐标数组每个元素都是[经度, 纬度]的二元组形状上就是一个闭合多边形的顶点列表。注意这里没有把首尾坐标重复写一遍绘制时由 Polygon 自动闭合。数据文件里如果出现首尾坐标完全相同的点那是冗余数据本类库封装时可以自动去掉。参数上需要关注的维度有三个points.length代表边界精细度“直辖市-城区”级别的边界一般要几百个点商圈这种小范围用几十个点就够坐标顺序我按顺时针存用来做面积计算或方向判断时不会踩正负号的坑商圈数据里保留district字段是为了在“按行政区筛选商圈”的场景里不用再做一次反向查找。2.3 为什么必须用 BD-09 而不是 WGS-84百度地图 API 1.5 及其后续版本统一使用 BD-09 坐标系。这个坐标系是 GCJ-02 基础上二次加密得到的和 GPS 原始输出的 WGS-84 坐标之间存在几十米到上百米的偏移。CityList 数据源里的坐标点是在百度地图工具里采集的落地的就是 BD-09。如果你的运营后台存的是 GPS 坐标直接拿去和 CityList 数据里的多边形做判断结果会整体偏移在 App 端看不出在 Web 端一对比底图就露馅。一个判断数据是不是 BD-09 的土办法随便取一个点丢到百度地图的坐标拾取器里看位置是否和实际地点吻合如果偏了几百米说明源头数据混入了 WGS-84。这时候不能直接在 CityList 里面换算要么统一在数据入库时转换要么在拿到坐标点后调用百度提供的坐标转换接口不要一半转换一半不转。2.4 CityList 对外暴露的主入口方法主入口类 CityList 实例化后核心方法就下面这几个方法参数返回说明getCities无Array返回已配置的城市名称列表getDistrictcity, districtArray返回指定行政区多边形顶点数组getAllDistrictscityObject返回某城市下全部行政区key 为区名getBusinessCirclecity, circleArray返回指定商圈多边形顶点数组searchkeywordArray按名称模糊匹配城市、区、商圈代码用法很直接var cl new CityList(); var cities cl.getCities(); var chaoyang cl.getDistrict(北京市, 朝阳区); var guomao cl.getBusinessCircle(北京市, 国贸); console.log(cities.length); // 城市总数 console.log(chaoyang.length); // 朝阳区边界点数 console.log(guomao.length); // 国贸商圈边界点数getDistrict返回的是顶点数组而不是 BMap.Polygon 对象这是刻意设计。类库只负责给坐标数据地图覆盖物的创建交给业务方自己控制这样在要不要描边、要不要填充、要不要绑定点击事件上调用方有完全决定权。后面章节里你会看到这种“只给点不给覆盖物”的做法在多个图层叠加时优势特别明显。3. 接入百度地图 API 1.5 渲染行政区与商圈边界3.1 页面加载顺序和密钥配置CityList 依赖百度地图 JavaScript API 1.5 的坐标体系和 BMap 命名空间页面里引入顺序有讲究必须是官方 API 在前、数据文件和类库在后script typetext/javascript srchttp://api.map.baidu.com/api?v1.5ak你的AK密钥/script script typetext/javascript src2城市商圈及行政区域.js/script script typetext/javascript srcCityList.js/scriptak 是你申请的服务密钥没有它 API 1.5 的脚本会加载失败。1.5 是较早的版本分支不支持 HTTP/2 的页面如果用了这个脚本注意页面的资源加载协议要和站点保持一致避免混合内容拦截。如果项目被迫升级到 2.0 或 3.0CityList 本身的解析逻辑不用动但 BMap.Polygon 的构造参数和覆盖物事件在新版本里有差异需要逐项核对。提示1.5 版本的脚本地址走的是 http 协议正式站点要确保你的页面协议和脚本协议一致否则会被浏览器当成混合内容拦截。3.2 把顶点数组转成 BMap.PolygonCityList 返回的是经纬度二元组数组BMap.Polygon 接收的是 BMap.Point 数组中间要做一次转换。这是所有接入代码里最容易漏的一步var map new BMap.Map(map-container); map.centerAndZoom(new BMap.Point(116.404, 39.915), 11); var cl new CityList(); var pts cl.getDistrict(北京市, 朝阳区); var bPoints []; for (var i 0; i pts.length; i) { bPoints.push(new BMap.Point(pts[i][0], pts[i][1])); } var polygon new BMap.Polygon(bPoints, { strokeColor: #1a6fd6, strokeWeight: 2, strokeOpacity: 0.8, fillColor: #1a6fd6, fillOpacity: 0.12 }); map.addOverlay(polygon);这个 for 循环没有用map方法是因为 API 1.5 时代的包体积考虑和部分浏览器兼容性手动循环最稳。pt[0]是经度、pt[1]是纬度别在两个坐标系里把它们混掉。strokeWeight是描边宽度fillOpacity是填充透明度行政区一般用 0.10.15 的低透明度既能看清边界又不遮挡底图上的 POI商圈可以用 0.2 左右让它从视觉上突出出来。3.3 在同一张图上叠加商圈的坑行政区和商圈经常会同时展示。直接两个循环往 map 上丢 Polygon 不会报错但层级和交互会打架先画的 Polygon 可能挡住后画的点击事件。我一般给行政区和商圈各建一个 Overlay 分组或者用 Polyline 只描边不填充来画行政区Polygon 带半透明填充来画商圈// 行政区只描边不参与点击 var line new BMap.Polyline(bDistrictPoints, { strokeColor: #333, strokeWeight: 1 }); map.addOverlay(line); // 商圈带填充并绑定点击 var circleArea new BMap.Polygon(bCirclePoints, { fillColor: #ff8800, fillOpacity: 0.25, strokeColor: #ff6600 }); circleArea.addEventListener(click, function(e) { console.log(商圈被点击, e.point.lng, e.point.lat); }); map.addOverlay(circleArea);这么做的原因是 BMap 的覆盖物没有严格的 z-index 语义Polyline 和 Polygon 混用时后 add 的默认在上层但遇到上一级覆盖物比如 InfoWindow就不可控。用“行政区描线、商圈填面”的方式把视觉层级固定下来是最省事的方案。3.4 WordPress 页面和 Qt WebView 集成这个类库本质是纯 JS不绑定框架。在 WordPress 里做房产板块图时可以在页面模板中加载百度地图脚本和 CityList再用短代码输出一个div#map-container脚本初始化逻辑照常。在 Qt 的 WebView 中嵌入时只需把三个 JS 放到本地资源目录通过runJavaScript调用 CityList 的方法把获取到的坐标数组回传到 C 侧做空间运算。这样底图渲染交给 WebView商圈判断逻辑留在原生层两边都不改数据结构。4. 边界坐标抽稀与多边形绘制性能调优4.1 点数膨胀带来的问题看一组实际数据一个直辖市的行政区边界精细级别下通常有 8003000 个顶点一个城市的商圈加行政区合计动辄上万点。把这些点全部直接转成 BMap.Point 并 addOverlay在低端移动设备上拖动地图时重绘会造成明显卡顿。抽稀的目的不是偷工减料而是在保持边界形状肉眼可辨识的前提下把顶点的数量降下来。4.2 道格拉斯-普克抽稀算法的实现对离线坐标数组做抽稀我一般用道格拉斯-普克Douglas-Peucker算法。它的思路连接首末点找离这条线段最远的顶点如果最大距离超过阈值就保留该点并递归处理两段否则丢掉中间所有点function pointToSegmentDistance(p, a, b) { var x a[0], y a[1]; var dx b[0] - x, dy b[1] - y; if (dx 0 dy 0) { return Math.sqrt(Math.pow(p[0] - x, 2) Math.pow(p[1] - y, 2)); } var t ((p[0] - x) * dx (p[1] - y) * dy) / (dx * dx dy * dy); t Math.max(0, Math.min(1, t)); return Math.sqrt(Math.pow(p[0] - (x t * dx), 2) Math.pow(p[1] - (y t * dy), 2)); } function douglasPeucker(points, epsilon) { if (points.length 3) return points; var start 0, end points.length - 1; var maxDist 0, index 0; for (var i 1; i end; i) { var dist pointToSegmentDistance(points[i], points[start], points[end]); if (dist maxDist) { maxDist dist; index i; } } if (maxDist epsilon) { var left douglasPeucker(points.slice(start, index 1), epsilon); var right douglasPeucker(points.slice(index, end 1), epsilon); return left.slice(0, -1).concat(right); } return [points[start], points[end]]; }pointToSegmentDistance计算点到线段的垂直距离t用来判断投影点是否落在线段范围外。epsilon是抽稀阈值单位是经纬度。在城市尺度下0.001 大约对应 100 米行政区边界一般取 0.00150.002形状基本不变商圈范围小取 0.00050.001 比较安全。调用方式var rawPts cl.getAllDistricts(北京市)[海淀区]; var reduced douglasPeucker(rawPts, 0.0018); console.log(原始点数:, rawPts.length, 抽稀后:, reduced.length);按我的经验海淀区这种边界复杂的区0.0018 的阈值能把点数压缩到原来的 30%50%地图缩放出现轮廓毛刺的概率很低。如果底图是 15 级以上才放大地看阈值还可以再放大一点。4.3 抽稀对绘制耗时的实际影响下面的数据来自我的一次本地测试只做量级参考。处理方式顶点总数全部添加耗时拖动地图掉帧情况原始数据直接渲染约8500420ms掉帧明显抽稀至40%后渲染约3400160ms基本流畅这个数据因机器而异但趋势是一致的绘制耗时和顶点数几乎线性相关抽稀后首屏速度提升一半以上。4.4 多个多边形分块绘制的写法当页面需要同时画多个区时不要在一个循环里反复 addOverlay更不要每次拖动地图都重新创建 Polygon。一次地图视野调整后全量重绘代价很大。常用做法是将覆盖物数组在首次渲染时创建好拖动结束后只更新视野范围内的区域var overlays []; for (var d in districtData) { var bpts []; for (var j 0; j districtData[d].points.length; j) { var dp districtData[d].points[j]; bpts.push(new BMap.Point(dp[0], dp[1])); } overlays.push(new BMap.Polygon(bpts, { strokeWeight: 1, fillOpacity: 0.08 })); } map.addOverlay(overlays); // API 1.5 支持一次性添加覆盖物数组一次性传入数组BMap 内部会批量处理比单个 add 少很多次重绘计算。4.5 什么情况下不要抽稀做行政区之间的精确面积对比或人口热力统计时抽稀后的边界和真实边界会有微小面积差这种场景不要用算法压缩保留原始坐标点让后端去算。抽稀只适合“给人看”的渲染场景不适合“要算准”的分析场景。5. 射线法判断坐标归属与边界数据自检5.1 坐标点落在哪个商圈内拿到 CityList 的坐标点之后最常用的操作是判断“这个经纬度属于哪个行政区或商圈”。这不需要用地图覆盖物直接用射线法计算。核心思路从目标点向一侧引一条射线统计射线与多边形边的交点数奇数在内部偶数在外部。function isPointInPolygon(point, poly) { var inside false; for (var i 0, j poly.length - 1; i poly.length; j i) { var xi poly[i][0], yi poly[i][1]; var xj poly[j][0], yj poly[j][1]; var intersect ((yi point[1]) ! (yj point[1])) (point[0] (xj - xi) * (point[1] - yi) / (yj - yi) xi); if (intersect) inside !inside; } return inside; } var pt [116.462, 39.907]; var inGuomao isPointInPolygon(pt, cl.getBusinessCircle(北京市, 国贸)); console.log(是否在国贸商圈内:, inGuomao);这段代码的核心是((yi point[1]) ! (yj point[1]))它排除掉与射线平行的边和碰巧经过顶点的边避免重复计数。point[0] ... xi是射线与边的水平交点判断。在这个场景下 Point 数组是平面经纬度直接用这里的数学公式即可如果你在做全国级别的判断建议先按城市缩小范围再逐区判断。5.2 用相同逻辑做数据自检CityList 数据文件从网上整理或人工采集时常见的脏数据有三种首尾点坐标未闭合但看不出形状问题、同一区域出现两个完全相同的点导致面积计算异常、相邻行政区的边界互相重叠。重合点的去重可以这样处理function dedupPoints(points) { var seen new Set(); return points.filter(function(p) { var key p[0].toFixed(6) , p[1].toFixed(6); if (seen.has(key)) return false; seen.add(key); return true; }); }这段去重代码使用经度、纬度各保留 6 位小数作为 key能过滤掉采集时重复点击产生的临近点。注意toFixed之后会变成字符串比较前一定要拼成单个 key不要用对象当 key否则所有点都会因为引用不同而无法去重。跑完自检后把每个区的坐标点总量和去重点总量打出来对比超过 5% 的重复率说明数据源采集质量有问题。按这个方法把数据过一遍边界没有端点缺失、商圈与行政区没有明显重叠CityList 在多边形绘制和归属判断上就能直接用了。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进