座標系統詳解

概述

座標參考系統(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

小結

本章介紹了座標系統的核心知識:

  1. 座標系分類:地理座標系(經緯度)和投影座標系(平面米)
  2. 帶號計算:中國3度帶投影的帶號確定方法
  3. 座標轉換:不同座標系間的數據轉換方法
  4. 實踐要點:面積計算需使用投影座標系,空間分析需統一座標系