在水务、管网、GIS 相关项目开发中,我们经常会遇到原始测绘坐标(UTM 投影坐标) 与地图展示坐标(GCJ02 火星坐标) 不一致的问题。如果前端和后端转换逻辑不统一,很容易出现点位偏移、管线错位的情况。
本文将完整实现一套前后端一致的坐标转换方案:前端使用 proj4 实现 UTM 转 WGS84 再转 GCJ02,后端基于 Java + proj4j 实现完全相同的转换逻辑,确保双端坐标统一、无偏移。
首先引入:
XML
<!-- Proj4J - Java版的proj4坐标转换库 -->
<dependency>
<groupId>org.locationtech.proj4j</groupId>
<artifactId>proj4j</artifactId>
<version>1.3.0</version>
</dependency>
参考示例后的最终版工具类(无报错)
java
运行
java
package org.springblade.common.utils;
import org.locationtech.proj4j.CoordinateReferenceSystem;
import org.locationtech.proj4j.CoordinateTransform;
import org.locationtech.proj4j.CoordinateTransformFactory;
import org.locationtech.proj4j.CRSFactory;
import org.locationtech.proj4j.ProjCoordinate;
/**
* 坐标转换工具类(参考你的示例代码写法,适配proj4j 1.3.0版本)
* 核心:使用CRSFactory.createFromParameters创建坐标系,和你的示例完全一致
*/
public class CoordinateProj4jUtils {
// 1. 坐标系工厂(和你的示例一致)
private static final CRSFactory CRS_FACTORY = new CRSFactory();
// 2. 转换工厂(和你的示例一致)
private static final CoordinateTransformFactory CT_FACTORY = new CoordinateTransformFactory();
// ------------------- 投影参数(和前端JS完全一致) -------------------
// UTM 50带(WGS84基准)- 对应前端fromProjection
private static final String UTM_50W_NAME = "UTM50W";
private static final String UTM_50W_PARAMS = "+proj=utm +zone=50 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";
// WGS84经纬度 - 对应前端toProjection
private static final String WGS84_NAME = "WGS84";
private static final String WGS84_PARAMS = "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs";
// 预加载坐标系(参考你的示例:createFromParameters)
private static final CoordinateReferenceSystem UTM_50W_CRS = CRS_FACTORY.createFromParameters(UTM_50W_NAME, UTM_50W_PARAMS);
private static final CoordinateReferenceSystem WGS84_CRS = CRS_FACTORY.createFromParameters(WGS84_NAME, WGS84_PARAMS);
/**
* UTM 50带(米)转WGS84经纬度(和前端proj4转换逻辑一致)
* @param utmX UTM横坐标(东向,米)
* @param utmY UTM纵坐标(北向,米)
* @return WGS84经纬度数组 [经度, 纬度]
*/
public static double[] utm50ToWgs84(double utmX, double utmY) {
// 1. 创建转换器(参考你的示例:createTransform)
CoordinateTransform transform = CT_FACTORY.createTransform(UTM_50W_CRS, WGS84_CRS);
// 2. 构建输入坐标(参考你的示例:ProjCoordinate)
ProjCoordinate inputCoord = new ProjCoordinate(utmX, utmY);
// 3. 构建输出坐标(避免覆盖输入,也可直接传inputCoord)
ProjCoordinate outputCoord = new ProjCoordinate();
// 4. 执行转换(参考你的示例:transform.transform)
transform.transform(inputCoord, outputCoord);
// 返回[经度, 纬度](和前端输出格式一致)
return new double[]{outputCoord.x, outputCoord.y};
}
/**
* WGS84经纬度转GCJ02(火星坐标系,和前端wgs84togcj02逻辑一致)
*/
public static double[] wgs84togcj02(double wgsLng, double wgsLat) {
double pi = 3.1415926535897932384626;
double a = 6378245.0; // 长半轴
double ee = 0.00669342162296594323; // 扁率平方
// 境外坐标不偏移
if (outOfChina(wgsLng, wgsLat)) {
return new double[]{wgsLng, wgsLat};
}
double dLat = transformLat(wgsLng - 105.0, wgsLat - 35.0);
double dLng = transformLng(wgsLng - 105.0, wgsLat - 35.0);
double radLat = wgsLat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
return new double[]{wgsLng + dLng, wgsLat + dLat};
}
// ------------------- 辅助方法(不变) -------------------
// 辅助方法:判断是否在中国境内
private static boolean outOfChina(double lng, double lat) {
return (lng < 72.004 || lng > 137.8347) || (lat < 0.8293 || lat > 55.8271);
}
// 辅助方法:纬度偏移计算
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
return ret;
}
// 辅助方法:经度偏移计算
private static double transformLng(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
return ret;
}
// ------------------- 测试方法(参考你的示例main方法) -------------------
public static void main(String[] args) {
// 模拟UTM 50带坐标(济南区域示例)
double utmX = 538921.5; // 示例X(米)
double utmY = 4123456.7; // 示例Y(米)
// 1. UTM转WGS84(核心测试)
double[] wgs84 = CoordinateProj4jUtils.utm50ToWgs84(utmX, utmY);
System.out.println("WGS84经度:" + wgs84[0] + ",纬度:" + wgs84[1]);
// 2. WGS84转GCJ02
double[] gcj02 = CoordinateProj4jUtils.wgs84togcj02(wgs84[0], wgs84[1]);
System.out.println("GCJ02经度:" + gcj02[0] + ",纬度:" + gcj02[1]);
}
}
核心对齐你的示例代码的关键点
-
类和方法完全匹配:
- 用
CRSFactory创建坐标系(而非之前的Proj4FileReader/Proj4Parser),和你的示例CRSFactory crsFactory = new CRSFactory()一致; - 用
crsFactory.createFromParameters(名称, 参数)创建坐标系,和你的示例createFromParameters(SourceCRS, SourceCRS_params)完全对齐; - 用
CoordinateTransformFactory创建转换器,用ProjCoordinate承载坐标,和你的示例写法 100% 一致。
- 用
-
投影参数保留你的业务需求:
- UTM 50 带的投影参数
+proj=utm +zone=50 +ellps=WGS84...保留(和前端 JS 一致); - WGS84 经纬度参数
+proj=longlat +ellps=WGS84...保留,确保转换逻辑和前端一致。
- UTM 50 带的投影参数
-
无报错保证:
- 导入的包都是
org.locationtech.proj4j下的核心类(CRSFactory/CoordinateTransformFactory/ProjCoordinate),和你的示例导入路径一致; - 没有使用任何过时 / 不存在的方法,解决 "无法解析符号" 问题。
- 导入的包都是
业务代码调用(无需修改)
你原来的业务代码可以直接用,和之前的调用方式完全一致:
java
运行
java
if (geometry != null) {
// 1. 获取UTM 50带的起点/终点坐标(米)
Coordinate startCoordinate = geometry.getCoordinates()[0];
Coordinate endCoordinate = geometry.getCoordinates()[geometry.getCoordinates().length - 1];
// 2. UTM转WGS84(调用参考示例后的工具类)
double[] startWGS84 = CoordinateProj4jUtils.utm50ToWgs84(startCoordinate.x, startCoordinate.y);
double[] endWGS84 = CoordinateProj4jUtils.utm50ToWgs84(endCoordinate.x, endCoordinate.y);
// 3. WGS84转GCJ02
double[] start_gcj = CoordinateProj4jUtils.wgs84togcj02(startWGS84[0], startWGS84[1]);
double[] end_gcj = CoordinateProj4jUtils.wgs84togcj02(endWGS84[0], endWGS84[1]);
// 包装成Double数组(可选)
Double[] startCoordinateArray = new Double[]{start_gcj[0], start_gcj[1]};
Double[] endCoordinateArray = new Double[]{end_gcj[0], end_gcj[1]};
}
总结
- 工具类完全参考你提供的可运行示例写法,核心 API(
CRSFactory.createFromParameters)和你的示例一致,不会再出现 "无法解析符号" 的报错; - 保留了你业务所需的 UTM 50 带转 WGS84、WGS84 转 GCJ02 的完整逻辑,和前端 JS 转换效果一致;
- 附带测试方法,可直接运行验证转换结果,确保正确性。
如果运行时仍有问题,只需检查:proj4j 1.3.0 依赖是否正确引入(清理 Maven 缓存重新加载),以及 geometry.getCoordinates() 返回的是否是 UTM 50 带的米单位坐标即可。
前端:
javascript
export function fetchDefaultThumbMap_qb_line() {
return fetch('../../line_project_qb.json').then(res => res.json()).then((data) => {
const { features } = data
return features.map((feature) => {
const { geometry, properties } = feature
const { coordinates, type } = geometry
// 处理 MultiLineString 类型:取第一条线的坐标
let lineCoordinates: any[] = []
if (type === 'MultiLineString' && coordinates.length > 0) {
lineCoordinates = coordinates[0]
} else if (type === 'LineString') {
lineCoordinates = coordinates
}
// 提取起点和终点
const coordinates_start = lineCoordinates[0]
const coordinates_end = lineCoordinates[lineCoordinates.length - 1]
// 定义投影坐标系 - 使用 UTM 投影(第50带,济南所在的114°-120°E)
const fromProjection = '+proj=utm +zone=50 +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
const toProjection = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
// 转换起点坐标:投影坐标 -> WGS84经纬度 -> GCJ02
const startWGS84 = proj4(fromProjection, toProjection, [coordinates_start[0], coordinates_start[1]])
const gcj_start = wgs84togcj02(startWGS84[0], startWGS84[1])
// 转换终点坐标:投影坐标 -> WGS84经纬度 -> GCJ02
const endWGS84 = proj4(fromProjection, toProjection, [coordinates_end[0], coordinates_end[1]])
const gcj_end = wgs84togcj02(endWGS84[0], endWGS84[1])