Работа с картой

Виджет “карта” (Map Canvas) является одним из наиболее важных, так как именно он отвечает за отображение карты, состоящей из наложенных друг на друга слоёв, и позволяет взаимодействовать как со всей картой, так и с отдельными слоями. Виджет отображает только часть карты, заданную текущим охватом. Взаимодействие выполняется при помощи инструментов карты (map tools): среди которых присутствуют инструменты панорамирования, масштабирования, определения слоёв, измерения, редактирования и другие. Как и в других программах, активным в каждый момент времени может быть только один инструмент, при необходимости выполняется переключение между ними.

Карта реализуется классом QgsMapCanvas модуля qgis.gui. В основе реализации лежит Qt Graphics View framework. Фреймворк предоставляет пользователю поверхность для рисования и объект для отображения пользовательских элементов, а также даёт возможность взаимодействовать с ними. Предполагается, что читатель достаточно знаком с Qt чтобы разобраться в основных понятиях сцены, вида и элементов. Если это не так, пожалуйста, ознакомьтесь с описанием фреймворка.

Всякий раз, когда пользователь выполняет панорамирование, масштабирование (или любое другое действие, вызывающее обновление карты), происходит перерисовка карты в пределах текущего охвата. Отрисовка слоёв выполняется в изображение (за это отвечает класс QgsMapRenderer), которое затем отображается на карте. Графическим объектом (в терминах фреймвока Qt — graphics view), отвечающим за отображение карты, является класс QgsMapCanvasMap. Этот же класс следит за обновлением карты. Помимо этого объекта, который служит фоном, может существовать множество элементов карты. Обычно, в роли элементов карты выступают “резиновые” линии (используемые при измерении и редактировании слоёв) или маркеры вершин. Чаще всего элементы карты используются для визуализации работы инструментов карты. Например, при создании нового полигона, инструмент карты создает “резиновый” элемент карты, показывающий текущую форму полигона. Все элементы карты являются наследниками QgsMapCanvasItem и добавляют свой функционал к базовому объекту QGraphicsItem.

Таким образом, архитектурно карта состоит из трёх элементов:

  • карта — для отображения данных

  • элементы карты — дополнительные объекты, которые можно отобразить на карте

  • инструменты карты — обеспечивают взаимодействие с картой

Встраивание карты

Так как карта это такой же элемент интерфейса, как и любой другой виджет Qt, её использование, создание и отображение весьма просто:

canvas = QgsMapCanvas()
canvas.show()

Этот код создаст новое окно с картой. Точно так же можно встраивать карту в существующий виджет или окно. При использовании Qt Designer и файлов .ui удобно делать так: на форму положить QWidget и объявить его новым классом, установив в качестве имени класса QgsMapCanvas и qgis.gui в качестве заголовочного файла. Всё остальное сделает программа pyuic4. Как видите, это очень простой и удобный способ встраивания карты в приложение. Ещё один способ — создать виджет карты и другие элементы интерфейса динамически (в качестве дочерних объектов основного или диалогового окна) и разместить их на компоновке.

По умолчанию, фон карты чёрный, сглаживание при отрисовке отключено. Чтобы установит цвет фона в белый и активировать сглаживание выполните:

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

(если вас интересует, то приставка Qt используется модулем PyQt4.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 и попробовать выполнить в Консоли Python QGIS. Код ниже показывает как поместить текущий выделенный слой на созданную только что карту:

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

Перед этим необходимо убедиться, что файл mywnd.py находится в каталоге где Python ищет модули (sys.path). Если это не так, просто добавьте его: sys.path.insert(0, '/my/path') — иначе импорт завершится с ошибкой, из-за того, что модуль не найден.

Резиновые полосы и маркеры вершин

Для отображения дополнительных данных поверх карты используются элементы карты. Можно как создавать свои собственные элементы карты (рассматривается дальше), так и использовать существующие классы: 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)

Обратите внимание, что узлы полигона представлены не плоским списком: на самом деле это список границ полигона. Первое кольцо описывает внешний контур, все остальные (не обязательные) — соответствуют дыркам в полигоне.

Резиновые полосы можно настраивать, а именно менять их цвет и толщину:

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

Элементы карты связанны с графической сценой карты. Их можно скрыть (а потом снова отобразить) вызывая функции func:hide и show(). Для полного удаления элемента необходимо удалить его из графической сцены:

canvas.scene().removeItem(r)

(при использовании C++ можно просто удалить элемент, однако в Python 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)

For temporary hiding of vertex markers and removing them from canvas, the same applies as for the rubber bands.

Создание собственных инструментов карты

You can write your custom tools, to implement a custom behaviour to actions perfored by users on the canvas.

Map tools should inherit from the QgsMapTool class or any derived class, and selected as active tools in the canvas using the setMapTool() method as we have already seen.

Here is an example of a map tool that allows to define a rectangular extent by clicking and draggin on the canvas. When the rectangle is defined, it prints its boundary coordinates in the console. It uses the rubber band elements described before to show the selected rectangle as it is being defined.

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.xMin(), r.yMin(), r.xMax(), r.yMax()

  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: how to create a map canvas item