맵 캔버스 다루기

맵 캔버스 위젯은 QGIS에서 가장 중요한 위젯이라고 할 수 있습니다. 맵 레이어들을 층층이 쌓아올려 만들어진 맵을 보여주며 맵과 레이어가 상호작용할 수 있도록 해주기 때문입니다. 캔버스는 항상 현재 캔버스 범위에 따라 정의된 맵의 일부분을 보여줍니다. 상호작용은 맵 도구 를 통해 이루어집니다. 이동, 확대/축소, 레이어 정보조회, 측정, 벡터 편집 외에도 많은 도구들이 있습니다. 다른 그래픽 프로그램과 마찬가지로, 언제나 도구들 중 하나는 활성화되어 있으며 사용자가 사용할 도구를 선택할 수 있습니다.

맵 캔버스는 qgis.gui 모듈의 QgsMapCanvas 클래스로 구현됩니다. 이 구현체는 Qt Graphic View 프레임워크를 기반으로 합니다. 이 프레임워크는 일반적으로 외관(surface)과 사용자 지정 그래픽 아이템이 들어가 사용자와 상호작용할 수 있는 뷰를 제공합니다. 여러분이 그래픽 신(scene), 뷰, 그리고 아이템의 개념을 이해할 정도로 Qt를 잘 알고 있다고 가정할 것입니다. 그렇지 않다면 반드시 프레임워크의 개요 를 읽어보도록 하십시오.

맵을 이동 또는 확대/축소(또는 새로고침이 필요한 다른 작업)시킬 때마다, QGIS는 현재 범위 내에서 맵을 다시 렌더링합니다. 레이어는 (QgsMapRenderer 클래스를 사용해서) 이미지로 렌더링된 다음 캔버스에 표출됩니다. 맵을 표출하는 역할을 하는 (Qt 그래픽 뷰 프레임워크 측면의) 그래픽 아이템이 QgsMapCanvasMap 클래스입니다. 이 클래스는 렌더링된 맵의 새로고침도 제어합니다. 백그라운드로 작동하는 이 아이템 외에도 많은 맵 캔버스 아이템 들이 있을 수 있습니다. 전형적인 맵 캔버스 아이템으로는 버텍스 마커나 (측정, 벡터 편집 등에 사용되는) 고무줄(rubber band)이 있습니다. 보통 맵 도구에 대한 시각적인 피드백을 주는 데 캔버스 아이템을 이용합니다. 예를 들면 새 폴리곤 생성 시 맵 도구는 폴리곤의 현재 형상을 보여주는 캔버스 아이템인 고무줄을 생성합니다. 모든 맵 캔버스 아이템은 QgsMapCanvasItem 클래스의 하위클래스인데, 이 QgsMapCanvasItem 클래스는 기본 QGraphicsItem 오브젝트에 좀 더 많은 기능이 추가되어 있습니다.

요약하면 맵 캔버스 아키텍처는 다음 3가지 개념으로 이루어집니다.

  • 맵 캔버스 — 맵을 보여주는 데 쓰입니다.

  • 맵 캔버스 아이템 — 맵 캔버스에 표출될 수 있는 추가적인 아이템들입니다.

  • 맵 도구 — 맵 캔버스와의 상호작용에 쓰입니다.

맵 캔버스 내장시키기

맵 캔버스는 다른 Qt 위젯들과 마찬가지로 위젯이므로, 생성, 표출은 물론 사용법도 간단합니다.

canvas = QgsMapCanvas()
canvas.show()

이렇게 하면 맵 캔버스를 가진 독립적인 창이 만들어집니다. 이를 기존 위젯이나 창에 들어가게 할 수도 있습니다. .ui 파일과 Qt 디자이너를 사용해서, 폼 윈도우에 QWidget 을 만든 다음 클래스 명으로 QgsMapCanvas 를 그리고 헤더 파일로 qgis.gui 를 설정해 새로운 클래스로 바꿔주십시오. pyuic4 유틸리티가 이 파일을 변환해 줄 것입니다. 이렇게 하면 매우 편리하게 캔버스를 내장시킬 수 있습니다. 다른 방법으로는 직접 코드를 작성해서 (메인 창 또는 대화상자의 자식으로) 맵 캔버스 및 다른 위젯들을 구성해서 레이아웃을 만들 수도 있습니다.

맵 캔버스의 초기값은 배경은 검은색, 안티알리아싱은 사용하지 않는 것입니다. 배경을 하얀색으로 설정하고 부드러운 렌더링을 위해 안티알리아싱을 활성화하려면 다음과 같이 하십시오.

canvas.setCanvasColor(Qt.white)
canvas.enableAntiAliasing(True)

(추가로 설명하자면 QtPyQt4.QtCore 모듈에서 나왔고 Qt.white 는 미리 정의된 QColor 인스턴스 가운데 하나입니다.)

이제 맵 레이어 몇 개를 추가할 순서입니다. 먼저 레이어를 불러와 맵 레이어 레지스트리에 추가할 것입니다. 그 다음 캔버스 범위를 설정하고, 캔버스에 표출할 레이어 목록을 설정할 것입니다.

layer = QgsVectorLayer(path, name, provider)
if not layer.isValid():
  raise IOError, "Failed to open the layer"

# add layer to the registry
QgsMapLayerRegistry.instance().addMapLayer(layer)

# set extent to the extent of our layer
canvas.setExtent(layer.extent())

# set the map canvas layer set
canvas.setLayerSet([QgsMapCanvasLayer(layer)])

이 명령어들을 실행하면, 사용자가 불러온 레이어가 캔버스에 보일 것입니다.

캔버스에서 맵 도구 사용하기

다음은 맵 캔버스와 이동 및 확대/축소를 위한 기본 맵 도구가 들어있는 윈도우를 만드는 예시 코드입니다. 액션들이 각 툴들의 동작을 위해 만들어집니다. 이동은 QgsMapToolPan 클래스, 축소와 확대는 QgsMapToolZoom 클래스 인스턴스로 동작합니다. 이 액션들은 체크 가능하도록 설정되어, 이후 액션의 체크/해제 상태가 자동적으로 도구에 반영되게 됩니다. 하나의 맵 도구가 활성화되면, 해당 액션이 선택된 것으로 표시되고, 이전 활성 맵 도구는 비활성화 됩니다. 이 맵 도구는 setMapTool() 메소드로 활성화 됩니다.

from qgis.gui import *
from PyQt4.QtGui import QAction, QMainWindow
from PyQt4.QtCore import SIGNAL, Qt, QString

class MyWnd(QMainWindow):
  def __init__(self, layer):
    QMainWindow.__init__(self)

    self.canvas = QgsMapCanvas()
    self.canvas.setCanvasColor(Qt.white)

    self.canvas.setExtent(layer.extent())
    self.canvas.setLayerSet([QgsMapCanvasLayer(layer)])

    self.setCentralWidget(self.canvas)

    actionZoomIn = QAction(QString("Zoom in"), self)
    actionZoomOut = QAction(QString("Zoom out"), self)
    actionPan = QAction(QString("Pan"), self)

    actionZoomIn.setCheckable(True)
    actionZoomOut.setCheckable(True)
    actionPan.setCheckable(True)

    self.connect(actionZoomIn, SIGNAL("triggered()"), self.zoomIn)
    self.connect(actionZoomOut, SIGNAL("triggered()"), self.zoomOut)
    self.connect(actionPan, SIGNAL("triggered()"), self.pan)

    self.toolbar = self.addToolBar("Canvas actions")
    self.toolbar.addAction(actionZoomIn)
    self.toolbar.addAction(actionZoomOut)
    self.toolbar.addAction(actionPan)

    # create the map tools
    self.toolPan = QgsMapToolPan(self.canvas)
    self.toolPan.setAction(actionPan)
    self.toolZoomIn = QgsMapToolZoom(self.canvas, False) # false = in
    self.toolZoomIn.setAction(actionZoomIn)
    self.toolZoomOut = QgsMapToolZoom(self.canvas, True) # true = out
    self.toolZoomOut.setAction(actionZoomOut)

    self.pan()

  def zoomIn(self):
    self.canvas.setMapTool(self.toolZoomIn)

  def zoomOut(self):
    self.canvas.setMapTool(self.toolZoomOut)

  def pan(self):
    self.canvas.setMapTool(self.toolPan)

이 예시 코드를 mywnd.py 같은 파일명으로 저장해서 QGIS의 파이썬 콘솔에서 실행해볼 수 있습니다. 아래 코드는 현재 선택되어 있는 레이어를 새로 만들어진 캔버스에 넣을 것입니다.

import mywnd
w = mywnd.MyWnd(qgis.utils.iface.activeLayer())
w.show()

다만 mywnd.py 파일이 파이썬 검색 경로(sys.path) 안에 위치하는지 확인할 필요가 있습니다. 만약 없다면 sys.path.insert(0, '/my/path') 명령어로 추가하면 됩니다. 파일이 검색 경로 안에 없다면 모듈을 찾지 못 해 임포트 선언이 실패할 것입니다.

고무줄과 버텍스 마커

캔버스의 맵 상에 추가적인 데이터들을 표시하려면, 맵 캔버스 아이템을 이용하십시오. 사용자 지정 캔버스 아이템 클래스를 (다음 단락에서 설명하는 대로) 생성할 수도 있지만, 편리하게 활용할 수 있는 캔버스 아이템 클래스가 2가지 있습니다. 폴리라인 또는 폴리곤을 그릴 때 쓰이는 QgsRubberBand 클래스와 포인트를 그릴 때 쓰이는 QgsVertexMarker 클래스입니다. 두 클래스 모두 맵 좌표와 함께 동작하므로, 캔버스를 이동하거나 확대/축소할 때마다 자동적으로 형상이 이동되고, 축척에 따라 변합니다.

폴리라인을 표시하는 방법은 다음과 같습니다.

r = QgsRubberBand(canvas, False)  # False = not a polygon
points = [QgsPoint(-1, -1), QgsPoint(0, 1), QgsPoint(1, -1)]
r.setToGeometry(QgsGeometry.fromPolyline(points), None)

폴리곤을 표시하는 방법은 다음과 같습니다.

r = QgsRubberBand(canvas, True)  # True = a polygon
points = [[QgsPoint(-1, -1), QgsPoint(0, 1), QgsPoint(1, -1)]]
r.setToGeometry(QgsGeometry.fromPolygon(points), None)

폴리곤의 포인트들이 1차원 리스트가 아니라는 점에 주의하십시오. 실제로, 폴리곤의 포인트들은 폴리곤의 선형 폐곡선을 담고 있는 폐곡선 리스트입니다. 첫 번째 폐곡선은 외곽 경계선이고, 그 다음의 (있을 수도 있고 없을 수도 있는) 폐곡선은 폴리곤 내부의 구멍에 해당합니다.

고무줄을 사용자 지정 할 수 있습니다. 즉 색상 및 선 두께를 변경할 수도 있습니다.

r.setColor(QColor(0, 0, 255))
r.setWidth(3)

캔버스 아이템은 캔버스 신(scene)에 종속되어 있습니다. 일시적으로 아이템을 숨기려면 (그리고 다시 표출시키려면) hide()show() 함수쌍을 사용하십시오. 아이템을 완전히 제거하려면, 캔버스 신으로부터 제거해야 합니다.

canvas.scene().removeItem(r)

(C++의 경우 아이템을 그냥 삭제하는 것도 가능하지만, 파이썬에서 del r 명령어는 참조만 삭제할 뿐 실제 오브젝트는 캔버스가 소유하고 있으므로 계속 남아 있을 것입니다.)

포인트를 그리는 데에도 고무줄을 쓸 수 있지만, 이 작업에는 QgsVertexMarker 클래스가 더 적합합니다. (QgsRubberBand 클래스는 지정된 위치 주변에 사각형을 그릴 뿐입니다.) 버텍스 마커는 다음과 같이 사용합니다.

m = QgsVertexMarker(canvas)
m.setCenter(QgsPoint(0, 0))

이 코드는 [0,0] 위치에 빨강색 십자가를 그릴 것입니다. 아이콘 유형, 크기, 색상, 그리고 펜 두께를 사용자 지정할 수 있습니다.

m.setColor(QColor(0, 255, 0))
m.setIconSize(5)
m.setIconType(QgsVertexMarker.ICON_BOX) # or ICON_CROSS, ICON_X
m.setPenWidth(3)

버텍스 마커를 임시적으로 숨기거나 캔버스에서 제거하려면, 고무줄의 경우와 비슷한 과정을 거치면 됩니다.

사용자 지정 맵 도구 작성

액션에 사용자 지정 동작방식을 구현해서, 캔버스 상에서 사용자 의도대로 동작하는 여러분만의 사용자 지정 도구를 만들 수 있습니다.

맵 도구들은 QgsMapTool 클래스 또는 그 파생 클래스를 상속 받아야만 합니다. 그리고 이전에 배웠던 대로 setMapTool() 메소드를 통해 캔버스에서 활성 도구로 설정할 수 있습니다.

다음 예시 코드는 캔버스 상에서 클릭과 드래그로 사각형 범위를 정의하도록 해주는 맵 도구입니다. 사각형이 정의되면, 콘솔에 그 범위 좌표를 출력합니다. 이전에 설명했던 고무줄 기능을 사용해서 확정되기 전의 사각형을 표시할 것입니다.

class RectangleMapTool(QgsMapToolEmitPoint):
  def __init__(self, canvas):
      self.canvas = canvas
      QgsMapToolEmitPoint.__init__(self, self.canvas)
      self.rubberBand = QgsRubberBand(self.canvas, QGis.Polygon)
      self.rubberBand.setColor(Qt.red)
      self.rubberBand.setWidth(1)
      self.reset()

  def reset(self):
      self.startPoint = self.endPoint = None
      self.isEmittingPoint = False
      self.rubberBand.reset(QGis.Polygon)

  def canvasPressEvent(self, e):
      self.startPoint = self.toMapCoordinates(e.pos())
      self.endPoint = self.startPoint
      self.isEmittingPoint = True
      self.showRect(self.startPoint, self.endPoint)

  def canvasReleaseEvent(self, e):
      self.isEmittingPoint = False
      r = self.rectangle()
      if r is not None:
        print "Rectangle:", r.xMinimum(), r.yMinimum(), r.xMaximum(), r.yMaximum()

  def canvasMoveEvent(self, e):
      if not self.isEmittingPoint:
        return

      self.endPoint = self.toMapCoordinates(e.pos())
      self.showRect(self.startPoint, self.endPoint)

  def showRect(self, startPoint, endPoint):
      self.rubberBand.reset(QGis.Polygon)
      if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
        return

      point1 = QgsPoint(startPoint.x(), startPoint.y())
      point2 = QgsPoint(startPoint.x(), endPoint.y())
      point3 = QgsPoint(endPoint.x(), endPoint.y())
      point4 = QgsPoint(endPoint.x(), startPoint.y())

      self.rubberBand.addPoint(point1, False)
      self.rubberBand.addPoint(point2, False)
      self.rubberBand.addPoint(point3, False)
      self.rubberBand.addPoint(point4, True)    # true to update canvas
      self.rubberBand.show()

  def rectangle(self):
      if self.startPoint is None or self.endPoint is None:
        return None
      elif self.startPoint.x() == self.endPoint.x() or self.startPoint.y() == self.endPoint.y():
        return None

      return QgsRectangle(self.startPoint, self.endPoint)

  def deactivate(self):
      QgsMapTool.deactivate(self)
      self.emit(SIGNAL("deactivated()"))

사용자 지정 맵 캔버스 아이템 작성

TODO:

맵 캔버스 아이템 생성 방법

import sys
from qgis.core import QgsApplication
from qgis.gui import QgsMapCanvas

def init():
  a = QgsApplication(sys.argv, True)
  QgsApplication.setPrefixPath('/home/martin/qgis/inst', True)
  QgsApplication.initQgis()
  return a

def show_canvas(app):
  canvas = QgsMapCanvas()
  canvas.show()
  app.exec_()
app = init()
show_canvas(app)