https://github.com/minorua/Qgis2threejs
Revision f8d5174c2c53f4ae36682c1cb6a1d30b36b91972 authored by Minoru Akagi on 19 April 2018, 06:23:23 UTC, committed by Minoru Akagi on 19 April 2018, 06:23:23 UTC
1 parent e0791e8
Raw File
Tip revision: f8d5174c2c53f4ae36682c1cb6a1d30b36b91972 authored by Minoru Akagi on 19 April 2018, 06:23:23 UTC
version 2.0.1
Tip revision: f8d5174
propertypages.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Qgis2threejs
                                 A QGIS plugin
 export terrain data, map canvas image and vector data to web browser
                             -------------------
        begin                : 2014-03-27
        copyright            : (C) 2014 Minoru Akagi
        email                : akaginch@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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
import re

from PyQt5.QtCore import Qt, QDir, QPoint
from PyQt5.QtWidgets import QCheckBox, QColorDialog, QComboBox, QFileDialog, QLineEdit, QRadioButton, QSlider, QSpinBox, QToolTip, QWidget
from PyQt5.QtGui import QColor
from qgis.core import QgsFieldProxyModel, QgsMapLayer, QgsWkbTypes
from qgis.gui import QgsFieldExpressionWidget

from .ui.worldproperties import Ui_WorldPropertiesWidget
from .ui.demproperties import Ui_DEMPropertiesWidget
from .ui.vectorproperties import Ui_VectorPropertiesWidget

from .conf import def_vals
from .qgis2threejscore import calculateDEMSize
from .qgis2threejstools import getLayersInProject, logMessage
from .stylewidget import StyleWidget
from . import qgis2threejstools as tools
from .vectorobject import objectTypeRegistry

PAGE_NONE = 0
PAGE_WORLD = 1
#PAGE_CONTROLS = 2
PAGE_DEM = 3
PAGE_VECTOR = 4


def is_number(val):
  try:
    float(val)
    return True
  except ValueError:
    return False


class PropertyPage(QWidget):

  def __init__(self, pageType, dialog, parent=None):
    QWidget.__init__(self, parent)
    self.pageType = pageType
    self.dialog = dialog
    self.propertyWidgets = []

  def itemChanged(self, item):
    pass

  def setLayoutVisible(self, layout, visible):
    for i in range(layout.count()):
      item = layout.itemAt(i)
      w = item.widget()
      if w is not None:
        w.setVisible(visible)
        continue
      l = item.layout()
      if l is not None:
        self.setLayoutVisible(l, visible)

  def setLayoutsVisible(self, layouts, visible):
    for layout in layouts:
      self.setLayoutVisible(layout, visible)

  def setWidgetsVisible(self, widgets, visible):
    for w in widgets:
      w.setVisible(visible)

  def setLayoutEnabled(self, layout, enabled):
    for i in range(layout.count()):
      item = layout.itemAt(i)
      w = item.widget()
      if w is not None:
        w.setEnabled(enabled)
        continue
      l = item.layout()
      if l is not None:
        self.setLayoutEnabled(l, enabled)

  def setLayoutsEnabled(self, layouts, enabled):
    for layout in layouts:
      self.setLayoutEnabled(layout, enabled)

  def setWidgetsEnabled(self, widgets, enabled):
    for w in widgets:
      w.setEnabled(enabled)

  def registerPropertyWidgets(self, widgets):
    self.propertyWidgets = widgets

  def properties(self):
    p = {}
    for w in self.propertyWidgets:
      v = None
      if isinstance(w, QComboBox):
        index = w.currentIndex()
        if index == -1:
          v = None
        else:
          v = w.itemData(index)
      elif isinstance(w, QRadioButton):
        if not w.isChecked():
          continue
        v = w.isChecked()
      elif isinstance(w, QCheckBox):
        v = w.isChecked()
      elif isinstance(w, (QSlider, QSpinBox)):
        v = w.value()
      elif isinstance(w, QLineEdit):
        v = w.text()
      elif isinstance(w, StyleWidget):
        v = w.values()
      elif isinstance(w, QgsFieldExpressionWidget):
        v = w.expression()
      else:
        logMessage("[propertypages.py] Not recognized widget type: " + str(type(w)))

      p[w.objectName()] = v
    return p

  def setProperties(self, properties):
    for n, v in properties.items():
      w = getattr(self, n, None)
      if w is None:
        continue
      if isinstance(w, QComboBox):
        if v is not None:
          index = w.findData(v)
          if index != -1:
            w.setCurrentIndex(index)
      elif isinstance(w, (QRadioButton, QCheckBox)):  # subclass of QAbstractButton
        w.setChecked(v)
      elif isinstance(w, (QSlider, QSpinBox)):
        w.setValue(v)
      elif isinstance(w, QLineEdit):
        w.setText(v)
      elif isinstance(w, StyleWidget):
        if len(v):
          w.setValues(v)
      elif isinstance(w, QgsFieldExpressionWidget):
        w.setExpression(v)
      else:
        logMessage("[propertypages.py] Cannot restore %s property" % n)


class WorldPropertyPage(PropertyPage, Ui_WorldPropertiesWidget):

  def __init__(self, dialog, parent=None):
    PropertyPage.__init__(self, PAGE_WORLD, dialog, parent)
    Ui_WorldPropertiesWidget.setupUi(self, self)

    self.registerPropertyWidgets([self.lineEdit_BaseSize, self.lineEdit_zFactor, self.lineEdit_zShift, self.radioButton_Color, self.lineEdit_Color, self.radioButton_WGS84])
    self.toolButton_Color.clicked.connect(self.colorButtonClicked)

  def setup(self, properties=None):
    # restore properties
    if properties:
      self.setProperties(properties)
    else:
      self.lineEdit_BaseSize.setText(str(def_vals.baseSize))
      self.lineEdit_zFactor.setText(str(def_vals.zExaggeration))
      self.lineEdit_zShift.setText(str(def_vals.zShift))

    # Supported projections
    # https://github.com/proj4js/proj4js
    projs = ["longlat", "merc"]
    projs += ["aea", "aeqd", "cass", "cea", "eqc", "eqdc", "gnom", "krovak", "laea", "lcc", "mill", "moll",
              "nzmg", "omerc", "poly", "sinu", "somerc", "stere", "sterea", "tmerc", "utm", "vandg"]

    canvas = self.dialog.iface.mapCanvas()
    mapSettings = canvas.mapSettings()
    proj = mapSettings.destinationCrs().toProj4()
    m = re.search("\+proj=(\w+)", proj)
    proj_supported = bool(m and m.group(1) in projs)

    if not proj_supported:
      self.radioButton_ProjectCRS.setChecked(True)
    self.radioButton_WGS84.setEnabled(proj_supported)

  def colorButtonClicked(self):
    color = QColorDialog.getColor(QColor(self.lineEdit_Color.text().replace("0x", "#")))
    if color.isValid():
      self.lineEdit_Color.setText(color.name().replace("#", "0x"))

  def properties(self):
    p = PropertyPage.properties(self)
    # check validity
    if not is_number(self.lineEdit_BaseSize.text()):
      p["lineEdit_BaseSize"] = str(def_vals.baseSize)
    if not is_number(self.lineEdit_zFactor.text()):
      p["lineEdit_zFactor"] = str(def_vals.zExaggeration)
    if not is_number(self.lineEdit_zShift.text()):
      p["lineEdit_zShift"] = str(def_vals.zShift)
    return p


class DEMPropertyPage(PropertyPage, Ui_DEMPropertiesWidget):

  def __init__(self, dialog, parent=None):
    PropertyPage.__init__(self, PAGE_DEM, dialog, parent)
    Ui_DEMPropertiesWidget.setupUi(self, self)

    # set read only to line edits of spin boxes
    self.spinBox_Size.findChild(QLineEdit).setReadOnly(True)
    self.spinBox_Roughening.findChild(QLineEdit).setReadOnly(True)

    self.layer = None
    self.layerImageIds = []

    dispTypeButtons = [self.radioButton_MapCanvas, self.radioButton_LayerImage, self.radioButton_ImageFile, self.radioButton_SolidColor]
    widgets = [self.spinBox_Opacity, self.horizontalSlider_DEMSize]
    widgets += [self.checkBox_Surroundings, self.spinBox_Size, self.spinBox_Roughening]
    widgets += dispTypeButtons
    widgets += [self.checkBox_TransparentBackground, self.lineEdit_ImageFile, self.lineEdit_Color, self.comboBox_TextureSize, self.checkBox_Shading]
    widgets += [self.checkBox_Clip, self.comboBox_ClipLayer]
    widgets += [self.checkBox_Sides, self.checkBox_Frame]
    self.registerPropertyWidgets(widgets)

    self.initLayerComboBox()
    self.initTextureSizeComboBox()

    self.horizontalSlider_DEMSize.valueChanged.connect(self.resolutionSliderChanged)
    self.checkBox_Surroundings.toggled.connect(self.surroundingsToggled)
    self.spinBox_Roughening.valueChanged.connect(self.rougheningChanged)
    for radioButton in dispTypeButtons:
      radioButton.toggled.connect(self.dispTypeChanged)
    self.toolButton_SelectLayer.clicked.connect(self.selectLayerClicked)
    self.toolButton_ImageFile.clicked.connect(self.browseClicked)
    self.toolButton_Color.clicked.connect(self.colorButtonClicked)

  def setup(self, layer=None):
    self.layer = layer
    properties = layer.properties

    # show/hide resampling slider
    self.setLayoutVisible(self.horizontalLayout_Resampling, layer.layerId != "FLAT")

    # use default properties if properties is not set
    if not properties:
      properties = self.properties()
      properties["comboBox_TextureSize"] = 100
      properties["checkBox_Sides"] = True

    # restore properties of the layer
    self.setProperties(properties)

    self.updateLayerImageLabel()

    # set enablement and visibility of widgets
    self.surroundingsToggled(self.checkBox_Surroundings.isChecked())
    self.comboBox_ClipLayer.setVisible(self.checkBox_Clip.isChecked())
    self.dispTypeChanged()

  def initLayerComboBox(self):
    # list of polygon layers
    self.comboBox_ClipLayer.clear()
    for layer in getLayersInProject():
      if layer.type() == QgsMapLayer.VectorLayer and layer.geometryType() == QgsWkbTypes.PolygonGeometry:
        self.comboBox_ClipLayer.addItem(layer.name(), layer.id())

  def initTextureSizeComboBox(self):
    canvas = self.dialog.iface.mapCanvas()
    outsize = canvas.mapSettings().outputSize()

    self.comboBox_TextureSize.clear()
    for i in [4, 2, 1]:
      percent = i * 100
      text = "{0} %  ({1} x {2} px)".format(percent, outsize.width() * i, outsize.height() * i)
      self.comboBox_TextureSize.addItem(text, percent)

  def resolutionSliderChanged(self, v):
    canvas = self.dialog.iface.mapCanvas()
    canvasSize = canvas.mapSettings().outputSize()
    resolutionLevel = self.horizontalSlider_DEMSize.value()
    roughening = self.spinBox_Roughening.value() if self.checkBox_Surroundings.isChecked() else 0
    demSize = calculateDEMSize(canvasSize, resolutionLevel, roughening)

    mupp = canvas.mapUnitsPerPixel()
    xres = (mupp * canvasSize.width()) / (demSize.width() - 1)
    yres = (mupp * canvasSize.height()) / (demSize.height() - 1)

    tip = """Level {0}
Grid Size: {1} x {2}
Grid Spacing: {3:.5f} x {4:.5f})""".format(resolutionLevel,
                                           demSize.width(), demSize.height(),
                                           xres, yres)
    QToolTip.showText(self.horizontalSlider_DEMSize.mapToGlobal(QPoint(0, 0)), tip, self.horizontalSlider_DEMSize)

  def selectLayerClicked(self):
    from .layerselectdialog import LayerSelectDialog
    dialog = LayerSelectDialog(self)
    dialog.initTree(self.layerImageIds)
    dialog.setMapSettings(self.dialog.iface.mapCanvas().mapSettings())
    if not dialog.exec_():
      return

    layers = dialog.visibleLayers()
    self.layerImageIds = [layer.id() for layer in layers]
    self.updateLayerImageLabel()

  def updateLayerImageLabel(self):
    self.label_LayerImage.setText(tools.shortTextFromSelectedLayerIds(self.layerImageIds))

  def browseClicked(self):
    directory = os.path.split(self.lineEdit_ImageFile.text())[0]
    if directory == "":
      directory = QDir.homePath()
    filterString = "Images (*.png *.jpg *.gif *.bmp);;All files (*.*)"
    filename, _ = QFileDialog.getOpenFileName(self, "Select image file", directory, filterString)
    if filename:
      self.lineEdit_ImageFile.setText(filename)

  def colorButtonClicked(self):
    color = QColorDialog.getColor(QColor(self.lineEdit_Color.text().replace("0x", "#")))
    if color.isValid():
      self.lineEdit_Color.setText(color.name().replace("#", "0x"))

  def surroundingsToggled(self, checked):
    self.setLayoutVisible(self.gridLayout_Surroundings, checked)
    self.setLayoutEnabled(self.verticalLayout_Clip, not checked)
    self.setWidgetsEnabled([self.radioButton_ImageFile], not checked)

    if checked:
      self.checkBox_Clip.setChecked(False)

    if checked and self.radioButton_ImageFile.isChecked():
      self.radioButton_MapCanvas.setChecked(True)

  def rougheningChanged(self, v):
    # possible value is a power of 2
    self.spinBox_Roughening.setSingleStep(v)
    self.spinBox_Roughening.setMinimum(max(v // 2, 1))

  def properties(self):
    p = PropertyPage.properties(self)
    item = self.dialog.currentItem
    if item is not None:
      p["visible"] = item.data(0, Qt.CheckStateRole) == Qt.Checked
    if self.layerImageIds:
      p["layerImageIds"] = self.layerImageIds
    return p

  def setProperties(self, properties):
    PropertyPage.setProperties(self, properties)
    self.layerImageIds = properties.get("layerImageIds", [])

  def dispTypeChanged(self, checked=True):
    if checked:
      if self.radioButton_MapCanvas.isChecked():
        t = 0
      elif self.radioButton_LayerImage.isChecked():
        t = 1
      elif self.radioButton_ImageFile.isChecked():
        t = 2
      else:   # self.radioButton_SolidColor.isChecked():
        t = 3

      self.setWidgetsEnabled([self.label_TextureSize, self.comboBox_TextureSize], t in [0, 1])

      self.checkBox_TransparentBackground.setEnabled(t in [0, 1, 2])
      if t in [0, 1]:
        self.checkBox_TransparentBackground.setText("Transparent background")
      elif t == 2:
        self.checkBox_TransparentBackground.setText("Enable transparency")


class VectorPropertyPage(PropertyPage, Ui_VectorPropertiesWidget):

  STYLE_MAX_COUNT = 6

  def __init__(self, dialog, parent=None):
    PropertyPage.__init__(self, PAGE_VECTOR, dialog, parent)
    Ui_VectorPropertiesWidget.setupUi(self, self)

    self.layer = None

    # initialize vector style widgets
    self.labelHeightWidget = StyleWidget(StyleWidget.LABEL_HEIGHT)
    self.labelHeightWidget.setObjectName("labelHeightWidget")
    self.labelHeightWidget.setEnabled(False)
    self.verticalLayout_Label.addWidget(self.labelHeightWidget)

    self.styleWidgetCount = 0
    self.styleWidgets = []
    for i in range(self.STYLE_MAX_COUNT):
      objName = "styleWidget" + str(i)

      widget = StyleWidget()
      widget.setVisible(False)
      widget.setObjectName(objName)
      self.styleWidgets.append(widget)
      self.verticalLayout_Styles.addWidget(widget)

      # assign the widget to property page attribute
      setattr(self, objName, widget)

    widgets = [self.comboBox_ObjectType]
    widgets += self.buttonGroup_altitude.buttons() + [self.fieldExpressionWidget_altitude, self.comboBox_altitudeMode]
    widgets += self.styleWidgets
    widgets += [self.radioButton_AllFeatures, self.radioButton_IntersectingFeatures, self.checkBox_Clip]
    widgets += [self.checkBox_ExportAttrs, self.comboBox_Label, self.labelHeightWidget]
    self.registerPropertyWidgets(widgets)

    self.comboBox_ObjectType.currentIndexChanged.connect(self.setupStyleWidgets)
    self.comboBox_altitudeMode.currentIndexChanged.connect(self.altitudeModeChanged)
    for btn in self.buttonGroup_altitude.buttons():
      btn.toggled.connect(self.zValueRadioButtonToggled)
    self.checkBox_ExportAttrs.toggled.connect(self.exportAttrsToggled)

  def setup(self, layer):
    self.layer = layer
    mapLayer = layer.mapLayer
    properties = layer.properties

    if self.dialog.currentItem:
      self.setEnabled(self.dialog.currentItem.data(0, Qt.CheckStateRole) == Qt.Checked)
    else:
      self.setEnabled(True)

    for i in range(self.STYLE_MAX_COUNT):
      self.styleWidgets[i].hide()

    # set up object type combo box
    self.comboBox_ObjectType.blockSignals(True)
    self.comboBox_ObjectType.clear()
    for obj_type in objectTypeRegistry().objectTypes(mapLayer.geometryType()):
      self.comboBox_ObjectType.addItem(obj_type.displayName(), obj_type.name)
    if properties:
      # restore object type selection
      idx = self.comboBox_ObjectType.findData(properties.get("comboBox_ObjectType"))
      if idx != -1:
        self.comboBox_ObjectType.setCurrentIndex(idx)
    self.comboBox_ObjectType.blockSignals(False)

    # set up altitude mode combo box
    self.comboBox_altitudeMode.blockSignals(True)
    self.comboBox_altitudeMode.clear()
    self.comboBox_altitudeMode.addItem("Absolute")
    for lyr in tools.getDEMLayersInProject():
      self.comboBox_altitudeMode.addItem('Relative to "{0}" layer'.format(lyr.name()), lyr.id())
    self.comboBox_altitudeMode.blockSignals(False)

    # set up z/m button
    wkbType = mapLayer.wkbType()
    hasZ = wkbType in [QgsWkbTypes.Point25D, QgsWkbTypes.LineString25D,
                       QgsWkbTypes.MultiPoint25D, QgsWkbTypes.MultiLineString25D]  #TODO: [Polygon z/m support] ,MultiPolygon25D
    hasZ = hasZ or (wkbType // 1000 in [1, 3])
    hasM = (wkbType // 1000 in [2, 3])
    self.radioButton_zValue.setEnabled(hasZ)
    self.radioButton_mValue.setEnabled(hasM)

    if hasZ:
      self.radioButton_zValue.setChecked(True)
    else:
      self.radioButton_Expression.setChecked(True)

    # set up field expression widget
    self.fieldExpressionWidget_altitude.setFilters(QgsFieldProxyModel.Numeric)
    self.fieldExpressionWidget_altitude.setLayer(mapLayer)
    self.fieldExpressionWidget_altitude.setExpression("0")

    # set up label height widget
    if mapLayer.geometryType() != QgsWkbTypes.LineGeometry:
      defaultLabelHeight = 5
      self.labelHeightWidget.setup(options={"layer": mapLayer, "defaultValue": defaultLabelHeight / self.dialog.mapTo3d().multiplierZ})
    else:
      self.labelHeightWidget.hide()

    # point layer has no geometry clip option
    self.checkBox_Clip.setVisible(mapLayer.geometryType() != QgsWkbTypes.PointGeometry)

    # set up style widgets for selected object type
    self.setupStyleWidgets()

    # set up label combo box
    hasPoint = (mapLayer.geometryType() in (QgsWkbTypes.PointGeometry, QgsWkbTypes.PolygonGeometry))
    self.setLayoutVisible(self.formLayout_Label, hasPoint)
    self.comboBox_Label.clear()
    if hasPoint:
      self.comboBox_Label.addItem("(No label)")
      fields = mapLayer.fields()
      for i in range(fields.count()):
        self.comboBox_Label.addItem(fields[i].name(), i)

    # restore other properties for the layer
    self.setProperties(properties or {})

  def setupStyleWidgets(self, index=None):
    # setup widgets
    obj_type = objectTypeRegistry().objectType(self.layer.mapLayer.geometryType(),
                                               self.comboBox_ObjectType.currentData())
    obj_type.setupWidgets(self,
                          self.dialog.mapTo3d(),     # to calculate default values
                          self.layer.mapLayer)

    self.altitudeModeChanged(self.comboBox_altitudeMode.currentIndex())

  def itemChanged(self, item):
    self.setEnabled(item.data(0, Qt.CheckStateRole) == Qt.Checked)

  def altitudeModeChanged(self, index):
    name = self.comboBox_ObjectType.currentData()
    only_clipped = False

    if name == "Overlay" and index:
      only_clipped = True
      self.radioButton_IntersectingFeatures.setChecked(True)
      self.checkBox_Clip.setChecked(True)

    self.groupBox_Features.setEnabled(not only_clipped)

  def zValueRadioButtonToggled(self, toggled=None):
    if toggled != False:
      self.label_zExpression.setText("" if self.radioButton_Expression.isChecked() else "Addend")

  def exportAttrsToggled(self, checked):
    self.setLayoutEnabled(self.formLayout_Label, checked)
    self.labelHeightWidget.setEnabled(checked)

  def properties(self):
    p = PropertyPage.properties(self)
    item = self.dialog.currentItem
    if item is not None:
      p["visible"] = item.data(0, Qt.CheckStateRole) == Qt.Checked
    return p

  def initStyleWidgets(self, color=True, opacity=True):
    self.styleWidgetCount = 0

    if color:
      self.addStyleWidget(StyleWidget.COLOR, {"layer": self.layer.mapLayer})

    if opacity:
      self.addStyleWidget(StyleWidget.OPACITY, {"layer": self.layer.mapLayer})

    for i in range(self.styleWidgetCount, self.STYLE_MAX_COUNT):
      self.styleWidgets[i].hide()

  def addStyleWidget(self, funcType=None, options=None):
    self.styleWidgets[self.styleWidgetCount].setup(funcType, options)
    self.styleWidgetCount += 1
back to top