经纬度转北斗网格位置码(非极地区域,编码篇)

前言

在 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级及以后:在父网格内细分

**思路:**每一级都是"在当前父网格内,找出坐标属于哪个子网格"。

步骤:

  1. 算出父网格起点坐标(上级起点 + 上级子网格偏移量)

  2. 用原始坐标减去父网格起点,得到相对偏移

  3. 除以当前级步长,取整 → 行列号

  4. 按编码规则转为字符,追加到网格码

以第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);
    }
}
相关推荐
唐青枫1 小时前
Java ActiveMQ 实战指南:从 JMS 队列、主题到 Spring Boot 消息处理
java
码兄科技9 小时前
Java AI智能体开发实战:从零构建智能对话系统指南
java·开发语言·人工智能
折哥的程序人生 · 物流技术专研9 小时前
第8篇:六大设计原则在工厂模式中的体现
java·设计模式·设计原则·工厂模式·编程进阶·扩充系列
小明bishe1810 小时前
计算机毕业设计之基于JAVA的植物科普网站
java·spring boot·spring·架构·课程设计
r_oo_ki_e_10 小时前
Java Map 集合学习笔记
java·笔记·学习
中科岩创10 小时前
宁波某大学高支模施工安全监测实训模型应用
科技·物联网·安全·自动化
SL-staff10 小时前
智慧园区2000+设备统一管理:JVS-IoT如何降低运维成本40%
java·开发语言·物联网·智慧园区·设备管理·运维优化·jvs-iot
咖啡八杯11 小时前
GoF设计模式——模板方法模式
java·后端·spring·设计模式