Skip to content
Snippets Groups Projects
cursorlocationvalue.py 19.9 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 *


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):
        def __init__(self, bandIndex, bandValue, bandName):
            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
    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])
                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 = [
    ComboBoxOption('VISIBLE', 'RGB', '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
        """
        Call to load / re-load the data for the specifed cursor location
        """
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())
            if not isinstance(pointLyr, SpatialPoint) or not 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':
                    bandNumbers = renderer.usesBands()
                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()
                    for b in bandNumbers:

                        info = RasterValueSet.BandInfo(b - 1, results[b], l.bandName(b))
                        v.bandValues.append(info)

                if False:
                    results = l.dataProvider().identify(pointLyr, QgsRaster.IdentifyFormatValue).results()


                    #x, y  = pointLyr.x(), pointLyr.y()
                    #dx = 0.5 * l.rasterUnitsPerPixelX()
                    #dy = 0.5 * l.rasterUnitsPerPixelY()
                    #bb = QgsRectangle(x-dx, y-dy, x+dx, y+dy)
                    #results2 = l.dataProvider().identify(pointLyr, QgsRaster.IdentifyFormatValue, boundingBox=bb, width=1, height=1).results()
                    if len(results) == 0:
                        # fallback: get the colors, e.g. in case of WMS
                        pt2 = QgsPointXY(pointLyr.x() + l.rasterUnitsPerPixelX() * 3,
                                         pointLyr.y() - l.rasterUnitsPerPixelY() * 3)
                        ext2Px = QgsRectangle(pointLyr.x(), pt2.y(), pt2.x(), pointLyr.y())

                        block = l.renderer().block(1, ext2Px, 3, 3)
                        color = QColor(block.color(0, 0))
                        v.bandValues.append(color)

                    else:
                        wl, wlu = parseWavelength(l)
                        for band, value in results.items():
                            v.bandValues.append(RasterValueSet.BandInfo(band-1, value, l.bandName(band)))

                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()