座標系統詳解
概述
座標參考系統(CRS,Coordinate Reference System)是GIS的基礎概念之一。不同的座標系會導致相同位置的座標值完全不同,理解和正確使用座標系是避免數據錯誤的關鍵。
座標系分類
地理座標系(Geographic CRS)
地理座標系使用經度和緯度來表示地球表面的位置:
- 經度(Longitude):東西方向,範圍 -180° 到 180°
- 緯度(Latitude):南北方向,範圍 -90° 到 90°
常用地理座標系:
|
EPSG代碼
|
名稱
|
説明
|
|
4490
|
CGCS2000
|
中國2000國家大地座標系
|
|
4326
|
WGS84
|
GPS使用的全球座標系
|
|
4214
|
BJ54
|
北京54座標系
|
|
4610
|
XA80
|
西安80座標系
|
投影座標系(Projected CRS)
投影座標系將球面座標轉換為平面座標,使用米作為單位:
常用投影類型:
- 高斯-克呂格投影:中國官方測繪使用
- UTM投影:國際通用
- 墨卡託投影:Web地圖常用
CGCS2000投影座標系:
|
EPSG代碼
|
帶號
|
中央經線
|
|
4491
|
13
|
75°E
|
|
4502
|
24
|
120°E
|
|
4526
|
38
|
114°E
|
投影座標系的命名規律:EPSG:4488 + 帶號 = 投影座標系代碼
座標系識別與管理
支持的座標系列表
系統預置了常用的座標系,並支持動態擴展:
/**
* 獲取支持的投影座標系列表
*/
private static Map<Integer, CoordinateReferenceSystem> supportedCRSList() {
if (supportedCRSList != null && !supportedCRSList.isEmpty()) {
return supportedCRSList;
}
supportedCRSList = new HashMap<>();
for (int i = 4490; i < 4555; i++) {
supportedCRSList.put(i, CRS.decode("EPSG:" + i, true));
}
return supportedCRSList;
}
動態獲取座標系
對於不在預置列表中的座標系,可以動態獲取:
/**
* 獲取被支持的EPSG Code和對應座標系信息
* 如果不存在則增加到支持列表
*/
public static Map.Entry<Integer, CoordinateReferenceSystem> getSupportedCRS(Integer wkid) {
if (!supportedCRSList().containsKey(wkid)) {
CoordinateReferenceSystem crs = CRS.decode("EPSG:" + wkid, true);
supportedCRSList().put(wkid, crs);
return new HashMap.SimpleEntry<>(wkid, crs);
}
return new HashMap.SimpleEntry<>(wkid, supportedCRSList().get(wkid));
}
座標系WKT解析
從WKT字符串解析座標系:
/**
* 在支持的座標系範圍內獲取標準EPSG Code
*/
public static Map.Entry<Integer, CoordinateReferenceSystem> standardizeCRS(String wkt) {
CoordinateReferenceSystem crs = CRS.parseWKT(wkt);
return standardizeCRS(crs);
}
座標系判斷
判斷是否為投影座標系
/**
* 判斷座標系是否為投影座標系
*/
public static boolean isProjectedCRS(CoordinateReferenceSystem crs) {
Map.Entry<Integer, CoordinateReferenceSystem> entry = standardizeCRS(crs);
return entry.getValue() instanceof ProjectedCRS;
}
判斷座標系是否相同
不同來源的座標系可能參數相同但定義方式不同,需要深度比較:
/**
* 判斷座標系是否相同
*/
public static boolean isSameCRS(CoordinateReferenceSystem sourceCrs,
CoordinateReferenceSystem targetCrs) {
if (sourceCrs instanceof ProjectedCRS && targetCrs instanceof ProjectedCRS) {
ProjectedCRS sourceProjectedCRS = (ProjectedCRS) sourceCrs;
ProjectedCRS targetProjectedCRS = (ProjectedCRS) targetCrs;
// 比較基礎地理座標系
boolean isSameBaseCRS = isSameCRS(
sourceProjectedCRS.getBaseCRS(),
targetProjectedCRS.getBaseCRS()
);
// 比較投影參數
boolean isSameConversionFromBase = true;
ParameterValueGroup sourceParams = sourceProjectedCRS
.getConversionFromBase().getParameterValues();
ParameterValueGroup targetParams = targetProjectedCRS
.getConversionFromBase().getParameterValues();
for (int i = 0; i < sourceParams.values().size(); i++) {
GeneralParameterValue s = sourceParams.values().get(i);
GeneralParameterValue t = targetParams.values().get(i);
if (!s.equals(t)) {
isSameConversionFromBase = false;
break;
}
}
return isSameBaseCRS && isSameConversionFromBase;
} else if (sourceCrs instanceof GeographicCRS &&
targetCrs instanceof GeographicCRS) {
// 比較橢球體參數
GeographicCRS sourceGeoCRS = (GeographicCRS) sourceCrs;
GeographicCRS targetGeoCRS = (GeographicCRS) targetCrs;
Ellipsoid s = sourceGeoCRS.getDatum().getEllipsoid();
Ellipsoid t = targetGeoCRS.getDatum().getEllipsoid();
return s.getSemiMajorAxis() == t.getSemiMajorAxis()
&& s.getSemiMinorAxis() == t.getSemiMinorAxis()
&& s.getInverseFlattening() == t.getInverseFlattening();
}
return false;
}
帶號計算
中國的3度帶投影需要根據經度確定帶號:
從幾何獲取帶號
/**
* 獲取幾何所在帶號
*/
public static int getDh(Geometry geometry) {
Point point = geometry.getCentroid();
int dh = 0;
if (point.getX() < 180) {
// 經緯度座標,根據經度計算帶號
dh = (int) ((point.getX() + 1.5) / 3);
} else if (point.getX() / 10000000 > 3) {
// 投影座標,從X座標提取帶號
dh = (int) (point.getX() / 1000000);
}
return dh;
}
/**
* 從WKT獲取帶號
*/
public static int getDh(String wkt) {
Geometry geom = GeometryConverter.wkt2Geometry(wkt);
return getDh(geom);
}
帶號與投影座標系換算
/**
* 從投影座標系WKID獲取帶號
*/
public static int getDh(int projectedWkid) {
return projectedWkid - 4488;
}
/**
* 從帶號獲取投影座標系WKID
*/
public static Integer getProjectedWkid(int dh) {
return 4488 + dh;
}
/**
* 從幾何獲取對應的投影座標系
*/
public static Integer getProjectedWkid(Geometry geometry) {
return getProjectedWkid(getDh(geometry));
}
智能識別幾何座標系
/**
* 獲取幾何WKID
* 經緯度返回4490,投影座標返回對應投影座標系
*/
public static Integer getWkid(Geometry geometry) {
Point point = geometry.getCentroid();
if (point.getX() < 180) {
return 4490; // CGCS2000地理座標系
} else {
return getProjectedWkid(getDh(geometry));
}
}
座標系容差
不同座標系的容差不同:
/**
* 獲取座標系容差
*/
public static double getTolerance(Integer wkid) {
Map.Entry<Integer, CoordinateReferenceSystem> entry = getSupportedCRS(wkid);
return getTolerance(entry.getValue());
}
public static double getTolerance(CoordinateReferenceSystem crs) {
if (isProjectedCRS(crs)) {
return 0.0001; // 投影座標系:0.1毫米
} else {
return 0.000000001; // 地理座標系:約0.1毫米(赤道處)
}
}
設計思路:容差的設置需要考慮座標系的單位。投影座標系以米為單位,0.0001米約等於0.1毫米;地理座標系以度為單位,需要更小的數值來表達相同的實際距離。
座標轉換
幾何對象座標轉換
/**
* 幾何對象轉換座標系
*/
public static Geometry transform(Geometry geometry,
Integer sourceWkid,
Integer targetWkid) {
if (sourceWkid.equals(targetWkid)) {
return geometry;
}
CoordinateReferenceSystem sourceCRS = getSupportedCRS(sourceWkid).getValue();
CoordinateReferenceSystem targetCRS = getSupportedCRS(targetWkid).getValue();
return transform(geometry, sourceCRS, targetCRS);
}
/**
* 使用CRS對象進行座標轉換
*/
public static Geometry transform(Geometry geometry,
CoordinateReferenceSystem sourceCRS,
CoordinateReferenceSystem targetCRS) {
if (isSameCRS(sourceCRS, targetCRS)) {
return geometry;
}
MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS);
return JTS.transform(geometry, transform);
}
WKT字符串座標轉換
/**
* WKT格式的座標轉換
*/
public static String transform(String wkt, Integer sourceWkid, Integer targetWkid) {
Geometry geom = GeometryConverter.wkt2Geometry(wkt);
geom = transform(geom, sourceWkid, targetWkid);
return geom == null ? null : geom.toText();
}
圖層座標轉換
/**
* 轉換WktLayer的座標系
*/
public static WktLayer reproject(WktLayer wktLayer, Integer targetWkid) {
wktLayer.check();
WktLayer clone = ObjectUtil.cloneByStream(wktLayer);
if (clone.getWkid().equals(targetWkid)) {
return clone;
}
// 轉換每個要素的幾何
for (WktFeature feature : clone.getFeatures()) {
Geometry geometry = new WKTReader().read(feature.getWkt());
Geometry targetGeometry = transform(geometry, wktLayer.getWkid(), targetWkid);
feature.setWkt(targetGeometry.toText());
}
// 更新圖層信息
clone.setWkid(targetWkid);
clone.setTolerance(getTolerance(targetWkid));
return clone;
}
要素集合座標轉換
/**
* 轉換FeatureCollection的座標系
*/
public static FeatureCollection<SimpleFeatureType, SimpleFeature> reproject(
FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection,
Integer targetWkid) {
CoordinateReferenceSystem sourceCRS = standardizeCRS(
featureCollection.getSchema().getCoordinateReferenceSystem()
).getValue();
CoordinateReferenceSystem targetCRS = getSupportedCRS(targetWkid).getValue();
if (isSameCRS(sourceCRS, targetCRS)) {
return featureCollection;
}
// 設置源座標系
if (sourceCRS != null) {
featureCollection = new ForceCoordinateSystemFeatureResults(
featureCollection, sourceCRS, false
);
}
// 轉換到目標座標系
if (targetCRS != null) {
featureCollection = new ReprojectingFeatureCollection(
featureCollection, targetCRS
);
}
return featureCollection;
}
實踐案例
案例1:經緯度轉投影座標
// 某地塊的經緯度座標
String wkt = "POLYGON((116.3 39.9, 116.4 39.9, 116.4 40.0, 116.3 40.0, 116.3 39.9))";
Geometry geom = GeometryConverter.wkt2Geometry(wkt);
// 確定帶號(116.3°E位於39帶)
int dh = CrsUtil.getDh(geom);
System.out.println("帶號: " + dh);
// 獲取投影座標系
int projWkid = CrsUtil.getProjectedWkid(dh);
System.out.println("投影座標系: EPSG:" + projWkid);
// 座標轉換
Geometry projGeom = CrsUtil.transform(geom, 4490, projWkid);
System.out.println("投影后座標: " + projGeom.toText());
// 計算面積(投影座標系下計算才準確)
double area = projGeom.getArea();
System.out.println("面積: " + area + " 平方米");
案例2:不同座標系數據統一
// 數據源1:CGCS2000地理座標系
WktLayer layer1 = loadLayer("data1.shp"); // wkid = 4490
// 數據源2:CGCS2000 38帶投影座標系
WktLayer layer2 = loadLayer("data2.shp"); // wkid = 4526
// 統一轉換為地理座標系
WktLayer layer2Reprojected = CrsUtil.reproject(layer2, 4490);
// 現在可以進行空間疊加分析
for (WktFeature f1 : layer1.getFeatures()) {
Geometry g1 = GeometryConverter.wkt2Geometry(f1.getWkt());
for (WktFeature f2 : layer2Reprojected.getFeatures()) {
Geometry g2 = GeometryConverter.wkt2Geometry(f2.getWkt());
if (g1.intersects(g2)) {
// 處理相交要素
}
}
}
案例3:Web展示座標準備
// 從數據庫讀取數據(投影座標)
WktLayer layer = loadFromPostGIS(dbConn, "buildings"); // wkid = 4526
// 轉換為經緯度(Web地圖通常使用WGS84或CGCS2000)
WktLayer webLayer = CrsUtil.reproject(layer, 4490);
// 轉換為GeoJSON供前端使用
WktLayerConverter.toGeoJSON(webLayer, "buildings.geojson", GisEngineType.GEOTOOLS);
常見問題
1. 座標系不匹配導致的問題
現象:疊加分析結果為空,或計算結果異常偏大/偏小
原因:兩組數據使用了不同的座標系
解決:在進行空間分析前,將所有數據統一到同一座標系
2. 面積計算結果不正確
現象:計算的面積值是一個很小的數或很大的數
原因:使用經緯度座標直接計算面積
解決:先將數據轉換到投影座標系,再計算面積
3. 帶號選擇
問題:如何選擇合適的帶號?
建議:根據數據的主要分佈區域選擇中央經線最接近的帶:
- 查看數據的經度範圍
- 選擇中心經度對應的帶號
- 3度帶:帶號 = (經度 + 1.5) / 3
4. CGCS2000與WGS84
問題:是否需要區分CGCS2000和WGS84?
説明:
- CGCS2000和WGS84的差異在釐米級,一般應用可以視為相同
- 高精度測量場景需要嚴格區分
- 互聯網地圖通常使用WGS84,國內官方數據使用CGCS2000
小結
本章介紹了座標系統的核心知識:
- 座標系分類:地理座標系(經緯度)和投影座標系(平面米)
- 帶號計算:中國3度帶投影的帶號確定方法
- 座標轉換:不同座標系間的數據轉換方法
- 實踐要點:面積計算需使用投影座標系,空間分析需統一座標系