Skip to content
Snippets Groups Projects
cursorlocationvalue.py 20.1 KiB
Newer Older
# -*- coding: utf-8 -*-
# noinspection PyPep8Naming
"""
***************************************************************************
    cursorlocationvalue.py
    ---------------------
    Date                 : August 2017
    Copyright            : (C) 2017 by Benjamin Jakimow
    Email                : benjamin.jakimow@geo.hu-berlin.de
***************************************************************************
*                                                                         *
*   This program is free software; you can redistribute it and/or modify  *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************
"""
import os, collections

import numpy as np
from qgis.core import *
from qgis.gui import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
from qps.utils import *
from qps.models import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
from qps.classification.classificationscheme import ClassInfo, ClassificationScheme

class SourceValueSet(object):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __init__(self, source, point:SpatialPoint):
        assert isinstance(point, SpatialPoint)
        self.source = source
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.point = point


    def baseName(self):
        return os.path.basename(self.source)

    def crs(self):
        return QgsCoordinateReferenceSystem(self.wktCrs)

Benjamin Jakimow's avatar
Benjamin Jakimow committed

class RasterValueSet(SourceValueSet):

    class BandInfo(object):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        def __init__(self, bandIndex, bandValue, bandName, classInfo=None):
            assert bandIndex >= 0
            if bandValue is not None:
                assert type(bandValue) in [float, int]
            if bandName is not None:

            self.bandIndex = bandIndex
            self.bandValue = bandValue
            self.bandName = bandName
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            self.classInfo = classInfo
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __init__(self, source, point, pxPosition):
        assert isinstance(pxPosition, QPoint)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        super(RasterValueSet, self).__init__(source, point)
        self.pxPosition = pxPosition
        self.noDataValue = None
        self.bandValues = []

class VectorValueSet(SourceValueSet):
    class FeatureInfo(object):
        def __init__(self, fid):
            assert isinstance(fid, int)
            self.fid = fid
            self.attributes = collections.OrderedDict()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __init__(self, source, point:SpatialPoint):
        super(VectorValueSet, self).__init__(source, point)
        self.features = []

    def addFeatureInfo(self, featureInfo):
        assert isinstance(featureInfo, VectorValueSet.FeatureInfo)
        self.features.append(featureInfo)


class CursorLocationInfoModel(TreeModel):
    ALWAYS_EXPAND = 'always'
    NEVER_EXPAND = 'never'
    REMAINDER = 'reminder'

    def __init__(self, parent=None):
        super(CursorLocationInfoModel, self).__init__(parent)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.mColumnNames = ['Band/Field', 'Value', 'Description']
        self.mExpandedNodeRemainder = {}
        self.mNodeExpansion = CursorLocationInfoModel.REMAINDER

    def setNodeExpansion(self, type):

        assert type in [CursorLocationInfoModel.ALWAYS_EXPAND,
                        CursorLocationInfoModel.NEVER_EXPAND,
                        CursorLocationInfoModel.REMAINDER]
        self.mNodeExpansion = type

    def setExpandedNodeRemainder(self, node=None):
        treeView = self.mTreeView
        assert isinstance(treeView, QTreeView)
        if node is None:
            for n in self.mRootNode.childNodes():
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                self.setExpandedNodeRemainder(node=n)
        else:
            self.mExpandedNodeRemainder[self.weakNodeId(node)] = self.mTreeView.isExpanded(self.node2idx(node))
            for n in node.childNodes():
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                self.setExpandedNodeRemainder(node=n)

    def weakNodeId(self, node):
        assert isinstance(node, TreeNode)
        n = node.name()
        while node.parentNode() != self.mRootNode:
            node = node.parentNode()
            n += '{}:{}'.format(node.name(), n)
        return n

    def flags(self, index):

        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable

    def addSourceValues(self, sourceValueSet):
        if not isinstance(sourceValueSet, SourceValueSet):
            return

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        # get-or-create node
        def gocn(root, name)->TreeNode:
            assert isinstance(root, TreeNode)
            n = TreeNode(root, name)
            weakId = self.weakNodeId(n)

            expand = False
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            if not isinstance(root.parentNode(), TreeNode):
                expand = True
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            else:
                if self.mNodeExpansion == CursorLocationInfoModel.REMAINDER:
                    expand = self.mExpandedNodeRemainder.get(weakId, False)
                elif self.mNodeExpansion == CursorLocationInfoModel.NEVER_EXPAND:
                    expand = False
                elif self.mNodeExpansion == CursorLocationInfoModel.ALWAYS_EXPAND:
                    expand = True

            self.mTreeView.setExpanded(self.node2idx(n), expand)
            return n

        bn = os.path.basename(sourceValueSet.source)

        if isinstance(sourceValueSet, RasterValueSet):
            root = gocn(self.mRootNode, name=bn)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            root.setIcon(QIcon(':/qps/ui/icons/raster.svg'))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            # add subnodes
            n = gocn(root, 'Pixel')
            n.setValues('{},{}'.format(sourceValueSet.pxPosition.x(), sourceValueSet.pxPosition.y()))

            for bv in sourceValueSet.bandValues:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                if isinstance(bv, RasterValueSet.BandInfo):
                    n = gocn(root, 'Band {}'.format(bv.bandIndex + 1))
                    n.setToolTip('Band {} {}'.format(bv.bandIndex + 1, bv.bandName).strip())
                    n.setValues([bv.bandValue, bv.bandName])
Benjamin Jakimow's avatar
Benjamin Jakimow committed

                    if isinstance(bv.classInfo, ClassInfo):
                        nc = gocn(root, 'Class')
                        nc.setValues(bv.classInfo.name())
                        nc.setIcon(bv.classInfo.icon())


Benjamin Jakimow's avatar
Benjamin Jakimow committed
                elif isinstance(bv, QColor):
                    n = gocn(root, 'Color')
                    n.setToolTip('Color selected from screen pixel')
                    n.setValues(bv.getRgb())


        if isinstance(sourceValueSet, VectorValueSet):
            if len(sourceValueSet.features) == 0:
                return
            root = gocn(self.mRootNode, name=bn)
            refFeature = sourceValueSet.features[0]
            assert isinstance(refFeature, QgsFeature)
            typeName = QgsWkbTypes.displayString(refFeature.geometry().wkbType()).lower()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            if 'polygon' in typeName:
                root.setIcon(QIcon(r':/images/themes/default/mIconPolygonLayer.svg'))
            elif 'line' in typeName:
                root.setIcon(QIcon(r':/images/themes/default/mIconLineLayer.svg'))
            if 'point' in typeName:
                root.setIcon(QIcon(r':/images/themes/default/mIconPointLayer.svg'))


            for field in refFeature.fields():
                assert isinstance(field, QgsField)

                fieldNode = gocn(root, name=field.name())

Benjamin Jakimow's avatar
Benjamin Jakimow committed

                for i, feature in enumerate(sourceValueSet.features):
                    assert isinstance(feature, QgsFeature)
                    nf = gocn(fieldNode, name='{}'.format(feature.id()))
                    nf.setValues([feature.attribute(field.name()), field.typeName()])
                    nf.setToolTip('Value of feature "{}" in field with name "{}"'.format(feature.id(), field.name()))

        s = ""

    def clear(self):
        self.mRootNode.removeChildNodes(0, self.mRootNode.childCount())


class ComboBoxOption(object):
    def __init__(self, value, name=None, tooltip=None, icon=None):
        self.value = value
        self.name = str(value) if name is None else str(name)
        self.tooltip = tooltip
        self.icon = icon

Benjamin Jakimow's avatar
Benjamin Jakimow committed

LUT_GEOMETRY_ICONS = {}

RASTERBANDS = [
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    ComboBoxOption('VISIBLE', 'Visible', 'Visible bands only.'),
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    ComboBoxOption('ALL', 'All', 'All raster bands.'),
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    ComboBoxOption('TOP_LAYER', 'Top layer', 'Show values of the top-most map layer only.'),
    ComboBoxOption('ALL_LAYERS', 'All layers', 'Show values of all map layers.')
]

LAYERTYPES = [
    ComboBoxOption('ALL', 'Raster and Vector', 'Show values of both, raster and vector layers.'),
    ComboBoxOption('VECTOR', 'Vector only', 'Show values of vector layers only.'),
    ComboBoxOption('RASTER', 'Raster only', 'Show values of raster layers only.')
Benjamin Jakimow's avatar
Benjamin Jakimow committed
]


class ComboBoxOptionModel(QAbstractListModel):

    def __init__(self, options, parent=None, ):
        super(ComboBoxOptionModel, self).__init__(parent)
        assert isinstance(options, list)

        for o in options:
            assert isinstance(o, ComboBoxOption)

        self.mOptions = options

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.mOptions)

    def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
        return 1

    def index2option(self, index):

        if isinstance(index, QModelIndex) and index.isValid():
            return self.mOptions[index.row()]
        elif isinstance(index, int):
            return self.mOptions[index]
        return None

    def option2index(self, option):
        assert option in self.mOptions
        return self.mOptions.index(option)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        option = self.index2option(index)
        assert isinstance(option, ComboBoxOption)
        value = None
        if role == Qt.DisplayRole:
            value = option.name
        if role == Qt.ToolTipRole:
            value = option.tooltip
        if role == Qt.DecorationRole:
            value = option.icon
        if role == Qt.UserRole:
            value = option
        return value


class CursorLocationInfoDock(QDockWidget,
                             loadUI('cursorlocationinfodock.ui')):
    sigLocationRequest = pyqtSignal()
    sigCursorLocationInfoAdded = pyqtSignal()

    def __init__(self, parent=None):
        """Constructor."""
        QWidget.__init__(self, parent)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        # super(CursorLocationValueWidget, self).__init__(parent)
        # Set up the user interface from Designer.
        # After setupUI you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)

        self.mMaxPoints = 1
        self.mLocationHistory = []

        self.mCrs = None
        self.mCanvases = []

        self.btnCrs.crsChanged.connect(self.setCrs)
        self.btnCrs.setCrs(QgsCoordinateReferenceSystem())

        self.mLocationInfoModel = CursorLocationInfoModel(parent=self.treeView)
        self.treeView.setModel(self.mLocationInfoModel)

        self.mLayerModeModel = ComboBoxOptionModel(LAYERMODES, parent=self)
        self.mLayerTypeModel = ComboBoxOptionModel(LAYERTYPES, parent=self)
        self.mRasterBandsModel = ComboBoxOptionModel(RASTERBANDS, parent=self)

        self.cbLayerModes.setModel(self.mLayerModeModel)
        self.cbLayerTypes.setModel(self.mLayerTypeModel)
        self.cbRasterBands.setModel(self.mRasterBandsModel)
        self.actionRequestCursorLocation.triggered.connect(self.sigLocationRequest)
        self.actionReload.triggered.connect(self.reloadCursorLocation)

        self.btnActivateMapTool.setDefaultAction(self.actionRequestCursorLocation)
        self.btnReload.setDefaultAction(self.actionReload)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.actionAllRasterBands.triggered.connect(
            lambda: self.btnRasterBands.setDefaultAction(self.actionAllRasterBands))
        self.actionVisibleRasterBands.triggered.connect(
            lambda: self.btnRasterBands.setDefaultAction(self.actionVisibleRasterBands))

    def options(self):

        layerType = self.mLayerTypeModel.index2option(self.cbLayerTypes.currentIndex()).value
        layerMode = self.mLayerModeModel.index2option(self.cbLayerModes.currentIndex()).value
        rasterBands = self.mRasterBandsModel.index2option(self.cbRasterBands.currentIndex()).value

        return (layerMode, layerType, rasterBands)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def loadCursorLocation(self, point:SpatialPoint, canvas:QgsMapCanvas):
        """
        :param point:
        :param canvas:
        :return:
        """
        assert isinstance(canvas, QgsMapCanvas)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        assert isinstance(point, SpatialPoint)
        crs = canvas.mapSettings().destinationCrs()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.setCursorLocation(point)
        self.setCanvas(canvas)
        self.reloadCursorLocation()

    def reloadCursorLocation(self):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        Call to load / re-load the data for the cursor location
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        ptInfo = self.cursorLocation()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if not isinstance(ptInfo, SpatialPoint) or len(self.mCanvases) == 0:
            return

        mode, type, rasterbands = self.options()

        def layerFilter(canvas):
            assert isinstance(canvas, QgsMapCanvas)
            lyrs = canvas.layers()
            if type == 'VECTOR':
                lyrs = [l for l in lyrs if isinstance(l, QgsVectorLayer)]
            if type == 'RASTER':
                lyrs = [l for l in lyrs if isinstance(l, QgsRasterLayer)]

            return lyrs

        lyrs = []
        for c in self.mCanvases:
            lyrs.extend(layerFilter(c))

        self.mLocationInfoModel.setExpandedNodeRemainder()
        self.mLocationInfoModel.clear()

        for l in lyrs:
            assert isinstance(l, QgsMapLayer)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            if mode == 'TOP_LAYER' and self.mLocationInfoModel.mRootNode.childCount() > 0:
                s = ""
                break
            assert isinstance(l, QgsMapLayer)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            pointLyr = ptInfo.toCrs(l.crs())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            if not (isinstance(pointLyr, SpatialPoint) and l.extent().contains(pointLyr)):
                continue

            if isinstance(l, QgsRasterLayer):
                renderer = l.renderer()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                px = geo2px(pointLyr, l)
                v = RasterValueSet(l.name(), pointLyr, px)

                # !Note: b is not zero-based -> 1st band means b == 1
                if rasterbands == 'VISIBLE':
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    if isinstance(renderer, QgsPalettedRasterRenderer):
                        bandNumbers = renderer.usesBands()
                        # sometime the rendere is set to band 0 (which does not exist)
                        # QGIS bug
                        if bandNumbers == [0] and l.bandCount() > 0:
                            bandNumbers = [1]
                    else:
                        bandNumbers = renderer.usesBands()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
                elif rasterbands == 'ALL':
                    bandNumbers = list(range(1, l.bandCount()+1))
                else:
                    bandNumbers = [1]

                pt2 = QgsPointXY(pointLyr.x() + l.rasterUnitsPerPixelX() * 3,
                                 pointLyr.y() - l.rasterUnitsPerPixelY() * 3)
                ext2Px = QgsRectangle(pointLyr.x(), pt2.y(), pt2.x(), pointLyr.y())

                if l.dataProvider().name() in ['wms']:
                    for b in bandNumbers:
                        block = l.renderer().block(b, ext2Px, 3, 3)
                        assert isinstance(block, QgsRasterBlock)
                        v.bandValues.append(QColor(block.color(0, 0)))
                else:
                    results = l.dataProvider().identify(pointLyr, QgsRaster.IdentifyFormatValue).results()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    classScheme = None
                    if isinstance(l.renderer(), QgsPalettedRasterRenderer):
                        classScheme = ClassificationScheme.fromRasterRenderer(l.renderer())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    for b in bandNumbers:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                        if b in results.keys():
                            bandValue = results[b]

                            classInfo = None
                            if isinstance(bandValue, (int, float)) \
                                and isinstance(classScheme, ClassificationScheme) \
                                and bandValue >= 0 \
                                and bandValue < len(classScheme):
                                classInfo = classScheme[int(bandValue)]
                            info = RasterValueSet.BandInfo(b - 1, bandValue, l.bandName(b), classInfo=classInfo)
                            v.bandValues.append(info)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

                self.mLocationInfoModel.addSourceValues(v)
                s = ""

            if isinstance(l, QgsVectorLayer):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                # searchRect = QgsRectangle(pt, pt)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                # searchRadius = QgsTolerance.toleranceInMapUnits(1, l, self.mCanvas.mapRenderer(), QgsTolerance.Pixels)
                searchRadius = QgsTolerance.toleranceInMapUnits(1, l, self.mCanvases[0].mapSettings(),
                                                                QgsTolerance.Pixels)
                # searchRadius = QgsTolerance.defaultTolerance(l, self.mCanvas.mapSettings())
                # searchRadius = QgsTolerance.toleranceInProjectUnits(1, self.mCanvas.mapRenderer(), QgsTolerance.Pixels)
                searchRect = QgsRectangle()
                searchRect.setXMinimum(pointLyr.x() - searchRadius);
                searchRect.setXMaximum(pointLyr.x() + searchRadius);
                searchRect.setYMinimum(pointLyr.y() - searchRadius);
                searchRect.setYMaximum(pointLyr.y() + searchRadius);

                flags = QgsFeatureRequest.ExactIntersect
                features = l.getFeatures(QgsFeatureRequest() \
                                         .setFilterRect(searchRect) \
                                         .setFlags(flags))
                feature = QgsFeature()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                s = VectorValueSet(l.source(), pointLyr)
                while features.nextFeature(feature):
                    s.features.append(QgsFeature(feature))

                self.mLocationInfoModel.addSourceValues(s)
                s = ""

                pass

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def setCursorLocation(self, spatialPoint:SpatialPoint):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        Set the cursor lcation to be loaded.
        :param crs: QgsCoordinateReferenceSystem
        :param point: QgsPointXY
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        assert isinstance(spatialPoint, SpatialPoint)
        self.mLocationHistory.insert(0, spatialPoint)
        if len(self.mLocationHistory) > self.mMaxPoints:
            del self.mLocationHistory[self.mMaxPoints:]

        if self.mCrs is None:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            self.setCrs(spatialPoint.crs())
        self.updateCursorLocationInfo()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def updateCursorLocationInfo(self):
        # transform this point to targeted CRS
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        pt = self.cursorLocation()
        if isinstance(pt, SpatialPoint):
            pt = pt.toCrs(self.mCrs)
            self.tbX.setText('{}'.format(pt.x()))
            self.tbY.setText('{}'.format(pt.y()))

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def setCanvas(self, mapCanvas):
        self.setCanvases([mapCanvas])

    def setCanvases(self, mapCanvases):
        assert isinstance(mapCanvases, list)
        for c in mapCanvases:
            assert isinstance(c, QgsMapCanvas)

        if len(mapCanvases) == 0:
            self.setCrs(None)
        else:
            setNew = True
            for c in mapCanvases:
                if c in self.mCanvases:
                    setNew = False
            if setNew:
                self.setCrs(mapCanvases[0].mapSettings().destinationCrs())
        self.mCanvases = mapCanvases

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def setCrs(self, crs):
        """
        Set the coordinate reference system in which coordinates are shown
        :param crs:
        :return:
        """
        assert isinstance(crs, QgsCoordinateReferenceSystem)
        if crs != self.mCrs:
            self.mCrs = crs
            self.btnCrs.setCrs(crs)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.updateCursorLocationInfo()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def cursorLocation(self)->SpatialPoint:
        """
        Returns the last location that was set.
        """
        if len(self.mLocationHistory) > 0:
            return self.mLocationHistory[0]
        else:
            return None, None


class Resulthandler(QObject):

    def __init__(self):
        super(Resulthandler, self).__init__()

    def onResult(self, *args):
        print(args)


R = Resulthandler()