경위도로 지정한 위치 사이의 흐름선을 3D로 표현하기

구에 지구에 대한 텍스쳐를 맵핑하고 경위도로 지정된 2개의 위치 사이에 흐름선을 표현하는 시각화에 대한 구현체입니다. 최종 실행 결과는 아래와 같습니다.

WebGL을 기반으로 하는 3차원 라이브러리 three.js를 사용했으며, class를 통한 모듈방식으로 구현하였는데, 전체 소스 코드는 다음 URL로 다운로드 받으시기 바랍니다.

GIS 개발자로써 가장 중요한 코드 중 하나를 언급하면 바로 경위도 좌표를 xyz 좌표계로 변환해 주는 함수인데요. 바로 아래의 코드입니다.

_getPosFromLatLonRad(lat, lon, radius) {
    var phi = (90 - lat) * (Math.PI / 180)
    var theta = (lon + 180) * (Math.PI / 180)

    let x = -((radius) * Math.sin(phi)*Math.cos(theta))
    let z = ((radius) * Math.sin(phi)*Math.sin(theta))
    let y = ((radius) * Math.cos(phi))

    return {x,y,z}
}

위도와 경도 그리고 구의 반지름을 받아 해당하는 xyz 축의 좌표를 반환합니다.

[PostGIS] 가장 가까운 위치 찾기

시나리오는 어떤 지점에서 가장 가까운 위치를 찾아야 한다.. 입니다. 그런데 위치에 대한 좌표 타입이 geometry가 아니고 일단 실수형이라는 점입니다. 그리고 기준 좌표와 찾아야 하는 좌표의 좌표 체계가 다릅니다. 하나는 구글 좌표계(EPSG:3857)이고 하나는 경위도 좌표계(EPSG:4326)입니다.

아래의 쿼리가 위의 내용에 대한 솔루션 중 하나입니다.

SELECT 
    station_id,
    address,
    lng,
    lat,
    ST_DISTANCE(
        ST_TRANSFORM(
            ('SRID=4326;POINT(' || lng || ' ' || lat || ')')::geometry,
            3857
        ),
        'SRID=3857;POINT(14128453 4506986)'::geometry
    ) AS distance
FROM 
    weather_station
ORDER BY
    distance

즉, 구글 좌표계로 (14128453, 4506986)인 지점과의 거리를 구하고 있습니다. 거리를 구하는 ST_DISTANCE는 서로 다른 좌표계를 사용할 수 없으므로 ST_TRANSFORM을 통해 경위도 좌표를 X,Y 좌표계로 변경하고 있습니다. 결과는 다음과 같습니다.

보시면 가장 첫번째 Row가 가장 가까운 대상이라는 것을 알 수 있습니다.

크리깅(Kriging) 분석에 대한 색상 시각화

아래의 결과는 크리깅(Kriging) 분석을 통한 보간 결과(공간상의 숫자값은 보간을 위한 입력 값이 아닌 단순 번호임)입니다. 크리깅 보간의 결과값의 범위가 최대값 7.4, 최소값 0.6이 산출된 경우입니다. 최대값은 빨간색으로 최소값은 초록색으로 표현하고 있습니다.

단순히 시각적인 관점으로 보면 나쁘진 않은데, 문제는.. 빨간색은 위험 범위의 값일 경우 표현에 사용하고자 한다는데 있습니다. 위험 수위 값은 10부터라고 정의 했을때.. 위의 경우 최대값인 7.4는 위험 수위값이 아니므로 빨간색으로 표현되면 사용자에게 혼란을 야기합니다.

위의 문제점을 개선한 동일한 크리깅 결과를 기대한 색상으로 시각화한 결과는 아래와 같습니다.

모든 지점이 10보다 작으므로 위험을 나타내는 빨간색으로 표현되지 않습니다. 바로 이게 원하는 결과입니다.

이러한 결과를 얻기 위한 색상을 정의하는 코드는 다음과 같습니다.

const colors = [
    { value: 10, steps: 10, color: [255, 0, 0, 200] },
    { value: 5, steps: 10, color: [255, 255, 0, 200] },
    { value: 0, steps: 10, color: [0, 255, 0, 200] },
    { value: -5, steps: 10, color: [0, 255, 255, 200] },
    { value: -10, color: [0, 0, 255, 200] },
];

const clrTbl = new Xr.RangesColorTable(colors);
if (clrTbl.build()) {
    gridLyr.updateByRangesColorTable(clrTbl, psd);
    map.update();
}

위의 코드는 공간 데이터를 시각화 해주는 FingerEyes-Xr에 대한 코드입니다. FingerEyes-Xr은 자바스크립트(js) 기반의 웹 GIS 라이브러리입니다.

Line 그리기 (DDA 알고리즘)

C언어로 구현된 코드를 C#으로 변경한 코드입니다. 출처는 https://www.geeksforgeeks.org/dda-line-generation-algorithm-computer-graphics/ 입니다.

private void DrawLine(
    int X0, int Y0, int X1, int Y1, 
    XrMapLib.GridCells cells, 
    double Value)
{
    int dx = X1 - X0;
    int dy = Y1 - Y0;

    int steps = Math.Abs(dx) > Math.Abs(dy) ? Math.Abs(dx) : Math.Abs(dy);

    float Xinc = dx / (float)steps;
    float Yinc = dy / (float)steps;

    float X = X0;
    float Y = Y0;
    for (int i = 0; i <= steps; i++)
    {
        cells.SetValue((int)Math.Round(Y), (int)Math.Round(X), Value); 
        X += Xinc; 
        Y += Yinc;
    }
}

그리드 분석에서 특정 지점에 대해 값을 지정하는 방식이 아닌 값을 지정하는 대상을 선(Line)으로 정의하기 위한 코드입니다.

크리깅(Kriging) 공간분석

공간 상에 분포된 값 기반의 보간 방식 중 하나인 크리깅을 기능 단위로 만든 코드를 정리한 글입니다. GIS 엔진은 웹 GIS 컴포넌트인 FingerEyes-Xr를 사용했습니다. 크리깅 알고리즘은 오픈소스 라이브러리인 kriging.js를 사용하였으므로 Leaflet이나 OpenLayers에 대한 API에 익숙한 개발자라면 해당 GIS 컴포넌트로도 크리깅 결과에 대한 효과적인 시각화를 구현할 수 있을 것입니다. 웹 기반에서 수행되지만 별도의 서버가 필요하지 않습니다. 지도는 VWorld의 배경지도를 그대로 이용할 것이며 크리깅을 위한 입력 데이터는 실행시 동적으로 생성할 것이기 때문입니다.

결과를 이미지로 먼저 살펴보면 아래와 같습니다.

0~100까지의 값을 가지는 총 35개의 지점이 있고 특정 영역 안에서 크리깅 분석을 수행해 그 결과를 그라디언트 색상으로 표현하는 것인데요. 먼저 특정 영역에 대한 좌표를 지정하고 지도에 표시하는 코드는 다음과 같습니다.

let psd = new Xr.data.PolygonShapeData([[
    new Xr.PointD(14289447.80, 4437166.67),
    new Xr.PointD(14289920.87, 4437347.65),
    new Xr.PointD(14289963.20, 4437210.07),
    new Xr.PointD(14290033.05, 4437198.42),
    new Xr.PointD(14289981.19, 4436636.45),
    new Xr.PointD(14289942.04, 4436488.28),
    new Xr.PointD(14289879.60, 4436349.64),
    new Xr.PointD(14289733.55, 4436420.55),
    new Xr.PointD(14289717.67, 4436438.54)
]]);

let psr = new Xr.data.PolygonGraphicRow(0, psd);

psr.brushSymbol().opacity(0);
psr.penSymbol().color('#ff0000');

gl.rowSet().add(psr);

0~100까지 난수로 부여된 값을 가지는 총 35개의 지점을 지도에 표시하는 코드는 다음과 같구요.

const x = [
    14289539.88, 14289633.01, 14289713.44, 14289812.93, 14289912.41,
    14289598.08, 14289698.63, 14289809.75, 14289883.83, 14289952.62,
    14289747.31, 14289760.01, 14289779.06, 14289793.88, 14289815.04,
    14289836.21, 14289864.78, 14289920.87, 14289698.63, 14289665.82,
    14289758.95, 14289697.57, 14289771.65, 14289732.49, 14289804.46,
    14289828.8, 14289839.38, 14289853.14, 14289885.95, 14289853.14,
    14289878.54, 14289876.42, 14289942.04, 14289929.34, 14289912.41
];

const y = [
    4437041.79, 4437076.72, 4437109.52, 4437148.68, 4437185.72,
    4436892.57, 4436923.26, 4436970.88, 4436999.46, 4436999.46,
    4436877.75, 4436834.36, 4436785.67, 4436736.99, 4436681.96,
    4436626.92, 4436563.42, 4436512.62, 4436824.83, 4436761.33,
    4436740.17, 4436667.14, 4436638.57, 4436577.18, 4436553.90,
    4436927.49, 4436879.87, 4436834.36, 4436790.97, 4436723.23,
    4436687.25, 4436629.04, 4436890.45, 4436744.40, 4436600.47
];

const t = [];

for (let i = 0; i < x.length; i++) {
    t[i] = Math.random() * 100.0;

    let p = new Xr.PointD(x[i], y[i]);
    let tsd = new Xr.data.TextShapeData({ x: x[i], y: y[i], text: t[i] >> 0 });
    let pgr = new Xr.data.TextGraphicRow(i + 1, tsd);

    gl.rowSet().remove(i + 1);
    gl.rowSet().add(pgr);
}

크리깅을 위한 입력값이 준비되어 있으므로 크리깅 분석을 아래의 코드처럼 수행합니다.

const variogram = kriging.train(t, x, y, "spherical", 0, 100);

이제 크리깅 분석 결과를 지도에 표시하기 위해 GridLayer를 사용하게 되는데, 크리깅 분석 결과 모델을 토대로 모르는 지점에 대한 값도 예측할 수 있으며 이 값들을 토대로 적절하게 색상을 배합하면 됩니다. 이러한 코드는 다음과 같습니다.

gridLyr.reset();

const minX = psr.MBR().minX;
const maxX = psr.MBR().maxX;
const minY = psr.MBR().minY;
const maxY = psr.MBR().maxY;

for (let x = minX; x < maxX; x += cellRes) {
    for (let y = minY; y < maxY; y += cellRes) {
        const v = kriging.predict(x, y, variogram);
        gridLyr.value(x, y, v);
    }
}

let clrTbl = new Xr.ColorTable(6);
                    
clrTbl.set(5, 225, 228, 177, 230);
clrTbl.set(4, 190, 208, 122, 230);
clrTbl.set(3, 152, 193, 99, 230);
clrTbl.set(2, 97, 168, 93, 230);
clrTbl.set(1, 46, 146, 85, 230);
clrTbl.set(0, 20, 102, 59, 230);

if (clrTbl.build()) {
    gridLyr.updateByColorTable(clrTbl, psd);
    map.update();
}

GridLayer에 대한 변수는 gridLyr이며 크리깅 보간을 통해 gridLayer 내부의 전체 셀값들이 업데이트됩니다. updateByColorTable 함수를 통해 표현할 색상과 원하는 경계 이외의 셀은 투명하게 잘리게 됩니다. 원하는 경계는 psd라는 변수로 지정하고 있는데 이 psd는 앞서 생성한 특정 영역에 대한 그래픽 요소 객체입니다. 참고로 gridLyr 객체의 생성 코드는 다음과 같습니다.

let cellRes = 1;
let gridLyr = new Xr.layers.GridLayer("grid",
    {
        mbr: psr.MBR(),
        resolution: cellRes
    }
);

이상으로 잘 만들어진 크리깅 라이브러리를 이용하여 웹 상에서 크리깅 분석을 수행하고 그 분석 결과를 시각화하는 코드에 대해 살펴 보았습니다.