GIS

QGIS 실전 플러그인 개발 - (2) Vworld API를 활용한 Python 코드 작성

유병혁 2024. 12. 8. 09:56

안녕하세요? 이번 글은 "QGIS 실전 플러그인 개발" 시리즈의 두 번째로, "Vworld API를 활용한 Python 코드 작성"에 대해 다룹니다.

라이브러리 호출

먼저 HTTP 요청, 데이터 파싱, QGIS 작업, 그리고 PyQt를 활용한 GUI 및 데이터 유형 관리를 위해 필요한 라이브러리를 호출합니다.

import requests
import json
from qgis.core import *
from qgis.utils import iface
from PyQt5.QtCore import QVariant

QGIS 맵 캔버스의 BBox 반환 함수

다음으로, QGIS 맵 캔버스의 범위(BBox)를 반환하는 함수를 정의합니다. 이 BBox는 응답 결과의 좌표계를 기준으로 적용되므로, 먼저 사용할 좌표계를 정의해야 합니다. 여기서는 EPSG:5179 좌표계를 기준으로 코드를 작성했습니다. 따라서 현재 프로젝트의 좌표계를 기준으로 반환된 BBox 값을 EPSG:5179 좌표계로 변환한 뒤 반환해야 합니다.

아래 함수는, 현재 QGIS 맵 캔버스의 BBox를 EPSG:5179 좌표계로 변환하고, 최소 및 최대 좌표를 문자열 형태로 반환합니다.

def get_bbox_epsg5179():
    """
    현재 QGIS 맵 캔버스의 BBox를 EPSG:5179 좌표계로 변환하여 반환
    """
    canvas = iface.mapCanvas()
    extent = canvas.extent()
    canvas_crs = canvas.mapSettings().destinationCrs()
    target_crs = QgsCoordinateReferenceSystem("EPSG:5179")
    transform = QgsCoordinateTransform(canvas_crs, target_crs, QgsProject.instance())

    min_point = transform.transform(QgsPointXY(extent.xMinimum(), extent.yMinimum()))
    max_point = transform.transform(QgsPointXY(extent.xMaximum(), extent.yMaximum()))
    return f"{min_point.x()},{min_point.y()},{max_point.x()},{max_point.y()}"

Vworld API 데이터 요청 함수

이번에는 Vworld API에 HTTP 요청을 보내고, 검색 키워드, BBox, 페이지 정보를 기반으로 응답 데이터를 JSON 형식으로 반환하는 함수를 정의합니다. Vworld는 한 페이지에 최대 1000개까지 응답결과를 설정할 수 있습니다. 따라서 1000개가 초과될 경우, 이 함수를 반복적으로 호출하면 됩니다.

def fetch_vworld_data(api_key, bbox, query, page, page_size):
    """
    Vworld API 요청을 보내고 응답 데이터를 반환

    Args:
        api_key (str): Vworld API 키
        bbox (str): EPSG:5179 기준 BBox
        query (str): 검색 키워드
        page (int): 페이지 번호
        page_size (int): 페이지 크기

    Returns:
        dict: API 응답 데이터
    """
    url = (
        f"https://api.vworld.kr/req/search?service=search&request=search"
        f"&version=2.0&crs=EPSG:5179&bbox={bbox}&type=place&format=json"
        f"&errorformat=json&key={api_key}&query={query}&size={page_size}&page={page}"
    )
    response = requests.get(url)
    if response.status_code == 200:
        return response.json()
    else:
        print(f"Error: {response.status_code}")
        return None

Vworld 데이터를 QGIS 레이어에 추가하는 함수

Vworld API에서 검색된 데이터를 QGIS 메모리 레이어로 추가하는 함수도 정의합니다. 이 함수는 검색된 데이터를 점 피처로 변환하여 레이어에 저장하며, 속성 필드를 정의한 후 QGIS 메모리 레이어에 추가합니다. 또한, 성공적으로 추가된 데이터의 수를 메시지로 출력하는 기능도 포함하고 있습니다.

def add_data_to_qgis_layer(all_items):
    """
    Vworld API에서 가져온 데이터를 QGIS 레이어에 추가

    Args:
        all_items (list): 검색된 데이터 리스트
    """
    try:
        features = []
        for item in all_items:
            x = float(item["point"]["x"])
            y = float(item["point"]["y"])
            road = item.get("address", {}).get("road", "")
            parcel = item.get("address", {}).get("parcel", "")

            point = QgsPointXY(x, y)
            feature = QgsFeature()
            feature.setGeometry(QgsGeometry.fromPointXY(point))
            feature.setAttributes(
                [
                    item.get("id", ""),
                    item.get("title", ""),
                    item.get("category", ""),
                    road,
                    parcel,
                ]
            )
            features.append(feature)
        layer = QgsVectorLayer("Point?crs=EPSG:5179", "Vworld Search Results", "memory")
        provider = layer.dataProvider()
        provider.addAttributes(
            [
                QgsField("ID", QVariant.String),
                QgsField("Title", QVariant.String),
                QgsField("Category", QVariant.String),
                QgsField("Road", QVariant.String),
                QgsField("Parcel", QVariant.String),
            ]
        )
        layer.updateFields()
        provider.addFeatures(features)

        QgsProject.instance().addMapLayer(layer)
        iface.messageBar().pushMessage(
            "Vworld Search", f"Added {len(all_items)} items to QGIS.", level=1
        )
    except KeyError as e:
        print(f"KeyError: {e} - Check the response data structure.")

Vworld API 요청을 위한 기본 설정

이제 Vworld API 요청을 위한 필수 변수들을 초기화합니다. API 인증키, 검색 키워드, BBox, 페이지 번호, 페이지 크기, 그리고 데이터를 저장할 빈 리스트를 설정합니다. 검색어는 "국립공원"을 지정했습니다.

api_key = "3A99F800-2E60-3397-9F0E-2746BC4169F6"
query = "국립공원"
bbox = get_bbox_epsg5179()
page = 1
page_size = 1000
all_items = []

Vworld API 데이터 수집 루프 반복

while 루프를 사용하여 Vworld API에서 데이터를 반복적으로 요청하고 수집합니다. 각 페이지에서 가져온 데이터를all_items 리스트에 추가하며, 페이지 크기와 요청 결과를 기반으로 루프를 종료합니다.

while True:
    data = fetch_vworld_data(api_key, bbox, query, page, page_size)
    if data and "response" in data and "result" in data["response"]:
        items = data["response"]["result"]["items"]
        all_items.extend(items)

        total_results = int(data["response"]["record"]["total"])
        print(
            f"Total Results: {total_results}, Current Page: {page}, Items Retrieved: {len(items)}"
        )
        if len(items) < page_size:
            break
        page += 1
    else:
        print("Failed to fetch or parse data.")
        break

 

끝으로 수집한 데이터를 QGIS에 추가하는 함수를 호출합니다.

# 수집한 데이터를 QGIS에 추가
add_data_to_qgis_layer(all_items)

QGIS 파이썬 콘솔을 활용한 Python 코드 실행

이제 작성한 Python 코드를 시험해 보겠습니다. "탐색기 패널 > XYZ Tiles > OpenStreetMap"을 레이어 추가한 후, QGIS 맵 캔버스의 BBox를 적정하게 설정합니다.

 

"플러그인 > 파이썬 콘솔"을 실행합니다.

 

아래와 같이 파이썬 콘솔 패널이 추가됩니다.

 

"편집기 표시하기"를 클릭합니다.

 

편집기에 앞서 작성한 Python 코드를 붙여넣기 합니다.

import requests
import json
from qgis.core import *
from qgis.utils import iface
from PyQt5.QtCore import QVariant

def get_bbox_epsg5179():
    """
    현재 QGIS 맵 캔버스의 BBox를 EPSG:5179 좌표계로 변환하여 반환
    """
    canvas = iface.mapCanvas()
    extent = canvas.extent()
    canvas_crs = canvas.mapSettings().destinationCrs()
    target_crs = QgsCoordinateReferenceSystem("EPSG:5179")
    transform = QgsCoordinateTransform(canvas_crs, target_crs, QgsProject.instance())

    min_point = transform.transform(QgsPointXY(extent.xMinimum(), extent.yMinimum()))
    max_point = transform.transform(QgsPointXY(extent.xMaximum(), extent.yMaximum()))
    return f"{min_point.x()},{min_point.y()},{max_point.x()},{max_point.y()}"

def fetch_vworld_data(api_key, bbox, query, page, page_size):
    """
    Vworld API 요청을 보내고 응답 데이터를 반환

    Args:
        api_key (str): Vworld API 키
        bbox (str): EPSG:5179 기준 BBox
        query (str): 검색 키워드
        page (int): 페이지 번호
        page_size (int): 페이지 크기

    Returns:
        dict: API 응답 데이터
    """
    url = (
        f"https://api.vworld.kr/req/search?service=search&request=search"
        f"&version=2.0&crs=EPSG:5179&bbox={bbox}&type=place&format=json"
        f"&errorformat=json&key={api_key}&query={query}&size={page_size}&page={page}"
    )
    response = requests.get(url)
    if response.status_code == 200:
        return response.json()
    else:
        print(f"Error: {response.status_code}")
        return None

def add_data_to_qgis_layer(all_items):
    """
    Vworld API에서 가져온 데이터를 QGIS 레이어에 추가

    Args:
        all_items (list): 검색된 데이터 리스트
    """
    try:
        features = []
        for item in all_items:
            x = float(item["point"]["x"])
            y = float(item["point"]["y"])
            road = item.get("address", {}).get("road", "")
            parcel = item.get("address", {}).get("parcel", "")

            point = QgsPointXY(x, y)
            feature = QgsFeature()
            feature.setGeometry(QgsGeometry.fromPointXY(point))
            feature.setAttributes(
                [
                    item.get("id", ""),
                    item.get("title", ""),
                    item.get("category", ""),
                    road,
                    parcel,
                ]
            )
            features.append(feature)
        layer = QgsVectorLayer("Point?crs=EPSG:5179", "Vworld Search Results", "memory")
        provider = layer.dataProvider()
        provider.addAttributes(
            [
                QgsField("ID", QVariant.String),
                QgsField("Title", QVariant.String),
                QgsField("Category", QVariant.String),
                QgsField("Road", QVariant.String),
                QgsField("Parcel", QVariant.String),
            ]
        )
        layer.updateFields()
        provider.addFeatures(features)

        QgsProject.instance().addMapLayer(layer)
        iface.messageBar().pushMessage(
            "Vworld Search", f"Added {len(all_items)} items to QGIS.", level=1
        )
    except KeyError as e:
        print(f"KeyError: {e} - Check the response data structure.")

api_key = "3A99F800-2E60-3397-9F0E-2746BC4169F6"
query = "국립공원"

bbox = get_bbox_epsg5179()
page = 1
page_size = 1000
all_items = []

while True:
    data = fetch_vworld_data(api_key, bbox, query, page, page_size)
    if data and "response" in data and "result" in data["response"]:
        items = data["response"]["result"]["items"]
        all_items.extend(items)

        total_results = int(data["response"]["record"]["total"])
        print(
            f"Total Results: {total_results}, Current Page: {page}, Items Retrieved: {len(items)}"
        )
        if len(items) < page_size:
            break
        page += 1
    else:
        print("Failed to fetch or parse data.")
        break

# 수집한 데이터를 QGIS에 추가
add_data_to_qgis_layer(all_items)

 

"스크립트 실행" 버튼을 클릭합니다.

 

국립공원과 연관된 1,834개 데이터가 QGIS 임시 레이어로 추가되었습니다! 다음 글에서는 Plugin Builder 3 플러그인을 통해 우리가 개발할 "GeoQuery for QGIS" 플러그인의 템플릿을 생성해 보겠습니다.