经纬度转北斗网格位置码(非极地区域,编码篇)
前言在 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×10Z字形编码 0~5第5段2第4级1×1横竖编码十六进制第6段2第5级4×4横竖编码十六进制第7段1第6级2×2Z字形编码 0~3第8~11段各2第7~10级横竖编码十六进制各级网格的步长和子网格数如下级别经度步长秒纬度步长秒子网格列×行121600 (6°)14400 (4°)60×44全球21800 (30)1800 (30)12×83900 (15)600 (10)2×3460 (1)60 (1)15×1054415×156222×270.250.258×881/321/328×891/2561/2568×8101/20481/20488×8核心编码思路所有级别的计算本质上都是同一件事在当前父网格内确定坐标落在哪个子网格中。坐标 → 减去父网格起点 → 除以当前级步长 → 取整得行列号 → 按规则编码为字符区别只在于编码方式第1级特殊全球统一编号列号 01~60行号 A~V第3、6级Z字形级子网格为 2×3 或 2×2用 Z 字形曲线编号单个数字 0~5 或 0~3其余级别横竖编码级列号和行号分别用十六进制字符表示二、逐级实现从理解到代码为了便于理解我们先用最直白的方式逐级实现后面再做统一优化。2.1 准备工作常量定义网格步长用两个常量数组存储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 │... └───┴───┴───┴───┴───┴───┴───┴───┘ ↑ 本初子午线 ↑// 取绝对值统一当作正数计算 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×3012×8 子网格横竖编码// 父网格第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 // 横竖编码列号和行号分别用十六进制字符 // 例如col4, row7 → 472.4 Z字形编码第3、6级第3级将父网格分成 2×3 个子网格用 Z 字形曲线编号┌───┬───┐ │ 4 │ 5 │ ← row 2 ├───┼───┤ │ 2 │ 3 │ ← row 1 ├───┼───┤ │ 0 │ 1 │ ← row 0 └───┴───┘ col col 0 1编码值 zOrderMatrix[col][row]结果是一个 0~5 的单个数字。第6级类似但只有 2×2 4 个子网格编码 0~3。2.5 小结三种编码模式模式适用级别编码方式示例全球编号第1级列号01~60 行号A~VN11JZ字形第3、6级查Z序矩阵单个数字5横竖编码第2、4、5、7、8、9、10级列号行号十六进制B8三、完整实现理解了上面的规则后最自然的写法是每个级别写一个方法。但仔细观察会发现2~10 级的逻辑几乎一模一样只有步长和编码方式不同。所以我们可以用一个循环来处理所有级别/** * 将经纬度编码为北斗网格码 * * 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(); }常量定义// 各级经度步长秒索引 级别 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 };异常类定义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); } }