ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 다양한 좌표계를 위경도 좌표로 변환해보기 수난시대 (feat. geotools)
    📖 개발 공부 2023. 11. 5. 16:17

    회사에서 외부 공공데이터들을 가져와서 POI화하는 업무를 맡았다.

    공공데이터들은 각기 다른 EPSG 좌표계를 사용하고 있다.

    DB에 저장할 최종 좌표계는 EPSG:4326이다. 이는 우리가 흔히 쓰는 위경도 좌표라고 보면 된다.

     

    여기서 EPSG가 뭐신가

     

    EPSG 코드는 전세계 좌표계 정의에 대한 고유한 명칭이다. EPSG 코드에 대한 상세 정의는 proj4와 wkt라는 문자열로 되어 있다.

     

    EPSG.io: Coordinate Systems Worldwide

    EPSG.io: Coordinate systems worldwide (EPSG/ESRI), preview location on a map, get transformation, WKT, OGC GML, Proj.4. https://EPSG.io/ made by @klokantech

    epsg.io

    요기가 공식 사이트인데, 원하는 좌표계로 변환할 수도 있고 EPSG 코드 정보도 얻을 수 있다. 좌표에 대한 위치를 지도로도 확인 가능하다.

    https://epsg.io/?format=json&q=5174 요렇게 proj4와 wkt 정보를 JSON으로 확인이 가능하다.

     

    (참고: 코프링으로 서버 개발 중이다)

    geotools 라는 라이브러리를 사용하여 좌표계 변환을 해볼 것이다.

     

    다음은 geotools를 사용한 CoordinateConverter 이다. (스포하자면,, 좀 문제가 있는 코드 😅)

     

    전체 코드

    object CoordinateConverter {
        init {
            System.setProperty("org.geotools.referencing.forceXY", "true")
        }
    
        fun convertToCommonCoordinate(coordinate: Coordinate, from: CoordinateType): Coordinate {
            return convert(coordinate, from, EPSG_4326)
        }
    
        fun convertToOriginCoordinate(coordinate: Coordinate, to: CoordinateType): Coordinate {
            return convert(coordinate, EPSG_4326, to)
        }
    
        private fun convert(coordinate: Coordinate, from: CoordinateType, to: CoordinateType): Coordinate {
            val transform = transformEPSG(from, to)
            val point = convertToPoint(coordinate)
            val target = JTS.transform(point, transform)
            return Coordinate(x = target.coordinate.x, y = target.coordinate.y)
        }
    
        enum class CoordinateType(val code: String) {
            // 중부원점TM(Bessel), 사용처: LocalData DB
            EPSG_5174("EPSG:5174"),
    
            // GRS80 UTM-K, 사용처: 주소 DB
            EPSG_5179("EPSG:5179"),
    
            // 최종 변환될 위경도 좌표계
            EPSG_4326("EPSG:4326"),
        }
    
        private val geometryFactory = JTSFactoryFinder.getGeometryFactory()
    
        private fun transformEPSG(from: CoordinateType, to: CoordinateType): MathTransform {
            return CRS.findMathTransform(CRS.decode(from.code), CRS.decode(to.code), true)
        }
    
        fun generateLocationFromCoordinate(lat: Double, lon: Double): Geometry {
            return convertToPoint(
                Coordinate(
                    x = lon,
                    y = lat,
                ),
            )
        }
    
        private fun convertToPoint(coordinate: Coordinate): Point {
            return geometryFactory.createPoint(
                coordinate.let {
                    org.locationtech.jts.geom.Coordinate(
                        coordinate.x,
                        coordinate.y,
                    )
                },
            )
        }
    }

     

    위의 코드는 많은 개발 블로그들을 참고하여 작성한 코드이다.

     

    하나하나씩 설명해보겠당

    fun convertToCommonCoordinate(coordinate: Coordinate, from: CoordinateType): Coordinate {
        return convert(coordinate, from, EPSG_4326)
    }
    
    fun convertToOriginCoordinate(coordinate: Coordinate, to: CoordinateType): Coordinate {
        return convert(coordinate, EPSG_4326, to)
    }
    

     

     

    우선 public 함수 convertToCommonCoordinate(), convertToOriginCoordinate()!

    여기서 CommonCoordinate는 EPSG:4326을 가리키고 있다(최종 좌표계). OriginCoordinate는 변환할 좌표계를 가리킨다.

    enum class CoordinateType(val code: String) {
        // 중부원점TM(Bessel), 사용처: LocalData DB
        EPSG_5174("EPSG:5174"), // OriginCoordinate
    
        // GRS80 UTM-K, 사용처: 주소 DB
        EPSG_5179("EPSG:5179"), // OriginCoordinate
    
        // 최종 변환될 위경도 좌표계
        EPSG_4326("EPSG:4326"), // CommonCoordinate
    }
    

     

    이렇게 주석을 추가할 수 있겠다.

    이제 private 메서드인 convert() 함수를 살펴보자.

    private fun convert(coordinate: Coordinate, from: CoordinateType, to: CoordinateType): Coordinate {
        val transform = transformEPSG(from, to) // (1) 
        val point = convertToPoint(coordinate) // (2)
        val target = JTS.transform(point, transform) // (3)
        return Coordinate(x = target.coordinate.x, y = target.coordinate.y) // (4)
    }
    

    (1) 우선 좌표 변환을 해주는 transform을 찾는다.

    (2) originCoordinate를 Geometry Point로 변환한다.

    (3) (1)에서 가져온 transform 객체를 사용하여 다른 좌표계로 변환한다. 변환된 자표는 새로운 좌표계에 맞게 변환된다.

    (4) 변환된 좌표를 반환한다.

     

     

    하지만 EPSG 공식 사이트에서 변화된 자표와 코드에서 실제 변환된 좌표를 구글맵에 찍어 비교해보니 너무나도 달랐다……..

     

    EPSG:5174 → EPSG:4326으로 변환된 좌표

    [input] x: 206043.358791333, y: 448141.665395069 (EPSG:5174)

    [output] 비교

    EPSG 공식 사이트에서 변환된 좌표: 127.0691642, 37.5355053

    코드에서 변환된 좌표: 127.07127240781794, 37.533282895594795

    좌표가 너무나도 중요한 POI인데 이렇게 다른 위치를 찍고 있었던 것이다. 😱 (흑흑 운영 상에도 나가있는데..)

    주소 데이터는 올바른데, EPSG:5179를 사용중인 LocalData의 좌표만 다르게 나온다. 🤔

     

    좀더 찾아보니 MathTransform대신 CoordinateTransform도 쓸 수 있어서 변경해보았다.

    object CoordinateConverter {
        init {
            System.setProperty("org.geotools.referencing.forceXY", "true")
        }
    
        fun convertToCommonCoordinate(coordinate: Coordinate, from: CoordinateType): Coordinate {
            return convert(coordinate, from, EPSG_4326)
        }
    
        fun convertToOriginCoordinate(coordinate: Coordinate, to: CoordinateType): Coordinate {
            return convert(coordinate, EPSG_4326, to)
        }
    
        private fun convert(coordinate: Coordinate, from: CoordinateType, to: CoordinateType): Coordinate {
            val transform = transformEPSG(from, to)
            val projCoordinate = transform.transform(ProjCoordinate(coordinate.x, coordinate.y), ProjCoordinate())
            return Coordinate(x = projCoordinate.x, y = projCoordinate.y)
        }
    
        enum class CoordinateType(val proj4: String) {
            // 중부원점TM(Bessel), 사용처: LocalData DB
            EPSG_5174("+proj=tmerc +lat_0=38 +lon_0=127.002890277778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +towgs84=-145.907,505.034,685.756,-1.162,2.347,1.592,6.342 +units=m +no_defs"),
    
            // GRS80 UTM-K, 사용처: 주소 DB
            EPSG_5179("+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"),
    
            // 위경도 좌표계
            EPSG_4326("+proj=longlat +datum=WGS84 +no_defs"),
        }
    
        private val geometryFactory = JTSFactoryFinder.getGeometryFactory()
        private val crsFactory = CRSFactory()
    
        private fun transformEPSG(from: CoordinateType, to: CoordinateType): CoordinateTransform {
            val sourceCrs = crsFactory.createFromParameters(from.name, from.proj4)
            val targetCrs = crsFactory.createFromParameters(to.name, to.proj4)
            val coordinateTransformFactory = CoordinateTransformFactory()
            return coordinateTransformFactory.createTransform(sourceCrs, targetCrs)
        }
    
        fun generateLocationFromCoordinate(lat: Double, lon: Double): Geometry {
            return convertToPoint(
                Coordinate(
                    x = lon,
                    y = lat,
                ),
            )
        }
    
        private fun convertToPoint(coordinate: Coordinate): Point {
            return geometryFactory.createPoint(
                coordinate.let {
                    org.locationtech.jts.geom.Coordinate(
                        coordinate.x,
                        coordinate.y,
                    )
                },
            )
        }
    }

     

    MathTransform 에서 CoordinateTransform로 변경하면서 수정된 코드만 뽑아내보았당,

    private fun convert(coordinate: Coordinate, from: CoordinateType, to: CoordinateType): Coordinate {
        val transform = transformEPSG(from, to)
        val projCoordinate = transform.transform(ProjCoordinate(coordinate.x, coordinate.y), ProjCoordinate())
        return Coordinate(x = projCoordinate.x, y = projCoordinate.y)
    }
    
    enum class CoordinateType(val proj4: String) {
        // 중부원점TM(Bessel), 사용처: LocalData DB
        EPSG_5174("+proj=tmerc +lat_0=38 +lon_0=127.002890277778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +towgs84=-145.907,505.034,685.756,-1.162,2.347,1.592,6.342 +units=m +no_defs"),
    
        // GRS80 UTM-K, 사용처: 주소 DB
        EPSG_5179("+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"),
    
        // 위경도 좌표계
        EPSG_4326("+proj=longlat +datum=WGS84 +no_defs"),
    }
    
    private fun transformEPSG(from: CoordinateType, to: CoordinateType): CoordinateTransform {
        val sourceCrs = crsFactory.createFromParameters(from.name, from.proj4)
        val targetCrs = crsFactory.createFromParameters(to.name, to.proj4)
        val coordinateTransformFactory = CoordinateTransformFactory()
        return coordinateTransformFactory.createTransform(sourceCrs, targetCrs)
    }
    

    이렇게 EPSG 코드별 proj4 문자열을 직접 넣어서 CoordinateTransform을 얻으니 위치가 정확해졌다.!!

     

    변환된 좌표: 37.53562239999344,127.06904191561026

     

    DefaultMathTransformFactory 공식 문서 설명을 보니

    MathTransform이 좌표의 수학적 변환만 다루며, 실제 지구의 좌표 시스템과 관련된 의미를 알지 못하고 신경 쓰지 않는다. 따라서 MathTransform은 실제 지구 좌표 변환에 사용하기보다는 기하학적 좌표 변환 및 수학적 계산에 적합한 도구로 사용되는 듯하다..! ㅠㅠ

     

    테스트코드에서는 1m미만이면 통과하도록 되어있는데 여기선 통과되어서 별다른 의심을 하지 않았다. 하지만 직접 찍어보니 너무나도 달랐다. 내가 참고한 개발 블로그들은 정확히 잘 변환되는건지 좀 궁금해졌다. 

    그리고 기존 코드로 EPSG:5179 좌표계는 잘 변환되는데 왜 EPSG:5174 좌표계는 너무나도 다른 위치를 찍어주는 걸까? 각 좌표계 특성이 있는 듯한데,, 악 어렵다!!!!

     

    좌표 수정하러 가야지 ..⭐

     

     

    728x90
    반응형

    댓글

Designed by Tistory.