前言
在 GPS 和北斗卫星导航系统的加持下,经纬度已成为描述地球位置的标准方式。但在城市管理、物流配送、无人机路径规划等场景中,直接使用经纬度存在数据量大、查询效率低等问题。
北斗网格码将地球表面划分为多层网格,为每个网格分配唯一编码,从而实现高效的空间索引。例如,经纬度 (116.312603°E, 39.993161°N) 可以编码为 N11J47539B8255346171,短短 20 个字符就精确到了约 0.24 米。
规范参考:北斗网格位置码
一、编码规则速览
北斗二维网格码最多 20 个字符,分为 11 段,对应半球标识 + 1~10 级网格:
| 段位 | 码元数 | 说明 | 编码方式 |
|---|---|---|---|
| 第1段 | 1 | 半球 | N(北)/ S(南) |
| 第2段 | 3 | 第1级(6°×4°) | 列号01~60 + 行号A~V |
| 第3段 | 2 | 第2级(30'×30') | 横竖编码(十六进制) |
| 第4段 | 1 | 第3级(15'×10') | Z字形编码 0~5 |
| 第5段 | 2 | 第4级(1'×1') | 横竖编码(十六进制) |
| 第6段 | 2 | 第5级(4"×4") | 横竖编码(十六进制) |
| 第7段 | 1 | 第6级(2"×2") | Z字形编码 0~3 |
| 第8~11段 | 各2 | 第7~10级 | 横竖编码(十六进制) |
各级网格的步长和子网格数如下:
| 级别 | 经度步长(秒) | 纬度步长(秒) | 子网格列×行 |
|---|---|---|---|
| 1 | 21600 (6°) | 14400 (4°) | 60×44(全球) |
| 2 | 1800 (30') | 1800 (30') | 12×8 |
| 3 | 900 (15') | 600 (10') | 2×3 |
| 4 | 60 (1') | 60 (1') | 15×10 |
| 5 | 4 | 4 | 15×15 |
| 6 | 2 | 2 | 2×2 |
| 7 | 0.25 | 0.25 | 8×8 |
| 8 | 1/32 | 1/32 | 8×8 |
| 9 | 1/256 | 1/256 | 8×8 |
| 10 | 1/2048 | 1/2048 | 8×8 |
核心编码思路:
所有级别的计算本质上都是同一件事:在当前父网格内,确定坐标落在哪个子网格中。
坐标 → 减去父网格起点 → 除以当前级步长 → 取整得行列号 → 按规则编码为字符
区别只在于编码方式:
-
**第1级:**特殊,全球统一编号(列号 01~60,行号 A~V)
-
**第3、6级(Z字形级):**子网格为 2×3 或 2×2,用 Z 字形曲线编号(单个数字 0~5 或 0~3)
-
**其余级别(横竖编码级):**列号和行号分别用十六进制字符表示
二、逐级实现(从理解到代码)
为了便于理解,我们先用"最直白"的方式逐级实现,后面再做统一优化。
2.1 准备工作:常量定义
网格步长用两个常量数组存储:
java
private static final double[] LON_STEPS = {
21600, 1800, 900, 60, 4, 2, 0.25, 1.0/32, 1.0/256, 1.0/2048
};
private static final double[] LAT_STEPS = {
14400, 1800, 600, 60, 4, 2, 0.25, 1.0/32, 1.0/256, 1.0/2048
};
2.2 第1级:全球统一编号
**思路:**第1级覆盖全球,经度从 -180° 到 +180° 按 6° 分成 60 列(编号 01~60),纬度按 4° 分成 22 行(编号 A~V)。
**关键点:**西半球的列号从 30 往左递减,东半球从 31 往右递增,就像一面镜子:
← 西半球递减 | 东半球递增 →
┌───┬───┬───┬───┬───┬───┬───┬───┐
│28 │29 │30 │31 │32 │33 │34 │35 │...
└───┴───┴───┴───┴───┴───┴───┴───┘
↑ 本初子午线 ↑
java
// 取绝对值,统一当作正数计算
double absLon = Math.abs(lon) * 3600;
double absLat = Math.abs(lat) * 3600;
// 第1级步长:经度21600秒(6°),纬度14400秒(4°)
int col = (int)(absLon / 21600);
int row = (int)(absLat / 14400);
// 经度列号:东半球从31递增,西半球从30递减
int num = lon < 0 ? 30 - col : 30 + col + 1;
// 拼接:半球符 + 两位列号 + 行号字母
// 例如:N11J(北半球,第11列,第J行)
2.3 第2级及以后:在父网格内细分
**思路:**每一级都是"在当前父网格内,找出坐标属于哪个子网格"。
步骤:
-
算出父网格起点坐标(上级起点 + 上级子网格偏移量)
-
用原始坐标减去父网格起点,得到相对偏移
-
除以当前级步长,取整 → 行列号
-
按编码规则转为字符,追加到网格码
以第2级为例(30'×30',12×8 子网格,横竖编码):
java
// 父网格(第1级)起点
double refLon = col * 21600; // 第1级列号 × 第1级步长
double refLat = row * 14400;
// 相对偏移
int subCol = (int)((absLon - refLon) / 1800); // 0~11
int subRow = (int)((absLat - refLat) / 1800); // 0~7
// 横竖编码:列号和行号分别用十六进制字符
// 例如:col=4, row=7 → "47"
2.4 Z字形编码(第3、6级)
第3级将父网格分成 2×3 个子网格,用 Z 字形曲线编号:
java
┌───┬───┐
│ 4 │ 5 │ ← row 2
├───┼───┤
│ 2 │ 3 │ ← row 1
├───┼───┤
│ 0 │ 1 │ ← row 0
└───┴───┘
col col
0 1
编码值 = zOrderMatrixcolrow,结果是一个 0~5 的单个数字。
第6级类似,但只有 2×2 = 4 个子网格,编码 0~3。
2.5 小结:三种编码模式
| 模式 | 适用级别 | 编码方式 | 示例 |
|---|---|---|---|
| 全球编号 | 第1级 | 列号01~60 + 行号A~V | N11J |
| Z字形 | 第3、6级 | 查Z序矩阵,单个数字 | 5 |
| 横竖编码 | 第2、4、5、7、8、9、10级 | 列号+行号(十六进制) | B8 |
三、完整实现
理解了上面的规则后,最自然的写法是每个级别写一个方法。但仔细观察会发现,2~10 级的逻辑几乎一模一样,只有步长和编码方式不同。所以我们可以用一个循环来处理所有级别:
java
/**
* 将经纬度编码为北斗网格码
*
* @param lng 经度(度)
* @param lat 纬度(度)
* @param depth 编码级别(1-10)
* @return 北斗网格编码
*/
public String encode(double lon, double lat, int level) {
// 参数校验
if (lon < -180.0 || lon > 180.0) throw new BeidouGridException("经度超出范围");
if (lat < -88.0 || lat > 88.0) throw new BeidouGridException("纬度超出范围");
if (level < 1 || level > 10) throw new BeidouGridException("编码级别超出范围");
StringBuilder buf = new StringBuilder(20);
buf.append(lat >= 0.0 ? 'N' : 'S');
double absLon = Math.abs(lon) * 3600;
double absLat = Math.abs(lat) * 3600;
double refLon = 0, refLat = 0;
for (int lv = 1; lv <= level; lv++) {
double cellW = COL_STEP[lv];
double cellH = ROW_STEP[lv];
int col = (int)((absLon - refLon) / cellW);
int row = (int)((absLat - refLat) / cellH);
refLon += col * cellW;
refLat += row * cellH;
if (lv == 1) {
// 第1级:全球统一编号
int num = lon < 0 ? 30 - col : 30 + col + 1;
if (num < 10) buf.append('0');
buf.append(num).append((char)('A' + row));
} else if (ZIGZAG[lv]) {
// Z字形级:单数字编码
buf.append(col + row * COL_NUM[lv]);
} else {
// 横竖编码级:十六进制
buf.append(Integer.toHexString(col))
.append(Integer.toHexString(row));
}
}
return buf.toString().toUpperCase();
}
常量定义:
java
// 各级经度步长(秒),索引 = 级别
private static final double[] COL_STEP = {
0, 21600, 1800, 900, 60, 4, 2, 0.25, 1.0/32, 1.0/256, 1.0/2048
};
// 各级纬度步长(秒)
private static final double[] ROW_STEP = {
0, 14400, 1800, 600, 60, 4, 2, 0.25, 1.0/32, 1.0/256, 1.0/2048
};
// 各级子网格列数
private static final int[] COL_NUM = {
0, 60, 12, 2, 15, 15, 2, 8, 8, 8, 8
};
// 是否使用Z字形编码
private static final boolean[] ZIGZAG = {
false, false, false, true, false, false, true, false, false, false, false
};
异常类定义:
java
public class BeidouGridException extends RuntimeException{
private static final long serialVersionUID = 1L;
public BeidouGridException() {
}
public BeidouGridException(String message) {
super(message);
}
public BeidouGridException(String message, Throwable cause) {
super(message, cause);
}
public BeidouGridException(Throwable cause) {
super(cause);
}
public BeidouGridException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}