地理热图统计核心是将连续经纬度离散化为可聚合单元(如网格或行政区),再用Collectors.groupingBy分组计数;常用网格法为按精度取整生成唯一字符串key,避免浮点误差,最后转为HeatPoint列表供前端渲染。

用 Collectors.groupingBy 做地理热图统计,核心不是“分组本身”,而是把连续的经纬度坐标合理离散化为可聚合的地理单元(比如网格、行政区、或自定义区域),再分组计数。Java 原生不提供地理栅格化能力,需手动设计坐标映射规则。
将经纬度映射到二维网格 ID
最常用且可控的方式是按固定精度对坐标取整或四舍五入,生成“网格码”。例如:保留小数点后 2 位,相当于约 1km × 1km 网格(赤道附近)。
- 经度 116.4823 →
Math.round(116.4823 * 100) / 100.0 = 116.48 - 纬度 39.9121 →
Math.round(39.9121 * 100) / 100.0 = 39.91 - 组合成唯一 key:
"116.48_39.91"或封装为GridPoint(roundedLon, roundedLat)
这样后续就能用 groupingBy(gridKey, counting()) 统计每个网格的业务数量。
按行政区(如省/市)归类需外部数据支持
groupingBy 本身不识别地理位置,必须借助行政区划边界数据(如 GeoJSON、高德/百度逆地理编码 API,或本地缓存的「坐标→城市」映射表)。
- 调用一次逆地址解析(适合少量数据):对每个坐标请求
https://restapi.amap.com/v3/geocode/regeo?location=116.48,39.91&key=xxx,提取city字段 - 离线匹配(推荐用于大批量):预加载全国市级多边形(WKT 或经纬度数组),用射线法判断某点是否在某市内,构建
Map缓存pointToCity - 分组写法:
groupingBy(p -> cityMap.getOrDefault(p, "未知"), counting())
避免浮点精度导致的分组偏差
直接用 Double 作 key 容易因二进制表示误差导致相同逻辑坐标的 key 不等价(如 116.48000000000002 ≠ 116.48)。
- 统一转为字符串格式化:
String.format("%.2f_%.2f", lon, lat) - 或用整型缩放:
(long)(lon * 1e5) + "_" + (long)(lat * 1e5)(避免小数) - 自定义
GridPoint类并重写equals/hashCode,内部用四舍五入后的BigDecimal或int存储
输出热图所需结构:List
分组结果是 Map,但前端热图库(如 Leaflet.heat)通常要 [lon, lat, weight] 三元组数组。
- 把分组 Map 转为流:
gridCountMap.entrySet().stream() - 拆解 key 得到原始坐标(如
"116.48_39.91"→116.48, 39.91) - 映射为对象:
.map(e -> new HeatPoint(parseLon(e.getKey()), parseLat(e.getKey()), e.getValue())) - 最终收集为
List,再转 JSON 返回给前端
不复杂但容易忽略细节。关键在坐标离散化方式的选择和一致性,而非 groupingBy 语法本身。










