简介:面向 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 | 返回已配置的城市名称列表 |
| getDistrict | city, district | Array | 返回指定行政区多边形顶点数组 |
| getAllDistricts | city | Object | 返回某城市下全部行政区,key 为区名 |
| getBusinessCircle | city, circle | Array | 返回指定商圈多边形顶点数组 |
| search | keyword | Array | 按名称模糊匹配城市、区、商圈 |
代码用法很直接:
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 type="text/javascript" src="http://api.map.baidu.com/api?v=1.5&ak=你的AK密钥"></script> <script type="text/javascript" src="2城市商圈及行政区域.js"></script> <script type="text/javascript" src="CityList.js"></script>ak 是你申请的服务密钥,没有它 API 1.5 的脚本会加载失败。1.5 是较早的版本分支,不支持 HTTP/2 的页面如果用了这个脚本,注意页面的资源加载协议要和站点保持一致,避免混合内容拦截。如果项目被迫升级到 2.0 或 3.0,CityList 本身的解析逻辑不用动,但 BMap.Polygon 的构造参数和覆盖物事件在新版本里有差异,需要逐项核对。
提示:1.5 版本的脚本地址走的是 http 协议,正式站点要确保你的页面协议和脚本协议一致,否则会被浏览器当成混合内容拦截。
3.2 把顶点数组转成 BMap.Polygon
CityList 返回的是经纬度二元组数组,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.1~0.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 点数膨胀带来的问题
看一组实际数据:一个直辖市的行政区边界,精细级别下通常有 800~3000 个顶点;一个城市的商圈加行政区合计动辄上万点。把这些点全部直接转成 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.0015~0.002,形状基本不变;商圈范围小,取 0.0005~0.001 比较安全。
调用方式:
var rawPts = cl.getAllDistricts("北京市")["海淀区"]; var reduced = douglasPeucker(rawPts, 0.0018); console.log("原始点数:", rawPts.length, "抽稀后:", reduced.length);按我的经验,海淀区这种边界复杂的区,0.0018 的阈值能把点数压缩到原来的 30%~50%,地图缩放出现轮廓毛刺的概率很低。如果底图是 15 级以上才放大地看,阈值还可以再放大一点。
4.3 抽稀对绘制耗时的实际影响
下面的数据来自我的一次本地测试,只做量级参考。
| 处理方式 | 顶点总数 | 全部添加耗时 | 拖动地图掉帧情况 |
|---|---|---|---|
| 原始数据直接渲染 | 约8500 | 420ms | 掉帧明显 |
| 抽稀至40%后渲染 | 约3400 | 160ms | 基本流畅 |
这个数据因机器而异,但趋势是一致的:绘制耗时和顶点数几乎线性相关,抽稀后首屏速度提升一半以上。
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 在多边形绘制和归属判断上就能直接用了。
本文还有配套的精品资源,点击获取