https://github.com/minorua/Qgis2threejs
Raw File
Tip revision: c29151dd8221c6226a744ec5bcb25cb49b8175ff authored by Minoru Akagi on 31 January 2024, 01:41:12 UTC
version 2.7.3
Tip revision: c29151d
q3dtreeview.py
# -*- coding: utf-8 -*-
# (C) 2017 Minoru Akagi
# SPDX-License-Identifier: GPL-2.0-or-later
# begin: 2017-05-30

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QAction, QMenu, QMessageBox, QTreeView
from qgis.core import QgsApplication

from .conf import PLUGIN_NAME
from .proppages import DEMPropertyPage
from .q3dconst import LayerType


class Q3DTreeView(QTreeView):
    """layer tree view"""

    LAYER_GROUP_ITEMS = ((LayerType.DEM, "DEM"),
                         (LayerType.POINT, "Point"),
                         (LayerType.LINESTRING, "Line"),
                         (LayerType.POLYGON, "Polygon"),
                         (LayerType.POINTCLOUD, "Point Cloud"))

    def __init__(self, parent=None):
        QTreeView.__init__(self, parent)

        self.layers = []
        self._index = -1

    def setup(self, iface, icons):
        self.iface = iface      # Q3DViewerInterface
        self.icons = icons

        model = QStandardItemModel(0, 1)
        self.layerGroupItems = {}
        for typ, name in self.LAYER_GROUP_ITEMS:
            item = QStandardItem(name)
            item.setData(typ)
            item.setIcon(self.icons[typ])
            item.setEditable(False)

            self.layerGroupItems[typ] = item
            model.invisibleRootItem().appendRow([item])

        self.setModel(model)
        self.expandAll()

        self.model().dataChanged.connect(self.treeDataChanged)

        # context menu
        self.actionProperties = QAction("Properties...", self)
        self.actionProperties.triggered.connect(self.onDoubleClicked)

        self.actionRemoveLayer = QAction("Remove from layer tree...", self)
        self.actionRemoveLayer.triggered.connect(self.removeAdditionalLayer)

        self.actionZoomToLayer = QAction("Zoom to layer objects", self)
        self.actionZoomToLayer.triggered.connect(self.zoomToLayer)

        # context menu for map layer
        self.contextMenuLyr = QMenu(self)
        self.contextMenuLyr.addAction(self.actionZoomToLayer)
        self.contextMenuLyr.addAction(self.actionProperties)

        # context menu for flat plane
        self.contextMenuFP = QMenu(self)
        self.contextMenuFP.addAction(self.actionZoomToLayer)
        self.contextMenuFP.addAction(self.actionProperties)
        self.contextMenuFP.addSeparator()
        self.contextMenuFP.addAction(self.actionRemoveLayer)

        # context menu for point cloud layer
        self.contextMenuPC = QMenu(self)
        self.contextMenuPC.addAction(self.actionZoomToLayer)
        self.contextMenuPC.addAction(self.actionProperties)
        self.contextMenuPC.addSeparator()
        self.contextMenuPC.addAction(self.actionRemoveLayer)

        # context menu for DEM material
        self.contextMenuMtl = QMenu(self)
        self.contextMenuMtl.addAction(self.actionProperties)

        # context menu for DEM group
        self.contextMenuDEM = QMenu(self)
        self.contextMenuDEM.addAction(self.iface.wnd.ui.actionAddPlane)

        # context menu for point cloud group
        self.contextMenuPCG = QMenu(self)
        self.contextMenuPCG.addAction(self.iface.wnd.ui.actionAddPointCloudLayer)

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.showContextMenu)

        self.setExpandsOnDoubleClick(False)
        self.doubleClicked.connect(self.onDoubleClicked)

    def addLayer(self, layer):
        # add a layer item to tree view
        item = QStandardItem(layer.name)
        item.setCheckable(True)
        item.setData(layer.layerId)
        # item.setIcon(self.icons[layer.type])
        item.setEditable(False)

        if layer.visible:
            item.setCheckState(Qt.Checked)

            font = item.font()
            font.setBold(True)
            item.setFont(font)

        self.layerGroupItems[layer.type].appendRow([item])

        self.updateLayerMaterials(item)

        return item

    def addLayers(self, layers):
        for layer in layers:
            self.addLayer(layer)

    def removeLayer(self, layerId):
        item = self.itemFromLayerId(layerId)
        if item:
            item.parent().removeRow(item.row())

    def clearLayers(self):
        for parent in self.layerGroupItems.values():
            if parent.hasChildren():
                parent.removeRows(0, parent.rowCount())

    def layerFromIndex(self, index):
        layerId = self.model().data(index, Qt.UserRole + 1)
        return self.iface.settings.getLayer(layerId)

    def itemFromLayerId(self, layerId):
        for parent in self.layerGroupItems.values():
            for row in range(parent.rowCount()):
                item = parent.child(row)
                if item.data() == layerId:
                    return item
        return None

    def updateLayerMaterials(self, layerItem, layer=None):
        layerItem.removeRows(0, layerItem.rowCount())

        layer = layer or self.iface.settings.getLayer(layerItem.data())
        if layer is None or not layer.visible:
            return

        # add material items
        mtls = layer.properties.get("materials", [])
        if not len(mtls):
            return

        currentId = layer.properties.get("mtlId")

        for mtl in mtls:
            id = mtl.get("id")
            item = QStandardItem(mtl.get("name", ""))
            item.setData(id)
            item.setIcon(DEMPropertyPage.iconForMtl(mtl))
            item.setEditable(False)

            if id == currentId:
                font = item.font()
                font.setBold(True)
                item.setFont(font)

            layerItem.appendRow([item])

        self.expand(layerItem.index())

    def uncheckAll(self):
        for parent in self.layerGroupItems.values():
            for idx in range(parent.rowCount()):
                parent.child(idx).setCheckState(Qt.Unchecked)

    def currentChanged(self, current, previous):
        QTreeView.currentChanged(self, current, previous)

        idx = current
        depth = 0
        while idx.parent().isValid():
            depth += 1
            idx = idx.parent()

        if depth == 2:
            # DEM material
            layer = self.layerFromIndex(current.parent())
            if layer:
                mtlId = self.model().data(current, Qt.UserRole + 1)  # set with item.setData()
                layer.properties["mtlId"] = mtlId

                layer = layer.clone()
                layer.opt.onlyMaterial = True
                self.iface.buildLayerRequest.emit(layer)

            item = self.model().itemFromIndex(current)
            parent = item.parent()
            font = item.font()
            for row in range(parent.rowCount()):
                font.setBold(row == item.row())
                parent.child(row).setFont(font)

    def treeDataChanged(self, topLeft, bottomRight, roles):
        if Qt.CheckStateRole not in roles:
            return

        # checkbox toggled
        item = self.model().itemFromIndex(topLeft)
        layer = self.iface.settings.getLayer(item.data())
        if layer is None:
            return

        checked = (item.checkState() == Qt.Checked)
        layer.visible = checked
        if layer.visible and not layer.properties:
            layer.properties = self.iface.wnd.getDefaultProperties(layer)

        self.iface.requestBuildLayer(layer)

        font = item.font()
        font.setBold(checked)
        item.setFont(font)

        self.updateLayerMaterials(item, layer)

        self.iface.wnd.ui.animationPanel.tree.setLayerHidden(layer.layerId, not checked)

    def indexDepth(self, idx):
        depth = 0
        while idx.parent().isValid():
            depth += 1
            idx = idx.parent()
        return depth

    def showContextMenu(self, pos):
        idx = self.indexAt(pos)
        depth = self.indexDepth(idx)

        m = None
        if depth == 1:
            layerId = self.model().data(idx, Qt.UserRole + 1)
            if layerId:
                if layerId.startswith("pc:"):
                    m = self.contextMenuPC
                elif layerId.startswith("fp:"):
                    m = self.contextMenuFP
                else:
                    m = self.contextMenuLyr

        elif depth == 2:
            m = self.contextMenuMtl

        elif depth == 0:
            m = {LayerType.DEM: self.contextMenuDEM,
                 LayerType.POINTCLOUD: self.contextMenuPCG}.get(self.model().data(idx, Qt.UserRole + 1))

        if m:
            m.exec_(self.mapToGlobal(pos))

    def onDoubleClicked(self, _=None):
        idx = self.currentIndex()
        depth = self.indexDepth(idx)

        if depth > 0:
            layer = self.layerFromIndex(idx if depth == 1 else idx.parent())
            self.iface.wnd.showLayerPropertiesDialog(layer)

    def removeAdditionalLayer(self, _=None):
        layer = self.layerFromIndex(self.currentIndex())
        if layer is None:
            return

        if QMessageBox.question(self, PLUGIN_NAME, "Are you sure you want to remove the layer '{0}' from layer tree?".format(layer.name)) != QMessageBox.Yes:
            return

        self.iface.layerRemoved.emit(layer.layerId)
        self.removeLayer(layer.layerId)

    def zoomToLayer(self):
        layer = self.layerFromIndex(self.currentIndex())
        if layer:
            s = "app.cameraAction.zoomToLayer(app.scene.mapLayers[{}])".format(layer.jsLayerId)
            self.iface.wnd.runScript(s, message="zoom to layer '{}'".format(layer.name))
back to top