VWorld와 호환성을 가진 (동일한 형식을 가진) 타일맵 가공시 주의할 점

기관에서 주로 사용하는 배경지도가 VWorld이다 보니 VWorld을 기본도로 해서 배경맵을 더 강화(Enrichment)시켜주는 작업이 필요할 때가 있습니다.

해당 기관은 VWorld 배경맵을 사용했는데 여러가지 문제로 관리하는 건물이나 시설물이 상당 부분 누락된 VWorld을 보강해야 할 필요가 있었고 이에 대한 작업을 수행하게 되었습니다. 예전에도 이런 비슷한 작업을 진행했는데.. 매번 할때마다 그 전의 작업 내역을 잊어 버리고 다시 VWorld의 구조를 분석해 작업 흐름을 추론해 진행했습니다. 해서.. 이번에는 작업 내역을 정리해 둡니다.

먼저 VWorld는 EPSG:3857 좌표계를 사용합니다. 레벨이 1~19까지 존재합니다. 각 단계의 축척에 대한 픽셀 당 논리단위 거리는 다음과 같습니다. 1레벨부터 시작합니다.

78000,39000,19600,9800,4900,2400,1222.9924523925781,611.4962261962891,305.74811309814453,152.87405654907226,76.43702827453613,38.218514137268066,19.109257068634033,9.554628534317017,4.777314267158508,2.388657133579254,1.194328566789627,0.5971642833948135,0.29858214169740677

그리고 타일맵 좌표의 전체 범위는 다음과 같습니다.

-20037508.342789244,-20037508.342789244,20037508.342789244,20037508.342789244

1레벨은 전세계의 지도가 표시됩니다. 반대로 마지막 레벨인 19레벨은 최대한으로 확대된 건물이 표시됩니다. 그리고 타일맵이 저정되는 디렉토리 구조가 Level/Column/Row 순서로 저장됩니다. 좀더 일반적인 타일맵의 디렉토리 구조는 Level/Row/Column 입니다. 끝으로 Row의 인덱스가 역순이라는 점도 주의해야 합니다.

작업시 초점을 맞춰야할 것들을 정리해서 요약하면 다음과 같습니다. 1) 타일맵 좌표의 전체 범위 2) 각 축척 단계의 픽셀 당 논리 단위 거리값 3) 디렉토리의 구조(Level/Row/Column) 4) Row 인덱스가 역순임

C#의 Parallel API를 이용하여 CPU 100% 활용하기

CPU는 여러 개의 Core로 구성되어 있고 각 Core 단위로 동시에 연산을 처리할 수 있습니다. C#에서 CPU를 최대한 이용하기 위해서 Parallel API를 이용한 코드를 정리합니다.

Task.Factory.StartNew(() => {
    Parallel.ForEach(addressData, new ParallelOptions { MaxDegreeOfParallelism = cntCores },
        (task) => {
            int iAddress = task.Index;
            string Address = task.Address;
            
            /* 
                시간이 많이 걸리는 연산을 처리하는 스코프
            */

            Invoke(new Action(() => {
                // UI 처리가 가능한 스코프
            }));

            //Application.DoEvents(); -> 더 이상 필요치 않음
        }
    );
});

중요한 점은 Parallel에서 만들어진 스레드는 Main 스레드에서 구동되면 안됩니다. 그래서 Task.Factory.StartNew를 통해 별도의 스레드를 하나 만들고.. 만들어진 스레드에서 Parallel의 스레드를 구동하게 합니다. Task.Factory.StartNew를 사용한 이유는 스레드를 간단하게 만들 수 있기 때문으로 다른 스레드를 만드는 코드도 유효합니다. addressData는 스레드를 통해 처리해야할 데이터가 담긴 컨테이너입니다. 예를 들어 다음과 같습니다.

List<ADDRESS_DATA> addressData = new List<ADDRESS_DATA>();

ADDRESS_DATA는 다음과 같구요. (올바른 캡슐화를 적용하지 않은 코드입니다)

private class ADDRESS_DATA
{
    public int Index;
    public String Address;

    public ADDRESS_DATA(int Index, String Address)
    {
        this.Index = Index;
        this.Address = Address;
    }
}

Paralleld의 ForEach 매서드에서 task를 통해 ADDRESS_DATA의 필드값에 접근할 수 있습니다. 그리고 cntCores는 CPU의 코어 수인데, 동시에 실행할 수 있는 스레드의 개수로 지정하기 적당한 값입니다. 다음처럼 얻을 수 있습니다.

int cntCores = Environment.ProcessorCount;

pyQGIS를 이용한 벡터 데이터 처리 10 : 버퍼(Buffer) 연산

지오메트리에 대한 공간 연산을 공간 분석을 위해 활용할 수 있는데 그 연산 중 버퍼 연산에 대한 코드를 설명합니다. 먼저 레이어를 추가하고 RN이라는 필드의 값이 “로”로 끝나는 피쳐를 선택하고 선택된 피쳐의 지오메트리에 대해 버퍼 연산을 수행한 뒤 그 결과를 다른 SHP 파일에 저장하는 코드를 작성해 보겠습니다.

먼저 레이어를 추가하고 RN 필드값이 “로”로 끝나는 피쳐를 선택하는 코드를 다음처럼 작성합니다.

QgsProject.instance().removeAllMapLayers()
layer = QgsVectorLayer("D:/__Data__/세종특별자치시_36000/TL_SPRD_MANAGE.shp", "TL_SPRD_MANAGE")
QgsProject.instance().addMapLayers([layer])
layer.selectByExpression('"RN" like \'%로\'')

버퍼 연산 결과를 저장할 SHP 파일 작성자(Writer)를 생성합니다.

fields = layer.fields()
fileName = "D:/__Data__/buffer.shp"
writer = QgsVectorFileWriter(
    fileName,
    "utf-8", 
    fields,
    QgsWkbTypes.Polygon,
    layer.sourceCrs(),
    "ESRI Shapefile"
)

이제 선택된 피쳐를 하나씩 순회하면서 버퍼 연산을 수행하고 그 결과를 새로운 SHP 파일에 기록합니다.

dist = 100
features = layer.selectedFeatures()
for feat in features:
    geom = feat.geometry()
    buff = geom.buffer(dist, 8)
    feat.setGeometry(buff)
    writer.addFeature(feat)

del(writer) # 새로운 SHP 파일 닫기

새로운 SHP 파일을 레이어로 추가하는 코드는 다음과 같습니다.

layer = QgsVectorLayer(fileName, "새로운 레이어", "ogr")
QgsProject.instance().addMapLayers([layer])

실행 결과는 다음과 같습니다.